index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2022-05-23 14:03:43.0 +00:00:00
committer GitHub <noreply@github.com> 2022-05-23 14:03:43.0 +00:00:00
commit
3d1776d6fb908c679e7f4a780c44892baf50a229 [patch]
tree
47056303438de8412c4261024d9abb9695b78e6e
parent
0879c84fa2a63e983f4a8596ccafd6780a2d12e8
parent
ae0dcfdc939dc03197b34e33f3a4a6240b3c4012
download
3d1776d6fb908c679e7f4a780c44892baf50a229.tar.gz

Merge pull request #110 from joseluisq/feature/windows_service_support

feat: windows service support

resolves #65

Diff

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

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 <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::<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..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<F>(self, cancel: Option<Receiver<()>>, 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<F>(self, cancel_recv: Option<Receiver<()>>, 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<F>(self, _cancel_recv: Option<Receiver<()>>, _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<PathBuf>,

    //
    // 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<Commands>,
}

#[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<F>(
    cancel_recv: Option<Receiver<()>>,
    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<OsString> 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<OsString>) {
    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<PathBuf>) -> 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(())
}