Skip to main content

omni_dev/daemon/
server.rs

1//! The daemon server core: bind the control socket, accept NDJSON connections,
2//! route envelopes to services (or built-in ops), and shut down gracefully.
3
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use anyhow::{Context, Result};
9use futures::{SinkExt, StreamExt};
10use serde_json::json;
11use tokio::net::{UnixListener, UnixStream};
12use tokio::task::{JoinError, JoinSet};
13use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
14use tokio_util::sync::CancellationToken;
15
16use super::accept::{AcceptBackoff, ConnectionSource};
17use super::lifecycle;
18use super::paths;
19use super::protocol::{DaemonEnvelope, DaemonReply, StatusReport, DAEMON_SERVICE, MAX_LINE_BYTES};
20use super::registry::ServiceRegistry;
21use super::service::ServiceStream;
22use super::single_instance;
23
24/// How long to wait for accepted-but-unfinished connections to drain on
25/// shutdown before aborting the stragglers. Generous enough for a normal
26/// in-flight dispatch+reply, bounded so a stuck or idle client cannot hang
27/// shutdown indefinitely (a service manager would `SIGKILL` us eventually).
28const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
29
30/// How long a connection may wait, silent, for its next request line before the
31/// daemon closes it (#2111).
32///
33/// Every client opens a connection per operation and sends at once, so a
34/// connection that stays silent this long is abandoned — and until it is closed it
35/// costs a descriptor, the very resource the daemon was running out of. Applies
36/// only while *waiting for a line*: a request being served is never interrupted,
37/// and a subscription ([`run_stream`]) takes the connection over and is exempt.
38const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
39
40/// Environment override for [`stream_tick`] (whole seconds; a blank,
41/// non-numeric, or `0` value falls back to [`DEFAULT_STREAM_TICK`]).
42const ENV_STREAM_TICK: &str = "OMNI_DEV_DAEMON_STREAM_TICK";
43
44/// Default push-subscription re-sample interval when `OMNI_DEV_DAEMON_STREAM_TICK`
45/// is unset: how often a subscription re-samples and diffs its snapshot even
46/// without a change notification, so purely on-disk state changes (a branch
47/// switch, new commits) — which fire **no** registry event — are still reflected
48/// within the interval.
49///
50/// Raised from 3 s to 10 s (#1305): registry changes (a window open/close, a
51/// show-closed toggle) still push promptly via the change-notify, so only the
52/// periodic re-sample of on-disk git state slows — a modest, tunable freshness
53/// cost for a background tree view.
54const DEFAULT_STREAM_TICK: Duration = Duration::from_secs(10);
55
56/// The resolved push-subscription re-sample interval: `OMNI_DEV_DAEMON_STREAM_TICK`
57/// (whole seconds) when valid, else [`DEFAULT_STREAM_TICK`].
58///
59/// The worktrees service sizes its coalescing snapshot cache to this same tick
60/// (#1303) by calling straight through here, so the shared `build_tree` runs at
61/// most once per tick regardless of how many windows are subscribed and the two
62/// can never drift.
63pub(crate) fn stream_tick() -> Duration {
64    duration_secs_from_env(ENV_STREAM_TICK, DEFAULT_STREAM_TICK)
65}
66
67/// Reads a whole-seconds [`Duration`] from environment variable `var`, falling
68/// back to `default` when the value is unset, blank, non-numeric, or `0` (a
69/// zero interval would busy-spin the timer loops that consume it). Shared by the
70/// daemon's interval knobs so they parse identically. Delegates to
71/// [`duration_secs_from_raw`] so the parse is unit-tested without touching the
72/// process environment (the Snowflake `heartbeat_interval_from` pattern).
73pub(crate) fn duration_secs_from_env(var: &str, default: Duration) -> Duration {
74    duration_secs_from_raw(std::env::var(var).ok(), default)
75}
76
77/// Parses a whole-seconds [`Duration`] from a raw env value, falling back to
78/// `default` when it is absent, blank, non-numeric, or `0`.
79fn duration_secs_from_raw(raw: Option<String>, default: Duration) -> Duration {
80    raw.and_then(|s| s.trim().parse::<u64>().ok())
81        .filter(|&secs| secs > 0)
82        .map_or(default, Duration::from_secs)
83}
84
85/// Configuration for a [`run`] invocation.
86#[derive(Debug, Clone)]
87pub struct DaemonOptions {
88    /// Path the control socket is bound to.
89    pub socket_path: PathBuf,
90}
91
92/// Runs the daemon until a `SIGTERM`/`SIGINT` or a built-in `shutdown` op,
93/// then drains every service and removes the socket.
94///
95/// Binding the socket doubles as the single-instance lock (see
96/// [`single_instance`]).
97pub async fn run(registry: ServiceRegistry, opts: DaemonOptions) -> Result<()> {
98    run_with_shutdown(Arc::new(registry), opts, CancellationToken::new()).await
99}
100
101/// Like [`run`], but with a shared registry and an externally-owned token.
102///
103/// The menu-bar host uses this to share the [`ServiceRegistry`] with the tray
104/// and to stop the daemon from a "Quit" menu action via the
105/// [`CancellationToken`].
106pub async fn run_with_shutdown(
107    registry: Arc<ServiceRegistry>,
108    opts: DaemonOptions,
109    shutdown: CancellationToken,
110) -> Result<()> {
111    // Before the first accept: launchd starts the daemon with a soft limit of 256
112    // descriptors, which a busy day crosses (#2111).
113    lifecycle::raise_nofile_limit();
114    if let Some(parent) = opts.socket_path.parent() {
115        paths::ensure_dir_0700(parent)?;
116    }
117    // macOS launchd creates the `StandardErrorPath`/`StandardOutPath` log sink
118    // (`daemon.log` beside the socket) under its own umask, not `0600`. Tighten it
119    // to owner-only before anything is written, matching the socket/token posture
120    // — launchd opens the file at spawn, so it already exists and is empty here.
121    // No-op when absent (the systemd-journal path, or a fresh self-bound run) or
122    // already tight (the detached-spawn launcher created it `0600`). See #1316.
123    tighten_daemon_log(&opts.socket_path);
124    paths::check_socket_path_len(&opts.socket_path)?;
125
126    let (listener, socket_activated) = acquire_listener(&opts.socket_path).await?;
127    tracing::info!("daemon listening on {}", opts.socket_path.display());
128
129    lifecycle::install_signal_handlers(shutdown.clone());
130
131    // Connection handlers are tracked here rather than detached, so accepted
132    // requests can be drained on shutdown instead of being abandoned (#992).
133    let mut conns: JoinSet<()> = JoinSet::new();
134    accept_loop(&listener, &registry, &shutdown, &mut conns).await;
135
136    // Close the control socket *before* draining (see #993). The accept loop has
137    // already exited, so any `connect`+`ping` arriving during the drain below
138    // would otherwise sit unaccepted in the backlog and block the caller until
139    // process exit. Dropping the listener makes those connects fail fast
140    // (ECONNREFUSED) on the self-bound path.
141    //
142    // Unlinking the path is conditional. On the self-bound path we remove it here
143    // — rather than after the drain — to avoid a restart race: a replacement
144    // daemon could reclaim the stale socket and rebind its *own* listener
145    // mid-drain, and a late unlink would then delete that fresh socket out from
146    // under it. On the socket-activated path the socket inode belongs to the
147    // service manager (launchd on macOS, systemd on Linux), not us: unlinking it
148    // would make the next `connect(path)` hit ENOENT and never re-activate the
149    // daemon — so we leave it in place for the manager to reuse on the next demand
150    // spawn (#1081).
151    drop(listener);
152    if !socket_activated {
153        remove_socket(&opts.socket_path);
154    }
155
156    // Drain in-flight connection handlers before stopping services (#992).
157    drain_connections(&mut conns, DRAIN_TIMEOUT).await;
158
159    tracing::info!("daemon shutting down; draining services");
160    registry.shutdown_all().await;
161    Ok(())
162}
163
164/// Accepts connections from `source` until `shutdown` fires, serving each on its
165/// own task tracked in `conns`.
166///
167/// A failed `accept` — in practice `EMFILE`/`ENFILE` once descriptors run out — is
168/// retried after an exponentially growing pause instead of at once (#2111). The
169/// pending connection stays in the backlog on failure, so an immediate retry
170/// fails identically: it would burn a core and flood the log, while a pause lets
171/// in-flight requests finish and hand their descriptors back. The pause races
172/// `shutdown`, so a stop request is never held up by it.
173async fn accept_loop<S: ConnectionSource>(
174    source: &S,
175    registry: &Arc<ServiceRegistry>,
176    shutdown: &CancellationToken,
177    conns: &mut JoinSet<()>,
178) {
179    let mut backoff = AcceptBackoff::new();
180    loop {
181        tokio::select! {
182            () = shutdown.cancelled() => break,
183            accepted = source.accept() => match accepted {
184                Ok(stream) => {
185                    if let Some(recovery) = backoff.on_success(Instant::now()) {
186                        registry.credit_accept_outage(recovery.outage);
187                        if recovery.announce {
188                            tracing::info!(
189                                failures = recovery.failures,
190                                outage = ?recovery.duration,
191                                "daemon accept recovered"
192                            );
193                        }
194                    }
195                    conns.spawn(handle_connection(
196                        stream,
197                        registry.clone(),
198                        shutdown.clone(),
199                        IDLE_TIMEOUT,
200                    ));
201                }
202                Err(e) => {
203                    let step = backoff.on_failure(Instant::now());
204                    // Credited as it happens, not just on recovery, because the
205                    // tray and `status` read (and so reap) the registries
206                    // throughout an outage that may never end.
207                    registry.credit_accept_outage(step.outage);
208                    if step.log {
209                        tracing::warn!(
210                            "daemon accept error: {e} (backing off; further errors are not logged for 30s)"
211                        );
212                    }
213                    // Finished handlers give their descriptor back when their task
214                    // completes, not when `join_next` reaps it, so not reaping
215                    // during this pause costs bookkeeping only.
216                    tokio::select! {
217                        () = shutdown.cancelled() => break,
218                        () = tokio::time::sleep(step.delay) => {}
219                    }
220                }
221            },
222            // Reap finished handlers during normal operation so the set does
223            // not grow unbounded over a long-lived daemon. The guard disables
224            // this arm when empty (an empty `JoinSet` yields `None` at once,
225            // which would otherwise busy-loop the select).
226            joined = conns.join_next(), if !conns.is_empty() => {
227                if let Some(result) = joined {
228                    note_reaped(result);
229                }
230            }
231        }
232    }
233}
234
235/// Acquires the control-socket listener, returning it alongside whether the
236/// service manager owns the socket inode (i.e. the daemon was socket-activated).
237///
238/// On macOS (launchd) and Linux (systemd) the daemon is normally
239/// **socket-activated**: the service manager creates and owns the listening
240/// socket and hands us the inherited fd (`launchd::launchd_listener` /
241/// `systemd::systemd_listener` — plain code spans, not intra-doc links, since
242/// those modules are OS-gated and absent from the cross-platform docs build), so
243/// there is no bind and no single-instance handling — the manager guarantees at
244/// most one spawn per socket. When that lookup reports no inherited socket (a
245/// manual `daemon run` from a shell, CI, the detached-spawn fallback, or any
246/// other platform) the daemon binds the socket itself via
247/// [`single_instance::bind_or_reclaim`], which doubles as the single-instance
248/// lock. The returned bool gates whether shutdown unlinks the path: a
249/// manager-owned inode must be left in place to re-activate (#1081).
250async fn acquire_listener(socket_path: &Path) -> Result<(UnixListener, bool)> {
251    #[cfg(target_os = "macos")]
252    if let Some(listener) = super::launchd::launchd_listener("Listener")? {
253        tracing::info!("daemon adopting launchd-activated control socket");
254        return Ok((listener, true));
255    }
256    #[cfg(target_os = "linux")]
257    if let Some(listener) = super::systemd::systemd_listener()? {
258        tracing::info!("daemon adopting systemd-activated control socket");
259        return Ok((listener, true));
260    }
261    let listener = single_instance::bind_or_reclaim(socket_path).await?;
262    Ok((listener, false))
263}
264
265/// Tightens the daemon log co-located with the socket (`daemon.log`) to
266/// owner-only (`0600`) if it exists.
267///
268/// The launchd-spawned daemon inherits its stdout/stderr from a
269/// `StandardErrorPath`/`StandardOutPath` sink launchd creates under its own umask
270/// (not `0600`), so the daemon re-tightens it to match the socket/token posture
271/// (#1316). Best-effort and idempotent: absent on the systemd-journal path and a
272/// no-op where the detached-spawn launcher already created it `0600`. A failure
273/// is logged, never fatal — the daemon must still come up.
274fn tighten_daemon_log(socket_path: &Path) {
275    let log_path = paths::log_path_for_socket(socket_path);
276    if !log_path.exists() {
277        return;
278    }
279    if let Err(e) = paths::set_file_0600(&log_path) {
280        tracing::warn!("failed to tighten {} to 0600: {e}", log_path.display());
281    }
282}
283
284/// Removes the control-socket file, tolerating its absence (a replacement
285/// daemon may have already reclaimed it). Any other error is logged, not fatal.
286fn remove_socket(path: &Path) {
287    if let Err(e) = std::fs::remove_file(path) {
288        if e.kind() != std::io::ErrorKind::NotFound {
289            tracing::warn!("failed to remove socket {}: {e}", path.display());
290        }
291    }
292}
293
294/// Logs a reaped connection task that ended by panicking; clean exits and
295/// cancellations are ignored. Shared by the accept-loop reaper and the drain so
296/// both report a crashed handler the same way.
297fn note_reaped(result: Result<(), JoinError>) {
298    if let Err(e) = result {
299        if e.is_panic() {
300            tracing::warn!("daemon connection task panicked: {e}");
301        }
302    }
303}
304
305/// Awaits outstanding connection handlers (bounded by `timeout`) so an accepted
306/// request finishes its dispatch+reply before the daemon tears down. Called once
307/// the accept loop has stopped and *before* `shutdown_all()`, since in-flight
308/// handlers may still be dispatching into live services. Stragglers past the
309/// deadline are aborted rather than allowed to hang shutdown. (`timeout` is a
310/// parameter, fixed to [`DRAIN_TIMEOUT`] in production, so tests can drive the
311/// abort path without a multi-second wait.)
312async fn drain_connections(conns: &mut JoinSet<()>, timeout: Duration) {
313    let count = conns.len();
314    if count == 0 {
315        return;
316    }
317    tracing::info!("draining {count} in-flight connection(s)");
318    let drain = async {
319        while let Some(result) = conns.join_next().await {
320            note_reaped(result);
321        }
322    };
323    if tokio::time::timeout(timeout, drain).await.is_err() {
324        tracing::warn!(
325            "timed out draining connections after {timeout:?}; aborting {} straggler(s)",
326            conns.len()
327        );
328        conns.abort_all();
329        while conns.join_next().await.is_some() {}
330    }
331}
332
333/// Serves one client connection: decode each NDJSON line, dispatch it, and
334/// write back one reply line, until the client hangs up or a read/write error.
335///
336/// The normal request→one-reply path has deliberately no `shutdown.cancelled()`
337/// arm: an accepted line always finishes its dispatch+reply, and shutdown is
338/// handled by the server draining these tasks (see [`drain_connections`]). A
339/// **subscription** op is the exception — it takes over the connection via
340/// [`run_stream`], which *does* select on `shutdown` so a long-lived stream is
341/// torn down promptly on drain rather than waiting out [`DRAIN_TIMEOUT`].
342/// `shutdown` is threaded through for both (also the built-in `shutdown` op, see
343/// [`handle_builtin`]).
344///
345/// A connection that sends nothing for `idle_timeout` is closed (#2111), so a
346/// client that connected and went away cannot hold a descriptor indefinitely.
347async fn handle_connection(
348    stream: UnixStream,
349    registry: Arc<ServiceRegistry>,
350    shutdown: CancellationToken,
351    idle_timeout: Duration,
352) {
353    let mut framed = Framed::new(stream, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
354    loop {
355        let line = match tokio::time::timeout(idle_timeout, framed.next()).await {
356            Ok(Some(line)) => line,
357            Ok(None) => break,
358            Err(_elapsed) => {
359                tracing::debug!(?idle_timeout, "closing an idle control-socket connection");
360                break;
361            }
362        };
363        let line = match line {
364            Ok(line) => line,
365            Err(e) => {
366                // A decode error ends the `Framed` stream (the next poll yields
367                // `None`), so there is nothing more to serve on this connection:
368                // reply once (best effort) and close. `MaxLineLengthExceeded`
369                // additionally puts the codec in discard mode — the
370                // unbounded-growth case the cap exists to stop (#989) — so it
371                // gets a clearer message.
372                let msg = match e {
373                    LinesCodecError::MaxLineLengthExceeded => {
374                        format!("request line exceeds the {MAX_LINE_BYTES}-byte limit")
375                    }
376                    LinesCodecError::Io(io) => format!("read error: {io}"),
377                };
378                let _ = send_reply(&mut framed, DaemonReply::err(msg)).await;
379                break;
380            }
381        };
382
383        // Parse once, so a subscription op can be detected before it is
384        // dispatched as a normal one-reply op. A malformed envelope replies with
385        // an error but keeps the connection open, matching the pre-#1267 path.
386        let envelope: DaemonEnvelope = match serde_json::from_str(&line) {
387            Ok(envelope) => envelope,
388            Err(e) => {
389                if !send_reply(
390                    &mut framed,
391                    DaemonReply::err(format!("invalid envelope: {e}")),
392                )
393                .await
394                {
395                    break;
396                }
397                continue;
398            }
399        };
400
401        // A streaming op takes over the connection for its whole lifetime: it
402        // never returns a single reply, so once `run_stream` finishes (client
403        // gone or daemon shutting down) the connection is done.
404        if let Some(name) = envelope.service.as_deref() {
405            if name != DAEMON_SERVICE {
406                if let Some(stream) = registry.subscribe(name, &envelope.op, &envelope.payload) {
407                    run_stream(&mut framed, stream, &shutdown, name, &envelope.op).await;
408                    return;
409                }
410            }
411        }
412
413        let reply = dispatch_envelope(envelope, &registry, &shutdown).await;
414        if !send_reply(&mut framed, reply).await {
415            break;
416        }
417    }
418}
419
420/// Reasons a subscription ends; no incoming line content is retained.
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422enum StreamEnd {
423    ClientCancel,
424    ClientEof,
425    ReadDecodeError,
426    DaemonShutdown,
427    WriteFailure,
428}
429
430fn inbound_end(line: Option<Result<String, LinesCodecError>>) -> StreamEnd {
431    match line {
432        Some(Ok(_)) => StreamEnd::ClientCancel,
433        None => StreamEnd::ClientEof,
434        Some(Err(_)) => StreamEnd::ReadDecodeError,
435    }
436}
437
438/// Drives a push subscription over `framed` until the client goes away or the
439/// daemon shuts down. Sends an initial snapshot, then re-samples the stream on
440/// each change notification and on a periodic [`stream_tick`], pushing **only**
441/// snapshots that differ from the last one sent — so identical frames are never
442/// duplicated (the acceptance criterion). Mirrors the browser bridge's
443/// `start_stream` coalescing shape, but on the control socket.
444///
445/// The subscription owns the connection for its lifetime: any further inbound
446/// line is treated as an explicit cancel and ends the stream, matching the
447/// one-op-per-connection the companion uses (a dedicated subscribe socket).
448async fn run_stream(
449    framed: &mut Framed<UnixStream, LinesCodec>,
450    mut stream: Box<dyn ServiceStream>,
451    shutdown: &CancellationToken,
452    service: &str,
453    op: &str,
454) -> StreamEnd {
455    tracing::debug!(service, op, "subscription_started");
456    let mut pushed = 0_u64;
457    let mut suppressed = 0_u64;
458    let mut last = stream.snapshot().await;
459    let end = if !send_reply(framed, DaemonReply::ok(last.clone())).await {
460        StreamEnd::WriteFailure
461    } else {
462        pushed += 1;
463        tracing::trace!(
464            service,
465            op,
466            trigger = "initial",
467            outcome = "pushed",
468            "subscription_sample"
469        );
470        let mut tick = tokio::time::interval(stream_tick());
471        tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
472        tick.tick().await;
473        loop {
474            let trigger = tokio::select! {
475                () = stream.changed() => "change_notification",
476                _ = tick.tick() => "periodic_tick",
477                line = framed.next() => {
478                    // A line is cancel, EOF is hangup, and a codec error is a
479                    // broken read. Never log the line (it may contain content).
480                    if let Some(Err(error)) = &line {
481                        tracing::debug!(service, op, %error, "subscription_read_failed");
482                    }
483                    break inbound_end(line);
484                },
485                () = shutdown.cancelled() => break StreamEnd::DaemonShutdown,
486            };
487            let snap = stream.snapshot().await;
488            if snap != last {
489                if !send_reply(framed, DaemonReply::ok(snap.clone())).await {
490                    break StreamEnd::WriteFailure;
491                }
492                pushed = pushed.saturating_add(1);
493                last = snap;
494                tracing::trace!(
495                    service,
496                    op,
497                    trigger,
498                    outcome = "pushed",
499                    "subscription_sample"
500                );
501            } else {
502                suppressed = suppressed.saturating_add(1);
503                tracing::trace!(
504                    service,
505                    op,
506                    trigger,
507                    outcome = "suppressed_identical",
508                    "subscription_sample"
509                );
510            }
511        }
512    };
513    tracing::debug!(service, op, outcome = ?end, pushed, suppressed, "subscription_ended");
514    end
515}
516
517/// Encodes and writes one reply line. Returns `false` when the connection
518/// should be closed (encode failed, or the write failed).
519async fn send_reply(framed: &mut Framed<UnixStream, LinesCodec>, reply: DaemonReply) -> bool {
520    let encoded = match serde_json::to_string(&reply) {
521        Ok(encoded) => encoded,
522        Err(e) => {
523            tracing::warn!("failed to encode daemon reply: {e}");
524            return false;
525        }
526    };
527    if let Err(e) = framed.send(encoded).await {
528        tracing::warn!(error = %e, "daemon_client_write_failed");
529        return false;
530    }
531    true
532}
533
534/// Produces the one-reply response for a (already-parsed, non-streaming)
535/// request envelope. Streaming ops are peeled off earlier in
536/// [`handle_connection`]; everything else routes here.
537async fn dispatch_envelope(
538    envelope: DaemonEnvelope,
539    registry: &ServiceRegistry,
540    shutdown: &CancellationToken,
541) -> DaemonReply {
542    match envelope.service.as_deref() {
543        None | Some(DAEMON_SERVICE) => {
544            let reply = handle_builtin(&envelope.op, registry, shutdown).await;
545            if !reply.ok {
546                tracing::warn!(service = DAEMON_SERVICE, op = %envelope.op, error = ?reply.error, "daemon_dispatch_failed");
547            }
548            reply
549        }
550        Some(name) => {
551            // Correlate any HTTP the service issues to the originating client's
552            // invocation, when it threaded its id across the socket (#1198).
553            // Built-in ops issue no HTTP, so only the service path is scoped.
554            let dispatch = registry.dispatch(name, &envelope.op, envelope.payload);
555            let result = match envelope.origin_invocation_id {
556                Some(origin) => crate::request_log::scope_origin_id(origin, dispatch).await,
557                None => dispatch.await,
558            };
559            match result {
560                Ok(payload) => DaemonReply::ok(payload),
561                // `{:#}` includes the full anyhow source chain (e.g. "Snowflake
562                // query failed: snowflake server error (000630): …") so the
563                // client can see the underlying cause, not just the top-level
564                // wrapper.
565                Err(e) => {
566                    tracing::warn!(service = name, op = %envelope.op, error = %format!("{e:#}"), "daemon_dispatch_failed");
567                    DaemonReply::err(format!("{e:#}"))
568                }
569            }
570        }
571    }
572}
573
574/// Handles the daemon's own built-in operations.
575async fn handle_builtin(
576    op: &str,
577    registry: &ServiceRegistry,
578    shutdown: &CancellationToken,
579) -> DaemonReply {
580    match op {
581        // Carry the daemon binary's version and git provenance so a client can
582        // detect it is talking to a stale resident daemon after a binary upgrade
583        // (#1113, #1374). Provenance keys are added only when present, keeping the
584        // reply byte-identical to a pre-#1374 daemon's when built without git.
585        "ping" => {
586            let mut payload = serde_json::Map::new();
587            payload.insert("pong".to_string(), json!(true));
588            payload.insert("version".to_string(), json!(crate::VERSION));
589            if let Ok(serde_json::Value::Object(prov)) =
590                serde_json::to_value(crate::build_info::provenance())
591            {
592                payload.extend(prov);
593            }
594            DaemonReply::ok(serde_json::Value::Object(payload))
595        }
596        "status" => {
597            // `current()` stamps the build-time fields (version + git provenance,
598            // #1374); the runtime rate-limit reading (#1375) is injected after.
599            let mut report = StatusReport::current(registry.statuses().await);
600            report.github_rate_limit = registry.github_rate_limit();
601            match serde_json::to_value(report) {
602                Ok(payload) => DaemonReply::ok(payload),
603                Err(e) => DaemonReply::err(format!("failed to encode status: {e}")),
604            }
605        }
606        "shutdown" => {
607            shutdown.cancel();
608            DaemonReply::ok(json!({ "stopping": true }))
609        }
610        other => DaemonReply::err(format!("unknown daemon op: {other}")),
611    }
612}
613
614/// Resolves the control-socket path: the explicit override, or the per-user
615/// default from [`paths::socket_path`].
616pub fn resolve_socket(socket: Option<PathBuf>) -> Result<PathBuf> {
617    match socket {
618        Some(path) => Ok(path),
619        None => paths::socket_path().context("failed to resolve the default daemon socket path"),
620    }
621}
622
623// The daemon-server tests that bind a socket (and thus mutate the process-global
624// umask via `bind_or_reclaim`) live in `tests/daemon_socket.rs`, isolated in
625// their own process so the umask write cannot race the library's other parallel
626// unit tests. See #1017. The tests below are socket-free: they exercise the
627// connection-draining logic directly, with no `bind`, so they stay here.
628#[cfg(test)]
629#[allow(clippy::unwrap_used, clippy::expect_used)]
630mod tests {
631
632    #[tokio::test]
633    async fn subscriptions_report_all_terminal_causes() {
634        use tokio::io::AsyncWriteExt;
635        for expected in [
636            StreamEnd::ClientCancel,
637            StreamEnd::ClientEof,
638            StreamEnd::ReadDecodeError,
639            StreamEnd::DaemonShutdown,
640            StreamEnd::WriteFailure,
641        ] {
642            let (mut client, server) = UnixStream::pair().unwrap();
643            let (_tx, rx) = watch::channel(0_u64);
644            let fake = FakeStream {
645                rx,
646                snap: Arc::new(StdMutex::new(json!({"n":0}))),
647            };
648            let token = CancellationToken::new();
649            let shutdown = token.clone();
650            let task = tokio::spawn(async move {
651                let mut framed = Framed::new(server, LinesCodec::new_with_max_length(64));
652                crate::test_support::capture_future_at(
653                    tracing::Level::TRACE,
654                    run_stream(
655                        &mut framed,
656                        Box::new(fake),
657                        &shutdown,
658                        "sessions",
659                        "subscribe",
660                    ),
661                )
662                .await
663            });
664            if expected == StreamEnd::WriteFailure {
665                drop(client);
666            } else {
667                let mut reader = BufReader::new(&mut client);
668                let _ = read_reply(&mut reader).await;
669                drop(reader);
670                match expected {
671                    StreamEnd::ClientCancel => {
672                        client.write_all(b"cancel\n").await.unwrap();
673                    }
674                    StreamEnd::ClientEof => {
675                        client.shutdown().await.unwrap();
676                    }
677                    StreamEnd::ReadDecodeError => {
678                        client.write_all(&[b'x'; 65]).await.unwrap();
679                    }
680                    StreamEnd::DaemonShutdown => token.cancel(),
681                    StreamEnd::WriteFailure => unreachable!(),
682                }
683            }
684            let (cause, logs) = tokio::time::timeout(Duration::from_secs(2), task)
685                .await
686                .unwrap()
687                .unwrap();
688            assert_eq!(cause, expected);
689            assert!(logs.contains("subscription_started"));
690            assert!(logs.contains("subscription_ended"));
691            assert!(logs.contains(&format!("outcome={expected:?}")));
692            if expected != StreamEnd::WriteFailure {
693                assert!(logs.contains("pushed=1"));
694            }
695        }
696    }
697
698    #[test]
699    fn inbound_subscription_end_reasons_are_distinct() {
700        assert_eq!(
701            super::inbound_end(Some(Ok("cancel".into()))),
702            super::StreamEnd::ClientCancel
703        );
704        assert_eq!(super::inbound_end(None), super::StreamEnd::ClientEof);
705        assert_eq!(
706            super::inbound_end(Some(Err(LinesCodecError::MaxLineLengthExceeded))),
707            super::StreamEnd::ReadDecodeError
708        );
709    }
710
711    use super::*;
712
713    #[test]
714    fn tighten_daemon_log_sets_0600_and_tolerates_absence() {
715        use std::os::unix::fs::PermissionsExt;
716
717        let dir = tempfile::tempdir().unwrap();
718        let socket = dir.path().join("daemon.sock");
719
720        // No log yet → best-effort no-op, no panic.
721        tighten_daemon_log(&socket);
722
723        // A launchd-created log lands with a looser umask mode; tighten it to 0600.
724        let log = paths::log_path_for_socket(&socket);
725        std::fs::write(&log, b"daemon listening on ...\n").unwrap();
726        std::fs::set_permissions(&log, std::fs::Permissions::from_mode(0o644)).unwrap();
727        tighten_daemon_log(&socket);
728        assert_eq!(
729            std::fs::metadata(&log).unwrap().permissions().mode() & 0o777,
730            0o600
731        );
732    }
733
734    #[tokio::test]
735    async fn drain_connections_returns_immediately_when_empty() {
736        let mut conns: JoinSet<()> = JoinSet::new();
737        drain_connections(&mut conns, Duration::from_secs(5)).await;
738        assert!(conns.is_empty());
739    }
740
741    #[tokio::test]
742    async fn drain_connections_awaits_completed_tasks() {
743        let mut conns: JoinSet<()> = JoinSet::new();
744        conns.spawn(async {});
745        drain_connections(&mut conns, Duration::from_secs(5)).await;
746        // Every tracked handler was joined.
747        assert!(conns.is_empty());
748    }
749
750    #[tokio::test]
751    async fn drain_connections_times_out_and_aborts_stragglers() {
752        let mut conns: JoinSet<()> = JoinSet::new();
753        // A task that never finishes on its own forces the timeout + abort path;
754        // the only way `drain_connections` can return is by aborting it.
755        conns.spawn(std::future::pending::<()>());
756        drain_connections(&mut conns, Duration::from_millis(50)).await;
757        assert!(
758            conns.is_empty(),
759            "straggler should have been aborted and joined"
760        );
761    }
762
763    #[tokio::test]
764    async fn note_reaped_ignores_success_and_logs_panic() {
765        // A clean exit is a no-op.
766        note_reaped(Ok(()));
767        // A panicked handler yields a `JoinError` with `is_panic()`, which
768        // `note_reaped` logs (and must not propagate).
769        let mut js: JoinSet<()> = JoinSet::new();
770        js.spawn(async { panic!("boom") });
771        let result = js.join_next().await.unwrap();
772        assert!(result.is_err());
773        note_reaped(result);
774    }
775
776    #[test]
777    fn duration_secs_from_raw_parses_seconds_and_falls_back_on_junk() {
778        let default = Duration::from_secs(10);
779        // Absent / blank / non-numeric / zero all fall back to the default;
780        // `0` must not slip through — it would busy-spin the timer loops.
781        assert_eq!(duration_secs_from_raw(None, default), default);
782        assert_eq!(
783            duration_secs_from_raw(Some(String::new()), default),
784            default
785        );
786        assert_eq!(
787            duration_secs_from_raw(Some("garbage".to_string()), default),
788            default
789        );
790        assert_eq!(
791            duration_secs_from_raw(Some(" 0 ".to_string()), default),
792            default
793        );
794        // A valid whole-seconds value wins over the default, trimming whitespace.
795        assert_eq!(
796            duration_secs_from_raw(Some("30".to_string()), default),
797            Duration::from_secs(30)
798        );
799        assert_eq!(
800            duration_secs_from_raw(Some("  5\n".to_string()), default),
801            Duration::from_secs(5)
802        );
803    }
804
805    // --- Push-subscription streaming (#1267) --------------------------------
806    //
807    // `UnixStream::pair()` is an unbound, connected socket pair — no `bind`, so
808    // no umask mutation — so these `run_stream` tests stay here (in-process)
809    // rather than in the socket-binding `tests/daemon_socket.rs` binary.
810
811    use std::sync::Mutex as StdMutex;
812    use tokio::io::{AsyncBufReadExt, BufReader};
813    use tokio::sync::watch;
814
815    /// A controllable [`ServiceStream`] for driving `run_stream` directly: the
816    /// test bumps `tx` to wake it and swaps `snap` to change what it reports.
817    struct FakeStream {
818        rx: watch::Receiver<u64>,
819        snap: Arc<StdMutex<serde_json::Value>>,
820    }
821
822    #[async_trait::async_trait]
823    impl ServiceStream for FakeStream {
824        async fn changed(&mut self) {
825            // Mirror the real impl: park (rather than spin) once the sender drops.
826            if self.rx.changed().await.is_err() {
827                std::future::pending::<()>().await;
828            }
829        }
830        async fn snapshot(&self) -> serde_json::Value {
831            self.snap.lock().unwrap().clone()
832        }
833    }
834
835    /// Reads one NDJSON reply line from the client end, asserting it is not EOF.
836    /// Generic over the reader so it works on both an owned `BufReader<UnixStream>`
837    /// and one wrapping a `&mut UnixStream` (test 2 keeps the stream to write to).
838    async fn read_reply<R: tokio::io::AsyncBufRead + Unpin>(reader: &mut R) -> DaemonReply {
839        let mut line = String::new();
840        let n = reader.read_line(&mut line).await.unwrap();
841        assert!(n > 0, "expected a reply line, got EOF");
842        serde_json::from_str(line.trim_end()).unwrap()
843    }
844
845    #[tokio::test]
846    async fn run_stream_pushes_initial_then_deltas_and_dedupes() {
847        let (client, server) = UnixStream::pair().unwrap();
848        let (tx, rx) = watch::channel(0u64);
849        let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
850        let fake = FakeStream {
851            rx,
852            snap: snap.clone(),
853        };
854        let shutdown = CancellationToken::new();
855        let server_shutdown = shutdown.clone();
856
857        let server_task = tokio::spawn(async move {
858            let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
859            run_stream(
860                &mut framed,
861                Box::new(fake),
862                &server_shutdown,
863                "fake",
864                "subscribe",
865            )
866            .await;
867        });
868
869        let mut reader = BufReader::new(client);
870
871        // 1) The initial snapshot is pushed up front.
872        let initial = read_reply(&mut reader).await;
873        assert!(initial.ok);
874        assert_eq!(initial.payload, json!({ "n": 0 }));
875
876        // 2) A wake whose snapshot is unchanged is NOT re-sent (the diff dedupes).
877        //    Then a real change is. Because the next frame we read is the changed
878        //    one, a spurious duplicate of `{n:0}` would fail this assertion.
879        tx.send(1).unwrap(); // wake; snapshot still {n:0} → suppressed
880        *snap.lock().unwrap() = json!({ "n": 1 });
881        tx.send(2).unwrap(); // wake; snapshot now {n:1} → pushed
882        let delta = read_reply(&mut reader).await;
883        assert_eq!(delta.payload, json!({ "n": 1 }));
884
885        // 3) Shutdown tears the stream down cleanly: the client hits EOF.
886        shutdown.cancel();
887        let mut tail = String::new();
888        let n = reader.read_line(&mut tail).await.unwrap();
889        assert_eq!(n, 0, "stream should close cleanly on shutdown");
890        server_task.await.unwrap();
891    }
892
893    #[tokio::test]
894    async fn run_stream_ends_when_client_sends_a_line() {
895        use tokio::io::AsyncWriteExt;
896
897        let (mut client, server) = UnixStream::pair().unwrap();
898        let (_tx, rx) = watch::channel(0u64);
899        let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
900        let fake = FakeStream { rx, snap };
901        let shutdown = CancellationToken::new();
902        let server_shutdown = shutdown.clone();
903
904        let server_task = tokio::spawn(async move {
905            let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
906            run_stream(
907                &mut framed,
908                Box::new(fake),
909                &server_shutdown,
910                "fake",
911                "subscribe",
912            )
913            .await;
914        });
915
916        let mut reader = BufReader::new(&mut client);
917        let _initial = read_reply(&mut reader).await;
918        // Release the borrow of `client` so it can be written to below.
919        drop(reader);
920
921        // Any inbound line is a cancel: the stream ends and the task completes
922        // even though shutdown was never signalled.
923        client.write_all(b"cancel\n").await.unwrap();
924        tokio::time::timeout(Duration::from_secs(2), server_task)
925            .await
926            .expect("run_stream should end after a client line")
927            .unwrap();
928    }
929
930    /// `handle_connection`'s parse/route path: a malformed envelope replies with
931    /// an error but keeps the connection open, and a well-formed non-subscribe op
932    /// then falls through the streaming check to the normal one-reply dispatch.
933    #[tokio::test]
934    async fn handle_connection_rejects_bad_envelope_then_serves_normal_op() {
935        use tokio::io::AsyncWriteExt;
936
937        let (client, server) = UnixStream::pair().unwrap();
938        let mut registry = ServiceRegistry::new();
939        registry.register(Arc::new(
940            crate::daemon::services::worktrees::WorktreesService::new(),
941        ));
942        let shutdown = CancellationToken::new();
943        let task = tokio::spawn(handle_connection(
944            server,
945            Arc::new(registry),
946            shutdown,
947            IDLE_TIMEOUT,
948        ));
949
950        let (read_half, mut write_half) = client.into_split();
951        let mut reader = BufReader::new(read_half);
952
953        // 1) A syntactically invalid line → error reply; the connection stays up.
954        write_half.write_all(b"not json\n").await.unwrap();
955        let bad = read_reply(&mut reader).await;
956        assert!(!bad.ok);
957        assert!(bad.error.unwrap().contains("invalid envelope"));
958
959        // 2) A well-formed non-subscribe op is served on the same connection
960        //    (the streaming check declines `list`, so it dispatches normally).
961        let env = serde_json::to_string(&DaemonEnvelope::service(
962            "worktrees",
963            "list",
964            serde_json::Value::Null,
965        ))
966        .unwrap();
967        write_half.write_all(env.as_bytes()).await.unwrap();
968        write_half.write_all(b"\n").await.unwrap();
969        let listed = read_reply(&mut reader).await;
970        assert!(listed.ok);
971        assert!(listed.payload.get("windows").is_some());
972
973        // Client hangs up → the handler task ends cleanly.
974        drop(write_half);
975        drop(reader);
976        tokio::time::timeout(Duration::from_secs(2), task)
977            .await
978            .expect("handler should end after the client hangs up")
979            .unwrap();
980    }
981
982    /// `handle_connection` routes a `subscribe` op into streaming mode: the
983    /// client gets the pushed initial snapshot, and daemon shutdown ends both the
984    /// stream and the handler task.
985    #[tokio::test]
986    async fn handle_connection_enters_streaming_for_subscribe() {
987        use tokio::io::AsyncWriteExt;
988
989        let (client, server) = UnixStream::pair().unwrap();
990        let mut registry = ServiceRegistry::new();
991        registry.register(Arc::new(
992            crate::daemon::services::worktrees::WorktreesService::new(),
993        ));
994        let shutdown = CancellationToken::new();
995        let task = tokio::spawn(handle_connection(
996            server,
997            Arc::new(registry),
998            shutdown.clone(),
999            IDLE_TIMEOUT,
1000        ));
1001
1002        let (read_half, mut write_half) = client.into_split();
1003        let mut reader = BufReader::new(read_half);
1004        let env = serde_json::to_string(&DaemonEnvelope::service(
1005            "worktrees",
1006            "subscribe",
1007            serde_json::Value::Null,
1008        ))
1009        .unwrap();
1010        write_half.write_all(env.as_bytes()).await.unwrap();
1011        write_half.write_all(b"\n").await.unwrap();
1012
1013        // The subscription pushes an initial snapshot (no windows → empty repos),
1014        // with the show/hide-closed toggle at its default (show all).
1015        let initial = read_reply(&mut reader).await;
1016        assert!(initial.ok);
1017        assert_eq!(initial.payload, json!({ "repos": [], "show_closed": true }));
1018
1019        // Shutdown ends the stream and the handler task.
1020        shutdown.cancel();
1021        tokio::time::timeout(Duration::from_secs(2), task)
1022            .await
1023            .expect("shutdown should end the streaming handler")
1024            .unwrap();
1025    }
1026
1027    /// A pushed delta that cannot be written ends the stream as a write failure.
1028    /// The server end shuts down its own write half after the initial snapshot,
1029    /// so the next push fails while the read side is still open — dropping the
1030    /// client instead would race the failing write against the EOF it also causes.
1031    #[tokio::test]
1032    async fn run_stream_ends_when_a_pushed_delta_cannot_be_written() {
1033        let (mut client, server) = UnixStream::pair().unwrap();
1034        let server = server.into_std().unwrap();
1035        let hangup = server.try_clone().unwrap();
1036        let server = UnixStream::from_std(server).unwrap();
1037        let (tx, rx) = watch::channel(0u64);
1038        let snap = Arc::new(StdMutex::new(json!({ "n": 0 })));
1039        let fake = FakeStream {
1040            rx,
1041            snap: Arc::clone(&snap),
1042        };
1043        let shutdown = CancellationToken::new();
1044        let task = tokio::spawn(async move {
1045            let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
1046            run_stream(&mut framed, Box::new(fake), &shutdown, "fake", "subscribe").await
1047        });
1048
1049        let mut reader = BufReader::new(&mut client);
1050        read_reply(&mut reader).await;
1051        hangup.shutdown(std::net::Shutdown::Write).unwrap();
1052        *snap.lock().unwrap() = json!({ "n": 1 });
1053        tx.send(1).unwrap();
1054
1055        let end = tokio::time::timeout(Duration::from_secs(2), task)
1056            .await
1057            .expect("run_stream should end when a push cannot be written")
1058            .unwrap();
1059        assert_eq!(end, StreamEnd::WriteFailure);
1060    }
1061
1062    /// `run_stream` returns immediately when even the initial snapshot cannot be
1063    /// sent (the client is already gone) rather than entering the select loop.
1064    #[tokio::test]
1065    async fn run_stream_returns_when_initial_send_fails() {
1066        let (client, server) = UnixStream::pair().unwrap();
1067        // Close the peer before `run_stream` writes, so the first send fails.
1068        drop(client);
1069        let (_tx, rx) = watch::channel(0u64);
1070        let fake = FakeStream {
1071            rx,
1072            snap: Arc::new(StdMutex::new(json!({ "n": 0 }))),
1073        };
1074        let shutdown = CancellationToken::new();
1075        let mut framed = Framed::new(server, LinesCodec::new_with_max_length(MAX_LINE_BYTES));
1076        tokio::time::timeout(
1077            Duration::from_secs(2),
1078            run_stream(&mut framed, Box::new(fake), &shutdown, "fake", "subscribe"),
1079        )
1080        .await
1081        .expect("run_stream should return promptly when the initial send fails");
1082    }
1083    // --- Idle connections (#2111) --------------------------------------------
1084
1085    fn ping_line() -> &'static [u8] {
1086        b"{\"op\":\"ping\"}\n"
1087    }
1088
1089    /// A client that connects and never sends is dropped after the idle timeout,
1090    /// handing its descriptor back, rather than being held until the daemon exits.
1091    #[tokio::test]
1092    async fn handle_connection_closes_a_connection_that_never_sends() {
1093        use tokio::io::AsyncReadExt;
1094
1095        let (mut client, server) = UnixStream::pair().unwrap();
1096        let task = tokio::spawn(handle_connection(
1097            server,
1098            worktrees_registry(),
1099            CancellationToken::new(),
1100            Duration::from_millis(50),
1101        ));
1102
1103        tokio::time::timeout(Duration::from_secs(2), task)
1104            .await
1105            .expect("an idle connection should be closed")
1106            .unwrap();
1107        // The daemon's end is gone, so the client reads EOF rather than blocking.
1108        let mut buf = Vec::new();
1109        assert_eq!(client.read_to_end(&mut buf).await.unwrap(), 0);
1110    }
1111
1112    /// The timeout is per wait, not per connection: a client that keeps sending
1113    /// inside the window stays connected for far longer than the window.
1114    #[tokio::test]
1115    async fn handle_connection_stays_open_while_requests_keep_arriving() {
1116        use tokio::io::AsyncWriteExt;
1117
1118        let (client, server) = UnixStream::pair().unwrap();
1119        let task = tokio::spawn(handle_connection(
1120            server,
1121            worktrees_registry(),
1122            CancellationToken::new(),
1123            Duration::from_millis(600),
1124        ));
1125        let (read_half, mut write_half) = client.into_split();
1126        let mut reader = BufReader::new(read_half);
1127
1128        for _ in 0..3 {
1129            write_half.write_all(ping_line()).await.unwrap();
1130            assert!(read_reply(&mut reader).await.ok);
1131            // Each pause is well inside the window; together they exceed it.
1132            tokio::time::sleep(Duration::from_millis(250)).await;
1133        }
1134        assert!(
1135            !task.is_finished(),
1136            "the connection was closed while active"
1137        );
1138    }
1139
1140    /// A subscription takes the connection over, so it may sit silent indefinitely.
1141    #[tokio::test]
1142    async fn handle_connection_does_not_time_out_a_subscription() {
1143        use tokio::io::AsyncWriteExt;
1144
1145        let (client, server) = UnixStream::pair().unwrap();
1146        let shutdown = CancellationToken::new();
1147        let task = tokio::spawn(handle_connection(
1148            server,
1149            worktrees_registry(),
1150            shutdown.clone(),
1151            Duration::from_millis(50),
1152        ));
1153        let (read_half, mut write_half) = client.into_split();
1154        let mut reader = BufReader::new(read_half);
1155        let env = serde_json::to_string(&DaemonEnvelope::service(
1156            "worktrees",
1157            "subscribe",
1158            serde_json::Value::Null,
1159        ))
1160        .unwrap();
1161        write_half.write_all(env.as_bytes()).await.unwrap();
1162        write_half.write_all(b"\n").await.unwrap();
1163        assert!(read_reply(&mut reader).await.ok);
1164
1165        // Several idle windows pass with nothing sent in either direction.
1166        tokio::time::sleep(Duration::from_millis(250)).await;
1167        assert!(!task.is_finished(), "the idle timeout cut a subscription");
1168
1169        shutdown.cancel();
1170        tokio::time::timeout(Duration::from_secs(2), task)
1171            .await
1172            .expect("shutdown still ends the stream")
1173            .unwrap();
1174    }
1175
1176    /// The timeout covers only the wait for a request line: a request being
1177    /// served is never cut off, however long it takes.
1178    #[tokio::test]
1179    async fn handle_connection_never_interrupts_a_request_in_progress() {
1180        use crate::daemon::testutil::StubService;
1181        use tokio::io::AsyncWriteExt;
1182
1183        let (client, server) = UnixStream::pair().unwrap();
1184        let mut registry = ServiceRegistry::new();
1185        // An answer that takes longer than the idle window.
1186        registry.register(Arc::new(StubService::slow(
1187            "slow",
1188            Duration::from_millis(250),
1189        )));
1190        let task = tokio::spawn(handle_connection(
1191            server,
1192            Arc::new(registry),
1193            CancellationToken::new(),
1194            Duration::from_millis(50),
1195        ));
1196        let (read_half, mut write_half) = client.into_split();
1197        let mut reader = BufReader::new(read_half);
1198        let env = serde_json::to_string(&DaemonEnvelope::service(
1199            "slow",
1200            "work",
1201            serde_json::Value::Null,
1202        ))
1203        .unwrap();
1204        write_half.write_all(env.as_bytes()).await.unwrap();
1205        write_half.write_all(b"\n").await.unwrap();
1206
1207        // The 250ms answer outlasts the 50ms window and still arrives.
1208        let reply = read_reply(&mut reader).await;
1209        assert!(reply.ok);
1210        assert_eq!(reply.payload, json!({ "done": true }));
1211        drop(task);
1212    }
1213
1214    // --- Accept-error backoff (#2111) ----------------------------------------
1215
1216    /// EMFILE ("Too many open files"), the error the daemon hit in production.
1217    fn emfile() -> std::io::Error {
1218        std::io::Error::from_raw_os_error(24)
1219    }
1220
1221    /// A [`ConnectionSource`] that replays a script, then parks (like a quiet
1222    /// listener). Counts every `accept` so a test can tell a paced retry from a spin.
1223    struct ScriptedSource {
1224        script: StdMutex<std::collections::VecDeque<std::io::Result<UnixStream>>>,
1225        calls: std::sync::atomic::AtomicUsize,
1226    }
1227
1228    impl ScriptedSource {
1229        fn new(script: Vec<std::io::Result<UnixStream>>) -> Self {
1230            Self {
1231                script: StdMutex::new(script.into()),
1232                calls: std::sync::atomic::AtomicUsize::new(0),
1233            }
1234        }
1235
1236        fn calls(&self) -> usize {
1237            self.calls.load(std::sync::atomic::Ordering::SeqCst)
1238        }
1239    }
1240
1241    #[async_trait::async_trait]
1242    impl ConnectionSource for ScriptedSource {
1243        async fn accept(&self) -> std::io::Result<UnixStream> {
1244            self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1245            let next = self.script.lock().unwrap().pop_front();
1246            match next {
1247                Some(result) => result,
1248                None => std::future::pending().await,
1249            }
1250        }
1251    }
1252
1253    fn worktrees_registry() -> Arc<ServiceRegistry> {
1254        let mut registry = ServiceRegistry::new();
1255        registry.register(Arc::new(
1256            crate::daemon::services::worktrees::WorktreesService::new(),
1257        ));
1258        Arc::new(registry)
1259    }
1260
1261    /// Accept errors are retried on a growing delay — not in a tight loop — and the
1262    /// loop then serves the first connection that does arrive, on the same
1263    /// instance, with nothing lost to the earlier failures.
1264    #[tokio::test]
1265    async fn accept_loop_backs_off_on_errors_then_serves_the_next_connection() {
1266        use tokio::io::AsyncWriteExt;
1267
1268        let (client, server) = UnixStream::pair().unwrap();
1269        let source = ScriptedSource::new(vec![
1270            Err(emfile()),
1271            Err(emfile()),
1272            Err(emfile()),
1273            Ok(server),
1274        ]);
1275        let registry = worktrees_registry();
1276        let shutdown = CancellationToken::new();
1277        let mut conns: JoinSet<()> = JoinSet::new();
1278
1279        let started = Instant::now();
1280        let serving = async {
1281            accept_loop(&source, &registry, &shutdown, &mut conns).await;
1282        };
1283        let talking = async {
1284            let (read_half, mut write_half) = client.into_split();
1285            let mut reader = BufReader::new(read_half);
1286            write_half.write_all(b"{\"op\":\"ping\"}\n").await.unwrap();
1287            let reply = read_reply(&mut reader).await;
1288            shutdown.cancel();
1289            reply
1290        };
1291        let ((), reply) = tokio::time::timeout(Duration::from_secs(5), async {
1292            tokio::join!(serving, talking)
1293        })
1294        .await
1295        .expect("the loop should recover and serve");
1296
1297        assert!(reply.ok, "{reply:?}");
1298        // Three failures back off 5ms + 10ms + 20ms before the fourth `accept`
1299        // succeeds; a loop that retried at once would finish in well under that.
1300        let elapsed = started.elapsed();
1301        assert!(
1302            elapsed >= Duration::from_millis(35),
1303            "retried too fast: {elapsed:?}"
1304        );
1305        // Three failed attempts, the success, and the parked fifth the shutdown
1306        // interrupted — nothing like the thousands a spinning loop would make.
1307        assert!(source.calls() <= 5, "spun: {} accept calls", source.calls());
1308    }
1309
1310    /// The time the daemon spent unable to accept is credited to its services as
1311    /// it passes, so a registry does not age clients out for silence the daemon
1312    /// itself caused (#2111). The credits sum to the whole outage.
1313    #[tokio::test]
1314    async fn accept_loop_credits_the_outage_to_the_services() {
1315        use crate::daemon::testutil::StubService;
1316        use tokio::io::AsyncWriteExt;
1317
1318        let (client, server) = UnixStream::pair().unwrap();
1319        let source = ScriptedSource::new(vec![
1320            Err(emfile()),
1321            Err(emfile()),
1322            Err(emfile()),
1323            Ok(server),
1324        ]);
1325        let recorder = Arc::new(StubService::new("recorder"));
1326        let mut registry = ServiceRegistry::new();
1327        registry.register(recorder.clone());
1328        let registry = Arc::new(registry);
1329        let shutdown = CancellationToken::new();
1330        let mut conns: JoinSet<()> = JoinSet::new();
1331
1332        let talking = async {
1333            // Serving the connection proves the loop got past the failures.
1334            let (read_half, mut write_half) = client.into_split();
1335            let mut reader = BufReader::new(read_half);
1336            write_half.write_all(b"{\"op\":\"ping\"}\n").await.unwrap();
1337            read_reply(&mut reader).await;
1338            shutdown.cancel();
1339        };
1340        tokio::time::timeout(Duration::from_secs(5), async {
1341            tokio::join!(
1342                accept_loop(&source, &registry, &shutdown, &mut conns),
1343                talking
1344            )
1345        })
1346        .await
1347        .expect("the loop should recover and serve");
1348
1349        let credited = recorder.credited();
1350        // Three failures and the recovery each report once; the first failure has
1351        // nothing to credit yet.
1352        assert_eq!(credited.len(), 4, "{credited:?}");
1353        assert_eq!(credited[0], Duration::ZERO);
1354        // The sleeps between the attempts (5 + 10 + 20 ms) are the outage, and a
1355        // sleep never ends early.
1356        let total: Duration = credited.iter().sum();
1357        assert!(total >= Duration::from_millis(35), "credited {total:?}");
1358        // A sanity ceiling only: the credits are wall time, so a loaded machine can
1359        // stretch them, but they cannot be an order of magnitude longer.
1360        assert!(total < Duration::from_secs(30), "credited {total:?}");
1361    }
1362
1363    /// A burst that follows another within the warning interval logs nothing, so
1364    /// its recovery is silent — but the outage is still credited, and the
1365    /// connection that ends it is still served.
1366    #[tokio::test]
1367    async fn accept_loop_recovers_from_an_unlogged_burst_and_still_credits_it() {
1368        use crate::daemon::testutil::StubService;
1369        use tokio::io::AsyncWriteExt;
1370
1371        let (client_a, server_a) = UnixStream::pair().unwrap();
1372        let (client_b, server_b) = UnixStream::pair().unwrap();
1373        // Two single-failure bursts, milliseconds apart: the first logs and the
1374        // second falls inside the 30s throttle, so it does not.
1375        let source = ScriptedSource::new(vec![
1376            Err(emfile()),
1377            Ok(server_a),
1378            Err(emfile()),
1379            Ok(server_b),
1380        ]);
1381        let recorder = Arc::new(StubService::new("recorder"));
1382        let mut registry = ServiceRegistry::new();
1383        registry.register(recorder.clone());
1384        let registry = Arc::new(registry);
1385        let shutdown = CancellationToken::new();
1386        let mut conns: JoinSet<()> = JoinSet::new();
1387
1388        let talking = async {
1389            for client in [client_a, client_b] {
1390                let (read_half, mut write_half) = client.into_split();
1391                let mut reader = BufReader::new(read_half);
1392                write_half.write_all(b"{\"op\":\"ping\"}\n").await.unwrap();
1393                assert!(read_reply(&mut reader).await.ok);
1394            }
1395            shutdown.cancel();
1396        };
1397        tokio::time::timeout(Duration::from_secs(5), async {
1398            tokio::join!(
1399                accept_loop(&source, &registry, &shutdown, &mut conns),
1400                talking
1401            )
1402        })
1403        .await
1404        .expect("the loop should serve both connections");
1405
1406        // Each burst reports its failure and its recovery.
1407        assert_eq!(recorder.credited().len(), 4, "{:?}", recorder.credited());
1408    }
1409
1410    /// A stop request during a long backoff sleep ends the loop at once, rather
1411    /// than waiting out the delay (which grows to a second).
1412    #[tokio::test]
1413    async fn accept_loop_stops_promptly_when_shutdown_arrives_mid_backoff() {
1414        let errors = (0..32).map(|_| Err(emfile())).collect();
1415        let source = ScriptedSource::new(errors);
1416        let registry = worktrees_registry();
1417        let shutdown = CancellationToken::new();
1418        let mut conns: JoinSet<()> = JoinSet::new();
1419
1420        let stopper = shutdown.clone();
1421        let stop_later = async move {
1422            // Eight failures take 5+10+…+640 = 1275ms; the ninth sleeps the full
1423            // 1s ceiling from there until 2275ms, so at 1400ms the loop is
1424            // mid-sleep with most of that second still to go.
1425            tokio::time::sleep(Duration::from_millis(1400)).await;
1426            stopper.cancel();
1427        };
1428        let started = Instant::now();
1429        tokio::time::timeout(Duration::from_secs(5), async {
1430            tokio::join!(
1431                accept_loop(&source, &registry, &shutdown, &mut conns),
1432                stop_later
1433            )
1434        })
1435        .await
1436        .expect("shutdown must interrupt the backoff sleep");
1437        // Interrupted, the loop ends right after the 1400ms stop; waiting the sleep
1438        // out would take until 2275ms.
1439        assert!(
1440            started.elapsed() < Duration::from_millis(1900),
1441            "shutdown waited out a backoff sleep: {:?}",
1442            started.elapsed()
1443        );
1444    }
1445}