index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2022-05-18 22:06:20.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2022-05-18 22:06:20.0 +00:00:00
commit
df293b9e0b85b6efceb1d5906f4f11932962c145 [patch]
tree
ad1f8a497bc3c42c862f63d52a6a629de44c4524
parent
0879c84fa2a63e983f4a8596ccafd6780a2d12e8
download
df293b9e0b85b6efceb1d5906f4f11932962c145.tar.gz

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(-)

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::<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)
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<Receiver<()>>,
}

impl Server {
    /// Create new multi-thread server instance.
    pub fn new() -> Result<Server> {
    pub fn new(cancel: Option<Receiver<()>>) -> Result<Server> {
        // 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<PathBuf>,

    #[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<Commands>,
}

#[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<Receiver<()>>, 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<OsString> 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<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");

    // 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(())
}