Skip to main content

tuwunel_service/config/
mod.rs

1#[cfg(all(feature = "systemd", target_os = "linux"))]
2use std::borrow::Cow;
3use std::{iter::empty, ops::Deref, path::Path, sync::Arc};
4
5use async_trait::async_trait;
6#[cfg(all(feature = "systemd", target_os = "linux"))]
7use sd_notify::{NotifyState, notify};
8#[cfg(all(feature = "systemd", target_os = "linux"))]
9use tuwunel_core::itertools::Itertools;
10use tuwunel_core::{
11	Result, Server,
12	config::{Config, check},
13	error, implement,
14};
15
16pub struct Service {
17	server: Arc<Server>,
18}
19
20const SIGNAL: &str = "SIGUSR1";
21
22/// Cap on the status reported to the service manager, which displays one line.
23#[cfg(all(feature = "systemd", target_os = "linux"))]
24const STATUS_MAX: usize = 192;
25
26#[async_trait]
27impl crate::Service for Service {
28	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
29		Ok(Arc::new(Self { server: args.server.clone() }))
30	}
31
32	async fn worker(self: Arc<Self>) -> Result {
33		let mut signaled = self.server.signal.subscribe();
34		while self.server.is_running() {
35			tokio::select! {
36				() = self.server.until_shutdown() => break,
37				signal = signaled.recv() => if signal !=  Ok(SIGNAL) { continue; },
38			}
39
40			if let Err(e) = self.handle_reload() {
41				error!("Failed to reload config: {e}");
42			}
43		}
44
45		Ok(())
46	}
47
48	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
49}
50
51impl Deref for Service {
52	type Target = Arc<Config>;
53
54	#[inline]
55	fn deref(&self) -> &Self::Target { &self.server.config }
56}
57
58#[implement(Service)]
59fn handle_reload(&self) -> Result {
60	// The handshake completes even when reloading is switched off, since the
61	// service manager is already waiting on it by the time the signal arrives.
62	#[cfg(all(feature = "systemd", target_os = "linux"))]
63	NotifyState::monotonic_usec_now()
64		.and_then(|monotonic| notify(&[NotifyState::Reloading, monotonic]))
65		.inspect_err(|e| error!(%e, "failed to notify systemd of reloading state"))
66		.ok();
67
68	let reloaded = self
69		.server
70		.config
71		.config_reload_signal
72		.then(|| self.reload(empty()))
73		.transpose();
74
75	// Ready even on failure, since the old config stays in service; the outcome
76	// travels in the status string instead.
77	#[cfg(all(feature = "systemd", target_os = "linux"))]
78	{
79		let status: Cow<'_, str> = match &reloaded {
80			| Ok(Some(_)) => "Configuration reloaded".into(),
81			| Ok(None) => "Configuration reloading is disabled".into(),
82			| Err(e) => format!("Configuration rejected: {e}").into(),
83		};
84
85		notify(&[NotifyState::Ready, NotifyState::Status(&one_line(&status))])
86			.inspect_err(|e| error!(%e, "failed to notify systemd of ready state"))
87			.ok();
88	};
89
90	reloaded?;
91
92	Ok(())
93}
94
95/// The notify protocol delimits assignments by newline and does no escaping, so
96/// a status carrying one would be read as further assignments.
97#[cfg(all(feature = "systemd", target_os = "linux"))]
98fn one_line(status: &str) -> String {
99	status
100		.split_whitespace()
101		.join(" ")
102		.chars()
103		.take(STATUS_MAX)
104		.collect()
105}
106
107#[implement(Service)]
108pub fn reload<'a, I>(&'a self, paths: I) -> Result<Arc<Config>>
109where
110	I: Iterator<Item = &'a Path>,
111{
112	let old = self.server.config.clone();
113
114	// Replay the startup command line so -c paths and -O overrides survive.
115	let new = self
116		.server
117		.config_sources
118		.load(paths)
119		.and_then(|raw| Config::new(&raw))?;
120
121	check::reload(&old, &new)?;
122	self.server.config.update(new)
123}