Skip to main content

tuwunel_router/
run.rs

1use std::{
2	sync::{Arc, Weak, atomic::Ordering},
3	time::Duration,
4};
5
6use futures::{FutureExt, future::join, pin_mut};
7#[cfg(all(feature = "systemd", target_os = "linux"))]
8use sd_notify::{NotifyState, notify, notify_and_unset_env, watchdog_enabled};
9use tuwunel_core::{
10	Error, Result, Server, debug, debug_error, debug_info, error, info, utils::BoolExt,
11};
12use tuwunel_service::Services;
13
14use crate::{handle::ServerHandle, serve};
15
16/// Main loop base
17#[tracing::instrument(skip_all)]
18pub(crate) async fn run(services: Arc<Services>) -> Result {
19	let server = &services.server;
20	debug!("Start");
21
22	// Install the admin command root here for now
23	tuwunel_admin::init(&services.admin);
24
25	// Execute configured startup commands.
26	services.admin.startup_execute().await?;
27
28	// Setup shutdown/signal handling
29	let handle = ServerHandle::new();
30	let sigs = server
31		.runtime()
32		.spawn(signal(server.clone(), handle.clone()));
33	#[cfg(all(feature = "systemd", target_os = "linux"))]
34	let watchdog = server.runtime().spawn(start_systemd_watchdog());
35
36	let non_listener = services
37		.config
38		.listening
39		.is_false()
40		.then_async(|| server.until_shutdown().map(Ok));
41
42	let listener = services.config.listening.then_async(|| {
43		server
44			.runtime()
45			.spawn(serve::serve(services.clone(), handle))
46			.map(|res| res.map_err(Error::from).unwrap_or_else(Err))
47	});
48
49	// Focal point
50	debug!("Running");
51	pin_mut!(listener, non_listener);
52	let res = tokio::select! {
53		res = join(&mut listener, &mut non_listener) => {
54			res.0.unwrap_or(res.1.unwrap_or(Ok(())))
55		},
56		res = services.poll() => {
57			server.until_shutdown().await;
58			handle_services_finish(server, res, listener.await)
59		},
60	};
61
62	// Join watchdog and the signal handler before we leave.
63	#[cfg(all(feature = "systemd", target_os = "linux"))]
64	{
65		watchdog.abort();
66		_ = watchdog.await;
67	};
68
69	sigs.abort();
70	_ = sigs.await;
71
72	// Remove the admin command root
73	tuwunel_admin::fini(&services.admin);
74
75	debug_info!("Finish");
76	res
77}
78
79/// Async initializations
80#[tracing::instrument(skip_all)]
81pub(crate) async fn start(server: Arc<Server>) -> Result<Arc<Services>> {
82	debug!("Starting...");
83
84	#[cfg(all(feature = "systemd", target_os = "linux"))]
85	let keepalive = server.runtime().spawn(extend_systemd_startup());
86
87	let services = async move { Services::build(server).await?.start().await }.await;
88
89	#[cfg(all(feature = "systemd", target_os = "linux"))]
90	{
91		keepalive.abort();
92		_ = keepalive.await;
93	};
94
95	let services = services?;
96
97	// The status is set here so it reads as a baseline rather than staying blank
98	// until the first reload replaces it.
99	#[cfg(all(feature = "systemd", target_os = "linux"))]
100	notify(&[NotifyState::Ready, NotifyState::Status("Running")])
101		.expect("failed to notify systemd of ready state");
102
103	debug!("Started");
104	Ok(services)
105}
106
107/// Async destructions
108#[tracing::instrument(skip_all)]
109pub(crate) async fn stop(services: Arc<Services>) -> Result {
110	debug!("Shutting down...");
111
112	#[cfg(all(feature = "systemd", target_os = "linux"))]
113	notify_systemd_shutdown(&services.server);
114
115	// Wait for all completions before dropping or we'll lose them to the module
116	// unload and explode.
117	services.stop().await;
118
119	// Check that Services and Database will drop as expected, The complex of Arc's
120	// used for various components can easily lead to references being held
121	// somewhere improperly; this can hang shutdowns.
122	debug!("Cleaning up...");
123	let db = Arc::downgrade(&services.db);
124	if let Err(services) = Arc::try_unwrap(services) {
125		debug_error!(
126			"{} dangling references to Services after shutdown",
127			Arc::strong_count(&services)
128		);
129	}
130
131	if Weak::strong_count(&db) > 0 {
132		debug_error!(
133			"{} dangling references to Database after shutdown",
134			Weak::strong_count(&db)
135		);
136	}
137
138	info!("Shutdown complete.");
139	Ok(())
140}
141
142#[cfg(all(feature = "systemd", target_os = "linux"))]
143fn notify_systemd_shutdown(server: &Server) {
144	// An in-place exec restart keeps this PID; report a reload, not an exit, so
145	// the unit stays active and NOTIFY_SOCKET survives for the next image. The
146	// watchdog stays armed while reloading, so reset it to give teardown and
147	// exec the full interval.
148	if server.is_restarting() {
149		let monotonic = NotifyState::monotonic_usec_now().expect("failed to get monotonic time");
150
151		notify(&[NotifyState::Reloading, monotonic, NotifyState::Watchdog])
152			.expect("failed to notify systemd of reloading state");
153
154		return;
155	}
156
157	// SAFETY: clears NOTIFY_SOCKET from the process environment. Safe because no
158	// other thread reads or writes that variable; this matches the previous
159	// `notify(unset_env=true, ...)` semantics from sd-notify 0.4.
160	unsafe { notify_and_unset_env(&[NotifyState::Stopping]) }
161		.expect("failed to notify systemd of stopping state");
162}
163
164#[tracing::instrument(skip_all)]
165async fn signal(server: Arc<Server>, handle: ServerHandle) {
166	server.until_shutdown().await;
167	handle_shutdown(&server, &handle);
168}
169
170fn handle_shutdown(server: &Arc<Server>, handle: &ServerHandle) {
171	let timeout = server.config.client_shutdown_timeout;
172	let timeout = Duration::from_secs(timeout);
173	debug!(
174		?timeout,
175		handle_active = ?server.metrics.requests_handle_active.load(Ordering::Relaxed),
176		"Notifying for graceful shutdown"
177	);
178
179	handle.graceful_shutdown(Some(timeout));
180}
181
182fn handle_services_finish(
183	server: &Arc<Server>,
184	result: Result,
185	listener: Option<Result>,
186) -> Result {
187	debug!("Service manager finished: {result:?}");
188
189	if server.is_running()
190		&& let Err(e) = server.shutdown()
191	{
192		error!("Failed to send shutdown signal: {e}");
193	}
194
195	if let Some(Err(e)) = listener {
196		error!("Client listener task finished with error: {e}");
197	}
198
199	result
200}
201
202#[cfg(all(feature = "systemd", target_os = "linux"))]
203#[expect(clippy::infinite_loop)]
204async fn start_systemd_watchdog() {
205	use tokio::time::MissedTickBehavior;
206
207	let Some(watchdog) = watchdog_enabled() else {
208		return;
209	};
210
211	let watchdog_usec = u64::try_from(watchdog.as_micros()).unwrap_or(u64::MAX);
212	let interval_usec = (watchdog_usec / 2).max(1);
213	let interval = Duration::from_micros(interval_usec);
214
215	let mut ticker = tokio::time::interval(interval);
216	ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
217	loop {
218		ticker.tick().await;
219
220		if let Err(e) = notify(&[NotifyState::Watchdog]) {
221			error!(%e, "failed to notify systemd watchdog state");
222		}
223	}
224}
225
226#[cfg(all(feature = "systemd", target_os = "linux"))]
227#[expect(clippy::infinite_loop)]
228async fn extend_systemd_startup() {
229	use std::env;
230
231	use tokio::time::MissedTickBehavior;
232
233	const INTERVAL: Duration = Duration::from_secs(15);
234
235	// Keep systemd's start timeout extended while a slow boot such as a database
236	// migration runs, so a healthy service is not killed before it signals ready.
237	if env::var_os("NOTIFY_SOCKET").is_none() {
238		return;
239	}
240
241	let extend_usec = u32::try_from(INTERVAL.as_micros())
242		.unwrap_or(u32::MAX)
243		.saturating_mul(2);
244
245	let mut ticker = tokio::time::interval(INTERVAL);
246	ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
247	loop {
248		ticker.tick().await;
249
250		if let Err(e) = notify(&[NotifyState::ExtendTimeoutUsec(extend_usec)]) {
251			error!(%e, "failed to extend systemd startup timeout");
252		}
253	}
254}