Cutover: promote the installer from edge to main #17
105
src/service.rs
105
src/service.rs
@@ -44,6 +44,11 @@ pub const WINDOWS_SERVICE: &str = "RunicGatewayLink";
|
|||||||
pub const SERVICE_USER: &str = "runicgateway";
|
pub const SERVICE_USER: &str = "runicgateway";
|
||||||
/// What both platforms show a human.
|
/// What both platforms show a human.
|
||||||
const DISPLAY_NAME: &str = "Runic Gateway uo-link sidecar";
|
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.
|
/// Which service manager this host has — or why it has none this installer can drive.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[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())
|
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)]
|
#[cfg(windows)]
|
||||||
fn register_windows(binary: &Path, config: &Path, restart: bool) -> Result<Outcome> {
|
fn register_windows(binary: &Path, config: &Path, restart: bool) -> Result<Outcome> {
|
||||||
let bin_path = windows_bin_path(binary, config);
|
let bin_path = windows_bin_path(binary, config);
|
||||||
@@ -373,13 +429,11 @@ fn register_windows(binary: &Path, config: &Path, restart: bool) -> Result<Outco
|
|||||||
// 1056 is ERROR_SERVICE_ALREADY_RUNNING, which is the desired end state, not a failure.
|
// 1056 is ERROR_SERVICE_ALREADY_RUNNING, which is the desired end state, not a failure.
|
||||||
let start = run("sc.exe", &["start", WINDOWS_SERVICE])?;
|
let start = run("sc.exe", &["start", WINDOWS_SERVICE])?;
|
||||||
if !start.status.success() && start.status.code() != Some(1056) {
|
if !start.status.success() && start.status.code() != Some(1056) {
|
||||||
anyhow::bail!(
|
anyhow::bail!(windows_start_failure(
|
||||||
"`{}` failed with exit code {}. Check the Windows event log; a service that exits \
|
|
||||||
immediately usually cannot read its config: {}",
|
|
||||||
crate::util::command_line("sc.exe", &["start", WINDOWS_SERVICE]),
|
|
||||||
start.status.code().unwrap_or(-1),
|
start.status.code().unwrap_or(-1),
|
||||||
config.display()
|
binary,
|
||||||
);
|
config
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Outcome::Registered {
|
Ok(Outcome::Registered {
|
||||||
@@ -994,6 +1048,45 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_1053_is_diagnosed_as_a_handshake_not_a_bad_config() {
|
||||||
|
// The regression this guards: 1053 used to be reported as "a service that exits immediately
|
||||||
|
// usually cannot read its config", which is the one thing it almost never is. A reader who
|
||||||
|
// follows that sentence goes and stares at a config file that is fine.
|
||||||
|
let msg = windows_start_failure(
|
||||||
|
1053,
|
||||||
|
Path::new(r"C:\Program Files\RunicGateway\uo-link-sidecar.exe"),
|
||||||
|
Path::new(r"C:\ProgramData\RunicGateway\sidecar.toml"),
|
||||||
|
);
|
||||||
|
assert!(msg.contains("1053"), "{msg}");
|
||||||
|
assert!(msg.contains("handshake"), "{msg}");
|
||||||
|
assert!(!msg.contains("cannot read its config"), "{msg}");
|
||||||
|
// It has to name the two things that actually resolve it: check the version, and prove the
|
||||||
|
// binary is healthy by running it in the foreground.
|
||||||
|
assert!(msg.contains("--version"), "{msg}");
|
||||||
|
assert!(msg.contains(MIN_SERVICE_SIDECAR), "{msg}");
|
||||||
|
assert!(msg.contains("uo-link-sidecar.exe"), "{msg}");
|
||||||
|
assert!(msg.contains("sidecar.toml"), "{msg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_logon_failure_points_at_policy_not_a_password() {
|
||||||
|
let msg = windows_start_failure(1069, Path::new("bin.exe"), Path::new("c.toml"));
|
||||||
|
assert!(msg.contains("NT SERVICE\\RunicGatewayLink"), "{msg}");
|
||||||
|
assert!(msg.contains("policy"), "{msg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unrecognized_code_still_says_how_to_see_the_real_error() {
|
||||||
|
// The fallback must not pretend to know the cause; it must hand over the two places the
|
||||||
|
// cause is actually written down.
|
||||||
|
let msg = windows_start_failure(5, Path::new("bin.exe"), Path::new("c.toml"));
|
||||||
|
assert!(msg.contains("exit code 5"), "{msg}");
|
||||||
|
assert!(msg.contains("event log"), "{msg}");
|
||||||
|
assert!(msg.contains("sc query RunicGatewayLink"), "{msg}");
|
||||||
|
assert!(msg.contains("--config"), "{msg}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_manual_steps_are_a_complete_recipe() {
|
fn the_manual_steps_are_a_complete_recipe() {
|
||||||
// This text is all an operator gets on a host the installer cannot drive, so it has to name
|
// This text is all an operator gets on a host the installer cannot drive, so it has to name
|
||||||
|
|||||||
Reference in New Issue
Block a user