Functions/GenXdev.Software/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, pre-accepts all End-User License Agreements (EULAs) via
registry keys, and handles the complete installation process automatically.

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.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,
            HelpMessage = ('Automatically consent to Sysinternals tools and ' +
                'WinGet module installation and set persistent flag.')
        )]
        [switch] $AutoConsent,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Automatically consent to third-party software ' +
                'installation and set persistent flag for all packages.')
        )]
        [switch] $AutoConsentAllPackages,
        ########################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Only auto consent during session')
        )]
        [switch]$SessionOnly
        ########################################################################
    )

    begin {

        $Extra = ";($ENV:LOCALAPPDATA)\Microsoft\WinGet\Links\";
        $ENV:PATH = "$($ENV:PATH)".Replace($Extra, "") + $Extra
    }

    process {

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

            # verify administrative privileges are available
            if (-not (GenXdev\CurrentUserHasElevatedRights)) {

                $json = $PSBoundParameters | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress

                GenXdev\Invoke-CommandElevated ([ScriptBlock]::Create(@"

            `$params = '$json' | ConvertFrom-Json -AsHashTable
            GenXdev\EnsurePSTools @params
"@
)) -JobDescription 'Installing Sysinternals tools';

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

            # request consent before installing sysinternals packages
            $sysinternalsConsent = GenXdev\Confirm-InstallationConsent `
                -ApplicationName 'Microsoft Sysinternals Suite' `
                -Source 'WinGet' `
                -Description ('System administration and diagnostic tools. ' +
                'By consenting, you accept all End-User License ' +
                'Agreements (EULAs) for all Sysinternals tools in one go.') `
                -Publisher 'Microsoft' `
                -AutoConsent:$AutoConsent `
                -AutoConsentAllPackages:$AutoConsentAllPackages

            if (-not $sysinternalsConsent) {
                throw 'Installation consent denied for Microsoft Sysinternals Suite. Cannot proceed without system tools.'
            }

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

            # install the package with force to ensure success
            $null = Microsoft.WinGet.Client\Install-WinGetPackage -Id 'Microsoft.Sysinternals.Suite' -Force -MatchOption EqualsCaseInsensitive

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

            # pre-accept all Sysinternals EULAs by creating registry keys
            # for each tool in the installation directory
            Microsoft.PowerShell.Utility\Write-Verbose (
                'Accepting Sysinternals EULAs via registry keys...')

            $procexpSource = (Microsoft.PowerShell.Core\Get-Command `
                    procexp.exe -ErrorAction SilentlyContinue).Source

            if ($procexpSource) {
                $installDir = Microsoft.PowerShell.Management\Split-Path `
                    -Parent (Microsoft.PowerShell.Management\Get-Item `
                        $procexpSource).Target

                # get unique tools by grouping 32/64-bit variants
                $tools = Microsoft.PowerShell.Management\Get-ChildItem `
                    $installDir -Filter *.exe |
                    Microsoft.PowerShell.Core\Where-Object {
                        $_.BaseName -notmatch '64$' -and
                        $_.BaseName -notmatch '-x86$'
                    }

                foreach ($tool in $tools) {
                    $productName = $tool.VersionInfo.ProductName
                    $keyName = if ($productName) {
                        $productName -replace '^Sysinternals\s+', '' `
                            -replace '^SysInternals\s+', ''
                    }
                    else {
                        $tool.BaseName
                    }

                    if (-not $keyName) { $keyName = $tool.BaseName }

                    $regPath = "HKCU:\SOFTWARE\Sysinternals\${keyName}"

                    if (-not (Microsoft.PowerShell.Management\Test-Path `
                                $regPath)) {
                        $null = Microsoft.PowerShell.Management\New-Item `
                            -Path $regPath -Force
                        $null = Microsoft.PowerShell.Management\New-ItemProperty `
                            -Path $regPath `
                            -Name 'EulaAccepted' `
                            -Value 1 `
                            -PropertyType DWord `
                            -Force
                        Microsoft.PowerShell.Utility\Write-Verbose (
                            " Accepted EULA for: ${keyName}")
                    }
                }
            }
        }

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

    end {
    }
}