1use std::{sync::Arc, time::Duration};
2
3use miette::{Result, miette};
4use tokio::sync::{mpsc, oneshot};
5use tracing::{error, info};
6
7use crate::{
8 DaemonConfig, LogError, canopy::CanopyClient, context::InternalContext, http_server, metrics,
9 tasks::TaskContext,
10};
11
12enum DaemonEvent {
13 Shutdown,
15 Restart,
18 WatchdogTimeout,
19}
20
21#[derive(Clone)]
27pub struct DaemonControl {
28 reload: Arc<tokio::sync::watch::Sender<u64>>,
29 events: mpsc::Sender<DaemonEvent>,
30}
31
32impl DaemonControl {
33 pub(crate) fn reload(&self) {
35 self.reload.send_modify(|n| *n = n.wrapping_add(1));
36 }
37
38 pub(crate) async fn request_restart(&self) {
40 let _ = self.events.send(DaemonEvent::Restart).await;
41 }
42
43 #[cfg(test)]
45 pub(crate) fn detached() -> Self {
46 let (reload, _) = tokio::sync::watch::channel(0);
47 let (events, _) = mpsc::channel(1);
48 Self {
49 reload: Arc::new(reload),
50 events,
51 }
52 }
53}
54
55#[derive(Clone, Debug)]
62pub struct RestartTrigger {
63 events: mpsc::Sender<DaemonEvent>,
64}
65
66impl RestartTrigger {
67 pub async fn request_restart(&self) {
69 let _ = self.events.send(DaemonEvent::Restart).await;
70 }
71}
72
73pub async fn run(daemon_config: DaemonConfig) -> Result<()> {
74 let (_shutdown_tx, shutdown_rx) = oneshot::channel();
75 run_with_shutdown(daemon_config, shutdown_rx).await
76}
77
78pub async fn run_with_shutdown(
79 daemon_config: DaemonConfig,
80 external_shutdown: oneshot::Receiver<()>,
81) -> Result<()> {
82 info!("starting alertd daemon");
83
84 crate::child_confinement::confine_children();
87
88 metrics::record_activity();
89
90 let pool = daemon_config.pg_pool.clone();
91
92 let canopy_client = match CanopyClient::new(
93 daemon_config.device_key_pem.as_ref().map(|r| r.0.as_str()),
94 crate::http_builder,
95 )
96 .await
97 {
98 Ok(Some(client)) => {
99 if client.is_tailscale().await {
100 info!("canopy client ready via tailscale");
101 } else {
102 info!("canopy client ready via mTLS");
103 }
104 let client = Arc::new(client);
105 let renew = client.clone();
106 tokio::spawn(async move {
107 let mut interval = tokio::time::interval(crate::canopy::CERT_RENEW_AFTER);
108 interval.tick().await; loop {
110 interval.tick().await;
111 if !renew.is_tailscale().await {
112 info!("renewing canopy mTLS certificate");
113 if let Err(err) = renew.renew().await {
114 error!("failed to renew canopy cert: {}", LogError(&err));
115 }
116 }
117 }
118 });
119 Some(client)
120 }
121 Ok(None) => {
122 info!(
123 "no canopy auth path available (no tailscale, no device key); canopy posting will be skipped"
124 );
125 None
126 }
127 Err(err) => {
128 error!("failed to build canopy client: {}", LogError(&err));
129 None
130 }
131 };
132
133 let (reload_tx, reload_rx) = tokio::sync::watch::channel(0u64);
136 let reload_tx = Arc::new(reload_tx);
137
138 let (event_tx, mut event_rx) = mpsc::channel(100);
139
140 let ctx = Arc::new(InternalContext {
141 pg_pool: pool,
142 http_client: crate::http_client(),
143 canopy_client,
144 reload: reload_rx,
145 restart: Some(RestartTrigger {
146 events: event_tx.clone(),
147 }),
148 });
149
150 let control = DaemonControl {
152 reload: reload_tx.clone(),
153 events: event_tx.clone(),
154 };
155
156 if !daemon_config.no_server {
158 let ctx_for_server = ctx.clone();
159 let background_tasks_for_server = daemon_config.background_tasks.clone();
160 let server_addrs = daemon_config.server_addrs.clone();
161 let watchdog_timeout = daemon_config.watchdog_timeout;
162 let backups = daemon_config.backups.clone();
163 let metrics = daemon_config.metrics.clone();
164 let binary_version = daemon_config.binary_version.clone();
165 tokio::spawn(async move {
166 http_server::start_server(
167 ctx_for_server,
168 server_addrs,
169 watchdog_timeout,
170 &background_tasks_for_server,
171 control,
172 backups,
173 metrics,
174 binary_version,
175 )
176 .await;
177 });
178 }
179
180 let signal_tx = event_tx.clone();
182 tokio::spawn(async move {
183 match tokio::signal::ctrl_c().await {
184 Ok(()) => {
185 info!("received SIGINT, shutting down");
186 let _ = signal_tx.send(DaemonEvent::Shutdown).await;
187 }
188 Err(err) => {
189 error!("unable to listen for shutdown signal: {}", err);
190 }
191 }
192 });
193
194 let external_signal_tx = event_tx.clone();
196 tokio::spawn(async move {
197 let _ = external_shutdown.await;
198 info!("received external shutdown signal");
199 let _ = external_signal_tx.send(DaemonEvent::Shutdown).await;
200 });
201
202 #[cfg(unix)]
203 {
204 use tokio::signal::unix::{SignalKind, signal};
205 let signal_tx_term = event_tx.clone();
206 tokio::spawn(async move {
207 let mut sigterm =
208 signal(SignalKind::terminate()).expect("failed to setup SIGTERM handler");
209 sigterm.recv().await;
210 info!("received SIGTERM, shutting down");
211 let _ = signal_tx_term.send(DaemonEvent::Shutdown).await;
212 });
213
214 tokio::spawn(async move {
219 let mut sighup = signal(SignalKind::hangup()).expect("failed to setup SIGHUP handler");
220 let mut sigusr1 =
221 signal(SignalKind::user_defined1()).expect("failed to setup SIGUSR1 handler");
222 loop {
223 tokio::select! {
224 _ = sighup.recv() => {}
225 _ = sigusr1.recv() => {}
226 }
227 info!("received reload signal; refreshing");
228 let mut reloading = vec![sd_notify::NotifyState::Reloading];
229 if let Ok(stamp) = sd_notify::NotifyState::monotonic_usec_now() {
230 reloading.push(stamp);
231 }
232 let _ = sd_notify::notify(&reloading);
233 reload_tx.send_modify(|n| *n = n.wrapping_add(1));
234 let _ = sd_notify::notify(&[sd_notify::NotifyState::Ready]);
235 }
236 });
237 }
238 #[cfg(not(unix))]
239 let _ = reload_tx; for task in &daemon_config.background_tasks {
244 let task = task.clone();
245 let task_ctx = TaskContext::from_internal(&ctx);
246 info!(name = task.name(), interval = ?task.interval(), "registering background task");
247 tokio::spawn(async move {
248 let mut tick = tokio::time::interval(task.interval());
249 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
250 loop {
251 tick.tick().await;
252 metrics::record_activity();
253 if let Err(err) = task.run(&task_ctx).await {
254 error!(
255 name = task.name(),
256 "background task failed: {}",
257 LogError(&err)
258 );
259 }
260 }
261 });
262 }
263
264 if let Some(watchdog_timeout) = daemon_config.watchdog_timeout {
267 let watchdog_tx = event_tx.clone();
268 tokio::spawn(async move {
269 let grace = watchdog_timeout.max(Duration::from_secs(60));
271 tokio::time::sleep(grace).await;
272
273 let mut check_interval = tokio::time::interval(Duration::from_secs(30));
274 check_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
275 loop {
276 check_interval.tick().await;
277 let last = metrics::last_activity_timestamp();
278 let now = jiff::Timestamp::now().as_second();
279 let elapsed = Duration::from_secs(now.saturating_sub(last) as u64);
280 if elapsed > watchdog_timeout {
281 error!(
282 ?elapsed,
283 ?watchdog_timeout,
284 "watchdog: no task activity detected within timeout, shutting down"
285 );
286 let _ = watchdog_tx.send(DaemonEvent::WatchdogTimeout).await;
287 break;
288 }
289 }
290 });
291 }
292
293 info!("daemon started successfully");
294 #[cfg(unix)]
298 {
299 let canopy = match &ctx.canopy_client {
300 Some(client) if client.is_tailscale().await => "canopy via tailscale",
301 Some(_) => "canopy via mTLS",
302 None => "canopy not connected",
303 };
304 let status = format!(
305 "monitoring; bestool {}; {canopy}",
306 daemon_config.binary_version
307 );
308 let _ = sd_notify::notify(&[
309 sd_notify::NotifyState::Ready,
310 sd_notify::NotifyState::Status(&status),
311 ]);
312 }
313
314 let event = event_rx.recv().await;
318 #[cfg(unix)]
319 let _ = sd_notify::notify(&[sd_notify::NotifyState::Stopping]);
320 match event {
321 Some(DaemonEvent::Shutdown) | None => {
322 info!("daemon stopped");
323 Ok(())
324 }
325 Some(DaemonEvent::Restart) => {
326 info!("restart requested; exiting for the service manager to restart");
329 Err(miette!("restart requested"))
330 }
331 Some(DaemonEvent::WatchdogTimeout) => {
332 error!("daemon exiting due to watchdog timeout");
333 Err(miette!("watchdog timeout: no task activity detected"))
334 }
335 }
336}