Feat MVP #1
@@ -0,0 +1,438 @@
|
||||
# Vagrant Smoke Test Debugging Notes
|
||||
|
||||
This document records the work done while replacing the PowerShell UI with a C# presentation layer, adding guest-side diagnostics, and stabilizing the Windows Vagrant smoke test. It is intended as a reference for future installer hangs where the VM console is not visible.
|
||||
|
||||
## Scope
|
||||
|
||||
The work covered these areas:
|
||||
|
||||
- Removed the PowerShell-hosted UI path.
|
||||
- Added a C# WinForms UI process.
|
||||
- Connected Rust business logic to the C# UI over Windows named pipes.
|
||||
- Added guest trace collection so hangs can be diagnosed without watching the VM console.
|
||||
- Rebuilt and tested the packaged installer in the Hyper-V Vagrant guest.
|
||||
- Extended the smoke test to install, verify installed state, uninstall, and verify removed state.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
The installer is still driven by Rust. The C# process is presentation only.
|
||||
|
||||
- Rust business logic lives primarily in `src/main.rs`.
|
||||
- Rust C# UI IPC lives in `src/ui.rs`.
|
||||
- C# WinForms UI lives in `ui/Covenant.Setup.Ui/Program.cs`.
|
||||
- `build.rs` publishes the C# UI as a self-contained `win-x64` single-file executable.
|
||||
- The Rust binary embeds the published C# UI executable with `include_bytes!`.
|
||||
- At runtime, Rust extracts the C# UI executable to `%TEMP%\covenant-setup-ui`, starts it, and connects to a named pipe.
|
||||
- The C# UI owns the pipe server and reads newline-delimited JSON messages.
|
||||
- Rust sends messages such as `init`, `progress`, `log`, `finish`, `prompt`, and `close`.
|
||||
- The C# UI writes prompt responses back as JSON.
|
||||
|
||||
## Trace Outputs
|
||||
|
||||
The guest trace directory is:
|
||||
|
||||
```text
|
||||
C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke\trace
|
||||
```
|
||||
|
||||
The host harness pulls this into:
|
||||
|
||||
```text
|
||||
dist\vagrant-self-test\trace
|
||||
```
|
||||
|
||||
Important trace files:
|
||||
|
||||
- `guest-events.jsonl`: host/guest harness events, scheduled task status, verification phases.
|
||||
- `installer-heartbeat-<pid>.jsonl`: Rust process heartbeat and installer phases.
|
||||
- `csharp-ui-pipe-<pid>.jsonl`: C# UI process and pipe receive/send events.
|
||||
- `interactive-context.json`: context captured by the interactive wrapper.
|
||||
- `interactive-processes.json`: relevant guest processes during wrapper diagnostics.
|
||||
- `interactive-windows.json`: visible windows and process window titles.
|
||||
- `interactive-application-events.json`: recent Application event log entries.
|
||||
- `abort-*.json`: snapshots created by the abort collector.
|
||||
|
||||
The host harness always tries to pull the trace bundle in `finally`, even when the smoke test fails.
|
||||
|
||||
## Errors Encountered
|
||||
|
||||
### 1. Host Sandbox and Vagrant Permissions
|
||||
|
||||
Running Vagrant and Hyper-V actions from the coding sandbox required escalation. This affected commands such as:
|
||||
|
||||
```powershell
|
||||
.\scripts\run-windows-vm-smoke.ps1 -SkipViewer -HaltAfter
|
||||
vagrant status
|
||||
vagrant winrm ...
|
||||
vagrant upload ...
|
||||
```
|
||||
|
||||
This was expected: Vagrant controls an external VM, uses WinRM, and interacts with Hyper-V.
|
||||
|
||||
### 2. Pre-main Guest Failure: Missing VCRUNTIME140.dll
|
||||
|
||||
The first meaningful guest diagnostics showed a Windows system error dialog:
|
||||
|
||||
```text
|
||||
covenant-setup-installer.exe - System Error
|
||||
The code execution cannot proceed because VCRUNTIME140.dll was not found.
|
||||
```
|
||||
|
||||
Evidence:
|
||||
|
||||
- `interactive-windows.json` showed a `covenant-setup-installer.exe - System Error` window.
|
||||
- `system-events.json` had an `Application Popup` event for the missing DLL.
|
||||
- There were no `installer-heartbeat-*.jsonl` files.
|
||||
- There were no `csharp-ui-pipe-*.jsonl` files.
|
||||
|
||||
Conclusion:
|
||||
|
||||
The executable failed before Rust `main()` ran. The heartbeat and pipe logs were absent because neither Rust nor the C# UI started.
|
||||
|
||||
Fix:
|
||||
|
||||
Added `.cargo/config.toml`:
|
||||
|
||||
```toml
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
```
|
||||
|
||||
This statically links the MSVC C runtime into the Rust executable, removing the guest dependency on `VCRUNTIME140.dll`.
|
||||
|
||||
### 3. Packaging Looked Successful but Produced an Unbundled EXE
|
||||
|
||||
Manual PowerShell invocations of the Windows-subsystem Rust executable were misleading. A direct command such as:
|
||||
|
||||
```powershell
|
||||
target\release\covenant-setup.exe --json package vm\self-test\install.toml --output dist\vagrant-self-test
|
||||
```
|
||||
|
||||
could return quickly with `EXIT=0` while the output file still matched the base executable size.
|
||||
|
||||
Reason:
|
||||
|
||||
The Rust binary is built as a Windows GUI subsystem executable. Direct invocation from PowerShell does not behave like a normal console command in all cases.
|
||||
|
||||
Fixes:
|
||||
|
||||
- The smoke harness invokes the packager with `Start-Process -Wait -PassThru`.
|
||||
- The harness now validates that the packaged installer ends with the bundle magic marker `COVENANT_SETUP_BUNDLE_V1`.
|
||||
- The embedded bundle format was changed from JSON byte arrays to an appended raw bundle with a JSON index plus raw file data. This avoids large JSON expansion of payload bytes.
|
||||
|
||||
### 4. Journal Written Beside the Smoke Installer
|
||||
|
||||
After the VCRUNTIME fix, the installer succeeded but the smoke verifier failed with:
|
||||
|
||||
```text
|
||||
Journal missing: C:\Users\vagrant\AppData\Local\CovenantSetupSelfTest\journal.json
|
||||
```
|
||||
|
||||
Evidence:
|
||||
|
||||
The Rust heartbeat showed:
|
||||
|
||||
```json
|
||||
{"phase":"install_journal_written","detail":{"journal":"C:\\Users\\vagrant\\AppData\\Local\\Temp\\covenant-setup-smoke\\journal.json"}}
|
||||
```
|
||||
|
||||
Cause:
|
||||
|
||||
The packaged install path was passing an explicit journal path next to the packaged installer. The expected product behavior is to infer the install root and write `journal.json` there.
|
||||
|
||||
Fix:
|
||||
|
||||
`run_bundled_installer` now calls:
|
||||
|
||||
```rust
|
||||
install(&manifest_path, None, true, ui_mode, logger)
|
||||
```
|
||||
|
||||
This lets `build_install_runtime` infer the journal path from the install root.
|
||||
|
||||
### 5. Scheduled Task Re-ran During Diagnostics
|
||||
|
||||
The guest harness originally registered a scheduled task with a trigger one minute in the future and also started it manually.
|
||||
|
||||
Failure mode:
|
||||
|
||||
- The manual task run completed.
|
||||
- If verification or diagnostics took long enough, the scheduled trigger fired and launched a second copy.
|
||||
|
||||
Fix:
|
||||
|
||||
The trigger is now set far in the future:
|
||||
|
||||
```powershell
|
||||
New-ScheduledTaskTrigger -Once -At (Get-Date).AddDays(1)
|
||||
```
|
||||
|
||||
The harness still starts the task manually with `Start-ScheduledTask`.
|
||||
|
||||
### 6. WinRM Error 1726 During Success Diagnostics
|
||||
|
||||
After the installer succeeded, the host sometimes saw:
|
||||
|
||||
```text
|
||||
WSMAN ERROR CODE: 1726
|
||||
The WSMan provider host process did not return a proper response.
|
||||
```
|
||||
|
||||
The trace showed the installer succeeded and verification reached the success diagnostics phase, but the WinRM command failed while returning.
|
||||
|
||||
Fix:
|
||||
|
||||
- The success path now writes `guest-result.json` immediately after verification.
|
||||
- Heavy diagnostics are retained for failure paths.
|
||||
- Diagnostic file writes now emit `diagnostic_file_start`, `diagnostic_file_finish`, and `diagnostic_file_error` markers so future diagnostic hangs show the exact capture that blocked.
|
||||
|
||||
### 7. Install-plus-uninstall Harness Hung
|
||||
|
||||
When uninstall testing was added, the install scheduled task exited with `LastTaskResult=1`. The parent loop waited until timeout because no result file was written.
|
||||
|
||||
Evidence:
|
||||
|
||||
- `guest-events.jsonl` showed the install scheduled task was registered and started.
|
||||
- The task quickly moved to `Ready` with `LastTaskResult=1`.
|
||||
- There was no `interactive_installer_start`.
|
||||
- There was no Rust heartbeat.
|
||||
- There was no C# pipe log.
|
||||
|
||||
This meant the PowerShell wrapper failed before starting the installer.
|
||||
|
||||
Reproduction:
|
||||
|
||||
A harmless wrapper test failed:
|
||||
|
||||
```powershell
|
||||
Invoke-InteractiveInstaller.ps1 `
|
||||
-InstallerPath C:\Windows\System32\cmd.exe `
|
||||
-InstallerArguments "/c" "exit 0"
|
||||
```
|
||||
|
||||
Error:
|
||||
|
||||
```text
|
||||
A positional parameter cannot be found that accepts argument 'exit 0'.
|
||||
```
|
||||
|
||||
Cause:
|
||||
|
||||
Under `powershell.exe -File`, passing multiple values to a script `[string[]]` parameter was not binding as intended.
|
||||
|
||||
Fix:
|
||||
|
||||
The task wrapper now passes child process arguments as base64-encoded JSON:
|
||||
|
||||
```powershell
|
||||
$argumentsJson = ConvertTo-Json -InputObject $Arguments -Compress
|
||||
$argumentsBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($argumentsJson))
|
||||
```
|
||||
|
||||
The interactive wrapper decodes that back to a real argument array:
|
||||
|
||||
```powershell
|
||||
$argumentsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($InstallerArgumentsBase64))
|
||||
$decodedArguments = ConvertFrom-Json -InputObject $argumentsJson
|
||||
$InstallerArguments = @()
|
||||
foreach ($argument in $decodedArguments) {
|
||||
$InstallerArguments += [string]$argument
|
||||
}
|
||||
```
|
||||
|
||||
The harmless wrapper test then produced:
|
||||
|
||||
```json
|
||||
"installerArgs": ["/c", "exit 0"],
|
||||
"exitCode": 0
|
||||
```
|
||||
|
||||
## Uninstall Test Flow
|
||||
|
||||
The smoke harness now does this inside the guest:
|
||||
|
||||
1. Schedules an interactive install task.
|
||||
2. Runs the packaged installer with:
|
||||
|
||||
```text
|
||||
--headed --automation
|
||||
```
|
||||
|
||||
3. Verifies installed state:
|
||||
|
||||
- `%LOCALAPPDATA%\CovenantSetupSelfTest\bin\covenant-setup.exe`
|
||||
- `%LOCALAPPDATA%\CovenantSetupSelfTest\journal.json`
|
||||
- `%LOCALAPPDATA%\CovenantSetupSelfTest\covenant-setup-uninstall.exe`
|
||||
- `HKCU:\Software\CovenantSetupSelfTest`
|
||||
- `Desktop\Covenant Setup Self Test.lnk`
|
||||
|
||||
4. Schedules an interactive uninstall task.
|
||||
5. Runs the installed uninstaller with:
|
||||
|
||||
```text
|
||||
--headed --automation uninstall <journal path>
|
||||
```
|
||||
|
||||
6. Waits for cleanup helper completion.
|
||||
7. Verifies removed state:
|
||||
|
||||
- install root removed
|
||||
- payload removed
|
||||
- journal removed
|
||||
- installed uninstaller removed
|
||||
- desktop shortcut removed
|
||||
- application registry key removed
|
||||
- Installed Apps uninstall registration removed
|
||||
|
||||
## Automation Changes for Uninstall
|
||||
|
||||
The uninstall path can spawn a cleanup helper to delete the running uninstaller executable after the main uninstall process exits. The cleanup helper previously could still show a GUI success or reboot prompt.
|
||||
|
||||
Fix:
|
||||
|
||||
- `uninstall` now receives the automation flag.
|
||||
- `cleanup` now receives the automation flag.
|
||||
- `spawn_cleanup_helper` propagates `--automation`.
|
||||
- When the parent UI mode is GUI, `spawn_cleanup_helper` also passes `--headed`.
|
||||
- GUI success/reboot prompts are skipped in automation mode.
|
||||
|
||||
This keeps the C# progress UI visible while preventing blocking prompts during automated tests.
|
||||
|
||||
## Abort Collector
|
||||
|
||||
Added:
|
||||
|
||||
```text
|
||||
scripts/windows-vm/Abort-SmokeDiagnostics.ps1
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Write an explicit `abort_requested` event.
|
||||
- Capture processes, visible windows, scheduled tasks, task info, and recent event logs.
|
||||
- Stop installer, uninstaller, C# UI, and smoke scheduled tasks.
|
||||
- Unregister smoke scheduled tasks.
|
||||
- Zip and return the trace bundle as base64.
|
||||
|
||||
This is useful when the host-side test command is interrupted and the normal `finally` block does not complete.
|
||||
|
||||
## Final Verified Result
|
||||
|
||||
The final smoke test command was:
|
||||
|
||||
```powershell
|
||||
.\scripts\run-windows-vm-smoke.ps1 -SkipViewer -HaltAfter
|
||||
```
|
||||
|
||||
It passed with:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"exitCode": 0,
|
||||
"installExitCode": 0,
|
||||
"uninstallExitCode": 0,
|
||||
"uninstallVerified": true
|
||||
}
|
||||
```
|
||||
|
||||
The final trace showed both scheduled tasks completing:
|
||||
|
||||
- `CovenantSetupSelfInstall-Install-...`
|
||||
- `CovenantSetupSelfInstall-Uninstall-...`
|
||||
|
||||
It also showed:
|
||||
|
||||
- install Rust heartbeat
|
||||
- install C# pipe log
|
||||
- uninstall Rust heartbeat
|
||||
- uninstall C# pipe log
|
||||
- cleanup helper heartbeat
|
||||
- `uninstall_cleanup_observed` with every checked path/key absent
|
||||
|
||||
## Relevant Code Changes
|
||||
|
||||
### Build and Packaging
|
||||
|
||||
- `.cargo/config.toml`
|
||||
- Enables static MSVC runtime linking for the Rust executable.
|
||||
- `build.rs`
|
||||
- Publishes the C# WinForms UI as self-contained `win-x64`.
|
||||
- Sets `COVENANT_SETUP_UI_EXE` for Rust embedding.
|
||||
- `src/main.rs`
|
||||
- Adds trace events.
|
||||
- Uses raw embedded bundle format.
|
||||
- Uses C# UI IPC instead of PowerShell UI.
|
||||
- Writes packaged install journal to the inferred install root.
|
||||
- Propagates automation through uninstall cleanup.
|
||||
|
||||
### C# UI
|
||||
|
||||
- `src/ui.rs`
|
||||
- Extracts embedded C# UI executable.
|
||||
- Connects to a named pipe.
|
||||
- Sends JSON UI messages.
|
||||
- Logs Rust-side pipe events.
|
||||
- `ui/Covenant.Setup.Ui/Program.cs`
|
||||
- Hosts the named pipe server.
|
||||
- Displays progress, logs, and prompts.
|
||||
- Logs C# pipe events to `csharp-ui-pipe-<pid>.jsonl`.
|
||||
|
||||
### Vagrant Harness
|
||||
|
||||
- `scripts/run-windows-vm-smoke.ps1`
|
||||
- Requires `dotnet`.
|
||||
- Packages with `Start-Process -Wait`.
|
||||
- Validates embedded bundle marker.
|
||||
- Uploads guest scripts.
|
||||
- Pulls trace bundle in `finally`.
|
||||
- Reports install and uninstall status.
|
||||
- `scripts/windows-vm/Start-InteractiveSelfInstall.ps1`
|
||||
- Runs install and uninstall as separate interactive scheduled tasks.
|
||||
- Verifies installed state before uninstall.
|
||||
- Verifies removed state after uninstall.
|
||||
- Writes detailed guest trace events.
|
||||
- `scripts/windows-vm/Invoke-InteractiveInstaller.ps1`
|
||||
- Starts a target executable with decoded argument list.
|
||||
- Polls process state instead of relying only on `WaitForExit`.
|
||||
- Writes per-operation diagnostics.
|
||||
- `scripts/windows-vm/Abort-SmokeDiagnostics.ps1`
|
||||
- Captures and aborts an in-progress smoke run.
|
||||
|
||||
### Removed PowerShell UI
|
||||
|
||||
- `scripts/windows-vm/Approve-InstallerDialogs.ps1`
|
||||
- Removed because the automation path no longer clicks PowerShell UI dialogs.
|
||||
- `src/win.rs`
|
||||
- PowerShell/TaskDialog UI helpers were removed from the primary UI flow.
|
||||
|
||||
## Troubleshooting Guide for the Next Hang
|
||||
|
||||
1. Check whether the host command is still running:
|
||||
|
||||
```powershell
|
||||
Get-Process | Where-Object { $_.ProcessName -match 'vagrant|ruby|covenant' }
|
||||
```
|
||||
|
||||
2. If the host-side Vagrant process is stuck and the run should be aborted, stop only the Vagrant/Ruby processes for that run.
|
||||
|
||||
3. Pull guest diagnostics:
|
||||
|
||||
```powershell
|
||||
vagrant upload scripts\windows-vm\Abort-SmokeDiagnostics.ps1 C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke\scripts\Abort-SmokeDiagnostics.ps1
|
||||
vagrant winrm -s powershell -c "& 'C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke\scripts\Abort-SmokeDiagnostics.ps1'"
|
||||
```
|
||||
|
||||
4. Inspect `dist\vagrant-self-test\trace\guest-events.jsonl`.
|
||||
|
||||
5. Interpret missing logs:
|
||||
|
||||
- No `interactive_installer_start`: scheduled task or PowerShell wrapper failed before launching the installer.
|
||||
- `interactive_installer_start` exists, but no `installer-heartbeat-*.jsonl`: executable failed before Rust `main()`, usually loader/dependency/signing/OS error.
|
||||
- Rust heartbeat exists, but no `csharp-ui-pipe-*.jsonl`: C# UI failed to start or pipe connection failed.
|
||||
- Both heartbeat and pipe logs exist: inspect the last Rust phase and last C# pipe phase to find the blocked operation.
|
||||
|
||||
6. Check `interactive-windows.json` for modal system dialogs.
|
||||
|
||||
7. Check `abort-processes.json` and `abort-scheduled-task-info.json` for orphaned tasks or running installers.
|
||||
|
||||
Reference in New Issue
Block a user