Skip to main content

tuwunel_service/config/
mod.rs

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