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.
This commit is contained in:
Razvan Dimescu
2026-04-16 16:59:54 +03:00
parent d3eab73a31
commit 65e65028a0
2 changed files with 117 additions and 5 deletions

View File

@@ -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<()> { fn main() -> numa::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) 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(); let sub = std::env::args().nth(2).unwrap_or_default();
eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — service management\n"); eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — service management\n");
return match sub.as_str() { return match sub.as_str() {
"start" => install_service().map_err(|e| e.into()), "start" => start_service().map_err(|e| e.into()),
"stop" => uninstall_service().map_err(|e| e.into()), "stop" => stop_service().map_err(|e| e.into()),
"restart" => restart_service().map_err(|e| e.into()), "restart" => restart_service().map_err(|e| e.into()),
"status" => service_status().map_err(|e| e.into()), "status" => service_status().map_err(|e| e.into()),
_ => { _ => {

View File

@@ -698,6 +698,13 @@ fn install_windows() -> Result<(), String> {
let needs_reboot = disable_dnscache()?; 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 // Copy the binary to a stable path under ProgramData and register it
// as a real Windows service (SCM-managed, boot-time, auto-restart). // as a real Windows service (SCM-managed, boot-time, auto-restart).
let service_exe = install_service_binary()?; 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)] #[cfg(windows)]
fn uninstall_windows() -> Result<(), String> { fn uninstall_windows() -> Result<(), String> {
// Stop + remove the service before touching DNS, so port 53 is released // Stop + remove the service before touching DNS, so port 53 is released
@@ -1167,6 +1209,62 @@ pub fn install_service() -> Result<(), String> {
result 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. /// Uninstall the Numa system service.
pub fn uninstall_service() -> Result<(), String> { pub fn uninstall_service() -> Result<(), String> {
let _ = untrust_ca(); let _ = untrust_ca();
@@ -1236,7 +1334,14 @@ pub fn restart_service() -> Result<(), String> {
eprintln!(" Service restarted → {}\n", version); eprintln!(" Service restarted → {}\n", version);
Ok(()) 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()) Err("service restart not supported on this OS".to_string())
} }
@@ -1252,7 +1357,11 @@ pub fn service_status() -> Result<(), String> {
{ {
service_status_linux() 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()) Err("service status not supported on this OS".to_string())
} }