Skip to main content

webserver_base/webserver/
bootstrap.rs

1//! Process start-up, in the one order that works.
2
3use std::future::Future;
4
5use super::shutdown::Shutdown;
6
7/// Runs `body` with observability, a runtime, and signal handling set up.
8///
9/// The ordering is the point: observability on the main thread first (so the
10/// Sentry hub reaches the runtime's workers), then the runtime, then one signal
11/// listener whose [`Shutdown`] handle is passed to `body`, then the guard drops
12/// after `body` returns — flushing errors raised during the shutdown itself.
13///
14/// The application still owns `main`, so it can start as many servers as it
15/// likes.
16///
17/// It also owns the build-tool subcommands, so no project has to declare a
18/// second binary or write a line of glue to reach them:
19///
20/// ```text
21/// $ my-server gen-static-assets    # icons, then hash everything but scripts
22/// $ my-server gen-static-scripts   # hash the built JavaScript
23/// $ my-server                      # serve
24/// ```
25///
26/// Those two exit before any runtime or error monitoring starts. They still
27/// read `WSB_ENVIRONMENT`, because `main` resolves it before calling in — set
28/// it to `local` in the build stage.
29///
30/// ```no_run
31/// use webserver_base::{WebServer, WebServerError, bootstrap};
32///
33/// fn main() -> Result<(), WebServerError> {
34///     bootstrap!(|shutdown| async move { WebServer::from_env()?.run(shutdown).await })
35/// }
36/// ```
37///
38/// # Errors
39///
40/// Whatever `body` returns. `bootstrap` adds no failure of its own.
41///
42/// # Panics
43///
44/// If the Tokio runtime cannot be built. There is no useful way to continue,
45/// and no server to report it to yet.
46pub fn bootstrap_with_release<F, Fut, T, E>(release: &str, body: F) -> Result<T, E>
47where
48    F: FnOnce(Shutdown) -> Fut,
49    Fut: Future<Output = Result<T, E>>,
50    E: From<super::error::WebServerError>,
51{
52    // Build-tool modes run and exit before the runtime, before error
53    // monitoring, and before anything binds a port. Handling them here is what
54    // lets every project reach the static-asset pipeline through its own server
55    // binary, with no shim binary and no duplicated glue.
56    if let Some(phase) = std::env::args()
57        .nth(1)
58        .as_deref()
59        .and_then(crate::assets::Phase::from_subcommand)
60    {
61        match crate::assets::generate_static_assets(phase) {
62            Ok(()) => std::process::exit(0),
63            Err(error) => {
64                eprintln!("{error}");
65                std::process::exit(1);
66            }
67        }
68    }
69
70    // Resolved here rather than in every `main`: it is the same three lines in
71    // every project, and one of them is easy to get in the wrong order.
72    #[cfg(feature = "observability")]
73    let _guard: crate::observability::ObservabilityGuard = {
74        let environment: crate::Environment = crate::Environment::from_env()
75            .map_err(|error| E::from(super::error::WebServerError::from(error)))?;
76        crate::observability::Observability::from_env(environment)
77            .map_err(|error| E::from(super::error::WebServerError::from(error)))?
78            .with_release(release)
79            .init()
80    };
81
82    let runtime: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread()
83        .enable_all()
84        .build()
85        .expect("failed to build the tokio runtime");
86
87    runtime.block_on(async move {
88        let shutdown: Shutdown = Shutdown::listen();
89        body(shutdown).await
90    })
91    // `_guard` drops here, after the drain.
92}
93
94/// Starts a server, naming the running build from the *calling* crate.
95///
96/// A macro rather than a function because the release string has to come from
97/// the application's own `CARGO_PKG_NAME` and `CARGO_PKG_VERSION`, and those are
98/// resolved where the code is written. Called from inside this library — as
99/// `sentry::release_name!()` does — every project would report the same
100/// release, and Sentry could not tell one site's deploys from another's.
101///
102/// ```no_run
103/// use webserver_base::{WebServer, WebServerError, bootstrap};
104///
105/// fn main() -> Result<(), WebServerError> {
106///     bootstrap!(|shutdown| async move { WebServer::from_env()?.run(shutdown).await })
107/// }
108/// ```
109#[macro_export]
110macro_rules! bootstrap {
111    ($body:expr) => {
112        $crate::webserver::bootstrap_with_release(
113            concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
114            $body,
115        )
116    };
117}