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}