use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use futures::{SinkExt, StreamExt};
use serde_json::json;
use tokio::net::{UnixListener, UnixStream};
use tokio::task::{JoinError, JoinSet};
use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
use tokio_util::sync::CancellationToken;
use super::lifecycle;
use super::paths;
use super::protocol::{DaemonEnvelope, DaemonReply, StatusReport, DAEMON_SERVICE, MAX_LINE_BYTES};
use super::registry::ServiceRegistry;
use super::single_instance;
const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
pub struct DaemonOptions {
pub socket_path: PathBuf,
}
pub async fn run(registry: ServiceRegistry, opts: DaemonOptions) -> Result<()> {
run_with_shutdown(Arc::new(registry), opts, CancellationToken::new()).await
}
pub async fn run_with_shutdown(
registry: Arc<ServiceRegistry>,
opts: DaemonOptions,
shutdown: CancellationToken,
) -> Result<()> {
if let Some(parent) = opts.socket_path.parent() {
paths::ensure_dir_0700(parent)?;
}
paths::check_socket_path_len(&opts.socket_path)?;
let (listener, socket_activated) = acquire_listener(&opts.socket_path).await?;
tracing::info!("daemon listening on {}", opts.socket_path.display());
lifecycle::install_signal_handlers(shutdown.clone());
let mut conns: JoinSet<()> = JoinSet::new();
loop {
tokio::select! {
() = shutdown.cancelled() => break,
accepted = listener.accept() => {
match accepted {
Ok((stream, _addr)) => {
conns.spawn(handle_connection(
stream,
registry.clone(),
shutdown.clone(),
));
}
Err(e) => tracing::warn!("daemon accept error: {e}"),
}
}
joined = conns.join_next(), if !conns.is_empty() => {
if let Some(result) = joined {
note_reaped(result);
}
}
}
}
drop(listener);
if !socket_activated {
remove_socket(&opts.socket_path);
}
drain_connections(&mut conns, DRAIN_TIMEOUT).await;
tracing::info!("daemon shutting down; draining services");
registry.shutdown_all().await;
Ok(())
}
async fn acquire_listener(socket_path: &Path) -> Result<(UnixListener, bool)> {
#[cfg(target_os = "macos")]
if let Some(listener) = super::launchd::launchd_listener("Listener")? {
tracing::info!("daemon adopting launchd-activated control socket");
return Ok((listener, true));
}
#[cfg(target_os = "linux")]
if let Some(listener) = super::systemd::systemd_listener()? {
tracing::info!("daemon adopting systemd-activated control socket");
return Ok((listener, true));
}
let listener = single_instance::bind_or_reclaim(socket_path).await?;
Ok((listener, false))
}
fn remove_socket(path: &Path) {
if let Err(e) = std::fs::remove_file(path) {
if e.kind() != std::io::ErrorKind::NotFound {
tracing::warn!("failed to remove socket {}: {e}", path.display());
}
}
}
fn note_reaped(result: Result<(), JoinError>) {
if let Err(e) = result {
if e.is_panic() {
tracing::warn!("daemon connection task panicked: {e}");
}
}
}
async fn drain_connections(conns: &mut JoinSet<()>, timeout: Duration) {
let count = conns.len();
if count == 0 {
return;
}
tracing::info!("draining {count} in-flight connection(s)");
let drain = async {
while let Some(result) = conns.join_next().await {
note_reaped(result);
}
};
if tokio::time::timeout(timeout, drain).await.is_err() {
tracing::warn!(
"timed out draining connections after {timeout:?}; aborting {} straggler(s)",
conns.len()
);
conns.abort_all();
while conns.join_next().await.is_some() {}
}
}
async fn handle_connection(
stream: UnixStream,
registry: Arc<ServiceRegistry>,
shutdown: CancellationToken,
) {
let mut framed = Framed::new(stream, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
while let Some(line) = framed.next().await {
match line {
Ok(line) => {
let reply = dispatch_line(&line, ®istry, &shutdown).await;
if !send_reply(&mut framed, reply).await {
break;
}
}
Err(e) => {
let msg = match e {
LinesCodecError::MaxLineLengthExceeded => {
format!("request line exceeds the {MAX_LINE_BYTES}-byte limit")
}
LinesCodecError::Io(io) => format!("read error: {io}"),
};
let _ = send_reply(&mut framed, DaemonReply::err(msg)).await;
break;
}
}
}
}
async fn send_reply(framed: &mut Framed<UnixStream, LinesCodec>, reply: DaemonReply) -> bool {
let encoded = match serde_json::to_string(&reply) {
Ok(encoded) => encoded,
Err(e) => {
tracing::warn!("failed to encode daemon reply: {e}");
return false;
}
};
if let Err(e) = framed.send(encoded).await {
tracing::debug!("daemon client write failed: {e}");
return false;
}
true
}
async fn dispatch_line(
line: &str,
registry: &ServiceRegistry,
shutdown: &CancellationToken,
) -> DaemonReply {
let envelope: DaemonEnvelope = match serde_json::from_str(line) {
Ok(envelope) => envelope,
Err(e) => return DaemonReply::err(format!("invalid envelope: {e}")),
};
match envelope.service.as_deref() {
None | Some(DAEMON_SERVICE) => handle_builtin(&envelope.op, registry, shutdown).await,
Some(name) => {
let dispatch = registry.dispatch(name, &envelope.op, envelope.payload);
let result = match envelope.origin_invocation_id {
Some(origin) => crate::request_log::scope_origin_id(origin, dispatch).await,
None => dispatch.await,
};
match result {
Ok(payload) => DaemonReply::ok(payload),
Err(e) => DaemonReply::err(format!("{e:#}")),
}
}
}
}
async fn handle_builtin(
op: &str,
registry: &ServiceRegistry,
shutdown: &CancellationToken,
) -> DaemonReply {
match op {
"ping" => DaemonReply::ok(json!({ "pong": true })),
"status" => {
let report = StatusReport {
services: registry.statuses().await,
};
match serde_json::to_value(report) {
Ok(payload) => DaemonReply::ok(payload),
Err(e) => DaemonReply::err(format!("failed to encode status: {e}")),
}
}
"shutdown" => {
shutdown.cancel();
DaemonReply::ok(json!({ "stopping": true }))
}
other => DaemonReply::err(format!("unknown daemon op: {other}")),
}
}
pub fn resolve_socket(socket: Option<PathBuf>) -> Result<PathBuf> {
match socket {
Some(path) => Ok(path),
None => paths::socket_path().context("failed to resolve the default daemon socket path"),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[tokio::test]
async fn drain_connections_returns_immediately_when_empty() {
let mut conns: JoinSet<()> = JoinSet::new();
drain_connections(&mut conns, Duration::from_secs(5)).await;
assert!(conns.is_empty());
}
#[tokio::test]
async fn drain_connections_awaits_completed_tasks() {
let mut conns: JoinSet<()> = JoinSet::new();
conns.spawn(async {});
drain_connections(&mut conns, Duration::from_secs(5)).await;
assert!(conns.is_empty());
}
#[tokio::test]
async fn drain_connections_times_out_and_aborts_stragglers() {
let mut conns: JoinSet<()> = JoinSet::new();
conns.spawn(std::future::pending::<()>());
drain_connections(&mut conns, Duration::from_millis(50)).await;
assert!(
conns.is_empty(),
"straggler should have been aborted and joined"
);
}
#[tokio::test]
async fn note_reaped_ignores_success_and_logs_panic() {
note_reaped(Ok(()));
let mut js: JoinSet<()> = JoinSet::new();
js.spawn(async { panic!("boom") });
let result = js.join_next().await.unwrap();
assert!(result.is_err());
note_reaped(result);
}
}