omni_dev/daemon/
server.rs1use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::Duration;
7
8use anyhow::{Context, Result};
9use futures::{SinkExt, StreamExt};
10use serde_json::json;
11use tokio::net::UnixStream;
12use tokio::task::{JoinError, JoinSet};
13use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
14use tokio_util::sync::CancellationToken;
15
16use super::lifecycle;
17use super::paths;
18use super::protocol::{DaemonEnvelope, DaemonReply, StatusReport, DAEMON_SERVICE, MAX_LINE_BYTES};
19use super::registry::ServiceRegistry;
20use super::single_instance;
21
22const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
27
28#[derive(Debug, Clone)]
30pub struct DaemonOptions {
31 pub socket_path: PathBuf,
33}
34
35pub async fn run(registry: ServiceRegistry, opts: DaemonOptions) -> Result<()> {
41 run_with_shutdown(Arc::new(registry), opts, CancellationToken::new()).await
42}
43
44pub async fn run_with_shutdown(
50 registry: Arc<ServiceRegistry>,
51 opts: DaemonOptions,
52 shutdown: CancellationToken,
53) -> Result<()> {
54 if let Some(parent) = opts.socket_path.parent() {
55 paths::ensure_dir_0700(parent)?;
56 }
57 paths::check_socket_path_len(&opts.socket_path)?;
58
59 let listener = single_instance::bind_or_reclaim(&opts.socket_path).await?;
60 tracing::info!("daemon listening on {}", opts.socket_path.display());
61
62 lifecycle::install_signal_handlers(shutdown.clone());
63
64 let mut conns: JoinSet<()> = JoinSet::new();
67 loop {
68 tokio::select! {
69 () = shutdown.cancelled() => break,
70 accepted = listener.accept() => {
71 match accepted {
72 Ok((stream, _addr)) => {
73 conns.spawn(handle_connection(
74 stream,
75 registry.clone(),
76 shutdown.clone(),
77 ));
78 }
79 Err(e) => tracing::warn!("daemon accept error: {e}"),
80 }
81 }
82 joined = conns.join_next(), if !conns.is_empty() => {
87 if let Some(result) = joined {
88 note_reaped(result);
89 }
90 }
91 }
92 }
93
94 drop(listener);
103 remove_socket(&opts.socket_path);
104
105 drain_connections(&mut conns, DRAIN_TIMEOUT).await;
107
108 tracing::info!("daemon shutting down; draining services");
109 registry.shutdown_all().await;
110 Ok(())
111}
112
113fn remove_socket(path: &Path) {
116 if let Err(e) = std::fs::remove_file(path) {
117 if e.kind() != std::io::ErrorKind::NotFound {
118 tracing::warn!("failed to remove socket {}: {e}", path.display());
119 }
120 }
121}
122
123fn note_reaped(result: Result<(), JoinError>) {
127 if let Err(e) = result {
128 if e.is_panic() {
129 tracing::warn!("daemon connection task panicked: {e}");
130 }
131 }
132}
133
134async fn drain_connections(conns: &mut JoinSet<()>, timeout: Duration) {
142 let count = conns.len();
143 if count == 0 {
144 return;
145 }
146 tracing::info!("draining {count} in-flight connection(s)");
147 let drain = async {
148 while let Some(result) = conns.join_next().await {
149 note_reaped(result);
150 }
151 };
152 if tokio::time::timeout(timeout, drain).await.is_err() {
153 tracing::warn!(
154 "timed out draining connections after {timeout:?}; aborting {} straggler(s)",
155 conns.len()
156 );
157 conns.abort_all();
158 while conns.join_next().await.is_some() {}
159 }
160}
161
162async fn handle_connection(
170 stream: UnixStream,
171 registry: Arc<ServiceRegistry>,
172 shutdown: CancellationToken,
173) {
174 let mut framed = Framed::new(stream, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
175 while let Some(line) = framed.next().await {
176 match line {
177 Ok(line) => {
178 let reply = dispatch_line(&line, ®istry, &shutdown).await;
179 if !send_reply(&mut framed, reply).await {
180 break;
181 }
182 }
183 Err(e) => {
184 let msg = match e {
191 LinesCodecError::MaxLineLengthExceeded => {
192 format!("request line exceeds the {MAX_LINE_BYTES}-byte limit")
193 }
194 LinesCodecError::Io(io) => format!("read error: {io}"),
195 };
196 let _ = send_reply(&mut framed, DaemonReply::err(msg)).await;
197 break;
198 }
199 }
200 }
201}
202
203async fn send_reply(framed: &mut Framed<UnixStream, LinesCodec>, reply: DaemonReply) -> bool {
206 let encoded = match serde_json::to_string(&reply) {
207 Ok(encoded) => encoded,
208 Err(e) => {
209 tracing::warn!("failed to encode daemon reply: {e}");
210 return false;
211 }
212 };
213 if let Err(e) = framed.send(encoded).await {
214 tracing::debug!("daemon client write failed: {e}");
215 return false;
216 }
217 true
218}
219
220async fn dispatch_line(
222 line: &str,
223 registry: &ServiceRegistry,
224 shutdown: &CancellationToken,
225) -> DaemonReply {
226 let envelope: DaemonEnvelope = match serde_json::from_str(line) {
227 Ok(envelope) => envelope,
228 Err(e) => return DaemonReply::err(format!("invalid envelope: {e}")),
229 };
230 match envelope.service.as_deref() {
231 None | Some(DAEMON_SERVICE) => handle_builtin(&envelope.op, registry, shutdown).await,
232 Some(name) => match registry
233 .dispatch(name, &envelope.op, envelope.payload)
234 .await
235 {
236 Ok(payload) => DaemonReply::ok(payload),
237 Err(e) => DaemonReply::err(format!("{e:#}")),
241 },
242 }
243}
244
245async fn handle_builtin(
247 op: &str,
248 registry: &ServiceRegistry,
249 shutdown: &CancellationToken,
250) -> DaemonReply {
251 match op {
252 "ping" => DaemonReply::ok(json!({ "pong": true })),
253 "status" => {
254 let report = StatusReport {
255 services: registry.statuses().await,
256 };
257 match serde_json::to_value(report) {
258 Ok(payload) => DaemonReply::ok(payload),
259 Err(e) => DaemonReply::err(format!("failed to encode status: {e}")),
260 }
261 }
262 "shutdown" => {
263 shutdown.cancel();
264 DaemonReply::ok(json!({ "stopping": true }))
265 }
266 other => DaemonReply::err(format!("unknown daemon op: {other}")),
267 }
268}
269
270pub fn resolve_socket(socket: Option<PathBuf>) -> Result<PathBuf> {
273 match socket {
274 Some(path) => Ok(path),
275 None => paths::socket_path().context("failed to resolve the default daemon socket path"),
276 }
277}
278
279#[cfg(test)]
285#[allow(clippy::unwrap_used, clippy::expect_used)]
286mod tests {
287 use super::*;
288
289 #[tokio::test]
290 async fn drain_connections_returns_immediately_when_empty() {
291 let mut conns: JoinSet<()> = JoinSet::new();
292 drain_connections(&mut conns, Duration::from_secs(5)).await;
293 assert!(conns.is_empty());
294 }
295
296 #[tokio::test]
297 async fn drain_connections_awaits_completed_tasks() {
298 let mut conns: JoinSet<()> = JoinSet::new();
299 conns.spawn(async {});
300 drain_connections(&mut conns, Duration::from_secs(5)).await;
301 assert!(conns.is_empty());
303 }
304
305 #[tokio::test]
306 async fn drain_connections_times_out_and_aborts_stragglers() {
307 let mut conns: JoinSet<()> = JoinSet::new();
308 conns.spawn(std::future::pending::<()>());
311 drain_connections(&mut conns, Duration::from_millis(50)).await;
312 assert!(
313 conns.is_empty(),
314 "straggler should have been aborted and joined"
315 );
316 }
317
318 #[tokio::test]
319 async fn note_reaped_ignores_success_and_logs_panic() {
320 note_reaped(Ok(()));
322 let mut js: JoinSet<()> = JoinSet::new();
325 js.spawn(async { panic!("boom") });
326 let result = js.join_next().await.unwrap();
327 assert!(result.is_err());
328 note_reaped(result);
329 }
330}