feat(windows): run as a real SCM service, not a Run-key autostart #107
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -1359,6 +1359,7 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"tower",
|
"tower",
|
||||||
"webpki-roots 1.0.6",
|
"webpki-roots 1.0.6",
|
||||||
|
"windows-service",
|
||||||
"x509-parser",
|
"x509-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2583,6 +2584,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-service"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"widestring",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ rustls-pemfile = "2.2.0"
|
|||||||
qrcode = { version = "0.14", default-features = false, features = ["svg"] }
|
qrcode = { version = "0.14", default-features = false, features = ["svg"] }
|
||||||
webpki-roots = "1"
|
webpki-roots = "1"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-service = "0.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.8", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ pub mod system_dns;
|
|||||||
pub mod tls;
|
pub mod tls;
|
||||||
pub mod wire;
|
pub mod wire;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub mod windows_service;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod testutil;
|
pub(crate) mod testutil;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ async fn main() -> numa::Result<()> {
|
|||||||
// Handle CLI subcommands
|
// Handle CLI subcommands
|
||||||
let arg1 = std::env::args().nth(1).unwrap_or_default();
|
let arg1 = std::env::args().nth(1).unwrap_or_default();
|
||||||
match arg1.as_str() {
|
match arg1.as_str() {
|
||||||
|
#[cfg(windows)]
|
||||||
|
"--service" => {
|
||||||
|
// Entry point used by Windows SCM (`sc create … binPath="numa.exe --service"`).
|
||||||
|
// Hands control to the service dispatcher and blocks until Stop.
|
||||||
|
numa::windows_service::run_as_service()
|
||||||
|
.map_err(|e| format!("windows service dispatcher failed: {}", e))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
"install" => {
|
"install" => {
|
||||||
eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — installing\n");
|
eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — installing\n");
|
||||||
return install_service().map_err(|e| e.into());
|
return install_service().map_err(|e| e.into());
|
||||||
|
|||||||
85
src/windows_service.rs
Normal file
85
src/windows_service.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
//! Windows service wrapper.
|
||||||
|
//!
|
||||||
|
//! Lets the `numa.exe` binary act as a real Windows service registered with
|
||||||
|
//! the Service Control Manager (SCM). Invoked via `numa.exe --service` (the
|
||||||
|
//! form that `sc create … binPath=` uses).
|
||||||
|
//!
|
||||||
|
//! Interactive runs (`numa.exe`, `numa.exe run`, `numa.exe install`) do not
|
||||||
|
//! go through this module — they keep their existing console-attached
|
||||||
|
//! behaviour.
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use windows_service::service::{
|
||||||
|
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType,
|
||||||
|
};
|
||||||
|
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
|
||||||
|
use windows_service::{define_windows_service, service_dispatcher};
|
||||||
|
|
||||||
|
pub const SERVICE_NAME: &str = "Numa";
|
||||||
|
|
||||||
|
define_windows_service!(ffi_service_main, service_main);
|
||||||
|
|
||||||
|
/// Entry point the SCM hands control to after `StartServiceCtrlDispatcherW`.
|
||||||
|
/// Any panic here vanishes silently into the service host — log instead of
|
||||||
|
/// unwrapping.
|
||||||
|
fn service_main(_arguments: Vec<OsString>) {
|
||||||
|
if let Err(e) = run_service() {
|
||||||
|
log::error!("numa service exited with error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_service() -> windows_service::Result<()> {
|
||||||
|
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
|
||||||
|
|
||||||
|
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
||||||
|
match control_event {
|
||||||
|
ServiceControl::Stop | ServiceControl::Shutdown => {
|
||||||
|
let _ = shutdown_tx.send(());
|
||||||
|
ServiceControlHandlerResult::NoError
|
||||||
|
}
|
||||||
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||||
|
_ => ServiceControlHandlerResult::NotImplemented,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
|
||||||
|
|
||||||
|
status_handle.set_service_status(ServiceStatus {
|
||||||
|
service_type: ServiceType::OWN_PROCESS,
|
||||||
|
current_state: ServiceState::Running,
|
||||||
|
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
|
||||||
|
exit_code: ServiceExitCode::Win32(0),
|
||||||
|
checkpoint: 0,
|
||||||
|
wait_hint: Duration::default(),
|
||||||
|
process_id: None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// TODO(windows-service): call numa's async serve loop here once main.rs's
|
||||||
|
// server body is extracted into `numa::serve(config_path)`. For now the
|
||||||
|
// service registers, reports Running, and blocks until SCM sends Stop —
|
||||||
|
// useful for verifying the SCM plumbing end to end with `sc start Numa`
|
||||||
|
// and `sc stop Numa`.
|
||||||
|
let _ = shutdown_rx.recv();
|
||||||
|
|
||||||
|
status_handle.set_service_status(ServiceStatus {
|
||||||
|
service_type: ServiceType::OWN_PROCESS,
|
||||||
|
current_state: ServiceState::Stopped,
|
||||||
|
controls_accepted: ServiceControlAccept::empty(),
|
||||||
|
exit_code: ServiceExitCode::Win32(0),
|
||||||
|
checkpoint: 0,
|
||||||
|
wait_hint: Duration::default(),
|
||||||
|
process_id: None,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hand control to the SCM dispatcher. Blocks until the service stops.
|
||||||
|
/// Call only from the `--service` command path — interactive invocations
|
||||||
|
/// will hang here waiting for an SCM that isn't talking to them.
|
||||||
|
pub fn run_as_service() -> windows_service::Result<()> {
|
||||||
|
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user