Functions/GenXdev.Windows/EnsurePSTools.ps1

################################################################################
<#
.SYNOPSIS
Ensures Sysinternals tools (PSTools) are installed and available.
 
.DESCRIPTION
Verifies if Sysinternals tools like handle.exe are installed and properly
configured on the system. If not found, installs the Sysinternals Suite
using WinGet and handles the complete installation process automatically.
 
.PARAMETER Force
Switch to force reinstallation of Sysinternals tools even if they are already
installed.
 
.PARAMETER PSExeName
The executable name to check for verifying Sysinternals tools installation.
Default is 'handle.exe'.
 
.EXAMPLE
EnsurePSTools
Ensures Sysinternals tools are installed and properly configured.
 
.EXAMPLE
EnsurePSTools -Force -PSExeName 'procexp.exe'
Forces reinstallation of Sysinternals tools and uses procexp.exe to verify
installation.
#>

function EnsurePSTools {

    [CmdletBinding()]
    param(
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Force reinstallation of Sysinternals tools"
        )]
        [switch]$Force,
        ########################################################################
        [parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = "The executable name to verify installation"
        )]
        [string]$PSExeName = 'handle.exe'
        ########################################################################
    )

    begin {

        ########################################################################
        <#
        .SYNOPSIS
        Checks if the WinGet PowerShell module is installed.
 
        .DESCRIPTION
        Attempts to import the Microsoft.WinGet.Client module and verifies its
        presence by checking if the module is loaded after import.
 
        .EXAMPLE
        IsWinGetInstalled
        Returns $true if Microsoft.WinGet.Client module is installed, otherwise $false.
        #>

        function IsWinGetInstalled {

            # attempt to load the winget module silently
            Microsoft.PowerShell.Core\Import-Module "Microsoft.WinGet.Client" `
                -ErrorAction SilentlyContinue

            # verify if module was loaded successfully
            $module = Microsoft.PowerShell.Core\Get-Module "Microsoft.WinGet.Client" `
                -ErrorAction SilentlyContinue

            # return true if module exists, false otherwise
            return $null -ne $module
        }

        ########################################################################
        <#
        .SYNOPSIS
        Installs the WinGet PowerShell module.
 
        .DESCRIPTION
        Installs and imports the Microsoft.WinGet.Client module for package
        management using PowerShellGet.
 
        .EXAMPLE
        InstallWinGet
        Installs and imports the Microsoft.WinGet.Client module.
        #>

        function InstallWinGet {

            # log installation progress through verbose messages
            Microsoft.PowerShell.Utility\Write-Verbose "Installing WinGet PowerShell client..."

            # install winget module with force parameter to ensure success
            $null = PowerShellGet\Install-Module "Microsoft.WinGet.Client" -Force -AllowClobber

            # load the newly installed module into the current session
            Microsoft.PowerShell.Core\Import-Module "Microsoft.WinGet.Client"
        }
    }

    process {

        # check if we should install/reinstall based on force flag or missing executable
        if ($Force -or (@(Microsoft.PowerShell.Core\Get-Command $PSExeName `
                        -ErrorAction SilentlyContinue).Length -eq 0)) {

            # inform user about installation process starting
            Microsoft.PowerShell.Utility\Write-Host "Installing Sysinternals packages..."

            # make sure winget module is available before proceeding
            if (-not (IsWinGetInstalled)) {
                InstallWinGet
            }

            # search for available sysinternals packages using winget
            $sysinternalsPackages = Microsoft.WinGet.Client\Find-WinGetPackage `
                -Name "Microsoft.Sysinternals" |
            Microsoft.PowerShell.Core\Where-Object {
                $_.Id -like "Microsoft.Sysinternals*"
            }

            # install each found sysinternals package
            foreach ($package in $sysinternalsPackages) {
                try {
                    # log which package is being installed
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Installing $($package.Name) ($($package.Id))..."
                    )

                    # install the package with force to ensure success
                    $null = Microsoft.WinGet.Client\Install-WinGetPackage -Id $package.Id -Force
                }
                catch {
                    # log any installation failures with details
                    Microsoft.PowerShell.Utility\Write-Warning (
                        "Failed to install $($package.Name): $($_.Exception.Message)"
                    )
                }
            }

            # verify installation succeeded unless force was specified
            if (-not $Force -and (-not (Microsoft.PowerShell.Core\Get-Command $PSExeName `
                            -ErrorAction SilentlyContinue))) {
                throw "Sysinternals installation failed. $PSExeName not found."
            }
        }

        # log successful completion of the function
        Microsoft.PowerShell.Utility\Write-Verbose "✅ Sysinternals tools are ready."
    }

    end {
    }
}
################################################################################