1#[cfg(all(feature = "systemd", target_os = "linux"))]
2use std::env::var_os;
3use std::{
4 sync::{Arc, Weak, atomic::Ordering},
5 time::Duration,
6};
7
8use futures::{FutureExt, future::join, pin_mut};
9#[cfg(all(feature = "systemd", target_os = "linux"))]
10use sd_notify::{NotifyState, notify, notify_and_unset_env, watchdog_enabled};
11use tokio::time::{MissedTickBehavior, interval};
12use tuwunel_core::{
13 Error, Result, Server, debug, debug_error, debug_info, defer, error, info, utils::BoolExt,
14 warn,
15};
16use tuwunel_service::Services;
17
18use crate::{handle::ServerHandle, serve};
19
20const STARTUP_INTERVAL: Duration = Duration::from_secs(15);
26
27#[tracing::instrument(skip_all)]
29pub(crate) async fn run(services: Arc<Services>) -> Result {
30 let server = &services.server;
31 debug!("Start");
32
33 tuwunel_admin::init(&services.admin);
35
36 services.admin.startup_execute().await?;
38
39 let handle = ServerHandle::new();
41 let sigs = server
42 .runtime()
43 .spawn(signal(server.clone(), handle.clone()));
44 #[cfg(all(feature = "systemd", target_os = "linux"))]
45 let watchdog = server.runtime().spawn(start_systemd_watchdog());
46
47 let non_listener = services
48 .config
49 .listening
50 .is_false()
51 .then_async(|| server.until_shutdown().map(Ok));
52
53 let listener = services.config.listening.then_async(|| {
54 server
55 .runtime()
56 .spawn(serve::serve(services.clone(), handle))
57 .map(|res| res.map_err(Error::from).unwrap_or_else(Err))
58 });
59
60 debug!("Running");
62 pin_mut!(listener, non_listener);
63 let res = tokio::select! {
64 res = join(&mut listener, &mut non_listener) => {
65 res.0.unwrap_or(res.1.unwrap_or(Ok(())))
66 },
67 res = services.poll() => {
68 server.until_shutdown().await;
69 handle_services_finish(server, res, listener.await)
70 },
71 };
72
73 #[cfg(all(feature = "systemd", target_os = "linux"))]
75 {
76 watchdog.abort();
77 _ = watchdog.await;
78 };
79
80 sigs.abort();
81 _ = sigs.await;
82
83 tuwunel_admin::fini(&services.admin);
85
86 debug_info!("Finish");
87 res
88}
89
90#[tracing::instrument(skip_all)]
92pub(crate) async fn start(server: Arc<Server>) -> Result<Arc<Services>> {
93 debug!("Starting...");
94
95 let reporter = server
98 .runtime()
99 .spawn(report_startup_progress(server.clone()));
100
101 let abort = reporter.abort_handle();
102
103 defer! {{
104 abort.abort();
105 }}
106
107 let services = async move { Services::build(server).await?.start().await }.await;
108
109 reporter.abort();
110 _ = reporter.await;
111
112 let services = services?;
113
114 #[cfg(all(feature = "systemd", target_os = "linux"))]
117 notify(&[NotifyState::Ready, NotifyState::Status("Running")])
118 .expect("failed to notify systemd of ready state");
119
120 debug!("Started");
121 Ok(services)
122}
123
124#[tracing::instrument(skip_all)]
126pub(crate) async fn stop(services: Arc<Services>) -> Result {
127 debug!("Shutting down...");
128
129 #[cfg(all(feature = "systemd", target_os = "linux"))]
130 notify_systemd_shutdown(&services.server);
131
132 services.stop().await;
135
136 debug!("Cleaning up...");
140 let db = Arc::downgrade(&services.db);
141 if let Err(services) = Arc::try_unwrap(services) {
142 debug_error!(
143 "{} dangling references to Services after shutdown",
144 Arc::strong_count(&services)
145 );
146 }
147
148 if Weak::strong_count(&db) > 0 {
149 debug_error!(
150 "{} dangling references to Database after shutdown",
151 Weak::strong_count(&db)
152 );
153 }
154
155 info!("Shutdown complete.");
156 Ok(())
157}
158
159#[cfg(all(feature = "systemd", target_os = "linux"))]
160fn notify_systemd_shutdown(server: &Server) {
161 if server.is_restarting() {
166 let monotonic = NotifyState::monotonic_usec_now().expect("failed to get monotonic time");
167
168 notify(&[NotifyState::Reloading, monotonic, NotifyState::Watchdog])
169 .expect("failed to notify systemd of reloading state");
170
171 return;
172 }
173
174 unsafe { notify_and_unset_env(&[NotifyState::Stopping]) }
178 .expect("failed to notify systemd of stopping state");
179}
180
181#[tracing::instrument(skip_all)]
182async fn signal(server: Arc<Server>, handle: ServerHandle) {
183 server.until_shutdown().await;
184 handle_shutdown(&server, &handle);
185}
186
187fn handle_shutdown(server: &Arc<Server>, handle: &ServerHandle) {
188 let timeout = server.config.client_shutdown_timeout;
189 let timeout = Duration::from_secs(timeout);
190 debug!(
191 ?timeout,
192 handle_active = ?server.metrics.requests_handle_active.load(Ordering::Relaxed),
193 "Notifying for graceful shutdown"
194 );
195
196 handle.graceful_shutdown(Some(timeout));
197}
198
199fn handle_services_finish(
200 server: &Arc<Server>,
201 result: Result,
202 listener: Option<Result>,
203) -> Result {
204 debug!("Service manager finished: {result:?}");
205
206 if server.is_running()
207 && let Err(e) = server.shutdown()
208 {
209 error!("Failed to send shutdown signal: {e}");
210 }
211
212 if let Some(Err(e)) = listener {
213 error!("Client listener task finished with error: {e}");
214 }
215
216 result
217}
218
219#[cfg(all(feature = "systemd", target_os = "linux"))]
220#[expect(clippy::infinite_loop)]
221async fn start_systemd_watchdog() {
222 let Some(watchdog) = watchdog_enabled() else {
223 return;
224 };
225
226 let watchdog_usec = u64::try_from(watchdog.as_micros()).unwrap_or(u64::MAX);
227 let interval_usec = (watchdog_usec / 2).max(1);
228 let period = Duration::from_micros(interval_usec);
229
230 let mut ticker = interval(period);
231 ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
232 loop {
233 ticker.tick().await;
234
235 notify_systemd(&[NotifyState::Watchdog], "watchdog");
236 }
237}
238
239#[expect(clippy::infinite_loop)]
248async fn report_startup_progress(server: Arc<Server>) {
249 #[cfg(all(feature = "systemd", target_os = "linux"))]
250 let notifiable = var_os("NOTIFY_SOCKET").is_some();
251
252 let mut ticker = interval(STARTUP_INTERVAL);
253 let mut announced = false;
254
255 ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
256 loop {
257 ticker.tick().await;
258
259 let progress = server.progress.report();
260
261 #[cfg(all(feature = "systemd", target_os = "linux"))]
262 if notifiable {
263 notify_systemd_startup(progress.as_deref());
264 }
265
266 let Some(progress) = progress else {
267 continue;
268 };
269
270 if announced {
271 info!(%progress, "Database migration in progress");
272 continue;
273 }
274
275 announced = true;
276 warn!(
277 %progress,
278 "Database migration in progress. A large database can take many minutes. A stop \
279 request is honored between steps and every step that finished is recorded, so the \
280 migration resumes where it left off; killing the process instead can leave the \
281 database mid-write."
282 );
283 }
284}
285
286#[cfg(all(feature = "systemd", target_os = "linux"))]
293fn notify_systemd_startup(status: Option<&str>) {
294 let extend_usec = u32::try_from(STARTUP_INTERVAL.as_micros())
295 .unwrap_or(u32::MAX)
296 .saturating_mul(2);
297
298 notify_systemd(&[NotifyState::ExtendTimeoutUsec(extend_usec)], "startup timeout extension");
299
300 let Some(status) = status else {
301 return;
302 };
303
304 notify_systemd(&[NotifyState::Status(status)], "startup status");
305}
306
307#[cfg(all(feature = "systemd", target_os = "linux"))]
312fn notify_systemd(states: &[NotifyState<'_>], about: &'static str) {
313 if let Err(e) = notify(states) {
314 error!(%e, %about, "failed to notify systemd");
315 }
316}