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 metrics::init_metrics();
85 metrics::record_activity();
86
87 let pool = daemon_config.pg_pool.clone();
88
89 let canopy_client = match CanopyClient::new(
90 daemon_config.device_key_pem.as_ref().map(|r| r.0.as_str()),
91 crate::http_builder,
92 )
93 .await
94 {
95 Ok(Some(client)) => {
96 if client.is_tailscale().await {
97 info!("canopy client ready via tailscale");
98 } else {
99 info!("canopy client ready via mTLS");
100 }
101 let client = Arc::new(client);
102 let renew = client.clone();
103 tokio::spawn(async move {
104 let mut interval = tokio::time::interval(crate::canopy::CERT_RENEW_AFTER);
105 interval.tick().await; loop {
107 interval.tick().await;
108 if !renew.is_tailscale().await {
109 info!("renewing canopy mTLS certificate");
110 if let Err(err) = renew.renew().await {
111 error!("failed to renew canopy cert: {}", LogError(&err));
112 }
113 }
114 }
115 });
116 Some(client)
117 }
118 Ok(None) => {
119 info!(
120 "no canopy auth path available (no tailscale, no device key); canopy posting will be skipped"
121 );
122 None
123 }
124 Err(err) => {
125 error!("failed to build canopy client: {}", LogError(&err));
126 None
127 }
128 };
129
130 let (reload_tx, reload_rx) = tokio::sync::watch::channel(0u64);
133 let reload_tx = Arc::new(reload_tx);
134
135 let (event_tx, mut event_rx) = mpsc::channel(100);
136
137 let ctx = Arc::new(InternalContext {
138 pg_pool: pool,
139 http_client: crate::http_client(),
140 canopy_client,
141 reload: reload_rx,
142 restart: Some(RestartTrigger {
143 events: event_tx.clone(),
144 }),
145 });
146
147 let control = DaemonControl {
149 reload: reload_tx.clone(),
150 events: event_tx.clone(),
151 };
152
153 if !daemon_config.no_server {
155 let ctx_for_server = ctx.clone();
156 let background_tasks_for_server = daemon_config.background_tasks.clone();
157 let server_addrs = daemon_config.server_addrs.clone();
158 let watchdog_timeout = daemon_config.watchdog_timeout;
159 let backups = daemon_config.backups.clone();
160 tokio::spawn(async move {
161 http_server::start_server(
162 ctx_for_server,
163 server_addrs,
164 watchdog_timeout,
165 &background_tasks_for_server,
166 control,
167 backups,
168 )
169 .await;
170 });
171 }
172
173 let signal_tx = event_tx.clone();
175 tokio::spawn(async move {
176 match tokio::signal::ctrl_c().await {
177 Ok(()) => {
178 info!("received SIGINT, shutting down");
179 let _ = signal_tx.send(DaemonEvent::Shutdown).await;
180 }
181 Err(err) => {
182 error!("unable to listen for shutdown signal: {}", err);
183 }
184 }
185 });
186
187 let external_signal_tx = event_tx.clone();
189 tokio::spawn(async move {
190 let _ = external_shutdown.await;
191 info!("received external shutdown signal");
192 let _ = external_signal_tx.send(DaemonEvent::Shutdown).await;
193 });
194
195 #[cfg(unix)]
196 {
197 use tokio::signal::unix::{SignalKind, signal};
198 let signal_tx_term = event_tx.clone();
199 tokio::spawn(async move {
200 let mut sigterm =
201 signal(SignalKind::terminate()).expect("failed to setup SIGTERM handler");
202 sigterm.recv().await;
203 info!("received SIGTERM, shutting down");
204 let _ = signal_tx_term.send(DaemonEvent::Shutdown).await;
205 });
206
207 tokio::spawn(async move {
212 let mut sighup = signal(SignalKind::hangup()).expect("failed to setup SIGHUP handler");
213 let mut sigusr1 =
214 signal(SignalKind::user_defined1()).expect("failed to setup SIGUSR1 handler");
215 loop {
216 tokio::select! {
217 _ = sighup.recv() => {}
218 _ = sigusr1.recv() => {}
219 }
220 info!("received reload signal; refreshing");
221 let mut reloading = vec![sd_notify::NotifyState::Reloading];
222 if let Ok(stamp) = sd_notify::NotifyState::monotonic_usec_now() {
223 reloading.push(stamp);
224 }
225 let _ = sd_notify::notify(&reloading);
226 reload_tx.send_modify(|n| *n = n.wrapping_add(1));
227 let _ = sd_notify::notify(&[sd_notify::NotifyState::Ready]);
228 }
229 });
230 }
231 #[cfg(not(unix))]
232 let _ = reload_tx; for task in &daemon_config.background_tasks {
237 let task = task.clone();
238 let task_ctx = TaskContext::from_internal(&ctx);
239 info!(name = task.name(), interval = ?task.interval(), "registering background task");
240 tokio::spawn(async move {
241 let mut tick = tokio::time::interval(task.interval());
242 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
243 loop {
244 tick.tick().await;
245 metrics::record_activity();
246 if let Err(err) = task.run(&task_ctx).await {
247 error!(
248 name = task.name(),
249 "background task failed: {}",
250 LogError(&err)
251 );
252 }
253 }
254 });
255 }
256
257 if let Some(watchdog_timeout) = daemon_config.watchdog_timeout {
260 let watchdog_tx = event_tx.clone();
261 tokio::spawn(async move {
262 let grace = watchdog_timeout.max(Duration::from_secs(60));
264 tokio::time::sleep(grace).await;
265
266 let mut check_interval = tokio::time::interval(Duration::from_secs(30));
267 check_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
268 loop {
269 check_interval.tick().await;
270 let last = metrics::last_activity_timestamp();
271 let now = jiff::Timestamp::now().as_second();
272 let elapsed = Duration::from_secs(now.saturating_sub(last) as u64);
273 if elapsed > watchdog_timeout {
274 error!(
275 ?elapsed,
276 ?watchdog_timeout,
277 "watchdog: no task activity detected within timeout, shutting down"
278 );
279 let _ = watchdog_tx.send(DaemonEvent::WatchdogTimeout).await;
280 break;
281 }
282 }
283 });
284 }
285
286 info!("daemon started successfully");
287 #[cfg(unix)]
291 {
292 let canopy = match &ctx.canopy_client {
293 Some(client) if client.is_tailscale().await => "canopy via tailscale",
294 Some(_) => "canopy via mTLS",
295 None => "canopy not connected",
296 };
297 let status = format!(
298 "monitoring; bestool {}; {canopy}",
299 daemon_config.binary_version
300 );
301 let _ = sd_notify::notify(&[
302 sd_notify::NotifyState::Ready,
303 sd_notify::NotifyState::Status(&status),
304 ]);
305 }
306
307 let event = event_rx.recv().await;
311 #[cfg(unix)]
312 let _ = sd_notify::notify(&[sd_notify::NotifyState::Stopping]);
313 match event {
314 Some(DaemonEvent::Shutdown) | None => {
315 info!("daemon stopped");
316 Ok(())
317 }
318 Some(DaemonEvent::Restart) => {
319 info!("restart requested; exiting for the service manager to restart");
322 Err(miette!("restart requested"))
323 }
324 Some(DaemonEvent::WatchdogTimeout) => {
325 error!("daemon exiting due to watchdog timeout");
326 Err(miette!("watchdog timeout: no task activity detected"))
327 }
328 }
329}