VA-9: host the agent as a Windows service (SCM) #54

Merged
nrupard merged 2 commits from feat/VA-9-windows-service into main 2026-06-12 20:35:44 +02:00
Owner

Summary

Implements Windows service hosting (VA-9), the Phase-6 Windows-parity item, behind cfg(target_os = "windows"). The agent registers with the Service Control Manager (SCM), runs under it, and tears down cleanly on sc stop.

What changed

  • install_windows.rs (new): install_service registers an auto-start LocalSystem SCM service named vervain-agent, sealing the seed into %ProgramData%\vervain-agent and copying the binary to %ProgramFiles%\vervain-agent. uninstall_service stops and deletes it and is idempotent (a missing service is treated as success, mirroring the Linux uninstall).
  • win_service.rs (new): the bin-side service-control dispatcher. The hidden service-run subcommand is what the SCM launches; it reports Running, drives the shared agent loop on its own tokio runtime, and reports Stopped on exit. A Stop/Shutdown control trips a global signal that wait_for_shutdown now awaits alongside Ctrl-C, so sc stop runs the same graceful mDNS-goodbye teardown as a console interrupt.
  • main.rs: the agent run loop is factored out of main into run_agent so the foreground run path and the service dispatcher share one implementation. Windows gets install / uninstall / service-run subcommands parallel to the existing Linux install / uninstall.
  • Service name is vervain-agent (consistent with the binary and the Linux vervain-agent.service unit). The issue's meshagent naming was legacy MeshAgent carry-over; there is no meshagent crate.

Toolchain (the shared MinGW blocker)

  • gcc-mingw-w64-x86-64 and the x86_64-pc-windows-gnu rust target are now baked into both the dev image (Dockerfile) and the CI check image (oci-build/check.Dockerfile).
  • CI now runs cargo check --workspace --target x86_64-pc-windows-gnu. just check-windows runs the same cross-check locally.
  • This was the blocker shared with VA-11 / VA-12 / VA-13 / VA-14; the toolchain is now provisioned for all of them (ROADMAP updated).

Validation

  • The whole workspace cross-compiles clean for x86_64-pc-windows-gnu, and the Linux fmt / clippy --deny warnings / build / test suite stays green (verified via the oci-build/check.Dockerfile build).
  • The runtime acceptance criteria (sc query vervain-agent, reboot survival, uninstall idempotency) require a Windows host with the SCM. Per the agreed interim bar, this PR ships the implementation with a type-check / link cross-check in CI; the runtime ACs are validated manually on Windows.

Refs VA-9.

## Summary Implements Windows service hosting (VA-9), the Phase-6 Windows-parity item, behind `cfg(target_os = "windows")`. The agent registers with the Service Control Manager (SCM), runs under it, and tears down cleanly on `sc stop`. ## What changed - `install_windows.rs` (new): `install_service` registers an auto-start `LocalSystem` SCM service named `vervain-agent`, sealing the seed into `%ProgramData%\vervain-agent` and copying the binary to `%ProgramFiles%\vervain-agent`. `uninstall_service` stops and deletes it and is idempotent (a missing service is treated as success, mirroring the Linux uninstall). - `win_service.rs` (new): the bin-side service-control dispatcher. The hidden `service-run` subcommand is what the SCM launches; it reports Running, drives the shared agent loop on its own tokio runtime, and reports Stopped on exit. A Stop/Shutdown control trips a global signal that `wait_for_shutdown` now awaits alongside Ctrl-C, so `sc stop` runs the same graceful mDNS-goodbye teardown as a console interrupt. - `main.rs`: the agent run loop is factored out of `main` into `run_agent` so the foreground `run` path and the service dispatcher share one implementation. Windows gets `install` / `uninstall` / `service-run` subcommands parallel to the existing Linux `install` / `uninstall`. - Service name is `vervain-agent` (consistent with the binary and the Linux `vervain-agent.service` unit). The issue's `meshagent` naming was legacy MeshAgent carry-over; there is no `meshagent` crate. ## Toolchain (the shared MinGW blocker) - `gcc-mingw-w64-x86-64` and the `x86_64-pc-windows-gnu` rust target are now baked into both the dev image (`Dockerfile`) and the CI check image (`oci-build/check.Dockerfile`). - CI now runs `cargo check --workspace --target x86_64-pc-windows-gnu`. `just check-windows` runs the same cross-check locally. - This was the blocker shared with VA-11 / VA-12 / VA-13 / VA-14; the toolchain is now provisioned for all of them (ROADMAP updated). ## Validation - The whole workspace cross-compiles clean for `x86_64-pc-windows-gnu`, and the Linux `fmt` / `clippy --deny warnings` / `build` / `test` suite stays green (verified via the `oci-build/check.Dockerfile` build). - The runtime acceptance criteria (`sc query vervain-agent`, reboot survival, `uninstall` idempotency) require a Windows host with the SCM. Per the agreed interim bar, this PR ships the implementation with a type-check / link cross-check in CI; the runtime ACs are validated manually on Windows. Refs VA-9.
feat(windows): host the agent as a Windows service (SCM)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 2m18s
b6bfd5873d
Implements Windows service hosting behind `cfg(target_os = "windows")`, the Phase-6 Windows parity item.

