working install and uninstall flow in Vagrant
This commit is contained in:
@@ -65,6 +65,59 @@ function Invoke-Vagrant {
|
||||
Invoke-Tool -FilePath "vagrant" -Arguments $Arguments
|
||||
}
|
||||
|
||||
function Save-GuestTraceBundle {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$GuestTracePath,
|
||||
[Parameter(Mandatory)][string]$GuestZipPath,
|
||||
[Parameter(Mandatory)][string]$LocalTracePath
|
||||
)
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $LocalTracePath | Out-Null
|
||||
$guestTraceLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestTracePath
|
||||
$guestZipLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestZipPath
|
||||
$command = @(
|
||||
"`$tracePath = $guestTraceLiteral"
|
||||
"`$zipPath = $guestZipLiteral"
|
||||
"if (-not (Test-Path -LiteralPath `$tracePath)) { Write-Output '__COVENANT_TRACE_EMPTY__'; exit 0 }"
|
||||
"New-Item -ItemType File -Force -Path (Join-Path `$tracePath '.keep') | Out-Null"
|
||||
"Remove-Item -LiteralPath `$zipPath -Force -ErrorAction SilentlyContinue"
|
||||
"Compress-Archive -Path (Join-Path `$tracePath '*') -DestinationPath `$zipPath -Force"
|
||||
"Write-Output '__COVENANT_TRACE_B64_START__'"
|
||||
"[Convert]::ToBase64String([IO.File]::ReadAllBytes(`$zipPath))"
|
||||
"Write-Output '__COVENANT_TRACE_B64_END__'"
|
||||
) -join "; "
|
||||
|
||||
$output = Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $command)
|
||||
$lines = @($output | ForEach-Object { $_.ToString().Trim() })
|
||||
if ($lines -contains "__COVENANT_TRACE_EMPTY__") {
|
||||
Write-Warning "Guest trace path did not exist: $GuestTracePath"
|
||||
return
|
||||
}
|
||||
|
||||
$start = [Array]::IndexOf($lines, "__COVENANT_TRACE_B64_START__")
|
||||
$end = [Array]::IndexOf($lines, "__COVENANT_TRACE_B64_END__")
|
||||
if ($start -lt 0 -or $end -le $start) {
|
||||
Write-Warning "Guest trace bundle markers were not found in WinRM output."
|
||||
return
|
||||
}
|
||||
|
||||
$base64 = (($lines[($start + 1)..($end - 1)]) -join "").Trim()
|
||||
if (-not $base64) {
|
||||
Write-Warning "Guest trace bundle was empty."
|
||||
return
|
||||
}
|
||||
|
||||
$zipPath = Join-Path $LocalTracePath "guest-trace.zip"
|
||||
[IO.File]::WriteAllBytes($zipPath, [Convert]::FromBase64String($base64))
|
||||
Expand-Archive -LiteralPath $zipPath -DestinationPath $LocalTracePath -Force
|
||||
Write-Host "Trace bundle: $LocalTracePath"
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to collect guest trace bundle: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Open-HyperVViewer {
|
||||
param([Parameter(Mandatory)][string]$VmName)
|
||||
|
||||
@@ -91,6 +144,38 @@ function Invoke-Process {
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-PackagedInstallerBundle {
|
||||
param([Parameter(Mandatory)][string]$InstallerPath)
|
||||
|
||||
$magic = [Text.Encoding]::ASCII.GetBytes("COVENANT_SETUP_BUNDLE_V1")
|
||||
$stream = [IO.File]::Open(
|
||||
$InstallerPath,
|
||||
[IO.FileMode]::Open,
|
||||
[IO.FileAccess]::Read,
|
||||
[IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
if ($stream.Length -lt $magic.Length) {
|
||||
throw "Packaged installer is too small to contain the embedded bundle marker: $InstallerPath"
|
||||
}
|
||||
|
||||
$null = $stream.Seek(-1 * $magic.Length, [IO.SeekOrigin]::End)
|
||||
$actual = [byte[]]::new($magic.Length)
|
||||
$read = $stream.Read($actual, 0, $actual.Length)
|
||||
if ($read -ne $magic.Length) {
|
||||
throw "Could not read embedded bundle marker from packaged installer: $InstallerPath"
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $magic.Length; $i++) {
|
||||
if ($actual[$i] -ne $magic[$i]) {
|
||||
throw "Packaged installer is missing the embedded bundle marker: $InstallerPath"
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$releaseExe = Join-Path $repoRoot "target\release\covenant-setup.exe"
|
||||
$payloadRoot = Join-Path $repoRoot "vm\self-test\payload"
|
||||
@@ -99,18 +184,23 @@ $manifestPathAbs = Resolve-RepoPath -RepoRoot $repoRoot -Path $ManifestPath
|
||||
$outputRootAbs = Resolve-RepoPath -RepoRoot $repoRoot -Path $OutputRoot
|
||||
$installerPath = Join-Path $outputRootAbs "covenant-setup-installer.exe"
|
||||
$resultPath = Join-Path $outputRootAbs "guest-result.json"
|
||||
$traceRootAbs = Join-Path $outputRootAbs "trace"
|
||||
$guestRoot = "C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke"
|
||||
$guestInstallerPath = Join-Path $guestRoot "covenant-setup-installer.exe"
|
||||
$guestResultPath = Join-Path $guestRoot "guest-result.json"
|
||||
$guestTraceRoot = Join-Path $guestRoot "trace"
|
||||
$guestTraceZipPath = Join-Path $guestRoot "trace.zip"
|
||||
$guestScriptRoot = Join-Path $guestRoot "scripts"
|
||||
$guestCommand = "& '$guestScriptRoot\Start-InteractiveSelfInstall.ps1' -InstallerPath '$guestInstallerPath' -ResultPath '$guestResultPath' -ScriptRoot '$guestScriptRoot'"
|
||||
$guestCommand = "& '$guestScriptRoot\Start-InteractiveSelfInstall.ps1' -InstallerPath '$guestInstallerPath' -ResultPath '$guestResultPath' -ScriptRoot '$guestScriptRoot' -TracePath '$guestTraceRoot'"
|
||||
|
||||
Assert-Command -Name "cargo"
|
||||
Assert-Command -Name "dotnet"
|
||||
Assert-Command -Name "vagrant"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $payloadRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $outputRootAbs | Out-Null
|
||||
Remove-Item -LiteralPath $resultPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $traceRootAbs -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
if (-not $SkipBuild) {
|
||||
@@ -131,6 +221,7 @@ try {
|
||||
if (-not (Test-Path -LiteralPath $installerPath)) {
|
||||
throw "Packaged installer not found at $installerPath"
|
||||
}
|
||||
Assert-PackagedInstallerBundle -InstallerPath $installerPath
|
||||
|
||||
if (-not $SkipVmBoot) {
|
||||
Invoke-Vagrant -Arguments @("up", "--provider", $Provider)
|
||||
@@ -157,8 +248,15 @@ try {
|
||||
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $waitForShellCommand)
|
||||
|
||||
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", "New-Item -ItemType Directory -Force -Path '$guestRoot' | Out-Null; New-Item -ItemType Directory -Force -Path '$guestScriptRoot' | Out-Null")
|
||||
$clearGuestTraceCommand = @(
|
||||
"try {"
|
||||
"if (Test-Path -LiteralPath '$guestTraceRoot') { Remove-Item -LiteralPath '$guestTraceRoot' -Recurse -Force -ErrorAction SilentlyContinue }"
|
||||
"if (Test-Path -LiteralPath '$guestTraceZipPath') { Remove-Item -LiteralPath '$guestTraceZipPath' -Force -ErrorAction SilentlyContinue }"
|
||||
"} catch { Write-Warning `$_.Exception.Message }"
|
||||
"exit 0"
|
||||
) -join "; "
|
||||
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $clearGuestTraceCommand)
|
||||
Invoke-Vagrant -Arguments @("upload", $installerPath, $guestInstallerPath)
|
||||
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Approve-InstallerDialogs.ps1"), (Join-Path $guestScriptRoot "Approve-InstallerDialogs.ps1"))
|
||||
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Invoke-InteractiveInstaller.ps1"), (Join-Path $guestScriptRoot "Invoke-InteractiveInstaller.ps1"))
|
||||
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Start-InteractiveSelfInstall.ps1"), (Join-Path $guestScriptRoot "Start-InteractiveSelfInstall.ps1"))
|
||||
|
||||
@@ -180,9 +278,13 @@ try {
|
||||
Write-Host "Smoke test passed."
|
||||
Write-Host "Installer: $installerPath"
|
||||
Write-Host "Result JSON: $resultPath"
|
||||
Write-Host "InstallRoot: $($result.installRoot)"
|
||||
Write-Host "Trace: $traceRootAbs"
|
||||
Write-Host "InstallRoot: $($result.installRoot) (installed, then removed)"
|
||||
Write-Host "Uninstall: exit $($result.uninstallExitCode), verified=$($result.uninstallVerified)"
|
||||
}
|
||||
finally {
|
||||
Save-GuestTraceBundle -GuestTracePath $guestTraceRoot -GuestZipPath $guestTraceZipPath -LocalTracePath $traceRootAbs
|
||||
|
||||
if ($DestroyAfter) {
|
||||
try {
|
||||
Invoke-Vagrant -Arguments @("destroy", "-f")
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
param(
|
||||
[string]$TracePath = "C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke\trace",
|
||||
[string]$ZipPath = "C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke\trace-abort.zip",
|
||||
[string]$TaskNamePrefix = "CovenantSetupSelfInstall"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-TraceEvent {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Phase,
|
||||
[object]$Detail = $null
|
||||
)
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
$event = [ordered]@{
|
||||
time = (Get-Date).ToUniversalTime().ToString("o")
|
||||
pid = $PID
|
||||
script = Split-Path -Leaf $PSCommandPath
|
||||
phase = $Phase
|
||||
detail = $Detail
|
||||
}
|
||||
$event | ConvertTo-Json -Depth 12 -Compress | Add-Content -LiteralPath (Join-Path $TracePath "guest-events.jsonl") -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to write trace event '$Phase': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-DiagnosticFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
[Parameter(Mandatory)][scriptblock]$Capture
|
||||
)
|
||||
|
||||
$path = Join-Path $TracePath $Name
|
||||
Write-TraceEvent -Phase "abort_diagnostic_file_start" -Detail @{ name = $Name }
|
||||
try {
|
||||
$value = & $Capture
|
||||
$value | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
Write-TraceEvent -Phase "abort_diagnostic_file_finish" -Detail @{ name = $Name }
|
||||
}
|
||||
catch {
|
||||
[ordered]@{
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
Write-TraceEvent -Phase "abort_diagnostic_file_error" -Detail @{
|
||||
name = $Name
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
Write-TraceEvent -Phase "abort_requested"
|
||||
|
||||
Write-DiagnosticFile -Name "abort-processes.json" -Capture {
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object {
|
||||
$_.Name -match "covenant|Covenant|powershell|pwsh" -or
|
||||
$_.CommandLine -match "CovenantSetup|Invoke-InteractiveInstaller|Start-InteractiveSelfInstall"
|
||||
} |
|
||||
Select-Object ProcessId, ParentProcessId, Name, CommandLine, CreationDate
|
||||
}
|
||||
Write-DiagnosticFile -Name "abort-windows.json" -Capture {
|
||||
Get-Process |
|
||||
Where-Object { $_.MainWindowHandle -ne 0 -or $_.ProcessName -match "covenant|Covenant|powershell|pwsh" } |
|
||||
Select-Object Id, ProcessName, MainWindowTitle, MainWindowHandle, StartTime
|
||||
}
|
||||
Write-DiagnosticFile -Name "abort-scheduled-tasks.json" -Capture {
|
||||
Get-ScheduledTask -TaskName "$TaskNamePrefix*" -ErrorAction SilentlyContinue |
|
||||
Select-Object TaskName, State, TaskPath, Actions, Triggers
|
||||
}
|
||||
Write-DiagnosticFile -Name "abort-scheduled-task-info.json" -Capture {
|
||||
Get-ScheduledTask -TaskName "$TaskNamePrefix*" -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-ScheduledTaskInfo -TaskName $_.TaskName -ErrorAction SilentlyContinue } |
|
||||
Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns
|
||||
}
|
||||
Write-DiagnosticFile -Name "abort-application-events.json" -Capture {
|
||||
Get-WinEvent -FilterHashtable @{ LogName = "Application"; StartTime = (Get-Date).AddHours(-2) } -MaxEvents 200 -ErrorAction SilentlyContinue |
|
||||
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
|
||||
}
|
||||
Write-DiagnosticFile -Name "abort-system-events.json" -Capture {
|
||||
Get-WinEvent -FilterHashtable @{ LogName = "System"; StartTime = (Get-Date).AddHours(-2) } -MaxEvents 200 -ErrorAction SilentlyContinue |
|
||||
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
|
||||
}
|
||||
|
||||
Get-Process -Name "covenant-setup-installer", "covenant-setup", "covenant-setup-uninstall", "Covenant.Setup.Ui" -ErrorAction SilentlyContinue |
|
||||
Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Get-ScheduledTask -TaskName "$TaskNamePrefix*" -ErrorAction SilentlyContinue |
|
||||
Stop-ScheduledTask -ErrorAction SilentlyContinue
|
||||
Get-ScheduledTask -TaskName "$TaskNamePrefix*" -ErrorAction SilentlyContinue |
|
||||
Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
$scriptProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
$_.ProcessId -ne $PID -and
|
||||
$_.CommandLine -match "Invoke-InteractiveInstaller|Start-InteractiveSelfInstall"
|
||||
}
|
||||
foreach ($process in $scriptProcesses) {
|
||||
Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-TraceEvent -Phase "abort_cleanup_complete"
|
||||
|
||||
Remove-Item -LiteralPath $ZipPath -Force -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path (Join-Path $TracePath "*") -DestinationPath $ZipPath -Force
|
||||
Write-Output "__COVENANT_TRACE_B64_START__"
|
||||
[Convert]::ToBase64String([IO.File]::ReadAllBytes($ZipPath))
|
||||
Write-Output "__COVENANT_TRACE_B64_END__"
|
||||
@@ -1,26 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][int]$InstallerProcessId,
|
||||
[string]$WindowTitle = "covenant-setup",
|
||||
[int]$PollMilliseconds = 500
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
while ($true) {
|
||||
$process = Get-Process -Id $InstallerProcessId -ErrorAction SilentlyContinue
|
||||
if (-not $process) {
|
||||
break
|
||||
}
|
||||
|
||||
if ([Microsoft.VisualBasic.Interaction]::AppActivate($WindowTitle)) {
|
||||
Start-Sleep -Milliseconds 200
|
||||
[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds $PollMilliseconds
|
||||
}
|
||||
@@ -2,16 +2,120 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$InstallerPath,
|
||||
[Parameter(Mandatory)][string]$ResultPath,
|
||||
[int]$TimeoutSeconds = 600
|
||||
[int]$TimeoutSeconds = 600,
|
||||
[string]$TracePath = $(Join-Path (Split-Path -Parent $ResultPath) "trace"),
|
||||
[string]$OperationName = "installer",
|
||||
[string]$InstallerArgumentsBase64 = "",
|
||||
[string[]]$InstallerArguments = @("--headed", "--automation")
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-TraceEvent {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Phase,
|
||||
[object]$Detail = $null
|
||||
)
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
$event = [ordered]@{
|
||||
time = (Get-Date).ToUniversalTime().ToString("o")
|
||||
pid = $PID
|
||||
script = Split-Path -Leaf $PSCommandPath
|
||||
phase = $Phase
|
||||
detail = $Detail
|
||||
}
|
||||
$event | ConvertTo-Json -Depth 12 -Compress | Add-Content -LiteralPath (Join-Path $TracePath "guest-events.jsonl") -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to write trace event '$Phase': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-DiagnosticFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
[Parameter(Mandatory)][scriptblock]$Capture
|
||||
)
|
||||
|
||||
$path = Join-Path $TracePath $Name
|
||||
try {
|
||||
$value = & $Capture
|
||||
$value | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
[ordered]@{
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
}
|
||||
}
|
||||
|
||||
function Export-InstallerDiagnostics {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Reason,
|
||||
[System.Diagnostics.Process]$InstallerProcess = $null
|
||||
)
|
||||
|
||||
Write-TraceEvent -Phase "installer_diagnostics_start" -Detail @{ reason = $Reason; installerPid = $(if ($InstallerProcess) { $InstallerProcess.Id } else { $null }) }
|
||||
Write-DiagnosticFile -Name "interactive-context.json" -Capture {
|
||||
[ordered]@{
|
||||
reason = $Reason
|
||||
computerName = $env:COMPUTERNAME
|
||||
userName = $env:USERNAME
|
||||
sessionName = $env:SESSIONNAME
|
||||
installerPath = $InstallerPath
|
||||
installerArgs = $InstallerArguments
|
||||
operationName = $OperationName
|
||||
resultPath = $ResultPath
|
||||
tracePath = $TracePath
|
||||
timeoutSeconds = $TimeoutSeconds
|
||||
installerPid = $(if ($InstallerProcess) { $InstallerProcess.Id } else { $null })
|
||||
installerExited = $(if ($InstallerProcess) { $InstallerProcess.HasExited } else { $null })
|
||||
}
|
||||
}
|
||||
Write-DiagnosticFile -Name "interactive-processes.json" -Capture {
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.Name -match 'covenant|Covenant|powershell|pwsh|dotnet' } |
|
||||
Select-Object ProcessId, ParentProcessId, Name, CommandLine, CreationDate
|
||||
}
|
||||
Write-DiagnosticFile -Name "interactive-windows.json" -Capture {
|
||||
Get-Process |
|
||||
Where-Object { $_.MainWindowHandle -ne 0 -or $_.ProcessName -match 'covenant|Covenant|powershell|pwsh' } |
|
||||
Select-Object Id, ProcessName, MainWindowTitle, MainWindowHandle, StartTime
|
||||
}
|
||||
Write-DiagnosticFile -Name "interactive-application-events.json" -Capture {
|
||||
Get-WinEvent -FilterHashtable @{ LogName = "Application"; StartTime = (Get-Date).AddHours(-2) } -MaxEvents 200 -ErrorAction SilentlyContinue |
|
||||
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
|
||||
}
|
||||
Write-TraceEvent -Phase "installer_diagnostics_finish" -Detail @{ reason = $Reason }
|
||||
}
|
||||
|
||||
$installer = $null
|
||||
$startedAt = Get-Date
|
||||
|
||||
try {
|
||||
if (-not [string]::IsNullOrWhiteSpace($InstallerArgumentsBase64)) {
|
||||
$argumentsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($InstallerArgumentsBase64))
|
||||
$decodedArguments = ConvertFrom-Json -InputObject $argumentsJson
|
||||
$InstallerArguments = @()
|
||||
foreach ($argument in $decodedArguments) {
|
||||
$InstallerArguments += [string]$argument
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
Write-TraceEvent -Phase "interactive_installer_start" -Detail @{
|
||||
installerPath = $InstallerPath
|
||||
installerArgs = $InstallerArguments
|
||||
operationName = $OperationName
|
||||
resultPath = $ResultPath
|
||||
tracePath = $TracePath
|
||||
timeoutSeconds = $TimeoutSeconds
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $InstallerPath)) {
|
||||
throw "Installer not found: $InstallerPath"
|
||||
}
|
||||
@@ -22,17 +126,47 @@ try {
|
||||
}
|
||||
Remove-Item -LiteralPath $ResultPath -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$installer = Start-Process -FilePath $InstallerPath -ArgumentList @("--headed", "--automation") -PassThru
|
||||
$env:COVENANT_SETUP_TRACE_DIR = $TracePath
|
||||
Write-TraceEvent -Phase "trace_environment_set" -Detail @{ name = "COVENANT_SETUP_TRACE_DIR"; value = $TracePath }
|
||||
|
||||
if (-not $installer.WaitForExit($TimeoutSeconds * 1000)) {
|
||||
Stop-Process -Id $installer.Id -Force -ErrorAction SilentlyContinue
|
||||
throw "Installer timed out after $TimeoutSeconds seconds."
|
||||
$installer = Start-Process -FilePath $InstallerPath -ArgumentList $InstallerArguments -PassThru
|
||||
Write-TraceEvent -Phase "installer_process_started" -Detail @{ pid = $installer.Id; operationName = $OperationName }
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$lastPoll = Get-Date "2000-01-01"
|
||||
while (-not $installer.HasExited) {
|
||||
if ((Get-Date) -ge $deadline) {
|
||||
Export-InstallerDiagnostics -Reason "installer_timeout" -InstallerProcess $installer
|
||||
Stop-Process -Id $installer.Id -Force -ErrorAction SilentlyContinue
|
||||
throw "Installer timed out after $TimeoutSeconds seconds."
|
||||
}
|
||||
|
||||
if (((Get-Date) - $lastPoll).TotalSeconds -ge 10) {
|
||||
$lastPoll = Get-Date
|
||||
$installer.Refresh()
|
||||
Write-TraceEvent -Phase "installer_still_running" -Detail @{
|
||||
pid = $installer.Id
|
||||
operationName = $OperationName
|
||||
elapsedSeconds = [int]((Get-Date) - $startedAt).TotalSeconds
|
||||
responding = $installer.Responding
|
||||
mainWindow = $installer.MainWindowTitle
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
$installer.Refresh()
|
||||
}
|
||||
|
||||
Write-TraceEvent -Phase "installer_process_exited" -Detail @{ pid = $installer.Id; exitCode = $installer.ExitCode; operationName = $OperationName }
|
||||
Export-InstallerDiagnostics -Reason "installer_exit" -InstallerProcess $installer
|
||||
|
||||
$result = [ordered]@{
|
||||
success = ($installer.ExitCode -eq 0)
|
||||
exitCode = [int]$installer.ExitCode
|
||||
installerPath = $InstallerPath
|
||||
installerArgs = $InstallerArguments
|
||||
operationName = $OperationName
|
||||
tracePath = $TracePath
|
||||
startedAt = $startedAt.ToString("o")
|
||||
finishedAt = (Get-Date).ToString("o")
|
||||
}
|
||||
@@ -43,9 +177,15 @@ try {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-TraceEvent -Phase "interactive_installer_error" -Detail @{
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
}
|
||||
Export-InstallerDiagnostics -Reason "interactive_installer_error" -InstallerProcess $installer
|
||||
$failure = [ordered]@{
|
||||
success = $false
|
||||
error = $_.Exception.Message
|
||||
tracePath = $TracePath
|
||||
startedAt = $startedAt.ToString("o")
|
||||
finishedAt = (Get-Date).ToString("o")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ param(
|
||||
[Parameter(Mandatory)][string]$ResultPath,
|
||||
[string]$ScriptRoot = $PSScriptRoot,
|
||||
[int]$TimeoutSeconds = 600,
|
||||
[string]$TaskNamePrefix = "CovenantSetupSelfInstall"
|
||||
[string]$TaskNamePrefix = "CovenantSetupSelfInstall",
|
||||
[string]$TracePath = $(Join-Path (Split-Path -Parent $ResultPath) "trace")
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -16,16 +17,310 @@ function Quote-TaskArgument {
|
||||
return '"' + $Value.Replace('"', '""') + '"'
|
||||
}
|
||||
|
||||
function Write-TraceEvent {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Phase,
|
||||
[object]$Detail = $null
|
||||
)
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
$event = [ordered]@{
|
||||
time = (Get-Date).ToUniversalTime().ToString("o")
|
||||
pid = $PID
|
||||
script = Split-Path -Leaf $PSCommandPath
|
||||
phase = $Phase
|
||||
detail = $Detail
|
||||
}
|
||||
$event | ConvertTo-Json -Depth 12 -Compress | Add-Content -LiteralPath (Join-Path $TracePath "guest-events.jsonl") -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to write trace event '$Phase': $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-DiagnosticFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
[Parameter(Mandatory)][scriptblock]$Capture
|
||||
)
|
||||
|
||||
$path = Join-Path $TracePath $Name
|
||||
Write-TraceEvent -Phase "diagnostic_file_start" -Detail @{ name = $Name }
|
||||
try {
|
||||
$value = & $Capture
|
||||
$value | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
Write-TraceEvent -Phase "diagnostic_file_finish" -Detail @{ name = $Name }
|
||||
}
|
||||
catch {
|
||||
[ordered]@{
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||
Write-TraceEvent -Phase "diagnostic_file_error" -Detail @{
|
||||
name = $Name
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-DateTimeForTrace {
|
||||
param([object]$Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($Value -is [DateTime] -and $Value -eq [DateTime]::MinValue) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
return ([DateTime]$Value).ToString("o")
|
||||
}
|
||||
catch {
|
||||
return [string]$Value
|
||||
}
|
||||
}
|
||||
|
||||
function Export-SmokeDiagnostics {
|
||||
param([Parameter(Mandatory)][string]$Reason)
|
||||
|
||||
Write-TraceEvent -Phase "diagnostics_start" -Detail @{ reason = $Reason }
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
|
||||
Write-DiagnosticFile -Name "guest-context.json" -Capture {
|
||||
[ordered]@{
|
||||
reason = $Reason
|
||||
computerName = $env:COMPUTERNAME
|
||||
userName = $env:USERNAME
|
||||
installerPath = $InstallerPath
|
||||
resultPath = $ResultPath
|
||||
tracePath = $TracePath
|
||||
taskName = $taskName
|
||||
installRoot = $installRoot
|
||||
}
|
||||
}
|
||||
Write-DiagnosticFile -Name "processes.json" -Capture {
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.Name -match 'covenant|Covenant|powershell|pwsh|dotnet' } |
|
||||
Select-Object ProcessId, ParentProcessId, Name, CommandLine, CreationDate
|
||||
}
|
||||
Write-DiagnosticFile -Name "scheduled-task.json" -Capture {
|
||||
[ordered]@{
|
||||
task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
info = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
Write-DiagnosticFile -Name "scheduled-task-events.json" -Capture {
|
||||
Get-WinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" -MaxEvents 300 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Message -like "*$taskName*" } |
|
||||
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
|
||||
}
|
||||
Write-DiagnosticFile -Name "application-events.json" -Capture {
|
||||
Get-WinEvent -FilterHashtable @{ LogName = "Application"; StartTime = (Get-Date).AddHours(-2) } -MaxEvents 200 -ErrorAction SilentlyContinue |
|
||||
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
|
||||
}
|
||||
Write-DiagnosticFile -Name "system-events.json" -Capture {
|
||||
Get-WinEvent -FilterHashtable @{ LogName = "System"; StartTime = (Get-Date).AddHours(-2) } -MaxEvents 200 -ErrorAction SilentlyContinue |
|
||||
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
|
||||
}
|
||||
Write-DiagnosticFile -Name "install-root-files.json" -Capture {
|
||||
if (Test-Path -LiteralPath $installRoot) {
|
||||
Get-ChildItem -LiteralPath $installRoot -Force -Recurse |
|
||||
Select-Object FullName, Length, LastWriteTimeUtc, Attributes
|
||||
}
|
||||
else {
|
||||
[ordered]@{ exists = $false; path = $installRoot }
|
||||
}
|
||||
}
|
||||
Write-DiagnosticFile -Name "registry-state.json" -Capture {
|
||||
if (Test-Path -LiteralPath $registryPath) {
|
||||
Get-ItemProperty -LiteralPath $registryPath
|
||||
}
|
||||
else {
|
||||
[ordered]@{ exists = $false; path = $registryPath }
|
||||
}
|
||||
}
|
||||
Write-DiagnosticFile -Name "result-file.json" -Capture {
|
||||
if (Test-Path -LiteralPath $ResultPath) {
|
||||
Get-Content -LiteralPath $ResultPath -Raw
|
||||
}
|
||||
else {
|
||||
[ordered]@{ exists = $false; path = $ResultPath }
|
||||
}
|
||||
}
|
||||
Write-TraceEvent -Phase "diagnostics_finish" -Detail @{ reason = $Reason }
|
||||
}
|
||||
|
||||
function Invoke-InteractiveOperation {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$TaskName,
|
||||
[Parameter(Mandatory)][string]$ExecutablePath,
|
||||
[Parameter(Mandatory)][string]$RunResultPath,
|
||||
[Parameter(Mandatory)][string]$OperationName,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
|
||||
$script:taskName = $TaskName
|
||||
Remove-Item -LiteralPath $RunResultPath -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# The task is started manually below. Keep the trigger far enough out that it
|
||||
# cannot fire a second copy while the smoke harness is collecting diagnostics.
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddDays(1)
|
||||
$actionArgumentItems = @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Quote-TaskArgument -Value $interactiveScript),
|
||||
"-InstallerPath", (Quote-TaskArgument -Value $ExecutablePath),
|
||||
"-ResultPath", (Quote-TaskArgument -Value $RunResultPath),
|
||||
"-TimeoutSeconds", $TimeoutSeconds,
|
||||
"-TracePath", (Quote-TaskArgument -Value $TracePath),
|
||||
"-OperationName", (Quote-TaskArgument -Value $OperationName)
|
||||
)
|
||||
if ($Arguments.Count -gt 0) {
|
||||
$argumentsJson = ConvertTo-Json -InputObject $Arguments -Compress
|
||||
$argumentsBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($argumentsJson))
|
||||
$actionArgumentItems += "-InstallerArgumentsBase64"
|
||||
$actionArgumentItems += $argumentsBase64
|
||||
}
|
||||
$actionArguments = $actionArgumentItems -join " "
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArguments
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
try {
|
||||
Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Force | Out-Null
|
||||
Write-TraceEvent -Phase "scheduled_task_registered" -Detail @{
|
||||
taskName = $TaskName
|
||||
operationName = $OperationName
|
||||
executablePath = $ExecutablePath
|
||||
executableArgs = $Arguments
|
||||
actionArguments = $actionArguments
|
||||
runResultPath = $RunResultPath
|
||||
}
|
||||
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Write-TraceEvent -Phase "scheduled_task_started" -Detail @{ taskName = $TaskName; operationName = $OperationName }
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds + 120)
|
||||
$lastPoll = Get-Date "2000-01-01"
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path -LiteralPath $RunResultPath) {
|
||||
Write-TraceEvent -Phase "result_file_observed" -Detail @{
|
||||
taskName = $TaskName
|
||||
operationName = $OperationName
|
||||
runResultPath = $RunResultPath
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (((Get-Date) - $lastPoll).TotalSeconds -ge 10) {
|
||||
$lastPoll = Get-Date
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Write-TraceEvent -Phase "waiting_for_interactive_task" -Detail @{
|
||||
taskName = $TaskName
|
||||
operationName = $OperationName
|
||||
state = $(if ($task) { $task.State.ToString() } else { $null })
|
||||
lastRunTime = $(if ($taskInfo) { Convert-DateTimeForTrace -Value $taskInfo.LastRunTime } else { $null })
|
||||
lastTaskResult = $(if ($taskInfo) { $taskInfo.LastTaskResult } else { $null })
|
||||
nextRunTime = $(if ($taskInfo) { Convert-DateTimeForTrace -Value $taskInfo.NextRunTime } else { $null })
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $RunResultPath)) {
|
||||
Export-SmokeDiagnostics -Reason "${OperationName}_task_timeout"
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
throw "Timed out waiting for $OperationName interactive task. LastTaskResult=$($taskInfo.LastTaskResult)"
|
||||
}
|
||||
|
||||
$runResult = Get-Content -LiteralPath $RunResultPath -Raw | ConvertFrom-Json
|
||||
Write-TraceEvent -Phase "interactive_task_result_read" -Detail @{
|
||||
taskName = $TaskName
|
||||
operationName = $OperationName
|
||||
result = $runResult
|
||||
}
|
||||
if (-not $runResult.success) {
|
||||
Export-SmokeDiagnostics -Reason "${OperationName}_task_failed"
|
||||
throw "Interactive $OperationName task failed: $($runResult.error)"
|
||||
}
|
||||
|
||||
return $runResult
|
||||
}
|
||||
finally {
|
||||
Write-TraceEvent -Phase "scheduled_task_unregister" -Detail @{ taskName = $TaskName; operationName = $OperationName }
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ForUninstallCleanup {
|
||||
param([int]$TimeoutSeconds = 60)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$lastPoll = Get-Date "2000-01-01"
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$remaining = [ordered]@{
|
||||
installRoot = Test-Path -LiteralPath $installRoot
|
||||
installedExe = Test-Path -LiteralPath $installedExe
|
||||
journalPath = Test-Path -LiteralPath $journalPath
|
||||
uninstallExe = Test-Path -LiteralPath $uninstallExe
|
||||
shortcutPath = Test-Path -LiteralPath $shortcutPath
|
||||
registryPath = Test-Path -LiteralPath $registryPath
|
||||
uninstallRegistryPath = Test-Path -LiteralPath $uninstallRegistryPath
|
||||
}
|
||||
|
||||
if (-not ($remaining.installRoot -or $remaining.installedExe -or $remaining.journalPath -or $remaining.uninstallExe -or $remaining.shortcutPath -or $remaining.registryPath -or $remaining.uninstallRegistryPath)) {
|
||||
Write-TraceEvent -Phase "uninstall_cleanup_observed" -Detail $remaining
|
||||
return
|
||||
}
|
||||
|
||||
if (((Get-Date) - $lastPoll).TotalSeconds -ge 5) {
|
||||
$lastPoll = Get-Date
|
||||
Write-TraceEvent -Phase "waiting_for_uninstall_cleanup" -Detail $remaining
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
throw "Uninstall cleanup did not complete within $TimeoutSeconds seconds."
|
||||
}
|
||||
|
||||
$installRoot = Join-Path $env:LOCALAPPDATA "CovenantSetupSelfTest"
|
||||
$shortcutPath = Join-Path ([Environment]::GetFolderPath("Desktop")) "Covenant Setup Self Test.lnk"
|
||||
$registryPath = "HKCU:\Software\CovenantSetupSelfTest"
|
||||
$journalPath = Join-Path $installRoot "journal.json"
|
||||
$installedExe = Join-Path $installRoot "bin\covenant-setup.exe"
|
||||
$uninstallExe = Join-Path $installRoot "covenant-setup-uninstall.exe"
|
||||
$taskName = "{0}-{1}" -f $TaskNamePrefix, ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
|
||||
$uninstallRegistryPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Covenant_Setup_Self_Test"
|
||||
$taskStamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
$installTaskName = "{0}-Install-{1}" -f $TaskNamePrefix, $taskStamp
|
||||
$uninstallTaskName = "{0}-Uninstall-{1}" -f $TaskNamePrefix, $taskStamp
|
||||
$taskName = $installTaskName
|
||||
$interactiveScript = Join-Path $ScriptRoot "Invoke-InteractiveInstaller.ps1"
|
||||
$installRunResultPath = $null
|
||||
$uninstallRunResultPath = $null
|
||||
|
||||
try {
|
||||
Remove-Item -LiteralPath $TracePath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $TracePath | Out-Null
|
||||
Write-TraceEvent -Phase "self_install_start" -Detail @{
|
||||
installerPath = $InstallerPath
|
||||
resultPath = $ResultPath
|
||||
tracePath = $TracePath
|
||||
timeoutSeconds = $TimeoutSeconds
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $InstallerPath)) {
|
||||
throw "Installer not found in guest: $InstallerPath"
|
||||
}
|
||||
@@ -39,51 +334,24 @@ try {
|
||||
New-Item -ItemType Directory -Force -Path $resultDir | Out-Null
|
||||
}
|
||||
Remove-Item -LiteralPath $ResultPath -Force -ErrorAction SilentlyContinue
|
||||
$installRunResultPath = Join-Path $resultDir "install-run-result.json"
|
||||
$uninstallRunResultPath = Join-Path $resultDir "uninstall-run-result.json"
|
||||
Remove-Item -LiteralPath $installRunResultPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $uninstallRunResultPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $installRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $shortcutPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $registryPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $uninstallRegistryPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-TraceEvent -Phase "self_install_cleaned_previous_state"
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1)
|
||||
$actionArguments = @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Quote-TaskArgument -Value $interactiveScript),
|
||||
"-InstallerPath", (Quote-TaskArgument -Value $InstallerPath),
|
||||
"-ResultPath", (Quote-TaskArgument -Value $ResultPath),
|
||||
"-TimeoutSeconds", $TimeoutSeconds
|
||||
) -join " "
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArguments
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Force | Out-Null
|
||||
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds + 120)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path -LiteralPath $ResultPath) {
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ResultPath)) {
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $taskName
|
||||
throw "Timed out waiting for interactive installer task. LastTaskResult=$($taskInfo.LastTaskResult)"
|
||||
}
|
||||
|
||||
$runResult = Get-Content -LiteralPath $ResultPath -Raw | ConvertFrom-Json
|
||||
if (-not $runResult.success) {
|
||||
throw "Interactive installer task failed: $($runResult.error)"
|
||||
}
|
||||
$installRunResult = Invoke-InteractiveOperation `
|
||||
-TaskName $installTaskName `
|
||||
-ExecutablePath $InstallerPath `
|
||||
-RunResultPath $installRunResultPath `
|
||||
-OperationName "install" `
|
||||
-Arguments @("--headed", "--automation")
|
||||
|
||||
Write-TraceEvent -Phase "verification_start"
|
||||
if (-not (Test-Path -LiteralPath $installedExe)) {
|
||||
throw "Installed executable missing: $installedExe"
|
||||
}
|
||||
@@ -114,34 +382,90 @@ try {
|
||||
throw "Journal did not record the packaged payload copy."
|
||||
}
|
||||
|
||||
$uninstallRunResult = Invoke-InteractiveOperation `
|
||||
-TaskName $uninstallTaskName `
|
||||
-ExecutablePath $uninstallExe `
|
||||
-RunResultPath $uninstallRunResultPath `
|
||||
-OperationName "uninstall" `
|
||||
-Arguments @("--headed", "--automation", "uninstall", $journalPath)
|
||||
|
||||
Write-TraceEvent -Phase "uninstall_verification_start"
|
||||
Wait-ForUninstallCleanup -TimeoutSeconds 60
|
||||
if (Test-Path -LiteralPath $installedExe) {
|
||||
throw "Installed executable still exists after uninstall: $installedExe"
|
||||
}
|
||||
if (Test-Path -LiteralPath $journalPath) {
|
||||
throw "Journal still exists after uninstall: $journalPath"
|
||||
}
|
||||
if (Test-Path -LiteralPath $uninstallExe) {
|
||||
throw "Installed uninstaller still exists after uninstall: $uninstallExe"
|
||||
}
|
||||
if (Test-Path -LiteralPath $shortcutPath) {
|
||||
throw "Desktop shortcut still exists after uninstall: $shortcutPath"
|
||||
}
|
||||
if (Test-Path -LiteralPath $registryPath) {
|
||||
throw "Registry key still exists after uninstall: $registryPath"
|
||||
}
|
||||
if (Test-Path -LiteralPath $uninstallRegistryPath) {
|
||||
throw "Installed Apps registry key still exists after uninstall: $uninstallRegistryPath"
|
||||
}
|
||||
if (Test-Path -LiteralPath $installRoot) {
|
||||
throw "Install root still exists after uninstall: $installRoot"
|
||||
}
|
||||
|
||||
$verification = [ordered]@{
|
||||
success = $true
|
||||
exitCode = [int]$runResult.exitCode
|
||||
installerPath = $InstallerPath
|
||||
installRoot = $installRoot
|
||||
installedExe = $installedExe
|
||||
journalPath = $journalPath
|
||||
uninstallExe = $uninstallExe
|
||||
shortcutPath = $shortcutPath
|
||||
registryPath = $registryPath
|
||||
success = $true
|
||||
exitCode = 0
|
||||
installExitCode = [int]$installRunResult.exitCode
|
||||
uninstallExitCode = [int]$uninstallRunResult.exitCode
|
||||
installerPath = $InstallerPath
|
||||
installRoot = $installRoot
|
||||
installedExe = $installedExe
|
||||
journalPath = $journalPath
|
||||
uninstallExe = $uninstallExe
|
||||
shortcutPath = $shortcutPath
|
||||
registryPath = $registryPath
|
||||
uninstallRegistryPath = $uninstallRegistryPath
|
||||
tracePath = $TracePath
|
||||
installRunResultPath = $installRunResultPath
|
||||
uninstallRunResultPath = $uninstallRunResultPath
|
||||
journalActionTypes = $journalActionTypes
|
||||
taskName = $taskName
|
||||
startedAt = $runResult.startedAt
|
||||
finishedAt = $runResult.finishedAt
|
||||
installTaskName = $installTaskName
|
||||
uninstallTaskName = $uninstallTaskName
|
||||
installStartedAt = $installRunResult.startedAt
|
||||
installFinishedAt = $installRunResult.finishedAt
|
||||
uninstallStartedAt = $uninstallRunResult.startedAt
|
||||
uninstallFinishedAt = $uninstallRunResult.finishedAt
|
||||
uninstallVerified = $true
|
||||
}
|
||||
$verification | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
||||
Write-TraceEvent -Phase "self_install_success" -Detail $verification
|
||||
}
|
||||
catch {
|
||||
Write-TraceEvent -Phase "self_install_error" -Detail @{
|
||||
error = $_.Exception.Message
|
||||
type = $_.Exception.GetType().FullName
|
||||
}
|
||||
Export-SmokeDiagnostics -Reason "self_install_error"
|
||||
$failure = [ordered]@{
|
||||
success = $false
|
||||
error = $_.Exception.Message
|
||||
taskName = $taskName
|
||||
installRoot = $installRoot
|
||||
resultPath = $ResultPath
|
||||
success = $false
|
||||
error = $_.Exception.Message
|
||||
taskName = $taskName
|
||||
installTaskName = $installTaskName
|
||||
uninstallTaskName = $uninstallTaskName
|
||||
installRoot = $installRoot
|
||||
resultPath = $ResultPath
|
||||
installRunResultPath = $installRunResultPath
|
||||
uninstallRunResultPath = $uninstallRunResultPath
|
||||
tracePath = $TracePath
|
||||
}
|
||||
$failure | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
foreach ($taskToRemove in @($installTaskName, $uninstallTaskName)) {
|
||||
if ($taskToRemove) {
|
||||
Unregister-ScheduledTask -TaskName $taskToRemove -Confirm:$false -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user