Functions/GenXdev.Software/EnsurePSTools.ps1

<##############################################################################
Part of PowerShell module : GenXdev.Software
Original cmdlet filename : EnsurePSTools.ps1
Original author : René Vaessen / GenXdev
Version : 3.28.2026
################################################################################
Copyright (c) 2026 René Vaessen / GenXdev

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
################################################################################>


###############################################################################
<#
.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.

.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)) {

            # 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 {
    }
}