wip
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Provider = "hyperv",
|
||||
[string]$ManifestPath = "vm\self-test\install.toml",
|
||||
[string]$OutputRoot = "dist\vagrant-self-test",
|
||||
[string]$VmName = $(if ($env:COVENANT_VM_NAME) { $env:COVENANT_VM_NAME } else { "covenant-setup-windows" }),
|
||||
[string]$GuestUsername = $(if ($env:COVENANT_WINRM_USERNAME) { $env:COVENANT_WINRM_USERNAME } else { "vagrant" }),
|
||||
[string]$GuestPassword = $(if ($env:COVENANT_WINRM_PASSWORD) { $env:COVENANT_WINRM_PASSWORD } else { "vagrant" }),
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipVmBoot,
|
||||
[switch]$SkipViewer,
|
||||
[switch]$HaltAfter,
|
||||
[switch]$DestroyAfter
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Assert-Command {
|
||||
param([Parameter(Mandatory)][string]$Name)
|
||||
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required command not found on PATH: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-RepoPath {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$RepoRoot,
|
||||
[Parameter(Mandatory)][string]$Path
|
||||
)
|
||||
|
||||
if ([System.IO.Path]::IsPathRooted($Path)) {
|
||||
return $Path
|
||||
}
|
||||
|
||||
return Join-Path $RepoRoot $Path
|
||||
}
|
||||
|
||||
function Convert-ToSingleQuotedPowerShellLiteral {
|
||||
param([Parameter(Mandatory)][string]$Value)
|
||||
|
||||
return "'" + $Value.Replace("'", "''") + "'"
|
||||
}
|
||||
|
||||
function Invoke-Tool {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FilePath,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
|
||||
Write-Host "==> $FilePath $($Arguments -join ' ')"
|
||||
Remove-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue
|
||||
& $FilePath @Arguments
|
||||
$exitCodeVar = Get-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue
|
||||
$exitCode = if ($exitCodeVar) { [int]$exitCodeVar.Value } elseif ($?) { 0 } else { 1 }
|
||||
if ($exitCode -ne 0) {
|
||||
throw "Command failed with exit code ${exitCode}: $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Vagrant {
|
||||
param([string[]]$Arguments)
|
||||
|
||||
Invoke-Tool -FilePath "vagrant" -Arguments $Arguments
|
||||
}
|
||||
|
||||
function Open-HyperVViewer {
|
||||
param([Parameter(Mandatory)][string]$VmName)
|
||||
|
||||
$vmConnect = Join-Path $env:SystemRoot "System32\vmconnect.exe"
|
||||
if (-not (Test-Path -LiteralPath $vmConnect)) {
|
||||
Write-Warning "Hyper-V viewer not found at $vmConnect"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "==> $vmConnect localhost $VmName"
|
||||
Start-Process -FilePath $vmConnect -ArgumentList @("localhost", $VmName) | Out-Null
|
||||
}
|
||||
|
||||
function Invoke-Process {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FilePath,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
|
||||
Write-Host "==> $FilePath $($Arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -Wait -PassThru
|
||||
if ($process.ExitCode -ne 0) {
|
||||
throw "Command failed with exit code $($process.ExitCode): $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$releaseExe = Join-Path $repoRoot "target\release\covenant-setup.exe"
|
||||
$payloadRoot = Join-Path $repoRoot "vm\self-test\payload"
|
||||
$stagedPayload = Join-Path $payloadRoot "covenant-setup.exe"
|
||||
$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"
|
||||
$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"
|
||||
$guestScriptRoot = Join-Path $guestRoot "scripts"
|
||||
$guestCommand = "& '$guestScriptRoot\Start-InteractiveSelfInstall.ps1' -InstallerPath '$guestInstallerPath' -ResultPath '$guestResultPath' -ScriptRoot '$guestScriptRoot'"
|
||||
|
||||
Assert-Command -Name "cargo"
|
||||
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
|
||||
|
||||
try {
|
||||
if (-not $SkipBuild) {
|
||||
Invoke-Tool -FilePath "cargo" -Arguments @("build", "--release")
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $releaseExe)) {
|
||||
throw "Release binary not found at $releaseExe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $manifestPathAbs)) {
|
||||
throw "Self-test manifest not found at $manifestPathAbs"
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $releaseExe -Destination $stagedPayload -Force
|
||||
Invoke-Process -FilePath $releaseExe -Arguments @("package", $manifestPathAbs, "--output", $outputRootAbs)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $installerPath)) {
|
||||
throw "Packaged installer not found at $installerPath"
|
||||
}
|
||||
|
||||
if (-not $SkipVmBoot) {
|
||||
Invoke-Vagrant -Arguments @("up", "--provider", $Provider)
|
||||
|
||||
$guestUsernameLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestUsername
|
||||
$guestPasswordLiteral = Convert-ToSingleQuotedPowerShellLiteral -Value $GuestPassword
|
||||
$enableAutoLogonCommand = @(
|
||||
'$winlogon = ''HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'''
|
||||
"New-ItemProperty -Path `$winlogon -Name 'AutoAdminLogon' -PropertyType String -Value '1' -Force | Out-Null"
|
||||
"New-ItemProperty -Path `$winlogon -Name 'ForceAutoLogon' -PropertyType String -Value '1' -Force | Out-Null"
|
||||
"New-ItemProperty -Path `$winlogon -Name 'DefaultUserName' -PropertyType String -Value $guestUsernameLiteral -Force | Out-Null"
|
||||
"New-ItemProperty -Path `$winlogon -Name 'DefaultPassword' -PropertyType String -Value $guestPasswordLiteral -Force | Out-Null"
|
||||
"New-ItemProperty -Path `$winlogon -Name 'DefaultDomainName' -PropertyType String -Value `$env:COMPUTERNAME -Force | Out-Null"
|
||||
) -join "; "
|
||||
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $enableAutoLogonCommand)
|
||||
Invoke-Vagrant -Arguments @("reload")
|
||||
}
|
||||
|
||||
if ($Provider -ieq "hyperv" -and -not $SkipViewer) {
|
||||
Open-HyperVViewer -VmName $VmName
|
||||
}
|
||||
|
||||
$waitForShellCommand = "for (`$i = 0; `$i -lt 90; `$i++) { if (Get-Process -Name explorer -ErrorAction SilentlyContinue) { exit 0 }; Start-Sleep -Seconds 2 }; Write-Error 'Explorer shell did not start in time.'; exit 1"
|
||||
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")
|
||||
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"))
|
||||
|
||||
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $guestCommand)
|
||||
|
||||
$resultJson = Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", "Get-Content -LiteralPath '$guestResultPath' -Raw")
|
||||
Set-Content -LiteralPath $resultPath -Value $resultJson -Encoding UTF8
|
||||
|
||||
if (-not (Test-Path -LiteralPath $resultPath)) {
|
||||
throw "Guest smoke-test result file was not written: $resultPath"
|
||||
}
|
||||
|
||||
$result = $resultJson | ConvertFrom-Json
|
||||
if (-not $result.success) {
|
||||
throw "Guest reported a failed smoke test: $($result.error)"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Smoke test passed."
|
||||
Write-Host "Installer: $installerPath"
|
||||
Write-Host "Result JSON: $resultPath"
|
||||
Write-Host "InstallRoot: $($result.installRoot)"
|
||||
}
|
||||
finally {
|
||||
if ($DestroyAfter) {
|
||||
try {
|
||||
Invoke-Vagrant -Arguments @("destroy", "-f")
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to destroy VM after smoke test: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
elseif ($HaltAfter) {
|
||||
try {
|
||||
Invoke-Vagrant -Arguments @("halt")
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to halt VM after smoke test: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[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
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$InstallerPath,
|
||||
[Parameter(Mandatory)][string]$ResultPath,
|
||||
[int]$TimeoutSeconds = 600
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$installer = $null
|
||||
$startedAt = Get-Date
|
||||
|
||||
try {
|
||||
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
|
||||
|
||||
$installer = Start-Process -FilePath $InstallerPath -ArgumentList @("--headed", "--automation") -PassThru
|
||||
|
||||
if (-not $installer.WaitForExit($TimeoutSeconds * 1000)) {
|
||||
Stop-Process -Id $installer.Id -Force -ErrorAction SilentlyContinue
|
||||
throw "Installer timed out after $TimeoutSeconds seconds."
|
||||
}
|
||||
|
||||
$result = [ordered]@{
|
||||
success = ($installer.ExitCode -eq 0)
|
||||
exitCode = [int]$installer.ExitCode
|
||||
installerPath = $InstallerPath
|
||||
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 {
|
||||
$failure = [ordered]@{
|
||||
success = $false
|
||||
error = $_.Exception.Message
|
||||
startedAt = $startedAt.ToString("o")
|
||||
finishedAt = (Get-Date).ToString("o")
|
||||
}
|
||||
$failure | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$InstallerPath,
|
||||
[Parameter(Mandatory)][string]$ResultPath,
|
||||
[string]$ScriptRoot = $PSScriptRoot,
|
||||
[int]$TimeoutSeconds = 600,
|
||||
[string]$TaskNamePrefix = "CovenantSetupSelfInstall"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Quote-TaskArgument {
|
||||
param([Parameter(Mandatory)][string]$Value)
|
||||
|
||||
return '"' + $Value.Replace('"', '""') + '"'
|
||||
}
|
||||
|
||||
$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())
|
||||
$interactiveScript = Join-Path $ScriptRoot "Invoke-InteractiveInstaller.ps1"
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $InstallerPath)) {
|
||||
throw "Installer not found in guest: $InstallerPath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $interactiveScript)) {
|
||||
throw "Interactive installer script not found in guest: $interactiveScript"
|
||||
}
|
||||
|
||||
$resultDir = Split-Path -Parent $ResultPath
|
||||
if ($resultDir) {
|
||||
New-Item -ItemType Directory -Force -Path $resultDir | Out-Null
|
||||
}
|
||||
Remove-Item -LiteralPath $ResultPath -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
|
||||
|
||||
$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)"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $installedExe)) {
|
||||
throw "Installed executable missing: $installedExe"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $journalPath)) {
|
||||
throw "Journal missing: $journalPath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $uninstallExe)) {
|
||||
throw "Installed uninstaller missing: $uninstallExe"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $shortcutPath)) {
|
||||
throw "Desktop shortcut missing: $shortcutPath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $registryPath)) {
|
||||
throw "Registry key missing: $registryPath"
|
||||
}
|
||||
|
||||
$installRootValue = Get-ItemPropertyValue -Path $registryPath -Name "InstallRoot"
|
||||
if ($installRootValue -ne $installRoot) {
|
||||
throw "Unexpected registry InstallRoot value: $installRootValue"
|
||||
}
|
||||
|
||||
$journal = Get-Content -LiteralPath $journalPath -Raw | ConvertFrom-Json
|
||||
$journalActionTypes = @($journal.actions | ForEach-Object { $_.type })
|
||||
if ($journal.app_name -ne "Covenant Setup Self Test") {
|
||||
throw "Unexpected journal app name: $($journal.app_name)"
|
||||
}
|
||||
if ($journalActionTypes -notcontains "copy_file") {
|
||||
throw "Journal did not record the packaged payload copy."
|
||||
}
|
||||
|
||||
$verification = [ordered]@{
|
||||
success = $true
|
||||
exitCode = [int]$runResult.exitCode
|
||||
installerPath = $InstallerPath
|
||||
installRoot = $installRoot
|
||||
installedExe = $installedExe
|
||||
journalPath = $journalPath
|
||||
uninstallExe = $uninstallExe
|
||||
shortcutPath = $shortcutPath
|
||||
registryPath = $registryPath
|
||||
journalActionTypes = $journalActionTypes
|
||||
taskName = $taskName
|
||||
startedAt = $runResult.startedAt
|
||||
finishedAt = $runResult.finishedAt
|
||||
}
|
||||
$verification | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
$failure = [ordered]@{
|
||||
success = $false
|
||||
error = $_.Exception.Message
|
||||
taskName = $taskName
|
||||
installRoot = $installRoot
|
||||
resultPath = $ResultPath
|
||||
}
|
||||
$failure | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
}
|
||||
Reference in New Issue
Block a user