195 lines
7.6 KiB
PowerShell
195 lines
7.6 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)][string]$InstallerPath,
|
|
[Parameter(Mandatory)][string]$ResultPath,
|
|
[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"
|
|
}
|
|
|
|
$resultDir = Split-Path -Parent $ResultPath
|
|
if ($resultDir) {
|
|
New-Item -ItemType Directory -Force -Path $resultDir | Out-Null
|
|
}
|
|
Remove-Item -LiteralPath $ResultPath -Force -ErrorAction SilentlyContinue
|
|
|
|
$env:COVENANT_SETUP_TRACE_DIR = $TracePath
|
|
Write-TraceEvent -Phase "trace_environment_set" -Detail @{ name = "COVENANT_SETUP_TRACE_DIR"; value = $TracePath }
|
|
|
|
$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")
|
|
}
|
|
$result | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
|
|
|
if ($installer.ExitCode -ne 0) {
|
|
exit $installer.ExitCode
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
$failure | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
|
exit 1
|
|
}
|