From 65e65028a063521b05801b6c9aec34cdd3b325b8 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Thu, 16 Apr 2026 16:59:54 +0300 Subject: [PATCH] fix(windows): separate service lifecycle from install flow service start/stop/restart/status now map to proper SCM operations instead of re-running the full install/uninstall flow. On re-install, stop the running service first so the binary can be overwritten. --- src/main.rs | 9 ++-- src/system_dns.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 88f2128..b8893b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ -use numa::system_dns::{install_service, restart_service, service_status, uninstall_service}; +use numa::system_dns::{ + install_service, restart_service, service_status, start_service, stop_service, + uninstall_service, +}; fn main() -> numa::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) @@ -28,8 +31,8 @@ fn main() -> numa::Result<()> { let sub = std::env::args().nth(2).unwrap_or_default(); eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — service management\n"); return match sub.as_str() { - "start" => install_service().map_err(|e| e.into()), - "stop" => uninstall_service().map_err(|e| e.into()), + "start" => start_service().map_err(|e| e.into()), + "stop" => stop_service().map_err(|e| e.into()), "restart" => restart_service().map_err(|e| e.into()), "status" => service_status().map_err(|e| e.into()), _ => { diff --git a/src/system_dns.rs b/src/system_dns.rs index ca587b8..c4279cd 100644 --- a/src/system_dns.rs +++ b/src/system_dns.rs @@ -698,6 +698,13 @@ fn install_windows() -> Result<(), String> { let needs_reboot = disable_dnscache()?; + // On re-install, stop the running service first so the binary can be + // overwritten (SCM holds a handle to the exe while it's running). + let reinstall = is_service_registered(); + if reinstall { + stop_service_scm(); + } + // Copy the binary to a stable path under ProgramData and register it // as a real Windows service (SCM-managed, boot-time, auto-restart). let service_exe = install_service_binary()?; @@ -864,6 +871,41 @@ fn delete_service_scm() { } } +/// Check whether the service is registered with SCM (regardless of state). +#[cfg(windows)] +fn is_service_registered() -> bool { + run_sc(&["query", crate::windows_service::SERVICE_NAME]) + .map(|o| { + // sc query exits 0 if the service exists (running or stopped). + // Error 1060 = "service does not exist". + if o.status.success() { + return true; + } + let text = String::from_utf8_lossy(&o.stdout); + !text.contains("1060") + }) + .unwrap_or(false) +} + +/// Print service state from SCM. +#[cfg(windows)] +fn service_status_windows() -> Result<(), String> { + let out = run_sc(&["query", crate::windows_service::SERVICE_NAME])?; + let text = String::from_utf8_lossy(&out.stdout); + if text.contains("1060") { + eprintln!(" Service is not installed.\n"); + return Ok(()); + } + // Parse STATE line, e.g. "STATE : 4 RUNNING" + let state = text + .lines() + .find(|l| l.contains("STATE")) + .map(|l| l.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + eprintln!(" {}\n", state); + Ok(()) +} + #[cfg(windows)] fn uninstall_windows() -> Result<(), String> { // Stop + remove the service before touching DNS, so port 53 is released @@ -1167,6 +1209,62 @@ pub fn install_service() -> Result<(), String> { result } +/// Start the service. If already installed, just starts it via the platform +/// service manager. If not installed, falls through to a full install. +pub fn start_service() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + install_service() + } + #[cfg(target_os = "linux")] + { + install_service() + } + #[cfg(windows)] + { + if is_service_registered() { + start_service_scm()?; + eprintln!(" Service started.\n"); + Ok(()) + } else { + install_service() + } + } + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + { + Err("service start not supported on this OS".to_string()) + } +} + +/// Stop the service without uninstalling it. +pub fn stop_service() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + uninstall_service() + } + #[cfg(target_os = "linux")] + { + uninstall_service() + } + #[cfg(windows)] + { + let out = run_sc(&["stop", crate::windows_service::SERVICE_NAME])?; + if !out.status.success() { + let text = String::from_utf8_lossy(&out.stdout); + // 1062 = not started, 1060 = does not exist + if !text.contains("1062") && !text.contains("1060") { + return Err(format!("sc stop failed: {}", text.trim())); + } + } + eprintln!(" Service stopped.\n"); + Ok(()) + } + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + { + Err("service stop not supported on this OS".to_string()) + } +} + /// Uninstall the Numa system service. pub fn uninstall_service() -> Result<(), String> { let _ = untrust_ca(); @@ -1236,7 +1334,14 @@ pub fn restart_service() -> Result<(), String> { eprintln!(" Service restarted → {}\n", version); Ok(()) } - #[cfg(not(any(target_os = "macos", target_os = "linux")))] + #[cfg(windows)] + { + stop_service_scm(); + start_service_scm()?; + eprintln!(" Service restarted.\n"); + Ok(()) + } + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] { Err("service restart not supported on this OS".to_string()) } @@ -1252,7 +1357,11 @@ pub fn service_status() -> Result<(), String> { { service_status_linux() } - #[cfg(not(any(target_os = "macos", target_os = "linux")))] + #[cfg(windows)] + { + service_status_windows() + } + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] { Err("service status not supported on this OS".to_string()) }