Skip to main content

gwm/
daemon.rs

1//! Daemon mode: a long-running JSON-RPC 2.0 server exposing gwm's
2//! machine-readable surface over a unix domain socket (issue #38,
3//! phase 2). Editors, statusbars, and tooling connect once and call
4//! `list` / `doctor` / `path`, or `subscribe` for pushed updates when
5//! the worktree set changes — instead of spawning `gwm` per query and
6//! parsing human output.
7//!
8//! Layering, deliberately split so the testable core needs no socket and
9//! compiles on every platform:
10//!
11//! - **Pure RPC core** (`parse` → [`dispatch`] → serialize, tied together
12//!   by [`handle_line`]): always compiled, unit-tested in
13//!   `tests/daemon_tests.rs`. Does git I/O against a repo workdir but
14//!   never touches a socket.
15//! - **Socket server** ([`serve`] / [`socket_path`]): `cfg(all(unix,
16//!   feature = "daemon"))`. A thin accept loop that shuttles
17//!   newline-delimited JSON between the socket and [`handle_line`].
18//!
19//! Wire format: newline-delimited JSON (NDJSON). One request object per
20//! line, one response object per line. `subscribe` is the exception — it
21//! turns the connection into a one-way stream of `worktrees.changed`
22//! notifications (the first is the current snapshot).
23//!
24//! The reference scheme in issue #38 calls for a filesystem watch on
25//! `.git/worktrees/`; this MVP uses interval polling instead (no `notify`
26//! dependency, deterministic to test, MSRV-safe). The trade-off — update
27//! latency bounded by the poll interval — is documented on the `daemon`
28//! subcommand's `--poll-ms` flag.
29
30use crate::error::Result;
31use crate::json_api::{self, JsonDoctorReport, JsonPath, JsonWorktree};
32use crate::{config::Config, doctor, worktree};
33use serde::Deserialize;
34use serde_json::{json, Value};
35use std::path::Path;
36
37/// JSON-RPC 2.0 standard error codes (subset we emit).
38pub const PARSE_ERROR: i64 = -32700;
39pub const INVALID_REQUEST: i64 = -32600;
40pub const METHOD_NOT_FOUND: i64 = -32601;
41pub const INVALID_PARAMS: i64 = -32602;
42pub const INTERNAL_ERROR: i64 = -32603;
43
44/// A parsed JSON-RPC 2.0 request. `params` and `id` default so a minimal
45/// `{"method":"list"}` line still parses. When the `id` member is absent
46/// the request is a **notification** (no response is sent — see
47/// [`handle_line`]); an explicit `"id": null` is a request and is echoed
48/// back as `null`. The two are distinguished at parse time in
49/// [`handle_line`], not here.
50#[derive(Debug, Clone, Deserialize)]
51pub struct RpcRequest {
52  #[serde(default)]
53  pub jsonrpc: String,
54  pub method: String,
55  #[serde(default)]
56  pub params: Value,
57  #[serde(default)]
58  pub id: Value,
59}
60
61/// Build a JSON-RPC success envelope echoing the request `id`.
62pub fn success(id: &Value, result: Value) -> Value {
63  json!({ "jsonrpc": "2.0", "result": result, "id": id })
64}
65
66/// Build a JSON-RPC error envelope echoing the request `id`.
67pub fn error(id: &Value, code: i64, message: &str) -> Value {
68  json!({ "jsonrpc": "2.0", "error": { "code": code, "message": message }, "id": id })
69}
70
71/// Open the repo that owns `workdir`. A daemon is pinned to one repo
72/// (the one it was launched in); `discover_repo` walks back to the main
73/// workdir if `workdir` is itself a linked worktree.
74fn open_repo(workdir: &Path) -> Result<git2::Repository> {
75  worktree::discover_repo(Some(workdir))
76}
77
78fn run_list(workdir: &Path) -> Result<Vec<JsonWorktree>> {
79  let repo = open_repo(workdir)?;
80  json_api::worktrees(&repo)
81}
82
83fn run_path(workdir: &Path, pattern: &str) -> Result<JsonPath> {
84  let repo = open_repo(workdir)?;
85  let found = worktree::find_fuzzy(&repo, pattern)?;
86  Ok(JsonPath::from(&found))
87}
88
89fn run_doctor(workdir: &Path) -> Result<JsonDoctorReport> {
90  // Mirror `cli::repo_context_lenient` + `cmd_doctor`: lenient config
91  // load and the real global layer, so the daemon's doctor matches
92  // `gwm doctor --format json` byte-for-byte.
93  let repo = open_repo(workdir)?;
94  let repo_workdir = repo
95    .workdir()
96    .ok_or(crate::error::GwmError::NotInGitRepo)?
97    .to_path_buf();
98  let config = Config::load_for_repo(&repo_workdir).unwrap_or_default();
99  let global = crate::config::global_config_path();
100  let ctx = doctor::DoctorCtx {
101    repo_workdir: &repo_workdir,
102    repo: &repo,
103    config: &config,
104    global_config_path: global.as_deref(),
105  };
106  Ok(JsonDoctorReport::from(&doctor::run(&ctx)?))
107}
108
109/// Route one parsed request to its handler and build the response
110/// envelope. Pure of any socket concern; git I/O only. The single place
111/// that knows the method set, shared verbatim by the CLI-equivalent
112/// surface so `list`/`doctor`/`path` over RPC match the `--format=json`
113/// flags.
114pub fn dispatch(workdir: &Path, req: &RpcRequest) -> Value {
115  let id = &req.id;
116  match req.method.as_str() {
117    "list" => match run_list(workdir) {
118      Ok(list) => match serde_json::to_value(list) {
119        Ok(v) => success(id, v),
120        Err(e) => error(id, INTERNAL_ERROR, &e.to_string()),
121      },
122      Err(e) => error(id, INTERNAL_ERROR, &e.to_string()),
123    },
124    "doctor" => match run_doctor(workdir) {
125      Ok(report) => match serde_json::to_value(report) {
126        Ok(v) => success(id, v),
127        Err(e) => error(id, INTERNAL_ERROR, &e.to_string()),
128      },
129      Err(e) => error(id, INTERNAL_ERROR, &e.to_string()),
130    },
131    "path" => match req.params.get("pattern").and_then(|v| v.as_str()) {
132      None => error(id, INVALID_PARAMS, "method 'path' requires a string 'pattern' param"),
133      Some(pattern) => match run_path(workdir, pattern) {
134        Ok(p) => match serde_json::to_value(p) {
135          Ok(v) => success(id, v),
136          Err(e) => error(id, INTERNAL_ERROR, &e.to_string()),
137        },
138        Err(e) => error(id, INTERNAL_ERROR, &e.to_string()),
139      },
140    },
141    // `subscribe` is handled by the connection loop (it streams
142    // notifications), not here; reaching dispatch with it means a caller
143    // used a request/response transport that can't stream.
144    "subscribe" => error(
145      id,
146      INVALID_PARAMS,
147      "method 'subscribe' is only valid over a streaming socket connection",
148    ),
149    other => error(id, METHOD_NOT_FOUND, &format!("unknown method '{other}'")),
150  }
151}
152
153/// Parse one NDJSON request line and return the serialized response line,
154/// or `None` when no response must be sent.
155///
156/// JSON-RPC 2.0 notification handling: a request object with **no `id`
157/// member** is a notification — it is processed but MUST NOT be answered
158/// (returns `None`). An explicit `"id": null` is a normal request and is
159/// answered with `"id": null`. The absent-vs-null distinction is made on
160/// the raw value here (serde would collapse both to `Value::Null`).
161///
162/// A malformed line yields a JSON-RPC parse error (`null` id) rather than
163/// crashing the connection; a well-formed object that isn't a valid
164/// request yields an invalid-request error.
165pub fn handle_line(workdir: &Path, line: &str) -> Option<String> {
166  let value: Value = match serde_json::from_str(line) {
167    Ok(v) => v,
168    Err(e) => return Some(error(&Value::Null, PARSE_ERROR, &format!("parse error: {e}")).to_string()),
169  };
170  let req: RpcRequest = match serde_json::from_value(value.clone()) {
171    Ok(r) => r,
172    Err(e) => return Some(error(&Value::Null, INVALID_REQUEST, &format!("invalid request: {e}")).to_string()),
173  };
174  // Absent `id` ⇒ notification: process for side effects (none for our
175  // read-only methods) but send nothing back. The `?` short-circuits to
176  // `None` (no response) when the `id` member is missing.
177  value.get("id")?;
178  Some(dispatch(workdir, &req).to_string())
179}
180
181/// Build the `worktrees.changed` notification payload (no `id` — it's a
182/// JSON-RPC notification, not a response). Used for the initial
183/// `subscribe` snapshot and every subsequent change.
184///
185/// `params.schema_version` carries [`crate::contract::SCHEMA_VERSION`] so a
186/// long-lived `subscribe` client can detect a contract drift it was not
187/// built for (issue #317). It is an additive, ignorable field — older
188/// clients that only read `params.worktrees` are unaffected.
189pub fn worktrees_changed_notification(worktrees: &[JsonWorktree]) -> Value {
190  json!({
191    "jsonrpc": "2.0",
192    "method": "worktrees.changed",
193    "params": {
194      "schema_version": crate::contract::SCHEMA_VERSION,
195      "worktrees": worktrees,
196    },
197  })
198}
199
200/// True when two worktree snapshots differ in a way a `subscribe` client
201/// should be notified about.
202///
203/// Deliberately **excludes `age_seconds`**: it is recomputed from the
204/// current time on every poll, so for any non-trunk branch it ticks up
205/// each second. Comparing it (a naive `old != new`) would fire a spurious
206/// `worktrees.changed` on every poll, breaking the documented "one per
207/// detected change" contract. Every other field is compared.
208pub fn worktrees_differ(old: &[JsonWorktree], new: &[JsonWorktree]) -> bool {
209  if old.len() != new.len() {
210    return true;
211  }
212  old.iter().zip(new).any(|(a, b)| {
213    a.name != b.name
214      || a.id != b.id
215      || a.path != b.path
216      || a.branch != b.branch
217      || a.head != b.head
218      || a.is_main != b.is_main
219      || a.is_locked != b.is_locked
220      || a.is_prunable != b.is_prunable
221      || a.status != b.status
222      || a.issue != b.issue
223      || a.pr != b.pr
224      // Agents compared whole, `last_activity` included (Codex review round
225      // D): unlike `age_seconds` — recomputed from the clock every poll —
226      // `last_activity` only moves when the agent actually wrote an
227      // artefact, so it IS a real change a subscriber wants (a statusline's
228      // "Ns ago" would otherwise go stale while the agent works). Push
229      // frequency is bounded by the 30 s detection cache, not the poll rate.
230      || a.agents != b.agents
231  })
232}
233
234/// Decide what a `subscribe` stream should push next, given the previous
235/// snapshot (`None` until the first successful one) and the latest poll
236/// result. Returns `Some(snapshot)` to push, or `None` to stay quiet.
237///
238/// Issue #341: a **transient** `Err` from `run_list` (a flaky git scan, an
239/// index lock contended by a concurrent write) is swallowed — we keep the
240/// last good snapshot and push nothing. The pre-fix code did
241/// `run_list(..).unwrap_or_default()`, turning that `Err` into an **empty**
242/// list, which `worktrees_differ` then read as "everything vanished" and
243/// pushed a phantom `worktrees.changed` (subscribers flicker empty, then
244/// self-heal next poll). A genuine `Ok(empty)` — the last worktree really
245/// removed — is still a real change and IS pushed; only the error path is
246/// skipped. Pure so it can be unit-tested without a live socket.
247pub fn next_subscription_push(
248  last: &Option<Vec<JsonWorktree>>,
249  latest: Result<Vec<JsonWorktree>>,
250) -> Option<Vec<JsonWorktree>> {
251  let now = match latest {
252    Ok(now) => now,
253    Err(_) => return None,
254  };
255  match last {
256    None => Some(now),                                       // first snapshot
257    Some(prev) if worktrees_differ(prev, &now) => Some(now), // genuine change
258    Some(_) => None,                                         // unchanged
259  }
260}
261
262// ---------------------------------------------------------------------------
263// Client side — request lines + response/notification parsers (issue #309).
264// Cross-platform and pure: the statusline consumer and any other client
265// reuse these to talk to a running daemon. The socket transport that wraps
266// them lives in the `client` submodule below (unix + `daemon` feature).
267// ---------------------------------------------------------------------------
268
269/// Canonical `list` request line a client writes to the socket.
270pub const LIST_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"list","id":1}"#;
271
272/// Canonical `subscribe` request line a client writes to upgrade the
273/// connection into a one-way `worktrees.changed` stream.
274pub const SUBSCRIBE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"subscribe","id":1}"#;
275
276/// Parse a `list` JSON-RPC **response** line into its worktree vec — the
277/// client counterpart to [`dispatch`]'s `list` arm. A server-sent `error`
278/// envelope is surfaced as a [`GwmError`] rather than silently yielding an
279/// empty list.
280pub fn parse_list_result(line: &str) -> Result<Vec<JsonWorktree>> {
281  let v: Value = serde_json::from_str(line)
282    .map_err(|e| crate::error::GwmError::Other(format!("daemon: malformed list response: {e}")))?;
283  if let Some(err) = v.get("error") {
284    let msg = err.get("message").and_then(Value::as_str).unwrap_or("unknown error");
285    return Err(crate::error::GwmError::Other(format!("daemon list error: {msg}")));
286  }
287  let result = v
288    .get("result")
289    .ok_or_else(|| crate::error::GwmError::Other("daemon list response missing 'result'".into()))?;
290  serde_json::from_value(result.clone())
291    .map_err(|e| crate::error::GwmError::Other(format!("daemon: cannot decode worktree list: {e}")))
292}
293
294/// Parse a `worktrees.changed` **notification** line into its worktree vec
295/// (the `params.worktrees` array). The client counterpart to
296/// [`worktrees_changed_notification`]; consumed by a `subscribe` stream.
297pub fn parse_worktrees_changed(line: &str) -> Result<Vec<JsonWorktree>> {
298  let v: Value = serde_json::from_str(line)
299    .map_err(|e| crate::error::GwmError::Other(format!("daemon: malformed notification: {e}")))?;
300  let arr = v
301    .get("params")
302    .and_then(|p| p.get("worktrees"))
303    .ok_or_else(|| crate::error::GwmError::Other("daemon notification missing 'params.worktrees'".into()))?;
304  serde_json::from_value(arr.clone())
305    .map_err(|e| crate::error::GwmError::Other(format!("daemon: cannot decode notification worktrees: {e}")))
306}
307
308/// Daemon **client** transport — connect / one-shot `list` / `subscribe`
309/// stream. Unix + `daemon` feature, mirroring the server gate: the pure
310/// parsers above stay cross-platform, only the socket I/O is gated.
311#[cfg(all(unix, feature = "daemon"))]
312pub mod client {
313  use super::*;
314  use crate::error::GwmError;
315  use std::io::{BufRead, BufReader, Write};
316  use std::os::unix::net::UnixStream;
317  use std::time::Duration;
318
319  /// Bounded wait for the daemon's first response. A wedged or foreign process
320  /// can accept the connection and then stay silent; without a deadline the
321  /// blocking read would hang the caller — e.g. a shell prompt that shells out
322  /// to `gwm statusline` would freeze instead of degrading. On timeout the read
323  /// errors, which the CLI treats as the documented blank-line degradation.
324  /// Generous enough not to false-trip a slow `run_list` git scan on a large
325  /// repo. `subscribe` drops it once the first snapshot arrives so a long-lived
326  /// `--watch` stream can wait indefinitely between change pushes.
327  const CLIENT_TIMEOUT: Duration = Duration::from_secs(5);
328
329  fn connect(socket: &Path, timeout: Option<Duration>) -> Result<UnixStream> {
330    let stream = UnixStream::connect(socket)
331      .map_err(|e| GwmError::Other(format!("daemon: cannot connect to {}: {e}", socket.display())))?;
332    stream
333      .set_read_timeout(timeout)
334      .map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
335    stream
336      .set_write_timeout(timeout)
337      .map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
338    Ok(stream)
339  }
340
341  /// One-shot `list`: connect, send the request, read and parse the single
342  /// response line. Powers a non-`--watch` statusline render.
343  pub fn list_once(socket: &Path) -> Result<Vec<JsonWorktree>> {
344    list_once_with_timeout(socket, Some(CLIENT_TIMEOUT))
345  }
346
347  /// [`list_once`] with an explicit read/write deadline. Public so tests can
348  /// drive the timeout path quickly; production callers use [`list_once`],
349  /// which applies [`CLIENT_TIMEOUT`].
350  #[doc(hidden)]
351  pub fn list_once_with_timeout(socket: &Path, timeout: Option<Duration>) -> Result<Vec<JsonWorktree>> {
352    let stream = connect(socket, timeout)?;
353    let mut writer = stream
354      .try_clone()
355      .map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
356    writeln!(writer, "{LIST_REQUEST}").map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
357    writer.flush().map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
358
359    let mut reader = BufReader::new(stream);
360    let mut line = String::new();
361    reader
362      .read_line(&mut line)
363      .map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
364    parse_list_result(line.trim())
365  }
366
367  /// Subscribe to `worktrees.changed`: connect, send `subscribe`, then
368  /// invoke `on_snapshot` once per notification — the initial snapshot plus
369  /// every detected change. The loop ends when `on_snapshot` returns
370  /// `false` or the stream closes. Generic over the callback so a `--watch`
371  /// CLI loops forever while a test stops after a fixed number of updates.
372  pub fn subscribe(socket: &Path, mut on_snapshot: impl FnMut(&[JsonWorktree]) -> bool) -> Result<()> {
373    let stream = connect(socket, Some(CLIENT_TIMEOUT))?;
374    let mut writer = stream
375      .try_clone()
376      .map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
377    writeln!(writer, "{SUBSCRIBE_REQUEST}").map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
378    writer.flush().map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
379
380    let mut reader = BufReader::new(stream);
381    let mut delivered_any = false;
382    let mut line = String::new();
383    loop {
384      line.clear();
385      match reader.read_line(&mut line) {
386        Ok(0) => break, // EOF — peer closed
387        Ok(_) => {}
388        Err(_) => break, // timeout / dead link — end the stream
389      }
390      let trimmed = line.trim();
391      if trimmed.is_empty() {
392        continue;
393      }
394      let worktrees = parse_worktrees_changed(trimmed)?;
395      if !delivered_any {
396        // First snapshot arrived: drop the handshake deadline so the
397        // long-lived stream can wait indefinitely between change pushes.
398        let _ = reader.get_ref().set_read_timeout(None);
399      }
400      delivered_any = true;
401      if !on_snapshot(&worktrees) {
402        break;
403      }
404    }
405    // The stream ended without ever yielding a snapshot: the daemon accepted
406    // then closed before its first push (crash right after `accept`, or a
407    // foreign process on the path). Surface this as an error so the caller's
408    // graceful-degradation branch fires — e.g. `statusline --watch` still
409    // emits its promised empty line instead of nothing (issue #312).
410    if !delivered_any {
411      return Err(GwmError::Other(
412        "daemon: stream closed before the first snapshot".to_string(),
413      ));
414    }
415    Ok(())
416  }
417}
418
419// ---------------------------------------------------------------------------
420// Socket server — unix only, behind the `daemon` feature.
421// ---------------------------------------------------------------------------
422
423#[cfg(all(unix, feature = "daemon"))]
424mod server {
425  use super::*;
426  use crate::error::GwmError;
427  use std::io::{BufRead, BufReader, ErrorKind, Read, Write};
428  use std::os::unix::fs::{DirBuilderExt, FileTypeExt, MetadataExt, PermissionsExt};
429  use std::os::unix::net::{UnixListener, UnixStream};
430  use std::path::PathBuf;
431  use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
432  use std::sync::Arc;
433  use std::time::{Duration, Instant};
434
435  /// RAII counter of live client connections. [`ActiveGuard::try_acquire`]
436  /// increments (refusing past the configured cap); `Drop` decrements — so
437  /// even a panicking connection thread frees its slot (issue #341).
438  struct ActiveGuard(Arc<AtomicUsize>);
439
440  impl ActiveGuard {
441    fn try_acquire(active: &Arc<AtomicUsize>, max: usize) -> Option<Self> {
442      if active.fetch_add(1, Ordering::SeqCst) + 1 > max {
443        active.fetch_sub(1, Ordering::SeqCst);
444        return None;
445      }
446      Some(ActiveGuard(Arc::clone(active)))
447    }
448  }
449
450  impl Drop for ActiveGuard {
451    fn drop(&mut self) {
452      self.0.fetch_sub(1, Ordering::SeqCst);
453    }
454  }
455
456  /// How long the accept loop blocks before re-checking the shutdown
457  /// flag. Independent of the worktree poll interval; small enough that a
458  /// test's `serve` thread tears down promptly.
459  const ACCEPT_TICK: Duration = Duration::from_millis(50);
460
461  /// Configuration for [`serve`]. Construct with [`ServeOptions::new`] for
462  /// the production DoS defaults, then override individual guard fields in
463  /// tests (tiny caps / timeouts make the limits assertable without flaky
464  /// timing — issue #341).
465  pub struct ServeOptions {
466    /// Path to bind the unix domain socket at.
467    pub socket: PathBuf,
468    /// The repo this daemon answers for (its main workdir).
469    pub repo_workdir: PathBuf,
470    /// Interval between worktree-state polls for `subscribe` streams.
471    pub poll_interval: Duration,
472    /// Max bytes accepted for a single request line before the connection is
473    /// dropped. Caps memory a client can force the daemon to buffer by never
474    /// sending a newline (DoS guard).
475    pub max_line_len: usize,
476    /// Idle read timeout on the request/response path: a client that opens a
477    /// connection and then stalls (sends nothing, or a partial line) is
478    /// dropped after this, freeing its detached thread (slow-loris guard).
479    /// `None` disables the timeout.
480    pub read_timeout: Option<Duration>,
481    /// Max concurrent client connections. Excess connections are accepted
482    /// then immediately closed, so a connection flood can't exhaust threads
483    /// / file descriptors (DoS guard).
484    pub max_connections: usize,
485    /// Whether [`serve`] owns the socket's parent directory and must create
486    /// it and secure it to `0700`. Set ONLY for the default resolution's
487    /// private `gwm-<uid>/` fallback nest (see [`default_socket`]); never for
488    /// a user-supplied `--socket`, whose parent is left untouched even when
489    /// its name happens to match `gwm-<uid>` (issue #341 review).
490    pub manage_socket_dir: bool,
491  }
492
493  impl ServeOptions {
494    /// 64 KiB is far above any real JSON-RPC request line the daemon serves
495    /// (`list` / `path` / `doctor` / `subscribe`), but bounds a malicious
496    /// unterminated line.
497    pub const DEFAULT_MAX_LINE_LEN: usize = 64 * 1024;
498    /// Request/response clients do one short round-trip; 30 s is generous for
499    /// a real client yet promptly reaps a stalled one.
500    pub const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(30);
501    /// One daemon serves one repo's handful of consumers (TUI, statusline,
502    /// the odd `nc`); 128 concurrent connections is comfortably above that.
503    pub const DEFAULT_MAX_CONNECTIONS: usize = 128;
504
505    /// Build options with production DoS defaults. Tests override the guard
506    /// fields directly afterwards.
507    pub fn new(socket: PathBuf, repo_workdir: PathBuf, poll_interval: Duration) -> Self {
508      Self {
509        socket,
510        repo_workdir,
511        poll_interval,
512        max_line_len: Self::DEFAULT_MAX_LINE_LEN,
513        read_timeout: Some(Self::DEFAULT_READ_TIMEOUT),
514        max_connections: Self::DEFAULT_MAX_CONNECTIONS,
515        // Conservative: only the default `/tmp`-fallback resolution opts in.
516        manage_socket_dir: false,
517      }
518    }
519  }
520
521  /// The current real user id. `getuid(2)` is infallible and has no
522  /// preconditions, so the `unsafe` call is sound.
523  fn current_uid() -> u32 {
524    unsafe { libc::getuid() }
525  }
526
527  /// Name of the per-user private dir the `/tmp` fallback nests the socket
528  /// in (`gwm-<uid>`). The uid namespaces it so two users on the same host
529  /// don't collide on one shared `/tmp` dir.
530  fn private_subdir_name() -> String {
531    format!("gwm-{}", current_uid())
532  }
533
534  /// True when `dir` is a real directory we own with no group/other access
535  /// (`0700`-style). Used to decide whether a base dir is safe to drop the
536  /// socket into directly, or whether it needs a private `gwm-<uid>/` nest.
537  /// `symlink_metadata` so a symlinked base isn't trusted on its target.
538  fn is_private_dir(dir: &Path) -> bool {
539    match std::fs::symlink_metadata(dir) {
540      Ok(m) => m.file_type().is_dir() && m.uid() == current_uid() && m.mode() & 0o077 == 0,
541      Err(_) => false,
542    }
543  }
544
545  /// Place the socket directly in `base` when `base` is genuinely owner-only
546  /// (`$XDG_RUNTIME_DIR` per the XDG spec, macOS's per-user `$TMPDIR`) — the
547  /// `<base>/gwm.sock` path the consumer docs advertise. Otherwise (a base
548  /// that resolves to a shared dir like `/tmp`) nest the socket in a per-user
549  /// owner-only `gwm-<uid>/` sub-dir so it stays un-connectable cross-user.
550  pub fn socket_in(base: &Path) -> PathBuf {
551    if is_private_dir(base) {
552      base.join("gwm.sock")
553    } else {
554      base.join(private_subdir_name()).join("gwm.sock")
555    }
556  }
557
558  /// Resolve the default socket path: `$XDG_RUNTIME_DIR` → `$TMPDIR` → `/tmp`
559  /// for the base dir, then [`socket_in`] to decide direct vs. private-nested
560  /// placement based on the base's actual ownership/perms (issue #341). The
561  /// nested `gwm-<uid>/` dir is created + verified in [`serve`]. Pure modulo
562  /// reading the env and stat-ing the base — server and client agree on the
563  /// result since the base's perms are stable across their runs.
564  pub fn socket_path() -> PathBuf {
565    if let Some(base) = std::env::var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()) {
566      return socket_in(&PathBuf::from(base));
567    }
568    if let Some(base) = std::env::var_os("TMPDIR").filter(|s| !s.is_empty()) {
569      return socket_in(&PathBuf::from(base));
570    }
571    socket_in(Path::new("/tmp"))
572  }
573
574  /// The default [`socket_path`] plus whether [`serve`] should create +
575  /// secure its parent dir. The flag is `true` only when resolution nested
576  /// the socket in a private `gwm-<uid>/` fallback dir (a shared base);
577  /// `false` for the direct `$XDG_RUNTIME_DIR` / `$TMPDIR` paths. The CLI
578  /// passes a user `--socket` with the flag `false`, so a user-supplied
579  /// parent is never modified — even one coincidentally named `gwm-<uid>`
580  /// (issue #341 review).
581  pub fn default_socket() -> (PathBuf, bool) {
582    let path = socket_path();
583    let managed =
584      path.parent().and_then(|d| d.file_name()).and_then(|n| n.to_str()) == Some(private_subdir_name().as_str());
585    (path, managed)
586  }
587
588  /// Ensure `dir` exists as a directory we own with `0700` perms — creating
589  /// it if absent, tightening it if we own it but it's too permissive, and
590  /// refusing if it's a symlink / not a directory / owned by another user (a
591  /// squat on a shared `/tmp`). Only ever called on the gwm-managed
592  /// `gwm-<uid>` dir, never on a system base dir or a user's `--socket`
593  /// parent (issue #341).
594  fn ensure_private_dir(dir: &Path) -> Result<()> {
595    let meta = match std::fs::symlink_metadata(dir) {
596      Ok(m) => m,
597      Err(_) => {
598        return std::fs::DirBuilder::new()
599          .mode(0o700)
600          .create(dir)
601          .map_err(|e| GwmError::Other(format!("daemon: failed to create private dir {}: {e}", dir.display())));
602      }
603    };
604    if !meta.file_type().is_dir() {
605      return Err(GwmError::Other(format!(
606        "daemon: refusing to use {}: exists and is not a directory",
607        dir.display()
608      )));
609    }
610    if meta.uid() != current_uid() {
611      return Err(GwmError::Other(format!(
612        "daemon: refusing to use {}: not owned by the current user",
613        dir.display()
614      )));
615    }
616    // We own it — tighten loose perms rather than refuse (idempotent on a
617    // dir we created `0700` ourselves on a previous run).
618    if meta.mode() & 0o077 != 0 {
619      std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
620        .map_err(|e| GwmError::Other(format!("daemon: failed to restrict perms on {}: {e}", dir.display())))?;
621    }
622    Ok(())
623  }
624
625  /// If a socket file already exists at `path`, decide whether it's stale.
626  /// A successful connect means a live daemon owns it → refuse. A failed
627  /// connect means the previous daemon crashed and left the file →
628  /// unlink it so `bind` can succeed (a stale socket otherwise fails
629  /// `bind` with `EADDRINUSE`).
630  fn clear_stale_socket(path: &Path) -> Result<()> {
631    // `symlink_metadata` (not `metadata`) so a symlink is seen as a
632    // symlink, not followed to its target.
633    let meta = match std::fs::symlink_metadata(path) {
634      Ok(m) => m,
635      Err(_) => return Ok(()), // nothing there — bind will create it
636    };
637    // Refuse to touch anything that isn't a unix socket. A regular file or
638    // symlink at `--socket <path>` would otherwise be deleted as if it were
639    // a stale socket — a data-loss footgun (issue #38 review).
640    if !meta.file_type().is_socket() {
641      return Err(GwmError::Other(format!(
642        "daemon: refusing to use {}: exists and is not a unix socket",
643        path.display()
644      )));
645    }
646    if UnixStream::connect(path).is_ok() {
647      return Err(GwmError::Other(format!(
648        "daemon: socket {} is already in use by a live daemon",
649        path.display()
650      )));
651    }
652    // A socket that no one is listening on — left by a crashed daemon.
653    // Unlink it so `bind` can succeed (it otherwise fails `EADDRINUSE`).
654    let _ = std::fs::remove_file(path);
655    Ok(())
656  }
657
658  /// Bind the socket and serve connections until `shutdown` flips. Each
659  /// connection is handled on its own detached thread. In production the
660  /// flag never flips (the process runs until killed); tests pass a flag
661  /// they flip on teardown.
662  pub fn serve(opts: &ServeOptions, shutdown: Arc<AtomicBool>) -> Result<()> {
663    // When we own the socket's parent dir (the `/tmp` fallback's private
664    // `gwm-<uid>/`, flagged by `manage_socket_dir`), create + verify it
665    // `0700` before binding. `chmod 0600` on the socket alone doesn't block
666    // cross-user connect on platforms that don't enforce socket-file perms
667    // (macOS/BSD); an owner-only parent dir does, since directory-traversal
668    // perms are enforced everywhere. We never touch a system base dir or a
669    // user-supplied `--socket` parent — the flag, not a name match, gates
670    // this so a `--socket` path that happens to sit in a `gwm-<uid>` dir is
671    // left alone (issue #341).
672    if opts.manage_socket_dir {
673      if let Some(parent) = opts.socket.parent() {
674        ensure_private_dir(parent)?;
675      }
676    }
677    clear_stale_socket(&opts.socket)?;
678    let listener = UnixListener::bind(&opts.socket)
679      .map_err(|e| GwmError::Other(format!("daemon: failed to bind {}: {e}", opts.socket.display())))?;
680    // Restrict the socket to the owner (`0600`). A unix socket is created
681    // `0777 & ~umask`; the usual `022` umask leaves it group/other-
682    // connectable — and on Linux socket perms ARE enforced for connect, so
683    // on a shared host's `/tmp` fallback another local user could read the
684    // worktree list. `chmod` (not a `umask` twiddle) because `umask` is
685    // process-global and not thread-safe — a daemon under test runs many
686    // `serve`s in parallel. Fail closed: refuse to serve an over-permissive
687    // socket rather than expose it. The brief bind→chmod window is a
688    // negligible exposure for a read-only socket (issue #341).
689    std::fs::set_permissions(&opts.socket, std::fs::Permissions::from_mode(0o600)).map_err(|e| {
690      let _ = std::fs::remove_file(&opts.socket);
691      GwmError::Other(format!(
692        "daemon: failed to restrict permissions on {}: {e}",
693        opts.socket.display()
694      ))
695    })?;
696    listener
697      .set_nonblocking(true)
698      .map_err(|e| GwmError::Other(format!("daemon: set_nonblocking failed: {e}")))?;
699
700    // Live-connection counter shared with each connection's `ActiveGuard`,
701    // so a connection flood can't exhaust threads / file descriptors. Kept
702    // per-`serve` (not a global static) so parallel tests don't interfere.
703    let active = Arc::new(AtomicUsize::new(0));
704
705    // Announce readiness ONLY now that the socket is bound, so the line
706    // can't precede a bind failure and mislead a wrapper that treats it as
707    // a readiness signal (issue #38 review). stderr keeps stdout clean.
708    eprintln!("gwm daemon listening on {}", opts.socket.display());
709
710    loop {
711      if shutdown.load(Ordering::Relaxed) {
712        break;
713      }
714      match listener.accept() {
715        Ok((stream, _addr)) => {
716          // The listener is non-blocking so the accept loop can poll the
717          // shutdown flag. On macOS the accepted stream INHERITS that
718          // non-blocking flag (unlike Linux, where accept() clears it),
719          // which would make the per-connection blocking read loop spin
720          // out on the first `WouldBlock`. Force the connection back to
721          // blocking so reads wait for the next request.
722          if let Err(e) = stream.set_nonblocking(false) {
723            eprintln!("daemon: failed to set connection blocking: {e}");
724            continue;
725          }
726          // Refuse past the concurrency cap: accept then immediately drop
727          // the stream (closing it) so a flood can't pile up threads.
728          let Some(guard) = ActiveGuard::try_acquire(&active, opts.max_connections) else {
729            continue;
730          };
731          let workdir = opts.repo_workdir.clone();
732          let poll = opts.poll_interval;
733          let max_line_len = opts.max_line_len;
734          let read_timeout = opts.read_timeout;
735          let shutdown = Arc::clone(&shutdown);
736          // Detached: a long-running daemon must not accumulate JoinHandles
737          // for every short-lived client (`nc`, reconnecting integrations).
738          // Each connection thread observes the shared `shutdown` flag and
739          // exits on its own (issue #38 review). `guard` rides along and
740          // frees the connection slot when the thread ends.
741          std::thread::spawn(move || {
742            let _guard = guard;
743            handle_connection(stream, &workdir, poll, max_line_len, read_timeout, &shutdown);
744          });
745        }
746        Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
747          std::thread::sleep(ACCEPT_TICK);
748        }
749        Err(e) => {
750          // Transient accept error — log and keep serving rather than
751          // tearing the daemon down.
752          eprintln!("daemon: accept error: {e}");
753          std::thread::sleep(ACCEPT_TICK);
754        }
755      }
756    }
757
758    // Best-effort cleanup so the next launch sees no stale socket.
759    let _ = std::fs::remove_file(&opts.socket);
760    Ok(())
761  }
762
763  /// Read one newline-terminated request line, bounded two ways (issue
764  /// #341): `max_len` caps the bytes buffered (a never-terminated line is
765  /// dropped), and `deadline` caps the WALL time spent on the whole line.
766  /// The deadline is the real slow-loris guard — `SO_RCVTIMEO` alone resets
767  /// on every successful read, so a client dribbling one byte just under the
768  /// timeout could hold its connection slot until the length cap; shrinking
769  /// the socket timeout toward a fixed per-line deadline closes that.
770  ///
771  /// Returns the line bytes (without the trailing `\n`), or `None` on EOF /
772  /// timeout / oversize / error — the caller then drops the connection.
773  fn read_request_line(
774    stream: &UnixStream,
775    reader: &mut BufReader<UnixStream>,
776    max_len: usize,
777    deadline: Option<Instant>,
778  ) -> Option<Vec<u8>> {
779    let mut buf = Vec::new();
780    loop {
781      if let Some(dl) = deadline {
782        match dl.checked_duration_since(Instant::now()) {
783          // Cap the next read at the line's remaining time budget.
784          Some(rem) if !rem.is_zero() => {
785            let _ = stream.set_read_timeout(Some(rem));
786          }
787          _ => return None, // per-line deadline exceeded — slow-loris
788        }
789      }
790      let chunk = match reader.fill_buf() {
791        Ok(c) => c,
792        Err(e) if e.kind() == ErrorKind::Interrupted => continue,
793        Err(_) => return None, // timeout / reset / dead link
794      };
795      if chunk.is_empty() {
796        return None; // EOF (a partial line here is an incomplete request)
797      }
798      if let Some(pos) = chunk.iter().position(|&b| b == b'\n') {
799        if buf.len() + pos > max_len {
800          return None; // oversize before the newline
801        }
802        buf.extend_from_slice(&chunk[..pos]);
803        reader.consume(pos + 1);
804        return Some(buf);
805      }
806      if buf.len() + chunk.len() > max_len {
807        return None; // unterminated line past the cap
808      }
809      let n = chunk.len();
810      buf.extend_from_slice(chunk);
811      reader.consume(n);
812    }
813  }
814
815  /// Serve one connection: a loop of request→response lines, until the
816  /// client disconnects — or, on a `subscribe`, a switch into a one-way
817  /// notification stream.
818  ///
819  /// Three DoS guards apply on the request/response path (issue #341):
820  /// `read_timeout` reaps a client that opens the connection then stalls and
821  /// bounds the wall time per request line (slow-loris); `max_line_len` caps
822  /// how much an unterminated line can buffer.
823  fn handle_connection(
824    stream: UnixStream,
825    workdir: &Path,
826    poll: Duration,
827    max_line_len: usize,
828    read_timeout: Option<Duration>,
829    shutdown: &AtomicBool,
830  ) {
831    // Baseline blocking/timeout; `read_request_line` shrinks it per read when
832    // a deadline is set. With `read_timeout = None` the read simply blocks.
833    let _ = stream.set_read_timeout(read_timeout);
834    let read_half = match stream.try_clone() {
835      Ok(s) => s,
836      Err(_) => return,
837    };
838    let mut writer = stream;
839    let mut reader = BufReader::new(read_half);
840
841    loop {
842      // Fresh per-line deadline so each request gets the full budget, but no
843      // single line (and no dribbling client) can outlast it.
844      let deadline = read_timeout.map(|t| Instant::now() + t);
845      let Some(bytes) = read_request_line(&writer, &mut reader, max_line_len, deadline) else {
846        break; // EOF, timeout, oversize, or dead link — drop the connection
847      };
848      let line = match std::str::from_utf8(&bytes) {
849        Ok(s) => s.trim(),
850        Err(_) => break, // not a UTF-8 JSON-RPC client
851      };
852      if line.is_empty() {
853        continue;
854      }
855
856      // Peek the method: `subscribe` upgrades the connection to a stream
857      // and never returns to request/response mode.
858      let is_subscribe = serde_json::from_str::<RpcRequest>(line)
859        .map(|r| r.method == "subscribe")
860        .unwrap_or(false);
861      if is_subscribe {
862        stream_subscription(&mut writer, workdir, poll, shutdown);
863        return;
864      }
865
866      // A notification (no `id`) returns None — process, send nothing.
867      if let Some(response) = handle_line(workdir, line) {
868        if writeln!(writer, "{response}").is_err() || writer.flush().is_err() {
869          break;
870        }
871      }
872    }
873  }
874
875  /// Push `worktrees.changed` notifications: an immediate snapshot, then
876  /// one per detected change. Change detection uses [`worktrees_differ`],
877  /// which ignores the always-ticking `age_seconds` so a non-trunk branch
878  /// doesn't spam a notification every poll.
879  ///
880  /// The read timeout doubles as the poll cadence AND the disconnect
881  /// detector: a closed peer makes `read` return `Ok(0)` promptly. Without
882  /// it, the loop only ever *writes* (on change), so a subscriber that
883  /// disconnects during a no-change period would never be observed and the
884  /// detached thread would keep scanning git forever (issue #38 review).
885  fn stream_subscription(stream: &mut UnixStream, workdir: &Path, poll: Duration, shutdown: &AtomicBool) {
886    if stream.set_read_timeout(Some(poll)).is_err() {
887      return;
888    }
889    // `None` until the first SUCCESSFUL snapshot — so a transient git error
890    // on the very first poll defers the immediate snapshot to the next tick
891    // instead of pushing a phantom-empty one (issue #341).
892    let mut last: Option<Vec<JsonWorktree>> = None;
893    if let Some(snapshot) = next_subscription_push(&last, run_list(workdir)) {
894      if send_notification(stream, &snapshot).is_err() {
895        return;
896      }
897      last = Some(snapshot);
898    }
899    let mut buf = [0u8; 64];
900    loop {
901      if shutdown.load(Ordering::Relaxed) {
902        return;
903      }
904      // Blocks up to `poll` waiting for client input — this read IS the
905      // poll wait. A timeout (`WouldBlock`/`TimedOut`) is the normal idle
906      // tick; `Ok(0)` is the peer closing; other errors are a dead link.
907      match stream.read(&mut buf) {
908        Ok(0) => return,
909        Ok(_) => {} // unexpected client chatter on a push stream — ignore
910        Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
911        Err(_) => return,
912      }
913      if let Some(snapshot) = next_subscription_push(&last, run_list(workdir)) {
914        if send_notification(stream, &snapshot).is_err() {
915          return;
916        }
917        last = Some(snapshot);
918      }
919    }
920  }
921
922  fn send_notification(writer: &mut UnixStream, worktrees: &[JsonWorktree]) -> std::io::Result<()> {
923    let note = worktrees_changed_notification(worktrees);
924    writeln!(writer, "{note}")?;
925    writer.flush()
926  }
927}
928
929#[cfg(all(unix, feature = "daemon"))]
930pub use server::{default_socket, serve, socket_in, socket_path, ServeOptions};
931
932/// Injective pipe-name fragment for a Windows account identity (issue
933/// #439). ASCII alphanumerics pass through; every other character —
934/// including the `\` of `DOMAIN\user` — becomes `_XX` (uppercase hex of
935/// each UTF-8 byte), so `alice.smith` / `alice-smith`, or same-named
936/// accounts on two domains, can never share a default pipe name (a lossy
937/// fold would let the owner-only DACL lock the second user out of its own
938/// default). `_` itself is escaped too, which is what makes the mapping
939/// injective. Compiled on every platform so the property is unit-testable
940/// off Windows; only the Windows `socket_path` consumes it.
941pub fn pipe_user_fragment(raw: &str) -> String {
942  let mut out = String::with_capacity(raw.len());
943  for c in raw.chars() {
944    if c.is_ascii_alphanumeric() {
945      out.push(c);
946    } else {
947      let mut buf = [0u8; 4];
948      for b in c.encode_utf8(&mut buf).bytes() {
949        out.push_str(&format!("_{b:02X}"));
950      }
951    }
952  }
953  out
954}
955
956// ---------------------------------------------------------------------------
957// Named-pipe server & client — Windows only, behind the `daemon` feature
958// (issue #439). Exposes the same public interface as the unix module, so
959// `cmd_daemon` / `cmd_statusline` compile identically on both platforms.
960//
961// This is a sibling of `server`, not a shared generic core, on purpose:
962// the unix module's #341 hardening is battle-tested and stays byte-
963// identical, and the two transports differ exactly where a generic
964// abstraction would be the most contorted —
965// - `interprocess`'s sync streams have no read/write timeouts, and its
966//   NOWAIT mode is unusable (an empty-pipe read is downgraded to a fake
967//   EOF — see `peek_available`), so every guard unix gets from
968//   `set_read_timeout` (slow-loris line deadline, subscription poll tick,
969//   dead-peer detection) is rebuilt here on BLOCKING streams polled with
970//   `PeekNamedPipe` before every read;
971// - the cross-user barrier is the pipe's owner-only security descriptor,
972//   the named-pipe analogue of `chmod 0600` + the private socket dir
973//   (`\\.\pipe\` has no directories to restrict).
974// The small shared bits (`ActiveGuard`, `ACCEPT_TICK`) are deliberately
975// duplicated rather than hoisted, to keep the unix module untouched.
976// ---------------------------------------------------------------------------
977
978#[cfg(all(windows, feature = "daemon"))]
979mod server_win {
980  use super::*;
981  use crate::error::GwmError;
982  use interprocess::os::windows::named_pipe::{pipe_mode, PipeListenerOptions, PipeMode, PipeStream};
983  use interprocess::os::windows::security_descriptor::SecurityDescriptor;
984  use interprocess::ConnectWaitMode;
985  use std::io::{BufRead, BufReader, ErrorKind, Write};
986  use std::os::windows::io::{AsHandle, AsRawHandle};
987  use std::path::PathBuf;
988  use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
989  use std::sync::Arc;
990  use std::time::{Duration, Instant};
991
992  /// The accepted duplex byte stream this server speaks over.
993  type Conn = PipeStream<pipe_mode::Bytes, pipe_mode::Bytes>;
994
995  /// RAII counter of live client connections — duplicated from the unix
996  /// module (see the section comment above).
997  struct ActiveGuard(Arc<AtomicUsize>);
998
999  impl ActiveGuard {
1000    fn try_acquire(active: &Arc<AtomicUsize>, max: usize) -> Option<Self> {
1001      if active.fetch_add(1, Ordering::SeqCst) + 1 > max {
1002        active.fetch_sub(1, Ordering::SeqCst);
1003        return None;
1004      }
1005      Some(ActiveGuard(Arc::clone(active)))
1006    }
1007  }
1008
1009  impl Drop for ActiveGuard {
1010    fn drop(&mut self) {
1011      self.0.fetch_sub(1, Ordering::SeqCst);
1012    }
1013  }
1014
1015  /// How long the accept loop sleeps on `WouldBlock` before re-checking the
1016  /// shutdown flag — same value and role as the unix module's.
1017  const ACCEPT_TICK: Duration = Duration::from_millis(50);
1018
1019  /// Sleep quantum of the peek-driven wait loops (request reads, the
1020  /// subscription tick). Small enough that deadlines land promptly, large
1021  /// enough that an idle connection costs a negligible wakeup rate.
1022  const NB_TICK: Duration = Duration::from_millis(15);
1023
1024  /// Configuration for [`serve`] — same shape as the unix module's so the
1025  /// CLI builds it identically on both platforms. `socket` holds the PIPE
1026  /// NAME (`gwm-<user>.sock` → `\\.\pipe\gwm-<user>.sock`), not a
1027  /// filesystem path, and `manage_socket_dir` is accepted but meaningless
1028  /// (pipe names have no parent directory to secure).
1029  pub struct ServeOptions {
1030    /// Name of the pipe to create under `\\.\pipe\`.
1031    pub socket: PathBuf,
1032    /// The repo this daemon answers for (its main workdir).
1033    pub repo_workdir: PathBuf,
1034    /// Interval between worktree-state polls for `subscribe` streams.
1035    pub poll_interval: Duration,
1036    /// Max bytes accepted for a single request line before the connection
1037    /// is dropped (memory-bounding DoS guard, as on unix).
1038    pub max_line_len: usize,
1039    /// Per-request-line wall-time budget, rebuilt on `PeekNamedPipe`
1040    /// polling since the transport has no socket-level timeout. `None`
1041    /// disables the deadline. Writes are BLOCKING and unbudgeted, matching
1042    /// the unix server's `writeln!`.
1043    pub read_timeout: Option<Duration>,
1044    /// Max concurrent client connections (thread-bounding DoS guard).
1045    pub max_connections: usize,
1046    /// Interface parity with unix; no-op here (see the struct docs).
1047    pub manage_socket_dir: bool,
1048  }
1049
1050  impl ServeOptions {
1051    /// Same defaults, same rationale as the unix module.
1052    pub const DEFAULT_MAX_LINE_LEN: usize = 64 * 1024;
1053    pub const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(30);
1054    pub const DEFAULT_MAX_CONNECTIONS: usize = 128;
1055
1056    pub fn new(socket: PathBuf, repo_workdir: PathBuf, poll_interval: Duration) -> Self {
1057      Self {
1058        socket,
1059        repo_workdir,
1060        poll_interval,
1061        max_line_len: Self::DEFAULT_MAX_LINE_LEN,
1062        read_timeout: Some(Self::DEFAULT_READ_TIMEOUT),
1063        max_connections: Self::DEFAULT_MAX_CONNECTIONS,
1064        manage_socket_dir: false,
1065      }
1066    }
1067  }
1068
1069  /// Default pipe name. `\\.\pipe\` is machine-global, so the identity
1070  /// fragment namespaces the default and two users' daemons don't fight
1071  /// over one name — but the [`owner_only_descriptor`] is the actual
1072  /// access barrier; the name only prevents accidental clashes. The
1073  /// fragment is [`pipe_user_fragment`] over `USERDOMAIN\USERNAME`
1074  /// (injective escaping, Codex review #439): two accounts whose names
1075  /// differ only in punctuation, or same-named accounts on different
1076  /// domains, get distinct default names — a lossy fold would let the
1077  /// DACL lock the second user out of its own default.
1078  pub fn socket_path() -> PathBuf {
1079    let user = std::env::var("USERNAME").unwrap_or_else(|_| "default".to_string());
1080    let identity = match std::env::var("USERDOMAIN") {
1081      Ok(domain) if !domain.is_empty() => format!("{domain}\\{user}"),
1082      _ => user,
1083    };
1084    PathBuf::from(format!("gwm-{}.sock", pipe_user_fragment(&identity)))
1085  }
1086
1087  /// The default [`socket_path`] plus the manage-dir flag, which is always
1088  /// `false` here: pipe names are not filesystem paths, there is no parent
1089  /// directory to create or secure.
1090  pub fn default_socket() -> (PathBuf, bool) {
1091    (socket_path(), false)
1092  }
1093
1094  /// Owner-only DACL for the pipe: `D:P` (protected, no inheritance) with a
1095  /// single ACE granting `GENERIC_ALL` to OWNER RIGHTS (`S-1-3-4`) — the
1096  /// user the daemon runs as. A non-empty protected DACL implicitly denies
1097  /// every other SID, so a cross-user connect fails outright: the
1098  /// named-pipe analogue of the unix module's `chmod 0600`. Fail closed:
1099  /// a descriptor error refuses to serve rather than exposing the pipe
1100  /// with the default (Everyone-readable) DACL.
1101  fn owner_only_descriptor() -> Result<SecurityDescriptor> {
1102    let sddl = widestring::U16CString::from_str("D:P(A;;GA;;;OW)")
1103      .map_err(|e| GwmError::Other(format!("daemon: cannot encode the pipe SDDL: {e}")))?;
1104    SecurityDescriptor::deserialize(&sddl)
1105      .map_err(|e| GwmError::Other(format!("daemon: cannot build the pipe security descriptor: {e}")))
1106  }
1107
1108  /// Bytes currently readable on the connection, or `None` when the peer
1109  /// is gone. `PeekNamedPipe` is the canonical non-blocking poll for a
1110  /// BLOCKING named pipe — and blocking streams are a hard requirement
1111  /// here: in `PIPE_NOWAIT` mode an empty-pipe read surfaces raw
1112  /// `ERROR_NO_DATA`, whose kind is `BrokenPipe`, and interprocess's
1113  /// `downgrade_eof` then converts it to `Ok(0)` — indistinguishable from
1114  /// a real EOF, which silently closed idle connections and ended every
1115  /// subscription at its first empty poll (Codex review #439, witnessed in
1116  /// CI). Peeking sidesteps the whole NOWAIT minefield.
1117  fn peek_available(conn: &Conn) -> Option<usize> {
1118    let mut avail: u32 = 0;
1119    // SAFETY: PeekNamedPipe with a null buffer only queries the available
1120    // byte count; the handle is borrowed from `conn` and outlives the call.
1121    let ok = unsafe {
1122      windows_sys::Win32::System::Pipes::PeekNamedPipe(
1123        conn.as_handle().as_raw_handle(),
1124        std::ptr::null_mut(),
1125        0,
1126        std::ptr::null_mut(),
1127        &mut avail,
1128        std::ptr::null_mut(),
1129      )
1130    };
1131    if ok == 0 {
1132      None // broken / disconnected peer
1133    } else {
1134      Some(avail as usize)
1135    }
1136  }
1137
1138  /// Bind the pipe and serve connections until `shutdown` flips — the
1139  /// Windows counterpart of the unix `serve`, same loop shape.
1140  pub fn serve(opts: &ServeOptions, shutdown: Arc<AtomicBool>) -> Result<()> {
1141    // Courtesy probe for a clear "already in use" message, mirroring the
1142    // unix stale-socket check (pipes need no stale cleanup: they vanish
1143    // with their process). BOUNDED for real this time (Codex review #439,
1144    // twice): the `local_socket` adapter silently ignores
1145    // `ConnectOptions::wait_mode`, but the native API honours it — a
1146    // squatted pipe with no available instance times out instead of
1147    // hanging `gwm daemon` startup. The probe is not the real guard —
1148    // the first listener instance is created with
1149    // `FILE_FLAG_FIRST_PIPE_INSTANCE`, so an occupied name fails the bind
1150    // below even when the probe timed out.
1151    let path = widestring::U16CString::from_str(format!("\\\\.\\pipe\\{}", opts.socket.display()))
1152      .map_err(|e| GwmError::Other(format!("daemon: invalid pipe name {}: {e}", opts.socket.display())))?;
1153    let probe = Conn::connect_by_path_with_wait_mode(path.as_ucstr(), ConnectWaitMode::Timeout(Duration::from_secs(1)));
1154    if probe.is_ok() {
1155      return Err(GwmError::Other(format!(
1156        "daemon: pipe {} is already in use by a live daemon",
1157        opts.socket.display()
1158      )));
1159    }
1160    let mut options = PipeListenerOptions::new();
1161    options.path = std::borrow::Cow::Owned(path);
1162    options.mode = PipeMode::Bytes;
1163    options.security_descriptor = Some(owner_only_descriptor()?);
1164    let listener = options.create_duplex::<pipe_mode::Bytes>().map_err(|e| {
1165      GwmError::Other(format!(
1166        "daemon: failed to bind pipe {} (a name that is already claimed is refused — first-instance guard): {e}",
1167        opts.socket.display()
1168      ))
1169    })?;
1170    // Nonblocking ACCEPT so this loop can poll the shutdown flag (as on
1171    // unix). The listener flag also marks the accepted streams
1172    // nonblocking, so each one is flipped back to BLOCKING right after
1173    // accept — see `peek_available` for why NOWAIT streams are unusable.
1174    listener
1175      .set_nonblocking(true)
1176      .map_err(|e| GwmError::Other(format!("daemon: set_nonblocking failed: {e}")))?;
1177
1178    let active = Arc::new(AtomicUsize::new(0));
1179    eprintln!("gwm daemon listening on \\\\.\\pipe\\{}", opts.socket.display());
1180
1181    loop {
1182      if shutdown.load(Ordering::Relaxed) {
1183        break;
1184      }
1185      match listener.accept() {
1186        Ok(conn) => {
1187          if let Err(e) = conn.set_nonblocking(false) {
1188            eprintln!("daemon: failed to set connection blocking: {e}");
1189            continue;
1190          }
1191          let Some(guard) = ActiveGuard::try_acquire(&active, opts.max_connections) else {
1192            continue;
1193          };
1194          let workdir = opts.repo_workdir.clone();
1195          let poll = opts.poll_interval;
1196          let max_line_len = opts.max_line_len;
1197          let read_timeout = opts.read_timeout;
1198          let shutdown = Arc::clone(&shutdown);
1199          std::thread::spawn(move || {
1200            let _guard = guard;
1201            handle_connection(&conn, &workdir, poll, max_line_len, read_timeout, &shutdown);
1202          });
1203        }
1204        Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
1205          std::thread::sleep(ACCEPT_TICK);
1206        }
1207        Err(e) => {
1208          eprintln!("daemon: accept error: {e}");
1209          std::thread::sleep(ACCEPT_TICK);
1210        }
1211      }
1212    }
1213    Ok(())
1214  }
1215
1216  /// Read one newline-terminated request line: the unix `read_request_line`
1217  /// with the socket timeout replaced by a `PeekNamedPipe` wait loop
1218  /// bounded by `deadline` (slow-loris guard) and the shutdown flag. The
1219  /// peek only runs when the BufReader holds nothing — buffered bytes must
1220  /// drain first, or a pipelined second line would wait on a peek that can
1221  /// never see it. Same return contract: `None` on EOF / deadline /
1222  /// oversize / error, and the caller drops the connection.
1223  fn read_request_line(
1224    conn: &Conn,
1225    reader: &mut BufReader<&Conn>,
1226    max_len: usize,
1227    deadline: Option<Instant>,
1228    shutdown: &AtomicBool,
1229  ) -> Option<Vec<u8>> {
1230    let mut buf = Vec::new();
1231    loop {
1232      if shutdown.load(Ordering::Relaxed) {
1233        return None;
1234      }
1235      if let Some(dl) = deadline {
1236        if Instant::now() >= dl {
1237          return None; // per-line deadline exceeded — slow-loris
1238        }
1239      }
1240      if reader.buffer().is_empty() {
1241        match peek_available(conn) {
1242          None => return None, // peer gone
1243          Some(0) => {
1244            std::thread::sleep(NB_TICK);
1245            continue;
1246          }
1247          Some(_) => {} // bytes ready — the blocking fill below won't block
1248        }
1249      }
1250      let chunk = match reader.fill_buf() {
1251        Ok(c) => c,
1252        Err(e) if e.kind() == ErrorKind::Interrupted => continue,
1253        Err(_) => return None, // reset / dead link
1254      };
1255      if chunk.is_empty() {
1256        return None; // EOF (a partial line here is an incomplete request)
1257      }
1258      if let Some(pos) = chunk.iter().position(|&b| b == b'\n') {
1259        if buf.len() + pos > max_len {
1260          return None; // oversize before the newline
1261        }
1262        buf.extend_from_slice(&chunk[..pos]);
1263        reader.consume(pos + 1);
1264        return Some(buf);
1265      }
1266      if buf.len() + chunk.len() > max_len {
1267        return None; // unterminated line past the cap
1268      }
1269      let n = chunk.len();
1270      buf.extend_from_slice(chunk);
1271      reader.consume(n);
1272    }
1273  }
1274
1275  /// Blocking `write_all` + `flush` of one newline-terminated frame — the
1276  /// pipe counterpart of the unix server's `writeln!`. Blocking and
1277  /// unbudgeted on purpose: a subscriber that never drains its end parks
1278  /// only its own connection thread, exactly as on unix.
1279  fn write_frame(mut conn: &Conn, bytes: &[u8]) -> std::io::Result<()> {
1280    conn.write_all(bytes)?;
1281    conn.flush()
1282  }
1283
1284  /// Serve one connection — the unix `handle_connection` on a blocking
1285  /// duplex pipe stream. Same guards, same `subscribe` upgrade.
1286  fn handle_connection(
1287    conn: &Conn,
1288    workdir: &Path,
1289    poll: Duration,
1290    max_line_len: usize,
1291    read_timeout: Option<Duration>,
1292    shutdown: &AtomicBool,
1293  ) {
1294    let mut reader = BufReader::new(conn);
1295    loop {
1296      let deadline = read_timeout.map(|t| Instant::now() + t);
1297      let Some(bytes) = read_request_line(conn, &mut reader, max_line_len, deadline, shutdown) else {
1298        return; // EOF, deadline, oversize, or dead link — drop the connection
1299      };
1300      let line = match std::str::from_utf8(&bytes) {
1301        Ok(s) => s.trim(),
1302        Err(_) => return, // not a UTF-8 JSON-RPC client
1303      };
1304      if line.is_empty() {
1305        continue;
1306      }
1307      let is_subscribe = serde_json::from_str::<RpcRequest>(line)
1308        .map(|r| r.method == "subscribe")
1309        .unwrap_or(false);
1310      if is_subscribe {
1311        stream_subscription(conn, &mut reader, workdir, poll, shutdown);
1312        return;
1313      }
1314      if let Some(response) = handle_line(workdir, line) {
1315        let mut frame = response.into_bytes();
1316        frame.push(b'\n');
1317        if write_frame(conn, &frame).is_err() {
1318          return;
1319        }
1320      }
1321    }
1322  }
1323
1324  /// Push `worktrees.changed` notifications — the unix `stream_subscription`
1325  /// with the timeout-as-poll-tick replaced by a sliced `PeekNamedPipe`
1326  /// wait: each tick sleeps in `NB_TICK` steps while probing the peer, so
1327  /// a closed pipe and the shutdown flag are noticed promptly.
1328  fn stream_subscription(
1329    conn: &Conn,
1330    reader: &mut BufReader<&Conn>,
1331    workdir: &Path,
1332    poll: Duration,
1333    shutdown: &AtomicBool,
1334  ) {
1335    let mut last: Option<Vec<JsonWorktree>> = None;
1336    if let Some(snapshot) = next_subscription_push(&last, run_list(workdir)) {
1337      if send_notification(conn, &snapshot).is_err() {
1338        return;
1339      }
1340      last = Some(snapshot);
1341    }
1342    loop {
1343      let tick_end = Instant::now() + poll;
1344      loop {
1345        if shutdown.load(Ordering::Relaxed) {
1346          return;
1347        }
1348        // Client chatter ENDS the subscription here, unlike on unix
1349        // (which ignores it): the pipe client cannot close its recv half
1350        // from another thread the way a unix client's fd drop does, so
1351        // writing anything IS the client's hang-up signal — the close
1352        // then unblocks its reader thread via EOF (#439). Bytes may sit
1353        // in the BufReader (pipelined after the subscribe line) or on
1354        // the pipe itself.
1355        if !reader.buffer().is_empty() {
1356          return;
1357        }
1358        match peek_available(conn) {
1359          None => return,    // peer closed or dead link
1360          Some(0) => {}      // idle — keep ticking
1361          Some(_) => return, // chatter — hang up
1362        }
1363        let now = Instant::now();
1364        if now >= tick_end {
1365          break;
1366        }
1367        std::thread::sleep(NB_TICK.min(tick_end - now));
1368      }
1369      if let Some(snapshot) = next_subscription_push(&last, run_list(workdir)) {
1370        if send_notification(conn, &snapshot).is_err() {
1371          return;
1372        }
1373        last = Some(snapshot);
1374      }
1375    }
1376  }
1377
1378  fn send_notification(conn: &Conn, worktrees: &[JsonWorktree]) -> std::io::Result<()> {
1379    let mut frame = worktrees_changed_notification(worktrees).to_string().into_bytes();
1380    frame.push(b'\n');
1381    write_frame(conn, &frame)
1382  }
1383}
1384#[cfg(all(windows, feature = "daemon"))]
1385pub use server_win::{default_socket, serve, socket_path, ServeOptions};
1386
1387/// Daemon **client** transport for Windows — same public surface as the
1388/// unix `client` module. The sync pipe streams have no read timeout, so
1389/// every bounded wait runs the blocking read on a helper thread and takes
1390/// the deadline on the channel instead: a wedged daemon must degrade the
1391/// statusline to its documented blank line, never freeze the shell prompt.
1392#[cfg(all(windows, feature = "daemon"))]
1393pub mod client {
1394  use super::*;
1395  use crate::error::GwmError;
1396  use interprocess::os::windows::named_pipe::{pipe_mode, PipeStream};
1397  use interprocess::ConnectWaitMode;
1398  use std::io::{BufRead, BufReader, Write};
1399  use std::os::windows::io::{AsHandle, AsRawHandle};
1400  use std::sync::mpsc;
1401  use std::time::Duration;
1402
1403  /// The duplex byte stream this client speaks over — the NATIVE named-pipe
1404  /// API rather than the `local_socket` adapter, for two reasons (Codex
1405  /// review #439): the adapter silently ignores `ConnectOptions::wait_mode`
1406  /// (unbounded connects), and it hides the handle needed to authenticate
1407  /// the server (see [`verify_server_owner`]).
1408  type Conn = PipeStream<pipe_mode::Bytes, pipe_mode::Bytes>;
1409
1410  /// Same value and rationale as the unix client's handshake deadline.
1411  const CLIENT_TIMEOUT: Duration = Duration::from_secs(5);
1412
1413  /// `\\.\pipe\<name>` as the UTF-16 path the native connect expects.
1414  fn pipe_path(socket: &Path) -> Result<widestring::U16CString> {
1415    widestring::U16CString::from_str(format!("\\\\.\\pipe\\{}", socket.display()))
1416      .map_err(|e| GwmError::Other(format!("daemon: invalid pipe name {}: {e}", socket.display())))
1417  }
1418
1419  /// Refuse a pipe server not owned by the current user (or the builtin
1420  /// Administrators group, which an elevated same-user daemon can own):
1421  /// `\\.\pipe\` names are first-come-first-served and predictable, so
1422  /// another local account could squat `gwm-<user>.sock` with a permissive
1423  /// DACL and feed forged worktree data to the statusline and every other
1424  /// consumer (Codex review #439). The owner SID is read from the CONNECTED
1425  /// kernel object itself, so there is no PID-reuse race; any API failure
1426  /// fails closed. The unix analogue is the owner-only socket directory,
1427  /// which makes squatting the path impossible in the first place.
1428  fn verify_server_owner(conn: &Conn) -> Result<()> {
1429    use windows_sys::Win32::Foundation::{CloseHandle, LocalFree};
1430    use windows_sys::Win32::Security::Authorization::{GetSecurityInfo, SE_KERNEL_OBJECT};
1431    use windows_sys::Win32::Security::{
1432      CreateWellKnownSid, EqualSid, GetTokenInformation, TokenUser, WinBuiltinAdministratorsSid,
1433      OWNER_SECURITY_INFORMATION, PSID, SECURITY_MAX_SID_SIZE, TOKEN_QUERY, TOKEN_USER,
1434    };
1435    use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
1436
1437    let deny = |what: &str| GwmError::Other(format!("daemon: refusing untrusted pipe server ({what})"));
1438
1439    // Owner of the connected pipe object.
1440    let mut owner: PSID = std::ptr::null_mut();
1441    let mut descriptor = std::ptr::null_mut();
1442    // SAFETY: the handle is borrowed from the live connection; out-pointers
1443    // are valid locals. On success `owner` points INTO `descriptor`, which
1444    // must stay alive until the comparisons below and then be LocalFree'd.
1445    let status = unsafe {
1446      GetSecurityInfo(
1447        conn.as_handle().as_raw_handle(),
1448        SE_KERNEL_OBJECT,
1449        OWNER_SECURITY_INFORMATION,
1450        &mut owner,
1451        std::ptr::null_mut(),
1452        std::ptr::null_mut(),
1453        std::ptr::null_mut(),
1454        &mut descriptor,
1455      )
1456    };
1457    if status != 0 || owner.is_null() {
1458      return Err(deny("cannot read the pipe owner"));
1459    }
1460    // Free `descriptor` on every path from here on.
1461    let result = (|| {
1462      // SID of the user this process runs as.
1463      let mut token = std::ptr::null_mut();
1464      // SAFETY: querying our own process token; closed right after the copy.
1465      if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
1466        return Err(deny("cannot open the process token"));
1467      }
1468      // u64 storage so the buffer is 8-aligned: casting a byte array to
1469      // TOKEN_USER trips Rust's misaligned-dereference abort (witnessed
1470      // in CI as STATUS_STACK_BUFFER_OVERRUN).
1471      let mut user_buf = [0u64; 32];
1472      let mut len = 0u32;
1473      // SAFETY: advapi fills a TOKEN_USER into the (large enough) buffer.
1474      let got = unsafe {
1475        GetTokenInformation(
1476          token,
1477          TokenUser,
1478          user_buf.as_mut_ptr().cast(),
1479          (user_buf.len() * 8) as u32,
1480          &mut len,
1481        )
1482      };
1483      // SAFETY: `token` came from a successful OpenProcessToken.
1484      unsafe { CloseHandle(token) };
1485      if got == 0 {
1486        return Err(deny("cannot read the token user"));
1487      }
1488      // SAFETY: on success the buffer holds a valid TOKEN_USER.
1489      let user_sid = unsafe { (*user_buf.as_ptr().cast::<TOKEN_USER>()).User.Sid };
1490
1491      // SAFETY: both SIDs are valid for the duration of the call.
1492      if unsafe { EqualSid(owner, user_sid) } != 0 {
1493        return Ok(());
1494      }
1495      // An elevated daemon's objects can be owned by BUILTIN\Administrators
1496      // rather than the user SID. Accepting that group opens nothing to an
1497      // unprivileged attacker — a local admin already controls the machine.
1498      let mut admin_buf = [0u64; (SECURITY_MAX_SID_SIZE as usize).div_ceil(8)];
1499      let mut admin_len = (admin_buf.len() * 8) as u32;
1500      // SAFETY: CreateWellKnownSid fills the (max-sized) buffer.
1501      let admin_ok = unsafe {
1502        CreateWellKnownSid(
1503          WinBuiltinAdministratorsSid,
1504          std::ptr::null_mut(),
1505          admin_buf.as_mut_ptr().cast(),
1506          &mut admin_len,
1507        )
1508      };
1509      // SAFETY: both SIDs are valid; admin_buf holds a well-known SID.
1510      if admin_ok != 0 && unsafe { EqualSid(owner, admin_buf.as_ptr().cast_mut().cast()) } != 0 {
1511        return Ok(());
1512      }
1513      Err(deny("owned by another account"))
1514    })();
1515    // SAFETY: `descriptor` came from a successful GetSecurityInfo.
1516    unsafe { LocalFree(descriptor.cast()) };
1517    result
1518  }
1519
1520  /// Connect with a REAL bounded wait (the native API honours
1521  /// [`ConnectWaitMode`], unlike the `local_socket` adapter) and refuse a
1522  /// server we cannot authenticate.
1523  fn connect(socket: &Path) -> Result<Conn> {
1524    let path = pipe_path(socket)?;
1525    let conn = Conn::connect_by_path_with_wait_mode(path.as_ucstr(), ConnectWaitMode::Timeout(CLIENT_TIMEOUT))
1526      .map_err(|e| {
1527        GwmError::Other(format!(
1528          "daemon: cannot connect to \\\\.\\pipe\\{}: {e}",
1529          socket.display()
1530        ))
1531      })?;
1532    verify_server_owner(&conn)?;
1533    Ok(conn)
1534  }
1535
1536  /// One-shot `list` with the default handshake deadline.
1537  pub fn list_once(socket: &Path) -> Result<Vec<JsonWorktree>> {
1538    list_once_with_timeout(socket, Some(CLIENT_TIMEOUT))
1539  }
1540
1541  /// [`list_once`] with an explicit deadline (test seam, as on unix). The
1542  /// round-trip runs on a helper thread; on timeout that thread leaks
1543  /// until the short-lived CLI process exits — the accepted cost of the
1544  /// transport's missing read timeout.
1545  #[doc(hidden)]
1546  pub fn list_once_with_timeout(socket: &Path, timeout: Option<Duration>) -> Result<Vec<JsonWorktree>> {
1547    let socket = socket.to_path_buf();
1548    let (tx, rx) = mpsc::channel();
1549    std::thread::spawn(move || {
1550      let _ = tx.send(round_trip(&socket));
1551    });
1552    match timeout {
1553      Some(t) => rx
1554        .recv_timeout(t)
1555        .map_err(|_| GwmError::Other("daemon: timed out waiting for the response".to_string()))?,
1556      None => rx
1557        .recv()
1558        .map_err(|_| GwmError::Other("daemon: client thread died".to_string()))?,
1559    }
1560  }
1561
1562  fn round_trip(socket: &Path) -> Result<Vec<JsonWorktree>> {
1563    let conn = connect(socket)?;
1564    let (recv, mut send) = conn.split();
1565    writeln!(send, "{LIST_REQUEST}").map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
1566    send.flush().map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
1567    let mut reader = BufReader::new(recv);
1568    let mut line = String::new();
1569    reader
1570      .read_line(&mut line)
1571      .map_err(|e| GwmError::Other(format!("daemon: {e}")))?;
1572    parse_list_result(line.trim())
1573  }
1574
1575  /// Subscribe to `worktrees.changed` — same contract as the unix client:
1576  /// the first snapshot is bounded by [`CLIENT_TIMEOUT`], later pushes wait
1577  /// indefinitely, and a stream that closes before any snapshot errors so
1578  /// the caller's degradation branch fires (issue #312).
1579  ///
1580  /// Shape (Codex review #439): connect + handshake + the BLOCKING read
1581  /// loop all live on a helper thread, so the first `recv_timeout` bounds
1582  /// them all — a wedged daemon degrades `statusline --watch` within the
1583  /// deadline instead of hanging it. Ending the stream is a PROTOCOL
1584  /// affair: the send half is handed back to this side, and when
1585  /// `on_snapshot` asks to stop we write a hang-up line — the pipe server
1586  /// closes the connection on any client chatter, which unblocks the
1587  /// reader thread via EOF and frees the daemon's connection slot. If the
1588  /// daemon is wedged and never reads, the reader thread leaks until the
1589  /// short-lived CLI process exits — the same accepted cost as
1590  /// `list_once`'s timeout path.
1591  pub fn subscribe(socket: &Path, mut on_snapshot: impl FnMut(&[JsonWorktree]) -> bool) -> Result<()> {
1592    let socket = socket.to_path_buf();
1593    let (tx, rx) = mpsc::channel::<Result<String>>();
1594    let (half_tx, half_rx) = mpsc::channel();
1595    std::thread::spawn(move || {
1596      let conn = match connect(&socket) {
1597        Ok(s) => s,
1598        Err(e) => {
1599          let _ = tx.send(Err(e));
1600          return;
1601        }
1602      };
1603      let (recv, mut send) = conn.split();
1604      if let Err(e) = writeln!(send, "{SUBSCRIBE_REQUEST}").and_then(|()| send.flush()) {
1605        let _ = tx.send(Err(GwmError::Other(format!("daemon: {e}"))));
1606        return;
1607      }
1608      // Hand the send half to the consumer so it can hang up (see above).
1609      let _ = half_tx.send(send);
1610      let mut reader = BufReader::new(recv);
1611      loop {
1612        let mut line = String::new();
1613        match reader.read_line(&mut line) {
1614          Ok(0) | Err(_) => break, // EOF or dead link — dropping tx ends the stream
1615          Ok(_) => {
1616            if tx.send(Ok(line)).is_err() {
1617              break; // consumer stopped listening
1618            }
1619          }
1620        }
1621      }
1622    });
1623
1624    let mut delivered_any = false;
1625    let mut result = Ok(());
1626    loop {
1627      let msg = if delivered_any {
1628        rx.recv().ok()
1629      } else {
1630        rx.recv_timeout(CLIENT_TIMEOUT).ok()
1631      };
1632      let Some(msg) = msg else { break };
1633      let line = match msg {
1634        Ok(line) => line,
1635        Err(e) => {
1636          result = Err(e);
1637          break;
1638        }
1639      };
1640      let trimmed = line.trim();
1641      if trimmed.is_empty() {
1642        continue;
1643      }
1644      match parse_worktrees_changed(trimmed) {
1645        Ok(worktrees) => {
1646          delivered_any = true;
1647          if !on_snapshot(&worktrees) {
1648            break;
1649          }
1650        }
1651        Err(e) => {
1652          result = Err(e);
1653          break;
1654        }
1655      }
1656    }
1657    // Hang up: any client line makes the pipe server close this
1658    // connection, which unblocks the reader thread via EOF so both halves
1659    // drop and the daemon's slot frees. Best effort — a dead daemon
1660    // already ended the stream.
1661    if let Ok(mut send) = half_rx.try_recv() {
1662      let _ = writeln!(send, "bye").and_then(|()| send.flush());
1663    }
1664    result?;
1665    if !delivered_any {
1666      return Err(GwmError::Other(
1667        "daemon: stream closed before the first snapshot".to_string(),
1668      ));
1669    }
1670    Ok(())
1671  }
1672}