From df293b9e0b85b6efceb1d5906f4f11932962c145 Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Thu, 19 May 2022 00:06:20 +0200 Subject: [PATCH] feat: preliminary windows service support via new argument `--as-windows-service [-s]` and `install` and `uninstall` commands --- Cargo.lock | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++++ src/bin/server.rs | 30 ++++++++++++++++++++++++++++-- src/lib.rs | 3 +++ src/logger.rs | 18 ++++++++++++++---- src/server.rs | 29 ++++++++++++++++++++--------- src/settings/cli.rs | 25 +++++++++++++++++++++++++ src/settings/mod.rs | 6 ++++++ src/signals.rs | 29 ++++++++++++++++++++++++----- src/winservice.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 375 insertions(+), 20 deletions(-) create mode 100644 src/winservice.rs diff --git a/Cargo.lock b/Cargo.lock index 5bda3d0..04a7256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] +name = "err-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + +[[package]] name = "flate2" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -797,6 +811,12 @@ dependencies = [ ] [[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + +[[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -956,6 +976,8 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", + "windows-service", + "windows-sys", ] [[package]] @@ -994,6 +1016,18 @@ dependencies = [ ] [[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1211,6 +1245,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1331,6 +1371,12 @@ dependencies = [ ] [[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1353,6 +1399,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] +name = "windows-service" +version = "0.4.0" +source = "git+https://github.com/yescallop/windows-service-rs#5c9abd3d86a8a35d158807661f215c04661369dd" +dependencies = [ + "bitflags", + "err-derive", + "widestring", + "windows-sys", +] + +[[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 4e0bbfe..31a0cd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,10 @@ version = "0.4" signal-hook = { version = "0.3", features = ["extended-siginfo"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"], default-features = false } +[target.'cfg(windows)'.dependencies] +windows-service = { git = "https://github.com/yescallop/windows-service-rs" } +windows-sys = { version = "0.36.1", features = [ "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_System_Memory" ] } + [dev-dependencies] bytes = "1.1" diff --git a/src/bin/server.rs b/src/bin/server.rs index 05663a6..e58b9bc 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -7,10 +7,36 @@ #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -use static_web_server::{Result, Server}; +use static_web_server::Result; +#[cfg(unix)] fn main() -> Result { - Server::new()?.run()?; + use static_web_server::Server; + + Server::new(None)?.run()?; + + Ok(()) +} + +#[cfg(windows)] +fn main() -> Result { + use static_web_server::settings::Commands; + use static_web_server::winservice; + use static_web_server::Settings; + + // Get server config + let opts = Settings::get()?; + + if let Some(commands) = opts.general.commands { + match commands { + Commands::Install {} => winservice::install_service()?, + Commands::Uninstall {} => winservice::uninstall_service()?, + } + } else if opts.general.as_windows_service { + winservice::run_server_as_service()? + } else { + static_web_server::Server::new(None)?.run()?; + } Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 49c9ddc..9caa7ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,9 @@ pub mod static_files; pub mod tls; pub mod transport; +#[cfg(windows)] +pub mod winservice; + #[macro_use] pub mod error; diff --git a/src/logger.rs b/src/logger.rs index c81217d..84bab0a 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,11 +1,21 @@ -use std::io; use tracing::Level; use tracing_subscriber::fmt::format::FmtSpan; -use crate::Result; +use crate::{Context, Result}; + +/// Logging system initialization +pub fn init(log_level: &str) -> Result { + let log_level = log_level.to_lowercase(); + + configure("trace").with_context(|| "failed to initialize logging")?; + + tracing::info!("logging level: {}", log_level); + + Ok(()) +} /// Initialize logging builder with its levels. -pub fn init(level: &str) -> Result { +fn configure(level: &str) -> Result { let level = level.parse::()?; #[cfg(unix)] @@ -14,7 +24,7 @@ pub fn init(level: &str) -> Result { let enable_ansi = false; match tracing_subscriber::fmt() - .with_writer(io::stderr) + .with_writer(std::io::stderr) .with_max_level(level) .with_span_events(FmtSpan::CLOSE) .with_ansi(enable_ansi) diff --git a/src/server.rs b/src/server.rs index 38e5154..904e385 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,6 +2,7 @@ use hyper::server::conn::AddrIncoming; use hyper::server::Server as HyperServer; use listenfd::ListenFd; use std::net::{IpAddr, SocketAddr, TcpListener}; +use std::sync::mpsc::Receiver; use std::sync::Arc; use crate::handler::{RequestHandler, RequestHandlerOpts}; @@ -13,11 +14,12 @@ use crate::{service::RouterService, Context, Result}; pub struct Server { opts: Settings, threads: usize, + cancel: Option>, } impl Server { /// Create new multi-thread server instance. - pub fn new() -> Result { + pub fn new(cancel: Option>) -> Result { // Get server config let opts = Settings::get()?; @@ -28,11 +30,22 @@ impl Server { n => cpus * n, }; - Ok(Server { opts, threads }) + Ok(Server { + opts, + threads, + cancel, + }) } /// Build and run the multi-thread `Server`. pub fn run(self) -> Result { + // Logging system initialization + if self.cancel.is_none() { + logger::init(&self.opts.general.log_level)?; + } + + tracing::debug!("initializing tokio runtime with multi thread scheduler"); + tokio::runtime::Builder::new_multi_thread() .worker_threads(self.threads) .thread_name("static-web-server") @@ -41,6 +54,7 @@ impl Server { .block_on(async { let r = self.start_server().await; if r.is_err() { + tracing::error!("server failed to start up: {:?}", r); println!("server failed to start up: {:?}", r.unwrap_err()); std::process::exit(1) } @@ -58,11 +72,6 @@ impl Server { // Config-file "advanced" options let advanced_opts = self.opts.advanced; - // Logging system initialization - let log_level = &general.log_level.to_lowercase(); - logger::init(log_level).with_context(|| "failed to initialize logging")?; - tracing::info!("logging level: {}", log_level.to_lowercase()); - // Config file if general.config_file.is_some() && general.config_file.is_some() { tracing::info!("config file: {}", general.config_file.unwrap().display()); @@ -209,7 +218,8 @@ impl Server { let server = server.with_graceful_shutdown(signals::wait_for_signals(signals, grace_period)); #[cfg(windows)] - let server = server.with_graceful_shutdown(signals::wait_for_ctrl_c(grace_period)); + let server = + server.with_graceful_shutdown(signals::wait_for_ctrl_c(self.cancel, grace_period)); tracing::info!( parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads), @@ -241,7 +251,8 @@ impl Server { let server = server.with_graceful_shutdown(signals::wait_for_signals(signals, grace_period)); #[cfg(windows)] - let server = server.with_graceful_shutdown(signals::wait_for_ctrl_c(grace_period)); + let server = + server.with_graceful_shutdown(signals::wait_for_ctrl_c(self.cancel, grace_period)); tracing::info!( parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads), diff --git a/src/settings/cli.rs b/src/settings/cli.rs index 75f5395..18043bd 100644 --- a/src/settings/cli.rs +++ b/src/settings/cli.rs @@ -169,4 +169,29 @@ pub struct General { #[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")] /// Server TOML configuration file path. pub config_file: Option, + + #[structopt( + long, + short = "s", + parse(try_from_str), + default_value = "false", + env = "SERVER_AS_WINDOWS_SERVICE" + )] + /// Run the web server as a Windows Service. + pub as_windows_service: bool, + + // WINDOWS-ONLY: + #[structopt(subcommand)] + pub commands: Option, +} + +#[derive(Debug, StructOpt)] +pub enum Commands { + /// Install a Windows Service for the server. + #[structopt(name = "install")] + Install {}, + + /// Uninstall the current Windows Service. + #[structopt(name = "uninstall")] + Uninstall {}, } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index e168204..64cc6b3 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -7,6 +7,8 @@ use crate::{Context, Result}; mod cli; pub mod file; +pub use cli::Commands; + use cli::General; /// The `headers` file options. @@ -203,6 +205,10 @@ impl Settings { threads_multiplier, grace_period, page_fallback, + + // WINDOWS-ONLY: sub commands + as_windows_service: opts.as_windows_service, + commands: opts.commands, }, advanced: settings_advanced, }) diff --git a/src/signals.rs b/src/signals.rs index 3694272..a5f3d97 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,10 +1,13 @@ +use tokio::time::{sleep, Duration}; + #[cfg(unix)] use { crate::Result, futures_util::stream::StreamExt, signal_hook::consts::signal::*, signal_hook_tokio::Signals, }; -use tokio::time::{sleep, Duration}; +#[cfg(windows)] +use std::sync::mpsc::{Receiver, RecvTimeoutError}; #[cfg(unix)] /// It creates a common list of signals stream for `SIGTERM`, `SIGINT` and `SIGQUIT` to be observed. @@ -48,10 +51,26 @@ async fn delay_graceful_shutdown(grace_period_secs: u8) { #[cfg(windows)] /// It waits for an incoming `ctrl+c` signal on Windows. -pub async fn wait_for_ctrl_c(grace_period_secs: u8) { - tokio::signal::ctrl_c() - .await - .expect("failed to install ctrl+c signal handler"); +pub async fn wait_for_ctrl_c(cancel: Option>, grace_period_secs: u8) { + if let Some(recv) = cancel { + async { + loop { + match recv.recv_timeout(Duration::from_secs(60)) { + // Break the loop either upon stop or channel disconnect + Ok(_) | Err(RecvTimeoutError::Disconnected) => break, + + // Continue work if no events were received within the timeout + Err(RecvTimeoutError::Timeout) => (), + } + } + } + .await; + } else { + tokio::signal::ctrl_c() + .await + .expect("failed to install ctrl+c signal handler"); + } + delay_graceful_shutdown(grace_period_secs).await; tracing::info!("delegating server's graceful shutdown"); } diff --git a/src/winservice.rs b/src/winservice.rs new file mode 100644 index 0000000..0eccf15 --- /dev/null +++ b/src/winservice.rs @@ -0,0 +1,194 @@ +use std::env; +use std::ffi::OsString; +use std::thread; +use std::time::Duration; + +use windows_service::{ + define_windows_service, + service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceDependency, + ServiceErrorControl, ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, + ServiceStatus, ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult, ServiceStatusHandle}, + service_dispatcher, + service_manager::{ServiceManager, ServiceManagerAccess}, +}; + +use crate::{logger, Context, Result, Server, Settings}; + +const SERVICE_NAME: &str = "static-web-server"; +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; +const SERVICE_EXE: &str = "static-web-server.exe"; +const SERVICE_DESC: &str = "A blazing fast and asynchronous web server for static files-serving"; +const SERVICE_DISPLAY_NAME: &str = "Static Web Server"; + +// Generate the windows service boilerplate. +// The boilerplate contains the low-level service entry function (ffi_service_main) +// that parses incoming service arguments into Vec and passes them to +// user defined service entry (my_service_main). +define_windows_service!(ffi_service_main, my_service_main); + +fn set_service_state( + status_handle: &ServiceStatusHandle, + current_state: ServiceState, +) -> Result<()> { + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: SERVICE_TYPE, + // The new state + current_state, + // Accept stop events when running + controls_accepted: ServiceControlAccept::STOP, + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + // Unused for setting status + process_id: None, + }; + + // Tell the system that the service is now running + Ok(status_handle.set_service_status(next_status)?) +} + +fn my_service_main(_args: Vec) { + if let Err(err) = run_service() { + println!("error starting the service: {:?}", err); + } +} + +fn run_service() -> Result<()> { + let opts = Settings::get()?; + logger::init(&opts.general.log_level)?; + + println!("sws service started"); + + // Create a channel to be able to poll a stop event from the service worker loop. + // let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel(); + let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel(); + + // Define system service event handler that will be receiving service events. + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + // Notifies a service to report its current status information to the service + // control manager. Always return NoError even if not implemented. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + + // Handle stop + ServiceControl::Stop => { + shutdown_tx.send(()).unwrap(); + ServiceControlHandlerResult::NoError + } + + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler. + // The returned status handle should be used to report service status changes to the system. + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + println!("registering sws service"); + + // Service starts + set_service_state(&status_handle, ServiceState::StartPending)?; + println!("sws service start pending"); + + match Server::new(Some(shutdown_rx), false) { + Ok(server) => { + // Service is running + set_service_state(&status_handle, ServiceState::Running).unwrap(); + println!("sws service running"); + + let r = server.run(); + if r.is_err() { + println!("error starting the server: {:?}", r.unwrap_err()); + } + + set_service_state(&status_handle, ServiceState::Stopped).unwrap(); + println!("sws service stopping"); + } + Err(err) => { + println!("error starting the server: {:?}", err); + std::process::exit(1); + } + } + + set_service_state(&status_handle, ServiceState::Stopped)?; + println!("sws service stopping"); + + Ok(()) +} + +pub fn run_server_as_service() -> Result<()> { + // Set current directory to the same as the executable + let mut path = env::current_exe().unwrap(); + path.pop(); + env::set_current_dir(&path).unwrap(); + + // Register generated `ffi_service_main` with the system and start the + // service, blocking this thread until the service is stopped + service_dispatcher::start(&SERVICE_NAME, ffi_service_main) + .with_context(|| "error registering generated `ffi_service_main` with the system")?; + Ok(()) +} + +/// Install a Windows Service for SWS. +pub fn install_service() -> Result { + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; + + // Set the executable path to point the current binary + let service_binary_path = std::env::current_exe().unwrap().with_file_name(SERVICE_EXE); + + // Run the service as `System` + let service_info = ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY_NAME), + service_type: SERVICE_TYPE, + start_type: ServiceStartType::OnDemand, + error_control: ServiceErrorControl::Normal, + executable_path: service_binary_path, + launch_arguments: vec![OsString::from("--as-windows-service=true")], + dependencies: vec![ServiceDependency::from_system_identifier("+network")], + account_name: None, // run as System + account_password: None, + }; + + let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; + service.set_description(SERVICE_DESC)?; + + println!( + "Windows service ({}) is installed successfully!", + SERVICE_DISPLAY_NAME + ); + + Ok(()) +} + +/// Uninstall the current Windows Service for SWS. +pub fn uninstall_service() -> Result { + let manager_access = ServiceManagerAccess::CONNECT; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; + + let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; + let service = service_manager.open_service(SERVICE_NAME, service_access)?; + + let service_status = service.query_status()?; + if service_status.current_state != ServiceState::Stopped { + service.stop()?; + // Wait for service to stop + thread::sleep(Duration::from_secs(1)); + } + + service.delete()?; + + println!( + "Windows service ({}) is uninstalled successfully!", + SERVICE_DISPLAY_NAME + ); + + Ok(()) +} -- libgit2 1.7.2