hypertwist/scripts/Test-HyperTwistWindowsInstaller.ps1
2026-07-26 09:41:16 +00:00

707 lines
25 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[string]$InstallerPath,
[string]$ValidationRoot = 'C:\HyperTwistInstallerValidation',
[string]$ReportPath = '',
[int]$LaunchSmokeSeconds = 20,
[switch]$KeepInstalled
)
$ErrorActionPreference = 'Stop'
function Write-Utf8JsonFile {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[object]$Value
)
$ParentPath = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
{
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
}
$Json = $Value | ConvertTo-Json -Depth 12
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}
function Invoke-InstallerProcess {
param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath,
[Parameter(Mandatory = $true)]
[string[]]$Arguments,
[Parameter(Mandatory = $true)]
[string]$Label
)
$Process = Start-Process `
-FilePath $ExecutablePath `
-ArgumentList $Arguments `
-PassThru `
-Wait
if ($Process.ExitCode -notin @(0, 3010))
{
throw "$Label failed with exit code $($Process.ExitCode)."
}
return [int]$Process.ExitCode
}
function Invoke-InstallerProcessForExitCode {
param(
[Parameter(Mandatory = $true)]
[string]$ExecutablePath,
[Parameter(Mandatory = $true)]
[string[]]$Arguments
)
$Process = Start-Process `
-FilePath $ExecutablePath `
-ArgumentList $Arguments `
-PassThru `
-Wait
return [int]$Process.ExitCode
}
function Resolve-PayloadRelativeFilePath {
param(
[Parameter(Mandatory = $true)]
[string]$RootPath,
[Parameter(Mandatory = $true)]
[string]$RelativePath
)
if ([string]::IsNullOrWhiteSpace($RelativePath))
{
throw 'A payload-relative path cannot be empty.'
}
$WindowsRelativePath = $RelativePath.Replace('/', '\')
if ([System.IO.Path]::IsPathRooted($WindowsRelativePath))
{
throw "Payload-relative path '$RelativePath' must not be rooted."
}
$ResolvedRootPath = [System.IO.Path]::GetFullPath($RootPath)
$RootPrefix = $ResolvedRootPath.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
$ResolvedFilePath = [System.IO.Path]::GetFullPath(
(Join-Path $ResolvedRootPath $WindowsRelativePath)
)
if (-not $ResolvedFilePath.StartsWith(
$RootPrefix,
[System.StringComparison]::OrdinalIgnoreCase))
{
throw "Payload-relative path '$RelativePath' escapes root '$RootPath'."
}
return $ResolvedFilePath
}
function Test-PayloadManifest {
param(
[Parameter(Mandatory = $true)]
[string]$InstallRoot,
[Parameter(Mandatory = $true)]
[string]$ManifestPath
)
$Manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
$Failures = New-Object System.Collections.ArrayList
$ExpectedPaths = @{}
foreach ($Entry in @($Manifest.files))
{
$EntryRelativePath = [string]$Entry.relativePath
$EntryPathKey = $EntryRelativePath.Replace('\', '/').ToLowerInvariant()
if ($ExpectedPaths.ContainsKey($EntryPathKey))
{
[void]$Failures.Add("duplicate:$EntryRelativePath")
continue
}
$ExpectedPaths[$EntryPathKey] = $true
try
{
$InstalledPath = Resolve-PayloadRelativeFilePath `
-RootPath $InstallRoot `
-RelativePath $EntryRelativePath
}
catch
{
[void]$Failures.Add("invalid-path:$EntryRelativePath")
continue
}
if (-not (Test-Path -LiteralPath $InstalledPath -PathType Leaf))
{
[void]$Failures.Add("missing:$EntryRelativePath")
continue
}
$InstalledItem = Get-Item -LiteralPath $InstalledPath
if ($InstalledItem.Length -ne [Int64]$Entry.sizeBytes)
{
[void]$Failures.Add("size:$EntryRelativePath")
continue
}
$InstalledHash = (Get-FileHash -LiteralPath $InstalledPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($InstalledHash -ne ([string]$Entry.sha256).ToLowerInvariant())
{
[void]$Failures.Add("sha256:$EntryRelativePath")
}
}
$AllowedInstallerPaths = @{}
@(
'Installer/payload-manifest.json',
'Installer/Test-HyperTwistWindowsReadiness.ps1',
'Installer/HyperTwist.install-marker',
'Uninstall HyperTwist.exe'
) | ForEach-Object { $AllowedInstallerPaths[$_.ToLowerInvariant()] = $true }
$InstallRootPrefix = [System.IO.Path]::GetFullPath($InstallRoot).TrimEnd('\') + '\'
$UnexpectedFiles = @(
Get-ChildItem -LiteralPath $InstallRoot -File -Force -Recurse |
ForEach-Object {
$RelativePath = $_.FullName.Substring($InstallRootPrefix.Length).Replace('\', '/')
$RelativePathKey = $RelativePath.ToLowerInvariant()
if (-not $ExpectedPaths.ContainsKey($RelativePathKey) `
-and -not $AllowedInstallerPaths.ContainsKey($RelativePathKey))
{
$RelativePath
}
}
)
$UnexpectedPdbFiles = @(
Get-ChildItem -LiteralPath $InstallRoot -Filter '*.pdb' -File -Recurse -ErrorAction SilentlyContinue
)
return [pscustomobject][ordered]@{
passed = $Failures.Count -eq 0 `
-and $UnexpectedPdbFiles.Count -eq 0 `
-and $UnexpectedFiles.Count -eq 0
manifestEntryCount = @($Manifest.files).Count
failureCount = $Failures.Count
failures = @($Failures)
unexpectedFiles = $UnexpectedFiles
unexpectedPdbFiles = @($UnexpectedPdbFiles | Select-Object -ExpandProperty FullName)
}
}
function Get-OwnedProcessRecords {
param(
[Parameter(Mandatory = $true)]
[string]$InstallRoot
)
$InstallPrefix = $InstallRoot.TrimEnd('\') + '\'
return @(
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
-not [string]::IsNullOrWhiteSpace([string]$_.ExecutablePath) `
-and ([string]$_.ExecutablePath).StartsWith(
$InstallPrefix,
[System.StringComparison]::OrdinalIgnoreCase)
}
)
}
function Stop-OwnedProcesses {
param(
[Parameter(Mandatory = $true)]
[string]$InstallRoot
)
$StoppedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
for ($Attempt = 0; $Attempt -lt 8; $Attempt += 1)
{
$OwnedProcesses = @(Get-OwnedProcessRecords -InstallRoot $InstallRoot)
if ($OwnedProcesses.Count -eq 0)
{
break
}
foreach ($OwnedProcess in $OwnedProcesses)
{
Stop-Process -Id ([int]$OwnedProcess.ProcessId) -Force -ErrorAction SilentlyContinue
[void]$StoppedProcessIds.Add([int]$OwnedProcess.ProcessId)
}
Start-Sleep -Milliseconds 250
}
return @($StoppedProcessIds)
}
function Invoke-InstalledLaunchSmoke {
param(
[Parameter(Mandatory = $true)]
[string]$InstallRoot,
[Parameter(Mandatory = $true)]
[string]$EvidenceDirectory,
[int]$SmokeSeconds
)
$ExecutablePath = Join-Path $InstallRoot 'UnrealHyperTwist.exe'
$RuntimeLogPath = Join-Path $EvidenceDirectory 'installed-runtime.log'
$DiagnosticsPath = Join-Path $EvidenceDirectory 'installed-runtime.hyperdiagnostics.log'
$UserDirectory = Join-Path $EvidenceDirectory 'isolated-user'
New-Item -ItemType Directory -Force -Path $UserDirectory | Out-Null
$Arguments = @(
'-NullRHI',
'-nosound',
'-windowed',
'-ResX=1280',
'-ResY=720',
'-log',
'-FORCELOGFLUSH',
"-abslog=$RuntimeLogPath",
"-HyperTwistDiagnosticsLog=`"$DiagnosticsPath`"",
"-UserDir=`"$UserDirectory`""
)
$StartedAtUtc = [DateTime]::UtcNow
$LaunchProcess = Start-Process `
-FilePath $ExecutablePath `
-ArgumentList $Arguments `
-PassThru
Start-Sleep -Seconds $SmokeSeconds
$OwnedProcesses = @(Get-OwnedProcessRecords -InstallRoot $InstallRoot)
$OwnedProcessIds = @($OwnedProcesses | Select-Object -ExpandProperty ProcessId)
$ListeningEndpoints = @(
if ($OwnedProcessIds.Count -gt 0)
{
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $OwnedProcessIds -contains [int]$_.OwningProcess } |
ForEach-Object {
[pscustomobject][ordered]@{
processId = [int]$_.OwningProcess
localAddress = [string]$_.LocalAddress
localPort = [int]$_.LocalPort
}
}
}
)
$FatalPatterns = @(
'Fatal error:',
'LowLevelFatalError',
'Assertion failed:',
'Critical error:',
'Unhandled Exception:',
'StaticShutdownAfterError'
)
$RuntimeLines = @(
if (Test-Path -LiteralPath $RuntimeLogPath)
{
Get-Content -LiteralPath $RuntimeLogPath
}
)
$DiagnosticsLines = @(
if (Test-Path -LiteralPath $DiagnosticsPath)
{
Get-Content -LiteralPath $DiagnosticsPath
}
)
$FatalLines = @(
@($RuntimeLines + $DiagnosticsLines) |
Where-Object {
$Line = $_.ToString()
@($FatalPatterns | Where-Object { $Line -like "*$_*" }).Count -gt 0
}
)
$Initialized = @(
$DiagnosticsLines |
Where-Object {
$_.ToString().IndexOf(
'[Process] HyperTwist runtime module initialized.',
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0
}
).Count -gt 0
$StoppedProcessIds = @(Stop-OwnedProcesses -InstallRoot $InstallRoot)
$Passed = $OwnedProcessIds.Count -gt 0 `
-and (Test-Path -LiteralPath $DiagnosticsPath -PathType Leaf) `
-and $Initialized `
-and $FatalLines.Count -eq 0 `
-and @($ListeningEndpoints | Where-Object { $_.localPort -eq 1985 }).Count -eq 0
return [pscustomobject][ordered]@{
passed = $Passed
startedAtUtc = $StartedAtUtc.ToString('o')
launcherProcessId = [int]$LaunchProcess.Id
ownedProcessIds = $OwnedProcessIds
stoppedProcessIds = $StoppedProcessIds
runtimeLogPath = $RuntimeLogPath
diagnosticsPath = $DiagnosticsPath
diagnosticsInitialized = $Initialized
fatalLines = $FatalLines
listeningEndpoints = $ListeningEndpoints
}
}
function Get-ShortcutEvidence {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf))
{
return [pscustomobject][ordered]@{
path = $Path
exists = $false
targetPath = $null
arguments = $null
}
}
$Shell = New-Object -ComObject WScript.Shell
$Shortcut = $null
try
{
$Shortcut = $Shell.CreateShortcut($Path)
return [pscustomobject][ordered]@{
path = $Path
exists = $true
targetPath = [string]$Shortcut.TargetPath
arguments = [string]$Shortcut.Arguments
}
}
finally
{
if ($null -ne $Shortcut)
{
[void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($Shortcut)
}
[void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($Shell)
}
}
$CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal($CurrentIdentity)
if (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
{
throw 'Installer lifecycle validation requires an elevated Windows session.'
}
if (-not (Test-Path -LiteralPath $InstallerPath -PathType Leaf))
{
throw "Installer '$InstallerPath' was not found."
}
$ResolvedInstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
$ResolvedValidationRoot = [System.IO.Path]::GetFullPath($ValidationRoot)
$RunId = [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ')
$RunDirectory = Join-Path $ResolvedValidationRoot $RunId
$InstallRoot = Join-Path $RunDirectory 'HyperTwist'
$EvidenceDirectory = Join-Path $RunDirectory 'evidence'
New-Item -ItemType Directory -Force -Path $EvidenceDirectory | Out-Null
if ([string]::IsNullOrWhiteSpace($ReportPath))
{
$ReportPath = Join-Path $EvidenceDirectory 'windows-installer-lifecycle-report.json'
}
$RepositoryRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$ReadinessScriptPath = Join-Path $RepositoryRoot 'scripts\Test-HyperTwistWindowsReadiness.ps1'
$UninstallRegistryPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\HyperTwist'
$ProductRegistryPath = 'HKLM:\SOFTWARE\HyperTwist\HyperTwist'
$CommonProgramsPath = [Environment]::GetFolderPath('CommonPrograms')
$CommonDesktopPath = [Environment]::GetFolderPath('CommonDesktopDirectory')
$StartMenuDirectory = Join-Path $CommonProgramsPath 'HyperTwist'
$DesktopShortcutPath = Join-Path $CommonDesktopPath 'HyperTwist.lnk'
if (Test-Path -LiteralPath $UninstallRegistryPath)
{
throw 'A registered HyperTwist installation already exists; validation will not replace it.'
}
$InstallerItem = Get-Item -LiteralPath $ResolvedInstallerPath
$Report = [ordered]@{
reportVersion = 'ht-windows-installer-lifecycle/v1'
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
result = 'failed'
validationClass = 'isolated-current-host'
cleanMachineAvailable = $false
cleanMachineBoundary = 'No Windows Sandbox, clean Hyper-V VM, or base VHD was available on the validation host.'
installerPath = $ResolvedInstallerPath
installerSizeBytes = [Int64]$InstallerItem.Length
installerSha256 = (Get-FileHash -LiteralPath $ResolvedInstallerPath -Algorithm SHA256).Hash.ToLowerInvariant()
installerSignatureStatus = (Get-AuthenticodeSignature -LiteralPath $ResolvedInstallerPath).Status.ToString()
installRoot = $InstallRoot
unownedNonEmptyRefusal = $null
firstInstallExitCode = $null
upgradeInstallExitCode = $null
payloadParityAfterFirstInstall = $null
payloadParityAfterUpgrade = $null
readiness = $null
launchSmoke = $null
uninstallExitCode = $null
registrationVerified = $false
shortcutsVerified = $false
shortcutDetails = $null
userDataPreserved = $false
installRootRemoved = $false
registrationRemoved = $false
shortcutsRemoved = $false
cleanupCompleted = $false
error = $null
}
$UninstallerPath = Join-Path $InstallRoot 'Uninstall HyperTwist.exe'
$SentinelDirectory = Join-Path $env:LOCALAPPDATA 'UnrealHyperTwist\Saved\InstallerValidation'
$SentinelPath = Join-Path $SentinelDirectory "$RunId.txt"
$Installed = $false
try
{
New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
$UnownedSentinelPath = Join-Path $InstallRoot 'unrelated-owner-sentinel.txt'
[System.IO.File]::WriteAllText(
$UnownedSentinelPath,
"Unowned install-root refusal sentinel $RunId",
(New-Object System.Text.UTF8Encoding($false))
)
$UnownedRefusalExitCode = Invoke-InstallerProcessForExitCode `
-ExecutablePath $ResolvedInstallerPath `
-Arguments @('/S', "/D=$InstallRoot")
if ($UnownedRefusalExitCode -eq 0)
{
$Installed = Test-Path -LiteralPath $UninstallerPath -PathType Leaf
}
$Report.unownedNonEmptyRefusal = [pscustomobject][ordered]@{
passed = $UnownedRefusalExitCode -eq 14 `
-and (Test-Path -LiteralPath $UnownedSentinelPath -PathType Leaf) `
-and -not (Test-Path -LiteralPath $UninstallRegistryPath) `
-and -not (Test-Path -LiteralPath $ProductRegistryPath)
exitCode = $UnownedRefusalExitCode
expectedExitCode = 14
sentinelPreserved = Test-Path -LiteralPath $UnownedSentinelPath -PathType Leaf
registrationAbsent = `
-not (Test-Path -LiteralPath $UninstallRegistryPath) `
-and -not (Test-Path -LiteralPath $ProductRegistryPath)
}
if (-not $Report.unownedNonEmptyRefusal.passed)
{
throw 'The installer did not safely refuse an unowned non-empty installation root.'
}
Remove-Item -LiteralPath $InstallRoot -Recurse -Force
$Report.firstInstallExitCode = Invoke-InstallerProcess `
-ExecutablePath $ResolvedInstallerPath `
-Arguments @('/S', "/D=$InstallRoot") `
-Label 'Silent installer'
$Installed = $true
$ManifestPath = Join-Path $InstallRoot 'Installer\payload-manifest.json'
if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf))
{
throw 'The installed payload manifest is missing.'
}
$Report.payloadParityAfterFirstInstall = Test-PayloadManifest `
-InstallRoot $InstallRoot `
-ManifestPath $ManifestPath
if (-not $Report.payloadParityAfterFirstInstall.passed)
{
throw 'Installed payload parity failed after first install.'
}
$RegisteredProduct = Get-ItemProperty -LiteralPath $UninstallRegistryPath
$RegisteredOwner = Get-ItemProperty -LiteralPath $ProductRegistryPath
$Report.registrationVerified = `
[System.IO.Path]::GetFullPath([string]$RegisteredProduct.InstallLocation) -eq $InstallRoot `
-and [System.IO.Path]::GetFullPath([string]$RegisteredOwner.InstallLocation) -eq $InstallRoot
if (-not $Report.registrationVerified)
{
throw 'Add/Remove Programs or product registration does not match the isolated install root.'
}
$ExpectedApplicationPath = Join-Path $InstallRoot 'UnrealHyperTwist.exe'
$ExpectedReadinessScriptPath = Join-Path $InstallRoot 'Installer\Test-HyperTwistWindowsReadiness.ps1'
$ExpectedManifestPath = Join-Path $InstallRoot 'Installer\payload-manifest.json'
$ExpectedUninstallerPath = Join-Path $InstallRoot 'Uninstall HyperTwist.exe'
$ApplicationShortcut = Get-ShortcutEvidence `
-Path (Join-Path $StartMenuDirectory 'HyperTwist.lnk')
$ReadinessShortcut = Get-ShortcutEvidence `
-Path (Join-Path $StartMenuDirectory 'System Readiness Report.lnk')
$UninstallShortcut = Get-ShortcutEvidence `
-Path (Join-Path $StartMenuDirectory 'Uninstall HyperTwist.lnk')
$DesktopShortcut = Get-ShortcutEvidence -Path $DesktopShortcutPath
$Report.shortcutDetails = [pscustomobject][ordered]@{
application = $ApplicationShortcut
readiness = $ReadinessShortcut
uninstall = $UninstallShortcut
desktop = $DesktopShortcut
}
$Report.shortcutsVerified = `
$ApplicationShortcut.exists `
-and $ApplicationShortcut.targetPath -eq $ExpectedApplicationPath `
-and $ReadinessShortcut.exists `
-and $ReadinessShortcut.targetPath.EndsWith(
'\WindowsPowerShell\v1.0\powershell.exe',
[System.StringComparison]::OrdinalIgnoreCase
) `
-and $ReadinessShortcut.arguments.Contains($ExpectedReadinessScriptPath) `
-and $ReadinessShortcut.arguments.Contains($ExpectedManifestPath) `
-and $ReadinessShortcut.arguments.Contains('-WaitForUser') `
-and $UninstallShortcut.exists `
-and $UninstallShortcut.targetPath -eq $ExpectedUninstallerPath `
-and $DesktopShortcut.exists `
-and $DesktopShortcut.targetPath -eq $ExpectedApplicationPath
if (-not $Report.shortcutsVerified)
{
throw 'One or more installer-managed shortcuts are missing or have incorrect targets or arguments.'
}
$ReadinessReportPath = Join-Path $EvidenceDirectory 'installed-readiness-report.json'
$Report.readiness = & $ReadinessScriptPath `
-PayloadRoot $InstallRoot `
-TargetInstallRoot $InstallRoot `
-ExpectedPayloadManifestPath $ManifestPath `
-ReportPath $ReadinessReportPath `
-VerifyPayloadHashes `
-PassThru
if ($Report.readiness.result -eq 'blocked')
{
throw 'Installed-host readiness validation is blocked.'
}
New-Item -ItemType Directory -Force -Path $SentinelDirectory | Out-Null
[System.IO.File]::WriteAllText(
$SentinelPath,
"HyperTwist installer validation sentinel $RunId",
(New-Object System.Text.UTF8Encoding($false))
)
$Report.launchSmoke = Invoke-InstalledLaunchSmoke `
-InstallRoot $InstallRoot `
-EvidenceDirectory $EvidenceDirectory `
-SmokeSeconds $LaunchSmokeSeconds
if (-not $Report.launchSmoke.passed)
{
throw 'Installed executable launch smoke failed.'
}
$Report.upgradeInstallExitCode = Invoke-InstallerProcess `
-ExecutablePath $ResolvedInstallerPath `
-Arguments @('/S', "/D=$InstallRoot") `
-Label 'Silent in-place upgrade'
$ManifestPath = Join-Path $InstallRoot 'Installer\payload-manifest.json'
$Report.payloadParityAfterUpgrade = Test-PayloadManifest `
-InstallRoot $InstallRoot `
-ManifestPath $ManifestPath
if (-not $Report.payloadParityAfterUpgrade.passed)
{
throw 'Installed payload parity failed after in-place upgrade.'
}
if (-not $KeepInstalled)
{
$Report.uninstallExitCode = Invoke-InstallerProcess `
-ExecutablePath $UninstallerPath `
-Arguments @('/S') `
-Label 'Silent uninstaller'
$Installed = $false
Start-Sleep -Seconds 3
$Report.userDataPreserved = Test-Path -LiteralPath $SentinelPath -PathType Leaf
$Report.installRootRemoved = -not (Test-Path -LiteralPath $InstallRoot)
$Report.registrationRemoved = `
-not (Test-Path -LiteralPath $UninstallRegistryPath) `
-and -not (Test-Path -LiteralPath $ProductRegistryPath)
$Report.shortcutsRemoved = `
-not (Test-Path -LiteralPath $StartMenuDirectory) `
-and -not (Test-Path -LiteralPath $DesktopShortcutPath)
if (-not $Report.userDataPreserved)
{
throw 'Default uninstall removed current-user data unexpectedly.'
}
if (-not $Report.installRootRemoved)
{
throw 'The isolated install root remains after uninstall.'
}
if (-not $Report.registrationRemoved)
{
throw 'Installer registry state remains after uninstall.'
}
if (-not $Report.shortcutsRemoved)
{
throw 'Installer-managed shortcuts remain after uninstall.'
}
}
$Report.cleanupCompleted = -not $KeepInstalled
$Report.result = 'passed'
}
catch
{
$Report.error = $_.Exception.Message
throw
}
finally
{
[void](Stop-OwnedProcesses -InstallRoot $InstallRoot)
if ($Installed -and -not $KeepInstalled -and (Test-Path -LiteralPath $UninstallerPath -PathType Leaf))
{
try
{
[void](Invoke-InstallerProcess `
-ExecutablePath $UninstallerPath `
-Arguments @('/S') `
-Label 'Failure-cleanup uninstaller')
Start-Sleep -Seconds 3
$Installed = $false
}
catch
{
if ([string]::IsNullOrWhiteSpace([string]$Report.error))
{
$Report.error = $_.Exception.Message
}
else
{
$Report.error = "$($Report.error) | Cleanup: $($_.Exception.Message)"
}
}
}
if (-not $KeepInstalled `
-and (Test-Path -LiteralPath $InstallRoot) `
-and -not (Test-Path -LiteralPath $UninstallRegistryPath) `
-and -not (Test-Path -LiteralPath $ProductRegistryPath))
{
Remove-Item -LiteralPath $InstallRoot -Recurse -Force
}
if (Test-Path -LiteralPath $SentinelPath)
{
Remove-Item -LiteralPath $SentinelPath -Force
}
if ((Test-Path -LiteralPath $SentinelDirectory) `
-and @(Get-ChildItem -LiteralPath $SentinelDirectory -Force).Count -eq 0)
{
Remove-Item -LiteralPath $SentinelDirectory -Force
}
if (-not $KeepInstalled)
{
$Report.installRootRemoved = -not (Test-Path -LiteralPath $InstallRoot)
$Report.registrationRemoved = `
-not (Test-Path -LiteralPath $UninstallRegistryPath) `
-and -not (Test-Path -LiteralPath $ProductRegistryPath)
$Report.shortcutsRemoved = `
-not (Test-Path -LiteralPath $StartMenuDirectory) `
-and -not (Test-Path -LiteralPath $DesktopShortcutPath)
$Report.cleanupCompleted = `
-not $Installed `
-and $Report.installRootRemoved `
-and $Report.registrationRemoved `
-and $Report.shortcutsRemoved
}
Write-Utf8JsonFile -Path $ReportPath -Value $Report
}
[Console]::Out.WriteLine('HyperTwist Windows installer lifecycle validation passed.')
[Console]::Out.WriteLine("Report: $([System.IO.Path]::GetFullPath($ReportPath))")