use std::net::TcpListener;
use std::sync::Arc;
use tokio::sync::watch::Receiver;
use crate::handler::RequestHandler;
use crate::service::RouterService;
use crate::{Context, Result, Settings};
mod http1;
mod listener;
mod opts;
#[cfg(feature = "tls")]
mod http1_tls;
#[cfg(feature = "http2")]
mod http2;
#[cfg(feature = "tls")]
mod redirect;
#[cfg(unix)]
mod uds;
#[cfg(feature = "tls")]
pub(crate) struct TlsConfig {
pub tls_cert: std::path::PathBuf,
pub tls_key: std::path::PathBuf,
pub https_redirect: bool,
pub https_redirect_host: String,
pub https_redirect_from_port: u16,
pub https_redirect_from_hosts: String,
pub host: String,
pub port: u16,
pub page404: std::path::PathBuf,
pub page50x: std::path::PathBuf,
}
pub(crate) struct ShutdownCtx {
pub grace_period: u8,
pub cancel_recv: Option<Receiver<()>>,
#[cfg(windows)]
pub windows_service: bool,
#[cfg(windows)]
pub ctrl_c_recv: Receiver<()>,
#[cfg(windows)]
pub ctrlc_task: tokio::task::JoinHandle<crate::Result<()>>,
}
pub struct Server {
opts: Settings,
worker_threads: usize,
max_blocking_threads: usize,
pre_bound_listener: Option<(TcpListener, String)>,
}
impl Server {
pub fn new(opts: Settings) -> Result<Server> {
let cpus = std::thread::available_parallelism()
.with_context(|| {
"unable to get current platform cpus or lack of permissions to query available parallelism"
})?
.get();
let worker_threads = match opts.general.threads_multiplier {
0 | 1 => cpus,
n => cpus * n,
};
let max_blocking_threads = opts.general.max_blocking_threads;
Ok(Server {
opts,
worker_threads,
max_blocking_threads,
pre_bound_listener: None,
})
}
pub fn with_pre_bound_listener(mut self, listener: std::net::TcpListener) -> Self {
let addr = listener
.local_addr()
.map(|a| a.to_string())
.unwrap_or_else(|_| "pre-bound".into());
self.pre_bound_listener = Some((listener, addr));
self
}
pub fn run_standalone(self, cancel: Option<Receiver<()>>) -> Result {
self.run_server_on_rt(cancel, || {}, true)
}
#[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, true)
}
pub fn run_server_on_rt<F>(
self,
cancel_recv: Option<Receiver<()>>,
cancel_fn: F,
exit_on_error: bool,
) -> Result
where
F: FnOnce(),
{
tracing::debug!(
%self.worker_threads,
"initializing tokio runtime with multi-threaded scheduler"
);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(self.worker_threads)
.max_blocking_threads(self.max_blocking_threads)
.thread_name("static-web-server")
.enable_all()
.build()?;
let res = rt.block_on(async {
tracing::trace!("tokio runtime initialized");
self.start_server(cancel_recv, cancel_fn).await
});
if let Err(err) = &res {
tracing::error!("server failed to start up: {:?}", err);
if exit_on_error {
std::process::exit(1)
}
}
res
}
async fn start_server<F>(self, cancel_recv: Option<Receiver<()>>, cancel_fn: F) -> Result
where
F: FnOnce(),
{
tracing::trace!("starting web server");
tracing::info!(
name = env!("CARGO_PKG_NAME"),
version = env!("CARGO_PKG_VERSION"),
"starting Static Web Server"
);
let general = self.opts.general;
let advanced = self.opts.advanced;
let pre_bound = self.pre_bound_listener;
tracing::info!(log_level = %general.log_level, "log level");
if general.config_file.is_file() {
tracing::info!(path = %general.config_file.display(), "config file used");
} else {
tracing::debug!(
"config file path not found or not a regular file: {}",
general.config_file.display()
);
}
#[cfg(unix)]
let unix_listener_info = if let Some(path) = general.unix_socket.as_ref() {
use crate::server::listener::create_unix_listener;
Some(create_unix_listener(
path,
general.unix_socket_mode,
general.unix_socket_force,
)?)
} else {
None
};
#[cfg(unix)]
let tcp_listener_info = if unix_listener_info.is_none() {
Some(match pre_bound {
Some(pre) => pre,
None => crate::server::listener::create_tcp_listener(&general)?,
})
} else {
None
};
#[cfg(not(unix))]
let tcp_listener_info = Some(match pre_bound {
Some(pre) => pre,
None => crate::server::listener::create_tcp_listener(&general)?,
});
tracing::info!(
worker_threads = self.worker_threads,
"runtime worker threads"
);
tracing::info!(
max_blocking_threads = general.max_blocking_threads,
"runtime max blocking threads"
);
tracing::info!(
grace_period_seconds = general.grace_period,
"grace period before graceful shutdown"
);
let opts_result = opts::init(&general, advanced)?;
let router_service = RouterService::new(RequestHandler {
opts: Arc::from(opts_result.handler_opts),
});
#[cfg(windows)]
let (sender, ctrl_c_recv) = tokio::sync::watch::channel(());
#[cfg(windows)]
let windows_service = general.windows_service;
#[cfg(windows)]
let ctrlc_task = tokio::spawn(async move {
if !windows_service {
tracing::info!("installing graceful shutdown ctrl+c signal handler");
if let Err(err) = tokio::signal::ctrl_c().await {
return Err(
crate::Error::new(err).context("failed to install ctrl+c signal handler")
);
}
tracing::info!("graceful shutdown ctrl+c signal received");
let _ = sender.send(());
}
Ok::<_, crate::Error>(())
});
let ctx = ShutdownCtx {
grace_period: general.grace_period,
cancel_recv,
#[cfg(windows)]
windows_service,
#[cfg(windows)]
ctrl_c_recv,
#[cfg(windows)]
ctrlc_task,
};
#[cfg(unix)]
if let Some((unix_listener, socket_path, addr_str)) = unix_listener_info {
return uds::run(
unix_listener,
socket_path,
router_service,
&addr_str,
self.worker_threads,
ctx,
cancel_fn,
)
.await;
}
let (tcp_listener, addr_str) = tcp_listener_info.unwrap();
#[cfg(feature = "tls")]
if general.tls {
let tls_cert = general
.tls_cert
.ok_or_else(|| anyhow!("TLS cert file path is required when --tls is enabled"))?;
let tls_key = general
.tls_key
.ok_or_else(|| anyhow!("TLS key file path is required when --tls is enabled"))?;
let tls_cfg = TlsConfig {
tls_cert,
tls_key,
https_redirect: general.https_redirect,
https_redirect_host: general.https_redirect_host,
https_redirect_from_port: general.https_redirect_from_port,
https_redirect_from_hosts: general.https_redirect_from_hosts,
host: general.host,
port: general.port,
page404: opts_result.page404,
page50x: opts_result.page50x,
};
#[cfg(feature = "http2")]
if general.http2 {
return http2::run(
tcp_listener,
router_service,
&addr_str,
self.worker_threads,
tls_cfg,
ctx,
cancel_fn,
)
.await;
}
return http1_tls::run(
tcp_listener,
router_service,
&addr_str,
self.worker_threads,
tls_cfg,
ctx,
cancel_fn,
)
.await;
}
http1::run(
tcp_listener,
router_service,
&addr_str,
self.worker_threads,
ctx,
cancel_fn,
)
.await
}
}