- `vervain-agent install --from <seed>` registers an auto-start `LocalSystem` SCM service named `vervain-agent` (matching the Linux unit / binary name; the issue's `meshagent` was legacy MeshAgent carry-over), sealing the seed into `%ProgramData%\vervain-agent` and copying the binary to `%ProgramFiles%\vervain-agent`. `uninstall` stops and deletes it and is idempotent (a missing service is treated as success).
- A hidden `service-run` subcommand is what the SCM launches; it hands the process to the service-control dispatcher (`win_service`), which reports Running, drives the shared agent loop on its own tokio runtime, and reports Stopped on exit. A Stop/Shutdown control trips a global signal that `wait_for_shutdown` now awaits alongside Ctrl-C, so `sc stop` runs the same graceful mDNS-goodbye teardown.
- The agent run loop is factored out of `main` into `run_agent` so both the foreground path and the service dispatcher share it.

Toolchain: the MinGW cross toolchain (`gcc-mingw-w64-x86-64`) plus the `x86_64-pc-windows-gnu` rust target are now baked into the dev image and CI check image, and CI runs `cargo check --workspace --target x86_64-pc-windows-gnu`. This was the blocker shared with VA-11/12/13/14, now provisioned for all of them. A `just check-windows` recipe runs the same cross-check locally.

Validation: the workspace cross-compiles clean for Windows and the Linux fmt/clippy/build/test suite stays green. The runtime acceptance criteria (`sc query vervain-agent`, reboot survival, uninstall idempotency) require a Windows host with the SCM and are validated manually there; the CI cross-check is the type-check / link interim bar.

#VA-9

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(windows): address VA-9 service review findings
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 1m50s
Create release / Create release from merged PR (pull_request) Has been skipped
59c0a987f5
Follow-up to the code review on the Windows service work.

- Shutdown race (the blocker): the SCM control handler used `Notify::notify_waiters()`, which stores no permit, so a Stop arriving before the agent reached `wait_for_shutdown` (anywhere in settings/store/identity/connect startup) was dropped and the service would hang until the SCM kill timeout. Switched to `notify_one()`, which leaves a permit for the next `notified()`.
- Exit code surfaced to the SCM: `run_agent` now returns a raw `u8` and the dispatcher maps a non-zero code to `ServiceExitCode::ServiceSpecific(n)` instead of always reporting a clean `Win32(0)` stop, so a failed startup shows as an error in `sc query` rather than a normal stop.
- Restart-on-failure parity: the installer now sets SCM failure actions (restart after 5s, then 10s, then give up) with a 10-minute reset window, plus the non-crash-failures flag so a `ServiceSpecific` exit triggers them. Mirrors the Linux unit's `Restart=on-failure` without spinning on a persistent fatal error.
- Idempotent install: `install_service` now stops and deletes any prior registration before copying files and re-creating the service, so a re-install no longer fails with `ERROR_SERVICE_EXISTS` or leaves partial state. Shared `open_existing` / `stop_and_delete` helpers back both install and uninstall.
- Complete uninstall: `remove_files` now also removes the copied `vervain-agent.exe` and prunes its Program Files dir (best-effort), matching the Linux uninstall which removes the binary.

Verified via `oci-build/check.Dockerfile`: Linux fmt/clippy/build/test green and the workspace still cross-compiles for `x86_64-pc-windows-gnu`.

#VA-9

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/VA-9-windows-service 2026-06-12 20:35:44 +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!54
No description provided.