From 3d1776d6fb908c679e7f4a780c44892baf50a229 Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Mon, 23 May 2022 16:03:43 +0200 Subject: [PATCH] Merge pull request #110 from joseluisq/feature/windows_service_support feat: windows service support resolves #65 --- Cargo.lock | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++++ README.md | 1 + docs/content/configuration/command-line-arguments.md | 14 ++++++++++++++ docs/content/configuration/environment-variables.md | 7 +++++++ docs/content/features/windows-service.md | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/content/index.md | 1 + docs/mkdocs.yml | 1 + src/bin/server.rs | 26 ++++++++++++++++++++++++-- src/lib.rs | 3 +++ src/logger.rs | 18 ++++++++++++++---- src/server.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++-------------- src/settings/cli.rs | 31 +++++++++++++++++++++++++++++++ src/settings/mod.rs | 10 ++++++++++ src/signals.rs | 27 ++++++++++++++++++++++----- src/winservice.rs | 247 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 16 files changed, 625 insertions(+), 25 deletions(-) create mode 100644 docs/content/features/windows-service.md create mode 100644 src/winservice.rs diff --git a/Cargo.lock b/Cargo.lock index 5bda3d0..c011330 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/joseluisq/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..98bf276 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/joseluisq/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/README.md b/README.md index 67ee839..020c464 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ It's cross-platform and available for `Linux`, `macOS`, `Windows` and `FreeBSD` - Basic HTTP Authentication. - Customizable HTTP Response Headers for specific file requests via glob patterns. - Fallback pages for 404 errors useful for Single-page applications. +- Run the server as [Windows Service](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783643(v=ws.10)). - Configurable using CLI arguments, environment variables or a file. - Default and custom error pages. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available. diff --git a/docs/content/configuration/command-line-arguments.md b/docs/content/configuration/command-line-arguments.md index e167887..0be9bcc 100644 --- a/docs/content/configuration/command-line-arguments.md +++ b/docs/content/configuration/command-line-arguments.md @@ -100,3 +100,17 @@ OPTIONS: 32,768 though it is advised to keep this value on the smaller side [env: SERVER_THREADS_MULTIPLIER=] [default: 1] ``` + +## Windows + +Following options and commands are Windows platform specific. + +``` + -s, --windows-service + Run the web server as a Windows Service [env: SERVER_WINDOWS_SERVICE=] [default: false] + +SUBCOMMANDS: + help Prints this message or the help of the given subcommand(s) + install Install a Windows Service for the web server + uninstall Uninstall the current Windows Service +``` diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md index bab3034..04dbe09 100644 --- a/docs/content/configuration/environment-variables.md +++ b/docs/content/configuration/environment-variables.md @@ -71,3 +71,10 @@ Enable cache control headers for incoming requests based on a set of file types. ### SERVER_BASIC_AUTH It provides [The "Basic" HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) using credentials as `user-id:password` pairs, encoded using `Base64`. Password must be encoded using the [BCrypt](https://en.wikipedia.org/wiki/Bcrypt) password-hashing function. Default empty (disabled). + +## Windows + +Following options and commands are Windows platform specific. + +### SERVER_WINDOWS_SERVICE +Run the web server as a Windows Service. See [more details](../features/windows-service.md). diff --git a/docs/content/features/windows-service.md b/docs/content/features/windows-service.md new file mode 100644 index 0000000..7523042 --- /dev/null +++ b/docs/content/features/windows-service.md @@ -0,0 +1,143 @@ +# Windows Service + +**`SWS`** can be also executed as a [Windows Service](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783643(v=ws.10)). + +This feature is disabled by default and can be controlled by the boolean `-s, --windows-service` option or the equivalent [SERVER_WINDOWS_SERVICE](./../configuration/environment-variables.md#server_windows_service) env. + +![Static Web Server running as a Windows Service](https://user-images.githubusercontent.com/1700322/169807572-d62a7bab-b596-4597-85f7-31a7c02aeefe.png) +> _Static Web Server running as a Windows Service and displayed by 'services.msc' application._ + +**Important notes** + +- This is a Windows platform specific feature. It means the `--windows-service` argument and its env will not be present in Unix-like systems. +- Running SWS as a Windows service doesn't require to enable it via the [configuration file](../configuration/config-file.md) (`windows-service = true`) because it's already enabled during the service installation. + +## Service privileges + +To either install or uninstall the SWS Windows service requires *administrator* privileges, so make sure to open the terminal application as administrator or give to your [Powershell](https://docs.microsoft.com/en-us/powershell/scripting/overview?view=powershell-7.2) session enough privileges otherwise you will get an `"Access is denied"` error. + +We recommend a [Powershell](https://docs.microsoft.com/en-us/powershell/scripting/overview?view=powershell-7.2) session with administrator privileges. + +## Install the service + +To install the SWS service use the `install` command along with a [configuration file](../configuration/config-file.md) for further SWS options customization. + +Make sure to provide a configuration file to run SWS service properly. In particular, configure the server `address`, `port` and `root` directory accordingly. +If not so then the service might not start. + +The following command will create the SWS service called `static-web-server` with a "`Static Web Server`" display name. + +```powershell +static-web-server.exe -w C:\Users\MyUser\config.toml install +# Windows Service (static-web-server) is installed successfully! +# Start the service typing: sc.exe start "static-web-server" (it requires administrator privileges) or using the 'services.msc' application. +``` + +## Interact with the service + +SWS doesn't provide a way to interact with Windows services directly. Instead, use the Windows built-in tools to interact with the SWS service once created. + +For that purpose you can use either the Windows [sc.exe](https://docs.microsoft.com/en-us/windows/win32/services/configuring-a-service-using-sc) or the [services.msc](https://docs.microsoft.com/en-us/windows/win32/services/services) application. + +For example, using `sc.exe` you can show the SWS service configuration used once installed. + +```powershell +sc.exe qc "static-web-server" +# [SC] QueryServiceConfig SUCCESS + +# SERVICE_NAME: static-web-server +# TYPE : 10 WIN32_OWN_PROCESS +# START_TYPE : 3 DEMAND_START +# ERROR_CONTROL : 1 NORMAL +# BINARY_PATH_NAME : C:\Users\MyUser\static-web-server.exe +# --windows-service=true +# --config-file=C:\Users\MyUser\config.toml +# LOAD_ORDER_GROUP : +# TAG : 0 +# DISPLAY_NAME : Static Web Server +# DEPENDENCIES : +# SERVICE_START_NAME : LocalSystem +``` + +Remember that alternatively you can also use the `services.msc` application if you prefer GUI service control. + +### Start + +To start the service use the following `sc.exe` command. + +```powershell +sc.exe start "static-web-server" +# SERVICE_NAME: static-web-server +# TYPE : 10 WIN32_OWN_PROCESS +# STATE : 2 START_PENDING +# (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN) +# WIN32_EXIT_CODE : 0 (0x0) +# SERVICE_EXIT_CODE : 0 (0x0) +# CHECKPOINT : 0x0 +# WAIT_HINT : 0x7d0 +# PID : 3068 +# FLAGS : +``` + +### Status + +To show the service status use the following `sc.exe` command. + +```powershell +sc.exe query "static-web-server" +# SERVICE_NAME: static-web-server +# TYPE : 10 WIN32_OWN_PROCESS +# STATE : 4 RUNNING +# (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN) +# WIN32_EXIT_CODE : 0 (0x0) +# SERVICE_EXIT_CODE : 0 (0x0) +# CHECKPOINT : 0x0 +# WAIT_HINT : 0x0 +``` + +### Stop + +To stop the service use the following `sc.exe` command. + +```powershell +sc.exe stop "static-web-server" +# SERVICE_NAME: static-web-server +# TYPE : 10 WIN32_OWN_PROCESS +# STATE : 3 STOP_PENDING +# (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN) +# WIN32_EXIT_CODE : 0 (0x0) +# SERVICE_EXIT_CODE : 0 (0x0) +# CHECKPOINT : 0x2 +# WAIT_HINT : 0xbb8 +``` + +After stopping the service you can also show its status. + +```powershell +sc.exe query "static-web-server" +# SERVICE_NAME: static-web-server +# TYPE : 10 WIN32_OWN_PROCESS +# STATE : 1 STOPPED +# WIN32_EXIT_CODE : 0 (0x0) +# SERVICE_EXIT_CODE : 0 (0x0) +# CHECKPOINT : 0x0 +# WAIT_HINT : 0x0 +``` + +## Uninstall the service + +To uninstall the SWS service just use the `uninstall` command. Note that the service should be first stopped before to uninstall it. + +```powershell +static-web-server.exe uninstall +# Windows Service (static-web-server) is uninstalled! +``` + +After uninstalling the service you can verify if removed. + +```powershell +sc.exe qc "static-web-server" +# [SC] OpenService FAILED 1060: +# +# The specified service does not exist as an installed service. +``` diff --git a/docs/content/index.md b/docs/content/index.md index 96e4138..aee440a 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -58,6 +58,7 @@ It's cross-platform and available for `Linux`, `macOS`, `Windows` and `FreeBSD` - Basic HTTP Authentication. - Customizable HTTP Response Headers for specific file requests via glob patterns. - Fallback pages for 404 errors useful for Single-page applications. +- Run the server as [Windows Service](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783643(v=ws.10)). - Configurable using CLI arguments, environment variables or a file. - Default and custom error pages. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 898d8f5..42e4759 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -139,6 +139,7 @@ nav: - 'Worker Threads Customization': 'features/worker-threads.md' - 'Error Pages': 'features/error-pages.md' - 'Custom HTTP Headers': 'features/custom-http-headers.md' + - 'Windows Service': 'features/windows-service.md' - 'Platforms & Architectures': 'platforms-architectures.md' - 'Migration from v1 to v2': 'migration.md' - 'Changelog v2 (latest stable)': 'https://github.com/joseluisq/static-web-server/blob/master/CHANGELOG.md' diff --git a/src/bin/server.rs b/src/bin/server.rs index 05663a6..4d7732c 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -7,10 +7,32 @@ #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -use static_web_server::{Result, Server}; +use static_web_server::Result; fn main() -> Result { - Server::new()?.run()?; + #[cfg(windows)] + { + use static_web_server::settings::{Commands, Settings}; + use static_web_server::winservice; + + let opts = Settings::get()?; + + if let Some(commands) = opts.general.commands { + match commands { + Commands::Install {} => { + return winservice::install_service(opts.general.config_file); + } + Commands::Uninstall {} => { + return winservice::uninstall_service(); + } + } + } else if opts.general.windows_service { + return winservice::run_server_as_service(); + } + } + + // Run the server by default + static_web_server::Server::new()?.run_standalone()?; 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..1540a4c 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(&log_level).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..77f469b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,6 +3,7 @@ use hyper::server::Server as HyperServer; use listenfd::ListenFd; use std::net::{IpAddr, SocketAddr, TcpListener}; use std::sync::Arc; +use tokio::sync::oneshot::Receiver; use crate::handler::{RequestHandler, RequestHandlerOpts}; use crate::tls::{TlsAcceptor, TlsConfigBuilder}; @@ -31,17 +32,38 @@ impl Server { Ok(Server { opts, threads }) } - /// Build and run the multi-thread `Server`. - pub fn run(self) -> Result { + /// Build and run the multi-thread `Server` as standalone. + pub fn run_standalone(self) -> Result { + // Logging system initialization + logger::init(&self.opts.general.log_level)?; + + self.run_server_on_rt(None, || {}) + } + + /// Build and run the multi-thread `Server` which will be used by a Windows service. + #[cfg(windows)] + pub fn run_as_service(self, cancel: Option>, cancel_fn: F) -> Result + where + F: FnOnce(), + { + self.run_server_on_rt(cancel, cancel_fn) + } + + /// Build and run the multi-thread `Server` on Tokio runtime. + fn run_server_on_rt(self, cancel_recv: Option>, cancel_fn: F) -> Result + where + F: FnOnce(), + { + tracing::debug!("initializing tokio runtime with multi thread scheduler"); + tokio::runtime::Builder::new_multi_thread() .worker_threads(self.threads) .thread_name("static-web-server") .enable_all() .build()? .block_on(async { - let r = self.start_server().await; - if r.is_err() { - println!("server failed to start up: {:?}", r.unwrap_err()); + if let Err(err) = self.start_server(cancel_recv, cancel_fn).await { + tracing::error!("server failed to start up: {:?}", err); std::process::exit(1) } }); @@ -51,18 +73,16 @@ impl Server { /// Run the inner Hyper `HyperServer` (HTTP1/HTTP2) forever on the current thread // using the given configuration. - async fn start_server(self) -> Result { + async fn start_server(self, _cancel_recv: Option>, _cancel_fn: F) -> Result + where + F: FnOnce(), + { // Config "general" options let general = self.opts.general; // 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()); @@ -107,7 +127,7 @@ impl Server { // Number of worker threads option let threads = self.threads; - tracing::info!("runtime worker threads: {}", self.threads); + tracing::info!("runtime worker threads: {}", threads); // Security Headers option let security_headers = general.security_headers; @@ -209,7 +229,11 @@ 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( + _cancel_recv, + _cancel_fn, + grace_period, + )); tracing::info!( parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads), @@ -232,6 +256,10 @@ impl Server { #[cfg(unix)] let handle = signals.handle(); + tcp_listener + .set_nonblocking(true) + .with_context(|| "failed to set TCP non-blocking mode")?; + let server = HyperServer::from_tcp(tcp_listener) .unwrap() .tcp_nodelay(true) @@ -241,7 +269,11 @@ 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( + _cancel_recv, + _cancel_fn, + 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..9f2a033 100644 --- a/src/settings/cli.rs +++ b/src/settings/cli.rs @@ -169,4 +169,35 @@ pub struct General { #[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")] /// Server TOML configuration file path. pub config_file: Option, + + // + // Windows specific arguments and commands + // + #[cfg(windows)] + #[structopt( + long, + short = "s", + parse(try_from_str), + default_value = "false", + env = "SERVER_WINDOWS_SERVICE" + )] + /// Run the web server as a Windows Service. + pub windows_service: bool, + + // Windows commands + #[cfg(windows)] + #[structopt(subcommand)] + pub commands: Option, +} + +#[cfg(windows)] +#[derive(Debug, StructOpt)] +pub enum Commands { + /// Install a Windows Service for the web 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..dcb3707 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -7,6 +7,9 @@ use crate::{Context, Result}; mod cli; pub mod file; +#[cfg(windows)] +pub use cli::Commands; + use cli::General; /// The `headers` file options. @@ -203,6 +206,13 @@ impl Settings { threads_multiplier, grace_period, page_fallback, + + // NOTE: + // Windows-only options and commands + #[cfg(windows)] + windows_service: opts.windows_service, + #[cfg(windows)] + commands: opts.commands, }, advanced: settings_advanced, }) diff --git a/src/signals.rs b/src/signals.rs index 3694272..b5cf5d3 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 tokio::sync::oneshot::Receiver; #[cfg(unix)] /// It creates a common list of signals stream for `SIGTERM`, `SIGINT` and `SIGQUIT` to be observed. @@ -48,10 +51,24 @@ 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_recv: Option>, + cancel_fn: F, + grace_period_secs: u8, +) where + F: FnOnce(), +{ + if let Some(recv) = cancel_recv { + if let Err(err) = recv.await { + tracing::error!("error during cancel recv: {:?}", err) + } + cancel_fn() + } 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..27a40ba --- /dev/null +++ b/src/winservice.rs @@ -0,0 +1,247 @@ +use std::ffi::OsString; +use std::thread; +use std::time::Duration; +use std::{env, path::PathBuf}; + +use windows_service::{ + define_windows_service, + service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, 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 (custom_service_main). +define_windows_service!(ffi_service_main, custom_service_main); + +fn custom_service_main(_args: Vec) { + if let Err(err) = run_service() { + tracing::error!("error starting the service: {:?}", err); + } +} + +/// Assigns a particular server state with its properties. +fn set_service_state( + status_handle: &ServiceStatusHandle, + current_state: ServiceState, + checkpoint: u32, + wait_hint: Duration, +) -> 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, + // Only used for pending states, otherwise must be zero + wait_hint, + // Unused for setting status + process_id: None, + }; + + // Inform the system about the service status + Ok(status_handle.set_service_status(next_status)?) +} + +fn run_service() -> Result { + let opts = Settings::get()?; + + logger::init(&opts.general.log_level)?; + + tracing::info!("windows service: starting service setup"); + + // Create a channel to be able to poll a stop event from the service worker loop. + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + let mut shutdown_tx = Some(shutdown_tx); + + // 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 => { + tracing::debug!("windows service: handled 'ServiceControl::Stop' event"); + if let Some(sender) = shutdown_tx.take() { + tracing::debug!("windows service: delegated 'ServiceControl::Stop' event"); + sender.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)?; + tracing::info!("windows service: registering service"); + + // Service is running + set_service_state( + &status_handle, + ServiceState::Running, + 1, + Duration::default(), + )?; + tracing::info!("windows service: set service 'Running' state"); + + let stop_handler = || { + // Service is stop pending + match set_service_state( + &status_handle, + ServiceState::StopPending, + 2, + Duration::from_secs(3), + ) { + Ok(()) => tracing::info!("windows service: set service 'StopPending' state"), + Err(err) => tracing::error!( + "windows service: error when setting 'StopPending' state: {:?}", + err + ), + } + }; + + // Starting web server + match Server::new() { + Ok(server) => { + if let Err(err) = server.run_as_service(Some(shutdown_rx), stop_handler) { + tracing::error!( + "windows service: error after starting the server: {:?}", + err + ); + } + } + Err(err) => { + tracing::info!("windows service: error starting the server: {:?}", err); + } + } + + // Service is stopped + set_service_state( + &status_handle, + ServiceState::Stopped, + 3, + Duration::from_secs(3), + )?; + tracing::info!("windows service: set service 'Stopped' state"); + + Ok(()) +} + +/// Run web server as Windows Server +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(()) +} + +fn adjust_canonicalization(p: PathBuf) -> String { + const VERBATIM_PREFIX: &str = r#"\\?\"#; + let p = p.to_str().unwrap_or_default(); + let p = if p.starts_with(VERBATIM_PREFIX) { + p.strip_prefix(VERBATIM_PREFIX).unwrap_or_default() + } else { + p + }; + p.to_owned() +} + +/// Install a Windows Service for SWS. +pub fn install_service(config_file: Option) -> 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); + + // Set service binary default arguments + let mut service_binary_arguments = vec![OsString::from("--windows-service=true")]; + + // Append a `--config-file` path to the binary arguments if present + if let Some(f) = config_file { + let f = adjust_canonicalization(f); + if !f.is_empty() { + service_binary_arguments.push(OsString::from(["--config-file=", &f].concat())); + } + } + + // Run the current service as `System` type + 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: service_binary_arguments, + dependencies: vec![], + 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_NAME + ); + println!( + "Start the service typing: sc.exe start \"{}\" (it requires administrator privileges) or using the 'services.msc' application.", + SERVICE_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!", SERVICE_NAME); + + Ok(()) +} -- libgit2 1.7.2