feat(sidecar): start as a real Windows service #29
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/windows-service"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
The sidecar could never start as a Windows service. Every install failed at
sc startwith 1053:Nothing had crashed — exit code 0, and the same binary with the same config ran perfectly in the foreground. The sidecar was a plain console program, and the Windows SCM only supervises a process that calls
StartServiceCtrlDispatcherand identifies itself within ~30 seconds.The installer's design (
docs/installer/PLAN.md§8) had recorded thatsc createagainst the plain console binary worked and needed no change here. That was a false symmetry with systemd: systemd supervises any foreground process, the SCM does not. There is no third option wheresc.exeadopts an arbitrary console executable — it is a service-aware binary or a shim, and a shim was already rejected as a third binary to keep current.What changed
The entry point splits so the platform owns only starting and stopping:
src/app.rsis the whole sidecar, moved across unchanged. No#[cfg]anywhere on the data path — config, shard link, store, web server and the event loop are shared code.src/windows.rsspeaks the SCM handshake. The dispatcher is tried first and failing is expected:ERROR_FAILED_SERVICE_CONTROLLER_CONNECT(1063) means "not started by the SCM" and falls through to a normal foreground run. One binary does both, with no--serviceflag for an operator to forget. Any other dispatcher error is a real failure.Runningis reported only once the shard port is bound and the store is open, so a bad config fails the start rather than flapping Running → Stopped, and a failed run leaves a nonzeroSERVICE_EXIT_CODEinstead of the misleading0above.uo-link-sidecar.<date>.logbeside its config (daily, seven kept). Foreground runs still log to stdout unchanged.src/unix.rsadditionally handlesSIGTERM— whatsystemctl stopsends, and which previously took the default disposition mid-write.windows-serviceandtracing-appenderare declared under[target.'cfg(windows)'.dependencies], so Cargo neither resolves nor builds them for Linux.No protocol change, so
PROTOCOL_VERSIONstays at 3.Release note
This needs to release as v1.2.0, and a new bundle must carry it. Until then the installer keeps pulling v1.1.0 and hitting 1053.
installer#<fix/service-start-diagnosis>namesv1.2.0as the floor in its 1053 diagnosis, so if this lands under a different number that constant needs updating to match.How it was tested
Verified on Windows against a real SCM service — registered the way the installer does (virtual service account
NT SERVICE\RunicGatewayLink,--configinbinPath, failure actions), 13/13 checks:The service's own log, showing the shared path running under
ServiceMainand a graceful stop via the SCM's Stop control rather than a kill:Also:
/health.--versionand--print-configunchanged.cargo buildinrust:1-slim-bookwormsucceeds, and grepping that build log forwindows-service/tracing-appenderreturns 0 hits — neither crate is even resolved for Linux. (cargo check --target x86_64-unknown-linux-gnuis not usable here; sqlx's C needs a cross toolchain.)cargo fmt,cargo clippy --all-targetsclean,cargo test26 passed (4 new, covering the service log directory fallbacks and the service name matching the installer's).Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
`sc.exe start RunicGatewayLink` failed with 1053 on every Windows install: "a timeout was reached (30000 milliseconds) while waiting for the service to connect", with SERVICE_EXIT_CODE 0. Nothing had crashed. The sidecar was a plain console program, and the Windows service control manager only supervises a process that calls StartServiceCtrlDispatcher and identifies itself within ~30 seconds. The installer's design assumed symmetry with systemd, which supervises any foreground process. Windows has no equivalent: it is a service-aware binary or a shim, and a shim was already rejected as a third binary to keep current. Split the entry point so the platform only owns starting and stopping: systemd --> main --> unix::run ---------------+ +--> app::run SCM ------> main --> windows::run --> ServiceMain \-> console fallback - app.rs is the whole sidecar, unchanged and shared. No #[cfg] on the data path. - windows.rs speaks the SCM handshake. The dispatcher is tried first and failing is expected: ERROR_FAILED_SERVICE_CONTROLLER_CONNECT (1063) means "not started by the SCM" and falls through to a normal foreground run, so one binary does both with no --service flag to forget. - Running is reported only once the shard port is bound and the store is open, so a bad config fails the start instead of flapping Running -> Stopped, and a failed run leaves a nonzero SERVICE_EXIT_CODE instead of the misleading 0. - A service has no stdout, so service mode logs to uo-link-sidecar.log.<date> beside its config, rolled daily, seven kept. - unix.rs additionally handles SIGTERM, which is what systemctl stop sends and which previously took the default disposition mid-write. The Windows crates are declared under [target.'cfg(windows)'.dependencies]. Verified: a Linux build in rust:1-slim-bookworm succeeds and resolves neither windows-service nor tracing-appender. Co-Authored-By: Claude <noreply@anthropic.com>