omni_dev/daemon.rs
1//! The extensible omni-dev daemon: a long-lived supervisor that hosts pluggable
2//! [`DaemonService`](service::DaemonService)s over a local Unix-domain control
3//! socket.
4//!
5//! The daemon owns **lifecycle, single-instance supervision, status
6//! aggregation, and (on macOS) the menu-bar shell**. Each service wraps its own
7//! work and exposes status/control; the browser bridge is the first such
8//! service (#987). The control socket is a private operator/tray channel — it
9//! does **not** carry any service's own data plane (e.g. the bridge keeps its
10//! loopback-TCP planes per ADR-0036). See ADR-0039.
11//!
12//! Process model:
13//! - `daemon run` *becomes* the daemon ([`server::run`]), blocking until a
14//! signal or a built-in `shutdown` op.
15//! - `daemon start` launches it in the background (a launchd LaunchAgent on
16//! macOS, a systemd user unit on Linux); `stop` / `restart` / `status` are thin
17//! [`client::DaemonClient`]s.
18
19// The control plane is a Unix-domain socket (`UnixListener`/`UnixStream`), so the
20// daemon runtime is Unix-only and gated `#[cfg(unix)]`; running the daemon on
21// Windows is future work (#1237). `paths` stays cross-platform because the
22// request log and the browser thin-client token discovery depend on it.
23pub mod paths;
24
25#[cfg(unix)]
26pub mod client;
27#[cfg(unix)]
28pub mod lifecycle;
29#[cfg(unix)]
30pub mod protocol;
31#[cfg(unix)]
32pub mod registry;
33#[cfg(unix)]
34pub mod server;
35#[cfg(unix)]
36pub mod service;
37#[cfg(unix)]
38pub mod services;
39#[cfg(unix)]
40pub mod single_instance;
41
42#[cfg(all(unix, test))]
43pub(crate) mod testutil;
44
45#[cfg(target_os = "macos")]
46pub mod launchd;
47
48#[cfg(target_os = "linux")]
49pub mod systemd;
50
51#[cfg(all(target_os = "macos", feature = "menu-bar"))]
52pub mod tray;
53
54#[cfg(unix)]
55use std::path::Path;
56#[cfg(unix)]
57use std::path::PathBuf;
58#[cfg(unix)]
59use std::sync::Arc;
60
61#[cfg(unix)]
62use anyhow::Result;
63
64#[cfg(unix)]
65use crate::browser::BridgeConfig;
66#[cfg(unix)]
67use crate::snowflake::SnowflakeEngineConfig;
68#[cfg(unix)]
69use registry::ServiceRegistry;
70#[cfg(unix)]
71use server::DaemonOptions;
72#[cfg(unix)]
73use services::bridge::BridgeService;
74#[cfg(unix)]
75use services::sessions::SessionsService;
76#[cfg(unix)]
77use services::snowflake::SnowflakeService;
78#[cfg(unix)]
79use services::worktrees::WorktreesService;
80
81/// Everything `daemon run` needs to start the daemon, resolved from the CLI.
82///
83/// Shared by the headless path ([`run_headless`]) and the macOS menu-bar path
84/// (`tray::run`) so both start an identical daemon. The latter is a plain code
85/// span, not an intra-doc link, because the `tray` module is feature- and
86/// target-gated and absent from the docs build.
87#[cfg(unix)]
88#[derive(Debug, Clone)]
89pub struct DaemonRunConfig {
90 /// Control-socket path (also the single-instance lock).
91 pub socket_path: PathBuf,
92 /// Browser-bridge configuration (ports, allow-origin, limits).
93 pub bridge_config: BridgeConfig,
94 /// Optional file the bridge session token is read from instead of generated.
95 pub bridge_token_file: Option<PathBuf>,
96 /// Where the resolved bridge token is persisted (`0600`) for thin clients.
97 pub bridge_token_path: PathBuf,
98}
99
100/// Builds the daemon's default service registry.
101///
102/// Starts the browser bridge on its loopback-TCP planes and registers it
103/// alongside the Snowflake query service, the cross-window worktrees registry,
104/// and the Claude Code sessions tracker.
105///
106/// `bridge_token_file` overrides token generation; `bridge_token_path` is where
107/// the resolved token is persisted (`0600`) for thin-client discovery. The
108/// Snowflake service is registered cheaply (no eager auth or I/O); its sessions
109/// are authenticated lazily on first query. The worktrees and sessions services
110/// are likewise cheap (in-memory only); they fill as VS Code windows register and
111/// as Claude Code hooks/transcripts report.
112#[cfg(unix)]
113pub async fn build_default_registry(
114 bridge_config: BridgeConfig,
115 bridge_token_file: Option<&Path>,
116 bridge_token_path: PathBuf,
117) -> Result<ServiceRegistry> {
118 let mut registry = ServiceRegistry::new();
119 let bridge = BridgeService::start(bridge_config, bridge_token_file, bridge_token_path).await?;
120 registry.register(Arc::new(bridge));
121 let snowflake = SnowflakeService::new(SnowflakeEngineConfig::from_env_and_settings()?);
122 registry.register(Arc::new(snowflake));
123 // Start the off-thread menu-refresh loop so the tray serves a cached menu
124 // instead of running git enrichment on the macOS GUI thread (#1186 fix).
125 let worktrees = WorktreesService::new();
126 worktrees.start_menu_refresh();
127 // Keep PR check badges fresh for every open window from one `gh` call, rather
128 // than each window resolving its own and none of them ever re-asking (#1337).
129 worktrees.start_pr_poller();
130 registry.register(Arc::new(worktrees));
131 // The cross-window Claude Code sessions tracker; start its transcript watcher
132 // (Feed 2) so sessions predating the daemon — and the hook-silent thinking
133 // window — are still tracked (#1210).
134 let sessions = SessionsService::new();
135 sessions.start_watcher();
136 registry.register(Arc::new(sessions));
137 Ok(registry)
138}
139
140/// Runs the daemon headlessly (no tray).
141///
142/// Builds the registry and serves until a signal or `daemon stop`. The default
143/// `daemon run` path on every platform, and the only path when the `menu-bar`
144/// feature is off.
145#[cfg(unix)]
146pub async fn run_headless(cfg: DaemonRunConfig) -> Result<()> {
147 let registry = build_default_registry(
148 cfg.bridge_config,
149 cfg.bridge_token_file.as_deref(),
150 cfg.bridge_token_path,
151 )
152 .await?;
153 server::run(
154 registry,
155 DaemonOptions {
156 socket_path: cfg.socket_path,
157 },
158 )
159 .await
160}