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::service::ServiceStream;
use super::single_instance;
const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
const STREAM_TICK: Duration = Duration::from_secs(3);
#[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 {
let line = match line {
Ok(line) => line,
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;
}
};
let envelope: DaemonEnvelope = match serde_json::from_str(&line) {
Ok(envelope) => envelope,
Err(e) => {
if !send_reply(
&mut framed,
DaemonReply::err(format!("invalid envelope: {e}")),
)
.await
{
break;
}
continue;
}
};
if let Some(name) = envelope.service.as_deref() {
if name != DAEMON_SERVICE {
if let Some(stream) = registry.subscribe(name, &envelope.op, &envelope.payload) {
run_stream(&mut framed, stream, &shutdown).await;
return;
}
}
}
let reply = dispatch_envelope(envelope, ®istry, &shutdown).await;
if !send_reply(&mut framed, reply).await {
break;
}
}
}
async fn run_stream(
framed: &mut Framed<UnixStream, LinesCodec>,
mut stream: Box<dyn ServiceStream>,
shutdown: &CancellationToken,
) {
let mut last = stream.snapshot().await;
if !send_reply(framed, DaemonReply::ok(last.clone())).await {
return;
}
let mut tick = tokio::time::interval(STREAM_TICK);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
tick.tick().await;
loop {
tokio::select! {
() = stream.changed() => {}
_ = tick.tick() => {}
_ = framed.next() => break,
() = shutdown.cancelled() => break,
}
let snap = stream.snapshot().await;
if snap != last {
if !send_reply(framed, DaemonReply::ok(snap.clone())).await {
break;
}
last = snap;
}
}
}
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_envelope(
envelope: DaemonEnvelope,
registry: &ServiceRegistry,
shutdown: &CancellationToken,
) -> DaemonReply {
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);
}
use std::sync::Mutex as StdMutex;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::watch;
struct FakeStream {
rx: watch::Receiver<u64>,
snap: Arc<StdMutex<serde_json::Value>>,
}
#[async_trait::async_trait]
impl ServiceStream for FakeStream {
async fn changed(&mut self) {
if self.rx.changed().await.is_err() {
std::future::pending::<()>().await;
}
}
async fn snapshot(&self) -> serde_json::Value {
self.snap.lock().unwrap().clone()
}
}
async fn read_reply<R: tokio::io::AsyncBufRead + Unpin>(reader: &mut R) -> DaemonReply {
let mut line = String::new();
let n = reader.read_line(&mut line).await.unwrap();
assert!(n > 0, "expected a reply line, got EOF");
serde_json::from_str(line.trim_end()).unwrap()
}
#[tokio::test]
async fn run_stream_pushes_initial_then_deltas_and_dedupes() {
let (client, server) = UnixStream::pair().unwrap();
let (tx, rx) = watch::channel(0u64);
let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
let fake = FakeStream {
rx,
snap: snap.clone(),
};
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
let server_task = tokio::spawn(async move {
let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
run_stream(&mut framed, Box::new(fake), &server_shutdown).await;
});
let mut reader = BufReader::new(client);
let initial = read_reply(&mut reader).await;
assert!(initial.ok);
assert_eq!(initial.payload, json!({ "n": 0 }));
tx.send(1).unwrap(); *snap.lock().unwrap() = json!({ "n": 1 });
tx.send(2).unwrap(); let delta = read_reply(&mut reader).await;
assert_eq!(delta.payload, json!({ "n": 1 }));
shutdown.cancel();
let mut tail = String::new();
let n = reader.read_line(&mut tail).await.unwrap();
assert_eq!(n, 0, "stream should close cleanly on shutdown");
server_task.await.unwrap();
}
#[tokio::test]
async fn run_stream_ends_when_client_sends_a_line() {
use tokio::io::AsyncWriteExt;
let (mut client, server) = UnixStream::pair().unwrap();
let (_tx, rx) = watch::channel(0u64);
let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
let fake = FakeStream { rx, snap };
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
let server_task = tokio::spawn(async move {
let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
run_stream(&mut framed, Box::new(fake), &server_shutdown).await;
});
let mut reader = BufReader::new(&mut client);
let _initial = read_reply(&mut reader).await;
drop(reader);
client.write_all(b"cancel\n").await.unwrap();
tokio::time::timeout(Duration::from_secs(2), server_task)
.await
.expect("run_stream should end after a client line")
.unwrap();
}
#[tokio::test]
async fn handle_connection_rejects_bad_envelope_then_serves_normal_op() {
use tokio::io::AsyncWriteExt;
let (client, server) = UnixStream::pair().unwrap();
let mut registry = ServiceRegistry::new();
registry.register(Arc::new(
crate::daemon::services::worktrees::WorktreesService::new(),
));
let shutdown = CancellationToken::new();
let task = tokio::spawn(handle_connection(server, Arc::new(registry), shutdown));
let (read_half, mut write_half) = client.into_split();
let mut reader = BufReader::new(read_half);
write_half.write_all(b"not json\n").await.unwrap();
let bad = read_reply(&mut reader).await;
assert!(!bad.ok);
assert!(bad.error.unwrap().contains("invalid envelope"));
let env = serde_json::to_string(&DaemonEnvelope::service(
"worktrees",
"list",
serde_json::Value::Null,
))
.unwrap();
write_half.write_all(env.as_bytes()).await.unwrap();
write_half.write_all(b"\n").await.unwrap();
let listed = read_reply(&mut reader).await;
assert!(listed.ok);
assert!(listed.payload.get("windows").is_some());
drop(write_half);
drop(reader);
tokio::time::timeout(Duration::from_secs(2), task)
.await
.expect("handler should end after the client hangs up")
.unwrap();
}
#[tokio::test]
async fn handle_connection_enters_streaming_for_subscribe() {
use tokio::io::AsyncWriteExt;
let (client, server) = UnixStream::pair().unwrap();
let mut registry = ServiceRegistry::new();
registry.register(Arc::new(
crate::daemon::services::worktrees::WorktreesService::new(),
));
let shutdown = CancellationToken::new();
let task = tokio::spawn(handle_connection(
server,
Arc::new(registry),
shutdown.clone(),
));
let (read_half, mut write_half) = client.into_split();
let mut reader = BufReader::new(read_half);
let env = serde_json::to_string(&DaemonEnvelope::service(
"worktrees",
"subscribe",
serde_json::Value::Null,
))
.unwrap();
write_half.write_all(env.as_bytes()).await.unwrap();
write_half.write_all(b"\n").await.unwrap();
let initial = read_reply(&mut reader).await;
assert!(initial.ok);
assert_eq!(initial.payload, json!({ "repos": [] }));
shutdown.cancel();
tokio::time::timeout(Duration::from_secs(2), task)
.await
.expect("shutdown should end the streaming handler")
.unwrap();
}
#[tokio::test]
async fn run_stream_returns_when_initial_send_fails() {
let (client, server) = UnixStream::pair().unwrap();
drop(client);
let (_tx, rx) = watch::channel(0u64);
let fake = FakeStream {
rx,
snap: Arc::new(StdMutex::new(json!({ "n": 0 }))),
};
let shutdown = CancellationToken::new();
let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
tokio::time::timeout(
Duration::from_secs(2),
run_stream(&mut framed, Box::new(fake), &shutdown),
)
.await
.expect("run_stream should return promptly when the initial send fails");
}
}