Skip to main content

omni_dev/daemon/
mod.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); `stop` / `restart` / `status` are thin [`client::DaemonClient`]s.
17
18// The control plane is a Unix-domain socket (`UnixListener`/`UnixStream`), so the
19// daemon runtime is Unix-only and gated `#[cfg(unix)]`; running the daemon on
20// Windows is future work (#1041). `paths` stays cross-platform because the
21// request log and the browser thin-client token discovery depend on it.
22pub mod paths;
23
24#[cfg(unix)]
25pub mod client;
26#[cfg(unix)]
27pub mod lifecycle;
28#[cfg(unix)]
29pub mod protocol;
30#[cfg(unix)]
31pub mod registry;
32#[cfg(unix)]
33pub mod server;
34#[cfg(unix)]
35pub mod service;
36#[cfg(unix)]
37pub mod services;
38#[cfg(unix)]
39pub mod single_instance;
40
41#[cfg(target_os = "macos")]
42pub mod launchd;
43
44#[cfg(all(target_os = "macos", feature = "menu-bar"))]
45pub mod tray;
46
47#[cfg(unix)]
48use std::path::Path;
49#[cfg(unix)]
50use std::path::PathBuf;
51#[cfg(unix)]
52use std::sync::Arc;
53
54#[cfg(unix)]
55use anyhow::Result;
56
57#[cfg(unix)]
58use crate::browser::BridgeConfig;
59#[cfg(unix)]
60use crate::snowflake::SnowflakeEngineConfig;
61#[cfg(unix)]
62use registry::ServiceRegistry;
63#[cfg(unix)]
64use server::DaemonOptions;
65#[cfg(unix)]
66use services::bridge::BridgeService;
67#[cfg(unix)]
68use services::snowflake::SnowflakeService;
69#[cfg(unix)]
70use services::worktrees::WorktreesService;
71
72/// Everything `daemon run` needs to start the daemon, resolved from the CLI.
73///
74/// Shared by the headless path ([`run_headless`]) and the macOS menu-bar path
75/// (`tray::run`) so both start an identical daemon. The latter is a plain code
76/// span, not an intra-doc link, because the `tray` module is feature- and
77/// target-gated and absent from the docs build.
78#[cfg(unix)]
79#[derive(Debug, Clone)]
80pub struct DaemonRunConfig {
81    /// Control-socket path (also the single-instance lock).
82    pub socket_path: PathBuf,
83    /// Browser-bridge configuration (ports, allow-origin, limits).
84    pub bridge_config: BridgeConfig,
85    /// Optional file the bridge session token is read from instead of generated.
86    pub bridge_token_file: Option<PathBuf>,
87    /// Where the resolved bridge token is persisted (`0600`) for thin clients.
88    pub bridge_token_path: PathBuf,
89}
90
91/// Builds the daemon's default service registry: starts the browser bridge on
92/// its loopback-TCP planes and registers it alongside the Snowflake query
93/// service and the cross-window worktrees registry.
94///
95/// `bridge_token_file` overrides token generation; `bridge_token_path` is where
96/// the resolved token is persisted (`0600`) for thin-client discovery. The
97/// Snowflake service is registered cheaply (no eager auth or I/O); its sessions
98/// are authenticated lazily on first query. The worktrees service is likewise
99/// cheap (in-memory only); it fills as VS Code windows register.
100#[cfg(unix)]
101pub async fn build_default_registry(
102    bridge_config: BridgeConfig,
103    bridge_token_file: Option<&Path>,
104    bridge_token_path: PathBuf,
105) -> Result<ServiceRegistry> {
106    let mut registry = ServiceRegistry::new();
107    let bridge = BridgeService::start(bridge_config, bridge_token_file, bridge_token_path).await?;
108    registry.register(Arc::new(bridge));
109    let snowflake = SnowflakeService::new(SnowflakeEngineConfig::from_env_and_settings()?);
110    registry.register(Arc::new(snowflake));
111    registry.register(Arc::new(WorktreesService::new()));
112    Ok(registry)
113}
114
115/// Runs the daemon headlessly (no tray).
116///
117/// Builds the registry and serves until a signal or `daemon stop`. The default
118/// `daemon run` path on every platform, and the only path when the `menu-bar`
119/// feature is off.
120#[cfg(unix)]
121pub async fn run_headless(cfg: DaemonRunConfig) -> Result<()> {
122    let registry = build_default_registry(
123        cfg.bridge_config,
124        cfg.bridge_token_file.as_deref(),
125        cfg.bridge_token_path,
126    )
127    .await?;
128    server::run(
129        registry,
130        DaemonOptions {
131            socket_path: cfg.socket_path,
132        },
133    )
134    .await
135}