[CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateSet('Prepare', 'Status', 'Run', 'Collect', 'Stop', 'Reset')] [string] $Action, [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 } 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 = 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) { $output = & $VBoxManage @Arguments 2>&1 if ($LASTEXITCODE -ne 0) { throw "VBoxManage failed ($LASTEXITCODE): $($output -join ' ')" } return $output } function Require-RunId { if ([string]::IsNullOrWhiteSpace($RunId)) { throw "$Action requires -RunId" } } function Acquire-Lease { New-Item -ItemType Directory -Force -Path $LeaseRoot | Out-Null $path = Join-Path $LeaseRoot 'lease.lock' try { $script:Lease = [System.IO.File]::Open($path, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) $bytes = [Text.Encoding]::UTF8.GetBytes("$VmName`n$RunId`n$([DateTime]::UtcNow.ToString('o'))`n") $Lease.SetLength(0); $Lease.Write($bytes, 0, $bytes.Length); $Lease.Flush($true) } catch { throw "Windows test VM is already leased: $path" } } function Release-Lease { if ($script:Lease) { $script:Lease.Dispose(); $script:Lease = $null } } function Guest-Args([string[]] $Arguments) { 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.' } 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 { if ($Action -eq 'Status') { Invoke-VBox @('showvminfo', $VmName, '--machinereadable') | Where-Object { $_ -match '^(VMState|name|SnapshotName)=' } exit 0 } Require-RunId Acquire-Lease try { $state = (Invoke-VBox @('showvminfo', $VmName, '--machinereadable') | Where-Object { $_ -like 'VMState=*' }) -replace '^VMState="?([^"\r\n]+)"?$', '$1' switch ($Action) { 'Prepare' { if ($state -eq 'running') { Wait-GuestReady } else { Invoke-VBox @('startvm', $VmName, '--type', 'headless') | Out-Null Wait-GuestReady } 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', '--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' { $destination = Join-Path $LeaseRoot $RunId New-Item -ItemType Directory -Force -Path $destination | Out-Null $guestArtifacts = if ($env:RVBOX_WINDOWS_GUEST_ARTIFACTS) { $env:RVBOX_WINDOWS_GUEST_ARTIFACTS } else { 'C:\ProgramData\RVBox\test-artifacts' } Invoke-VBox (Guest-Args @('copyfrom', $guestArtifacts, $destination, '--recursive')) | Out-Null Write-Output "collected native artifacts under $destination" } 'Stop' { 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; Wait-PowerOff } Invoke-VBox @('snapshot', $VmName, 'restore', $Snapshot) | Out-Null Write-Output "restored baseline snapshot=$Snapshot VM=$VmName run=$RunId" } } } finally { Release-Lease } } catch { Release-Lease Write-Error $_ exit 1 }