diff --git a/src/service.rs b/src/service.rs index ce18d95..75f072a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -44,6 +44,11 @@ pub const WINDOWS_SERVICE: &str = "RunicGatewayLink"; pub const SERVICE_USER: &str = "runicgateway"; /// What both platforms show a human. const DISPLAY_NAME: &str = "Runic Gateway uo-link sidecar"; +/// The first `link` release whose sidecar speaks the Windows SCM startup protocol, and so the +/// oldest one that can be started as a service at all. Named only in the 1053 diagnosis; nothing +/// enforces it, because the Linux side has no such floor and a version gate on an installed binary +/// would refuse deployments that are working. +const MIN_SERVICE_SIDECAR: &str = "v1.2.0"; /// Which service manager this host has — or why it has none this installer can drive. #[derive(Debug, Clone, PartialEq, Eq)] @@ -307,6 +312,57 @@ pub fn windows_bin_path(binary: &Path, config: &Path) -> String { format!("\"{}\" --config \"{}\"", binary.display(), config.display()) } +/// What to tell the operator when `sc.exe start` fails. +/// +/// Pure and tested on both platforms, because the *wrong* explanation here is expensive. This +/// originally blamed every failure on the config file — "a service that exits immediately usually +/// cannot read its config" — which for the one error code that actually shows up sends the reader +/// to inspect a file that is almost certainly fine. +/// +/// **1053 is not a crash.** It is the SCM giving up after 30 seconds waiting for the service +/// process to call `StartServiceCtrlDispatcher` and identify itself. The process starts, runs, and +/// is very likely serving traffic; it simply never had the conversation the SCM required. A sidecar +/// older than the one that speaks the SCM protocol produces this *every time*, on a perfectly good +/// config — so the config is the last thing to look at, not the first. +pub fn windows_start_failure(code: i32, binary: &Path, config: &Path) -> String { + let command = crate::util::command_line("sc.exe", &["start", WINDOWS_SERVICE]); + match code { + 1053 => format!( + "`{command}` failed with 1053 — the service did not respond to the start request in \ + time.\n\n This is a handshake failure, not a crash: Windows waited 30 seconds for \ + the process to identify itself to the service control manager. The usual cause is a \ + sidecar built before the service support was added, which runs perfectly in the \ + foreground and can never start as a service. Check its version:\n\n \ + \"{binary}\" --version\n\n and confirm it is at least {MIN_SERVICE_SIDECAR}. To \ + see whether the sidecar itself is healthy, run it in the foreground — if that works, \ + the binary is the problem, not the configuration:\n\n \"{binary}\" --config \ + \"{config}\"", + binary = binary.display(), + config = config.display(), + ), + // ERROR_SERVICE_LOGON_FAILED. The account is the virtual one the SCM makes itself, so this + // is a policy that forbids virtual service accounts rather than a wrong password. + 1069 => format!( + "`{command}` failed with 1069 — the service could not log on as {account}.\n\n \ + That account is a virtual service account created by the SCM itself and has no \ + password, so this is a local policy forbidding them rather than a bad credential. \ + Register the service by hand against an account this host allows — INSTALL.md \ + Appendix A4.", + account = windows_service_account(), + ), + _ => format!( + "`{command}` failed with exit code {code}.\n\n Check the Windows event log \ + (System, source \"Service Control Manager\"), and `sc query {WINDOWS_SERVICE}` for \ + the service's own exit code. A sidecar that exits immediately usually cannot read its \ + config: {}\n\n Running it in the foreground prints the reason:\n\n \ + \"{}\" --config \"{}\"", + config.display(), + binary.display(), + config.display(), + ), + } +} + #[cfg(windows)] fn register_windows(binary: &Path, config: &Path, restart: bool) -> Result { let bin_path = windows_bin_path(binary, config); @@ -373,13 +429,11 @@ fn register_windows(binary: &Path, config: &Path, restart: bool) -> Result