feat: complete Windows client control and recovery paths

This commit is contained in:
2026-09-06 13:37:32 +00:00
parent 56b15c7f4f
commit 486894557d
38 changed files with 2188 additions and 106 deletions
+41 -14
View File
@@ -6,14 +6,18 @@ param(
[Parameter(Mandatory = $false)]
[ValidatePattern('^[a-z0-9][a-z0-9-]{0,63}$')]
[string] $RunId = '',
[string] $VmName = $(if ($env:RVBOX_WINDOWS_VM) { $env:RVBOX_WINDOWS_VM } else { 'rvbox-win10-test' }),
[string] $Snapshot = $(if ($env:RVBOX_WINDOWS_BASELINE_SNAPSHOT) { $env:RVBOX_WINDOWS_BASELINE_SNAPSHOT } else { 'baseline-disk-first' })
[string] $VmName = $(if ($env:RVBOX_WINDOWS_VM) { $env:RVBOX_WINDOWS_VM } elseif ($env:RVBOX_TEST_VBOX_VM) { $env:RVBOX_TEST_VBOX_VM } else { 'rvbox-win10-test' }),
[string] $Snapshot = $(if ($env:RVBOX_WINDOWS_BASELINE_SNAPSHOT) { $env:RVBOX_WINDOWS_BASELINE_SNAPSHOT } elseif ($env:RVBOX_TEST_VBOX_SNAPSHOT) { $env:RVBOX_TEST_VBOX_SNAPSHOT } else { 'baseline-disk-first' })
)
$ErrorActionPreference = 'Stop'
$VBoxManage = if ($env:VBOXMANAGE) { $env:VBOXMANAGE } else { 'VBoxManage' }
$GuestUser = $env:RVBOX_WINDOWS_GUEST_USER
$GuestPassword = $env:RVBOX_WINDOWS_GUEST_PASSWORD
$GuestUser = if ($env:RVBOX_WINDOWS_GUEST_USER) { $env:RVBOX_WINDOWS_GUEST_USER } else { $env:RVBOX_TEST_GUEST_USER }
$GuestPasswordFile = if ($env:RVBOX_WINDOWS_GUEST_PASSWORD_FILE) {
$env:RVBOX_WINDOWS_GUEST_PASSWORD_FILE
} else {
$env:RVBOX_TEST_GUEST_PASSWORD_FILE
}
$LeaseRoot = if ($env:RVBOX_WINDOWS_LEASE_DIR) { $env:RVBOX_WINDOWS_LEASE_DIR } else { Join-Path $PSScriptRoot '..\..\.test-runs\windows' }
function Invoke-VBox([string[]] $Arguments) {
@@ -45,10 +49,33 @@ function Release-Lease {
}
function Guest-Args([string[]] $Arguments) {
if ([string]::IsNullOrWhiteSpace($GuestUser) -or [string]::IsNullOrWhiteSpace($GuestPassword)) {
throw 'Set RVBOX_WINDOWS_GUEST_USER and RVBOX_WINDOWS_GUEST_PASSWORD in the host environment; secrets are never read from repository files or printed.'
if ([string]::IsNullOrWhiteSpace($GuestUser) -or [string]::IsNullOrWhiteSpace($GuestPasswordFile)) {
throw 'Set the guest user and password-file environment variables in the host environment; secrets are never read from repository files or printed.'
}
return @('guestcontrol', $VmName, '--username', $GuestUser, '--password', $GuestPassword) + $Arguments
if (-not (Test-Path -LiteralPath $GuestPasswordFile -PathType Leaf)) {
throw "Guest password file does not exist: $GuestPasswordFile"
}
return @('guestcontrol', $VmName, '--username', $GuestUser, '--passwordfile', $GuestPasswordFile) + $Arguments
}
function Wait-PowerOff {
for ($attempt = 0; $attempt -lt 60; $attempt++) {
$current = (Invoke-VBox @('showvminfo', $VmName, '--machinereadable') | Where-Object { $_ -like 'VMState=*' }) -replace '^VMState="?([^"\r\n]+)"?$', '$1'
if ($current -eq 'poweroff') { return }
Start-Sleep -Seconds 1
}
throw "VM did not power off within the bounded stop deadline: $VmName"
}
function Wait-GuestReady {
for ($attempt = 0; $attempt -lt 60; $attempt++) {
$properties = Invoke-VBox @('guestproperty', 'enumerate', $VmName)
if (($properties -match '/GuestAdd/Version') -and ($properties -match '/GuestInfo/OS/Release')) {
return
}
Start-Sleep -Seconds 1
}
throw "Guest Additions did not become ready within the bounded boot deadline: $VmName"
}
try {
@@ -62,18 +89,18 @@ try {
$state = (Invoke-VBox @('showvminfo', $VmName, '--machinereadable') | Where-Object { $_ -like 'VMState=*' }) -replace '^VMState="?([^"\r\n]+)"?$', '$1'
switch ($Action) {
'Prepare' {
if ($state -eq 'running') { Invoke-VBox (Guest-Args @('run', 'C:\Windows\System32\whoami.exe', '--', '/groups')) | Out-Null }
if ($state -eq 'running') { Wait-GuestReady }
else {
Invoke-VBox @('startvm', $VmName, '--type', 'headless') | Out-Null
Start-Sleep -Seconds 5
Invoke-VBox (Guest-Args @('run', 'C:\Windows\System32\whoami.exe', '--', '/groups')) | Out-Null
Wait-GuestReady
}
Invoke-VBox (Guest-Args @('run', 'C:\Windows\System32\query.exe', '--', 'user')) | Out-Null
Invoke-VBox (Guest-Args @('run', '--exe', 'C:\Windows\System32\whoami.exe', '--wait-stdout', '--wait-stderr', '--unquoted-args', '--', '/groups')) | Out-Null
Invoke-VBox (Guest-Args @('run', '--exe', 'C:\Windows\System32\query.exe', '--wait-stdout', '--wait-stderr', '--unquoted-args', '--', 'user')) | Out-Null
Write-Output "prepared VM=$VmName run=$RunId"
}
'Run' {
$guestBinary = if ($env:RVBOX_WINDOWS_GUEST_BINARY) { $env:RVBOX_WINDOWS_GUEST_BINARY } else { 'C:\ProgramData\RVBox\test\rvbox.exe' }
Invoke-VBox (Guest-Args @('run', $guestBinary, '--', '--check-config', '--config', 'C:\ProgramData\RVBox\client.toml')) | Out-Null
Invoke-VBox (Guest-Args @('run', '--exe', $guestBinary, '--wait-stdout', '--wait-stderr', '--unquoted-args', '--', '--check-config', '--config', 'C:\ProgramData\RVBox\client.toml')) | Out-Null
Write-Output "ran native Windows smoke run=$RunId"
}
'Collect' {
@@ -84,11 +111,11 @@ try {
Write-Output "collected native artifacts under $destination"
}
'Stop' {
if ($state -eq 'running') { Invoke-VBox @('controlvm', $VmName, 'acpipowerbutton') | Out-Null }
if ($state -eq 'running') { Invoke-VBox @('controlvm', $VmName, 'acpipowerbutton') | Out-Null; Wait-PowerOff }
Write-Output "requested graceful stop VM=$VmName"
}
'Reset' {
if ($state -eq 'running') { Invoke-VBox @('controlvm', $VmName, 'acpipowerbutton') | Out-Null; Start-Sleep -Seconds 3 }
if ($state -eq 'running') { Invoke-VBox @('controlvm', $VmName, 'acpipowerbutton') | Out-Null; Wait-PowerOff }
Invoke-VBox @('snapshot', $VmName, 'restore', $Snapshot) | Out-Null
Write-Output "restored baseline snapshot=$Snapshot VM=$VmName run=$RunId"
}