diff --git a/.gitignore b/.gitignore index e1ea826..ab1763f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # Added by cargo /dist* /target +/.vagrant/ +/vm/self-test/payload/ diff --git a/README.md b/README.md index 6908c6f..f9896e5 100644 --- a/README.md +++ b/README.md @@ -141,4 +141,37 @@ cargo fmt cargo check ``` -Interactive GUI/TUI flows have been exercised during development, but there is not yet a formal automated integration harness for packaged installer behavior. +Interactive GUI/TUI flows now have a Windows VM smoke harness for packaged installer behavior, while broader automated coverage is still limited. + +## Windows VM Smoke Test + +A Windows Hyper-V Vagrant VM now lives in [`Vagrantfile`](C:\Users\jasonross\workspace\covenant-setup\Vagrantfile), and the host harness in [`scripts/run-windows-vm-smoke.ps1`](C:\Users\jasonross\workspace\covenant-setup\scripts\run-windows-vm-smoke.ps1) packages `covenant-setup`, boots the VM, opens Hyper-V's console viewer, and runs the packaged installer inside the guest's interactive desktop session. + +The self-install manifest used for this path lives at [`vm/self-test/install.toml`](C:\Users\jasonross\workspace\covenant-setup\vm\self-test\install.toml). The guest verifies that install produced: + +- `%LOCALAPPDATA%\CovenantSetupSelfTest\bin\covenant-setup.exe` +- `%LOCALAPPDATA%\CovenantSetupSelfTest\journal.json` +- `%LOCALAPPDATA%\CovenantSetupSelfTest\covenant-setup-uninstall.exe` +- `HKCU\Software\CovenantSetupSelfTest\InstallRoot` +- `Desktop\Covenant Setup Self Test.lnk` + +Run the smoke test from the repo root: + +```powershell +$env:COVENANT_WINDOWS_BOX = "gusztavvargadr/windows-11" +$env:COVENANT_HYPERV_SWITCH = "Default Switch" +.\scripts\run-windows-vm-smoke.ps1 +``` + +Notes: + +- The Vagrant provider is `hyperv`, and the box you choose must support that provider. +- The harness opens `vmconnect.exe` after `vagrant up` so the guest desktop stays visible during the install. +- The default Vagrant synced folder is disabled to avoid SMB credential prompts; the harness uploads the installer and guest scripts over WinRM instead. +- The guest install is launched through an interactive scheduled task because WinRM sessions are not desktop-visible. +- The packaged installer now has a hidden automation mode that suppresses blocking GUI message boxes while leaving the progress window visible for the VM smoke test. +- The Windows box should auto-log the `vagrant` user into the desktop session for the visual install path to appear. +- Set `COVENANT_HYPERV_SWITCH` to the Hyper-V virtual switch name you want Vagrant to use. +- The harness writes its verification artifact to `dist\vagrant-self-test\guest-result.json`. +- Use `-SkipViewer` if you do not want the harness to open the Hyper-V console window. +- Use `-HaltAfter` or `-DestroyAfter` if you want the harness to stop the VM after the test run. diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..ff4729a --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,36 @@ +WINDOWS_BOX = ENV.fetch("COVENANT_WINDOWS_BOX", "gusztavvargadr/windows-11") +WINDOWS_BOX_VERSION = ENV["COVENANT_WINDOWS_BOX_VERSION"] +VM_NAME = ENV.fetch("COVENANT_VM_NAME", "covenant-setup-windows") +VM_MEMORY = ENV.fetch("COVENANT_VM_MEMORY", "6144") +VM_CPUS = ENV.fetch("COVENANT_VM_CPUS", "4") +WINRM_USERNAME = ENV.fetch("COVENANT_WINRM_USERNAME", "vagrant") +WINRM_PASSWORD = ENV.fetch("COVENANT_WINRM_PASSWORD", "vagrant") +HYPERV_SWITCH = ENV.fetch("COVENANT_HYPERV_SWITCH", "Default Switch") + +Vagrant.configure("2") do |config| + config.vm.box = WINDOWS_BOX + if WINDOWS_BOX_VERSION && !WINDOWS_BOX_VERSION.empty? + config.vm.box_version = WINDOWS_BOX_VERSION + end + + config.vm.hostname = VM_NAME + config.vm.guest = :windows + config.vm.communicator = "winrm" + config.vm.boot_timeout = ENV.fetch("COVENANT_BOOT_TIMEOUT", "1800").to_i + config.vm.graceful_halt_timeout = 180 + config.vm.box_check_update = false + config.vm.network "public_network", bridge: HYPERV_SWITCH + config.vm.synced_folder ".", "/vagrant", disabled: true + + config.winrm.username = WINRM_USERNAME + config.winrm.password = WINRM_PASSWORD + config.winrm.retry_limit = 60 + config.winrm.retry_delay = 10 + config.winrm.timeout = 1800 + + config.vm.provider "hyperv" do |h| + h.vmname = VM_NAME + h.memory = VM_MEMORY.to_i + h.cpus = VM_CPUS.to_i + end +end diff --git a/scripts/run-windows-vm-smoke.ps1 b/scripts/run-windows-vm-smoke.ps1 new file mode 100644 index 0000000..f0fcd1e --- /dev/null +++ b/scripts/run-windows-vm-smoke.ps1 @@ -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)" + } + } +} diff --git a/scripts/windows-vm/Approve-InstallerDialogs.ps1 b/scripts/windows-vm/Approve-InstallerDialogs.ps1 new file mode 100644 index 0000000..b7199ce --- /dev/null +++ b/scripts/windows-vm/Approve-InstallerDialogs.ps1 @@ -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 +} diff --git a/scripts/windows-vm/Invoke-InteractiveInstaller.ps1 b/scripts/windows-vm/Invoke-InteractiveInstaller.ps1 new file mode 100644 index 0000000..a111538 --- /dev/null +++ b/scripts/windows-vm/Invoke-InteractiveInstaller.ps1 @@ -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 +} diff --git a/scripts/windows-vm/Start-InteractiveSelfInstall.ps1 b/scripts/windows-vm/Start-InteractiveSelfInstall.ps1 new file mode 100644 index 0000000..9c4933e --- /dev/null +++ b/scripts/windows-vm/Start-InteractiveSelfInstall.ps1 @@ -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 +} diff --git a/src/main.rs b/src/main.rs index 53c4409..75c2dfd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,8 @@ struct Cli { headless: bool, #[arg(long, global = true, action = ArgAction::SetTrue, conflicts_with = "headless")] headed: bool, + #[arg(long, global = true, hide = true, action = ArgAction::SetTrue)] + automation: bool, #[arg(long, global = true, action = ArgAction::SetTrue)] elevate: bool, #[command(subcommand)] @@ -328,6 +330,7 @@ enum RuntimeMode { struct UiPreferences { headless: bool, headed: bool, + automation: bool, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -698,18 +701,21 @@ fn run_bundled_installer( match mode { RuntimeMode::Bundled => { let ui_mode = select_ui(UiPhase::Install, preferences, logger)?; - if ui_mode == UiMode::Gui && !win::gui_confirm_install(&metadata.app_name, logger)? { + if ui_mode == UiMode::Gui + && !preferences.automation + && !win::gui_confirm_install(&metadata.app_name, logger)? + { return Ok(()); } match install(&manifest_path, Some(journal_path), true, ui_mode, logger) { Ok(()) => { - if ui_mode == UiMode::Gui { + if ui_mode == UiMode::Gui && !preferences.automation { win::gui_report_success(&metadata.app_name, logger)?; } Ok(()) } Err(err) => { - if ui_mode == UiMode::Gui { + if ui_mode == UiMode::Gui && !preferences.automation { win::gui_report_error(&err.to_string(), logger)?; } Err(err) @@ -1335,6 +1341,7 @@ fn parse_ui_preferences(args: &[OsString]) -> UiPreferences { let mut preferences = UiPreferences { headless: false, headed: false, + automation: false, }; for arg in args.iter().skip(1) { let value = arg.to_string_lossy(); @@ -1342,6 +1349,8 @@ fn parse_ui_preferences(args: &[OsString]) -> UiPreferences { preferences.headless = true; } else if value == "--headed" { preferences.headed = true; + } else if value == "--automation" { + preferences.automation = true; } } preferences @@ -1351,6 +1360,7 @@ fn ui_preferences_from_cli(cli: &Cli) -> UiPreferences { UiPreferences { headless: cli.headless, headed: cli.headed, + automation: cli.automation, } } diff --git a/vm/self-test/install.toml b/vm/self-test/install.toml new file mode 100644 index 0000000..0a489ae --- /dev/null +++ b/vm/self-test/install.toml @@ -0,0 +1,25 @@ +app_name = "Covenant Setup Self Test" + +[[directories]] +path = "{LocalAppData}\\CovenantSetupSelfTest" + +[[directories]] +path = "{LocalAppData}\\CovenantSetupSelfTest\\bin" + +[[files]] +source = "payload\\covenant-setup.exe" +destination = "{LocalAppData}\\CovenantSetupSelfTest\\bin\\covenant-setup.exe" + +[[registry]] +key = "HKCU\\Software\\CovenantSetupSelfTest" +name = "InstallRoot" +value = "{LocalAppData}\\CovenantSetupSelfTest" + +[[shortcuts]] +path = "{Desktop}\\Covenant Setup Self Test.lnk" +target = "{LocalAppData}\\CovenantSetupSelfTest\\bin\\covenant-setup.exe" +description = "Launch the Covenant Setup self-installed test payload" + +[purge] +registry_branches = ["HKCU\\Software\\CovenantSetupSelfTest"] +paths = ["{LocalAppData}\\CovenantSetupSelfTest"]