feat: preliminary windows service support
via new argument `--as-windows-service [-s]`
and `install` and `uninstall` commands
Diff
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(-)
@@ -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"
@@ -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"
@@ -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;
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(())
}
@@ -28,6 +28,9 @@ pub mod static_files;
pub mod tls;
pub mod transport;
#[cfg(windows)]
pub mod winservice;
#[macro_use]
pub mod error;
@@ -1,11 +1,21 @@
use std::io;
use tracing::Level;
use tracing_subscriber::fmt::format::FmtSpan;
use crate::Result;
use crate::{Context, Result};
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(())
}
pub fn init(level: &str) -> Result {
fn configure(level: &str) -> Result {
let level = level.parse::<Level>()?;
#[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)
@@ -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<Receiver<()>>,
}
impl Server {
pub fn new() -> Result<Server> {
pub fn new(cancel: Option<Receiver<()>>) -> Result<Server> {
let opts = Settings::get()?;
@@ -28,11 +30,22 @@ impl Server {
n => cpus * n,
};
Ok(Server { opts, threads })
Ok(Server {
opts,
threads,
cancel,
})
}
pub fn run(self) -> Result {
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 {
let advanced_opts = self.opts.advanced;
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());
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),
@@ -169,4 +169,29 @@ pub struct General {
#[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")]
pub config_file: Option<PathBuf>,
#[structopt(
long,
short = "s",
parse(try_from_str),
default_value = "false",
env = "SERVER_AS_WINDOWS_SERVICE"
)]
pub as_windows_service: bool,
#[structopt(subcommand)]
pub commands: Option<Commands>,
}
#[derive(Debug, StructOpt)]
pub enum Commands {
#[structopt(name = "install")]
Install {},
#[structopt(name = "uninstall")]
Uninstall {},
}
@@ -7,6 +7,8 @@ use crate::{Context, Result};
mod cli;
pub mod file;
pub use cli::Commands;
use cli::General;
@@ -203,6 +205,10 @@ impl Settings {
threads_multiplier,
grace_period,
page_fallback,
as_windows_service: opts.as_windows_service,
commands: opts.commands,
},
advanced: settings_advanced,
})
@@ -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)]
@@ -48,10 +51,26 @@ async fn delay_graceful_shutdown(grace_period_secs: u8) {
#[cfg(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<Receiver<()>>, grace_period_secs: u8) {
if let Some(recv) = cancel {
async {
loop {
match recv.recv_timeout(Duration::from_secs(60)) {
Ok(_) | Err(RecvTimeoutError::Disconnected) => break,
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");
}
@@ -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";
define_windows_service!(ffi_service_main, my_service_main);
fn set_service_state(
status_handle: &ServiceStatusHandle,
current_state: ServiceState,
) -> Result<()> {
let next_status = ServiceStatus {
service_type: SERVICE_TYPE,
current_state,
controls_accepted: ServiceControlAccept::STOP,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
};
Ok(status_handle.set_service_status(next_status)?)
}
fn my_service_main(_args: Vec<OsString>) {
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");
let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel();
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop => {
shutdown_tx.send(()).unwrap();
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
println!("registering sws service");
set_service_state(&status_handle, ServiceState::StartPending)?;
println!("sws service start pending");
match Server::new(Some(shutdown_rx), false) {
Ok(server) => {
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<()> {
let mut path = env::current_exe().unwrap();
path.pop();
env::set_current_dir(&path).unwrap();
service_dispatcher::start(&SERVICE_NAME, ffi_service_main)
.with_context(|| "error registering generated `ffi_service_main` with the system")?;
Ok(())
}
pub fn install_service() -> Result {
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
let service_binary_path = std::env::current_exe().unwrap().with_file_name(SERVICE_EXE);
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, 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(())
}
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()?;
thread::sleep(Duration::from_secs(1));
}
service.delete()?;
println!(
"Windows service ({}) is uninstalled successfully!",
SERVICE_DISPLAY_NAME
);
Ok(())
}