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}