VA-13: apply staged self-update on Windows #58

Merged
nrupard merged 2 commits from feat/VA-13-windows-self-update into main 2026-06-13 00:44:27 +02:00
Owner

Summary

Apply a staged self-update on Windows (VA-13). apply_staged_update previously returned UpdateError::NotImplemented on non-Unix; Windows can't replace a running / service-held exe in place, so the swap is driven by a detached helper that is a copy of the agent.

Flow

  1. apply_staged_update (Windows) copies the running exe to <exe>.updater.exe (a separate, unlocked file), spawns it detached as the hidden update-swap subcommand, and exits so the service stops and the binary is released. Unix keeps the in-place swap + execv.
  2. The helper (update_swap::run_swap): stop the vervain-agent service -> move exe to <exe>.old -> move the staged binary into place (retry while the handle drains) -> start the service -> confirm it stays Running within a timeout.
  3. Rollback on failure: set the bad binary aside as <exe>.failed, restore <exe>.old, restart.
  4. Stale <exe>.old / <exe>.updater.exe / <exe>.failed are deleted on the next agent startup (cleanup_stale), since they may be locked at swap time.

Service control uses the windows-service ServiceManager (already a dep via VA-9), not sc + shell quoting. Service name from install_windows::SERVICE_NAME.

Decision

Self-copy + hidden update-swap subcommand (over sc+cmd or a separate helper binary): no extra build/package artifact, no shell quoting, and the swap/rollback sequence is driven by typed service-control calls. The three parked blockers are resolved - VA-9 (Windows service) merged, MinGW + the Windows cross-check are in CI (VA-9), and the open helper-mechanism question is decided here.

Validation

Typecheck-only interim (agreed): oci-build/check.Dockerfile passes - fmt, clippy --deny warnings, build, the full test suite, and the Windows cross-check compiling the swap module. The full-cycle survives-a-restart and rollback ACs (AC#1/#2) and the integration test (AC#3) need a Windows host with the SCM and are validated there.

Refs VA-13.

## Summary Apply a staged self-update on Windows (VA-13). `apply_staged_update` previously returned `UpdateError::NotImplemented` on non-Unix; Windows can't replace a running / service-held exe in place, so the swap is driven by a detached helper that is a copy of the agent. ## Flow 1. `apply_staged_update` (Windows) copies the running exe to `<exe>.updater.exe` (a separate, unlocked file), spawns it detached as the hidden `update-swap` subcommand, and exits so the service stops and the binary is released. Unix keeps the in-place swap + `execv`. 2. The helper (`update_swap::run_swap`): stop the `vervain-agent` service -> move exe to `<exe>.old` -> move the staged binary into place (retry while the handle drains) -> start the service -> confirm it stays Running within a timeout. 3. Rollback on failure: set the bad binary aside as `<exe>.failed`, restore `<exe>.old`, restart. 4. Stale `<exe>.old` / `<exe>.updater.exe` / `<exe>.failed` are deleted on the next agent startup (`cleanup_stale`), since they may be locked at swap time. Service control uses the `windows-service` `ServiceManager` (already a dep via VA-9), not `sc` + shell quoting. Service name from `install_windows::SERVICE_NAME`. ## Decision Self-copy + hidden `update-swap` subcommand (over `sc`+`cmd` or a separate helper binary): no extra build/package artifact, no shell quoting, and the swap/rollback sequence is driven by typed service-control calls. The three parked blockers are resolved - VA-9 (Windows service) merged, MinGW + the Windows cross-check are in CI (VA-9), and the open helper-mechanism question is decided here. ## Validation Typecheck-only interim (agreed): `oci-build/check.Dockerfile` passes - fmt, clippy --deny warnings, build, the full test suite, and the Windows cross-check compiling the swap module. The full-cycle survives-a-restart and rollback ACs (AC#1/#2) and the integration test (AC#3) need a Windows host with the SCM and are validated there. Refs VA-13.
feat(update): apply staged self-update on Windows (VA-13)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 4m40s
e4fd54a18d
Windows can't replace a running / service-held executable in place (`apply_staged_update` previously returned `UpdateError::NotImplemented` on non-Unix), so the swap is driven by a detached helper that is a copy of the agent.

- `apply_staged_update` on Windows copies the running exe to `<exe>.updater.exe` (a separate, unlocked file), spawns it detached as the hidden `update-swap` subcommand, and exits so the service stops and the binary is released. Unix keeps the in-place swap + `execv`.
- The helper (`update_swap::run_swap`) stops the `vervain-agent` service, moves the current exe to `<exe>.old`, moves the staged binary into place (retrying while the handle drains), starts the service, and confirms it stays Running within a timeout. On failure it rolls back: set the bad binary aside as `<exe>.failed`, restore `<exe>.old`, restart.
- Service control uses the `windows-service` `ServiceManager` (already a dependency via VA-9), not `sc` + shell quoting; the service name comes from `install_windows::SERVICE_NAME`.
- Stale `<exe>.old` / `<exe>.updater.exe` / `<exe>.failed` (which may be locked at swap time) are deleted on the next agent startup (`cleanup_stale`).

Decision: self-copy + hidden subcommand (over `sc`+`cmd` or a separate helper binary) - no extra build artifact, no shell quoting, and the swap/rollback sequence is driven by typed service-control calls.

Validation: typecheck-only interim (agreed). `oci-build/check.Dockerfile` passes - fmt, clippy --deny warnings, build, the full test suite, and the Windows cross-check compiling the swap module. The full-cycle + rollback acceptance criteria need a Windows host with the SCM and are validated there.

#VA-13

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(update): address VA-13 Windows self-update review findings
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 2m23s
ede7b46e54
Follow-up to the code review on PR #58.

- Clean service stop before exit (the SCM race): the agent `process::exit(0)`'d to hand off to the swap helper without reporting the service Stopped, so the SCM logged an unexpected termination and (per VA-9's failure actions) would schedule a restart that races the helper's swap. `win_service` now publishes its status handle and exposes `report_stopped()`; the update path runs it as a pre-exit hook (registered from `main`) so a clean Stopped is recorded first. No-op for a foreground run.
- Sustained running check: the post-start health check reached `Running` once, so a binary that starts then immediately crashes would not roll back. `confirm_running` now also requires it to still be Running after a short settle window before declaring the swap good.
- Guaranteed stale-artifact cleanup: a still-running `<exe>.updater.exe` is locked when the new agent starts, so `cleanup_stale` now falls back to `MoveFileExW(.., MOVEFILE_DELAY_UNTIL_REBOOT)` (new `windows-sys` Win32_Storage_FileSystem feature) to remove it at the next reboot instead of leaking it.

Verified via `oci-build/check.Dockerfile`: fmt, clippy --deny warnings, build, full test suite, and the Windows cross-check.

#VA-13

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/VA-13-windows-self-update 2026-06-13 00:44:27 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-agent!58
No description provided.