feat(sidecar): start as a real Windows service
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m22s

`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>
This commit is contained in:
2026-08-07 13:35:52 -05:00
parent 915f0296a9
commit 96af2afa68
7 changed files with 768 additions and 300 deletions

95
sidecar/Cargo.lock generated
View File

@@ -242,6 +242,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
[[package]]
name = "crossbeam-channel"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.13"
@@ -284,6 +293,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "digest"
version = "0.10.7"
@@ -921,6 +936,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -1048,6 +1069,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1565,6 +1592,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "2.0.118"
@@ -1642,6 +1675,36 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -1798,6 +1861,19 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
@@ -1913,7 +1989,9 @@ dependencies = [
"tokio",
"toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
"windows-service",
]
[[package]]
@@ -2025,6 +2103,12 @@ dependencies = [
"wasite",
]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -2075,6 +2159,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-service"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2"
dependencies = [
"bitflags",
"widestring",
"windows-sys 0.61.2",
]
[[package]]
name = "windows-strings"
version = "0.5.1"