Skip to main content

zshrs_daemon/
lib.rs

1// Daemon for zshrs — see docs/DAEMON.md.
2//
3// Module layout:
4//   paths.rs   — ~/.zshrs/* paths + 0700/0600 permissions
5//   log.rs     — tracing-subscriber setup, rolling file at zshrs-daemon.log
6//   ipc.rs     — u32-BE-length-prefixed JSON framing + message types
7//   pidlock.rs — singleton flock on daemon.pid + spawn-on-demand
8//   server.rs  — tokio accept loop + per-session connection handler
9//   state.rs   — DaemonState (sessions, tags, registry, broadcast channels)
10//   ops.rs     — IPC operation dispatch
11//   client.rs  — client-side IPC helpers used by z* builtins
12//   firstrun.rs — first-run detection + 6-line stderr notice
13/// `artifact` submodule.
14pub mod artifact;
15/// `auth` submodule.
16pub mod auth;
17/// `builtins` submodule.
18pub mod builtins;
19/// `cache` submodule.
20pub mod cache;
21/// `canonical` submodule.
22pub mod canonical;
23/// `catalog` submodule.
24pub mod catalog;
25/// `client` submodule.
26pub mod client;
27/// `definitions` submodule.
28pub mod definitions;
29/// `export` submodule.
30pub mod export;
31/// `firstrun` submodule.
32pub mod firstrun;
33/// `fsnotify` submodule.
34pub mod fsnotify;
35/// `history` submodule.
36pub mod history;
37/// `http` submodule.
38pub mod http;
39/// `ipc` submodule.
40pub mod ipc;
41/// `jobs` submodule.
42pub mod jobs;
43/// `lock` submodule.
44pub mod lock;
45/// `log` submodule.
46pub mod log;
47/// `metrics` submodule.
48pub mod metrics;
49/// `ops` submodule.
50pub mod ops;
51/// `paths` submodule.
52pub mod paths;
53/// `pidlock` submodule.
54pub mod pidlock;
55/// `pubsub` submodule.
56pub mod pubsub;
57/// `schedule` submodule.
58pub mod schedule;
59/// `server` submodule.
60pub mod server;
61/// `shard` submodule.
62pub mod shard;
63/// `snapshot` submodule.
64pub mod snapshot;
65/// `source_resolver` submodule.
66pub mod source_resolver;
67/// `state` submodule.
68pub mod state;
69/// `ticker` submodule.
70pub mod ticker;
71/// `zask` submodule.
72pub mod zask;
73/// `zask_builtin` submodule.
74pub mod zask_builtin;
75/// `zcomplete_builtin` submodule.
76pub mod zcomplete_builtin;
77/// `zd_dispatch` submodule.
78pub mod zd_dispatch;
79/// `zhistory_builtin` submodule.
80pub mod zhistory_builtin;
81/// `zjob_builtin` submodule.
82pub mod zjob_builtin;
83/// `zsource_builtin` submodule.
84pub mod zsource_builtin;
85/// `zsync` submodule.
86pub mod zsync;
87/// `zsync_builtin` submodule.
88pub mod zsync_builtin;
89
90// The static AST-walk pipeline (`ast_walker`, `walk`, `plugin_walk`,
91// `zshrc_analysis`) was removed: state attribution comes from
92// `zshrs-recorder` (runtime AOP intercept, see docs/RECORDER.md) instead
93// of parsing user config files. The daemon still services cache ops and
94// fsnotify-driven shard-update broadcasts, but never re-derives state by
95// walking sources. New plugin installs require the user to re-run
96// `zshrs-recorder`.
97
98pub use ipc::{Event, Frame, Hello, ProtocolVersion, Welcome, PROTOCOL_VERSION};
99pub use paths::CachePaths;
100
101/// Result type used throughout the daemon.
102pub type Result<T> = std::result::Result<T, DaemonError>;
103/// `DaemonError` — see variants.
104#[derive(thiserror::Error, Debug)]
105pub enum DaemonError {
106    /// `Io` variant.
107    #[error("io: {0}")]
108    Io(#[from] std::io::Error),
109    /// `Json` variant.
110    #[error("json: {0}")]
111    Json(#[from] serde_json::Error),
112    /// `Nix` variant.
113    #[error("nix: {0}")]
114    Nix(#[from] nix::Error),
115    /// `Sqlite` variant.
116    #[error("rusqlite: {0}")]
117    Sqlite(#[from] rusqlite::Error),
118    /// `AlreadyRunning` variant.
119    #[error("singleton: another daemon is running (pid {0})")]
120    AlreadyRunning(i32),
121
122    #[error("protocol: client v{client} incompatible with daemon v{daemon}")]
123    ProtocolMismatch { client: u32, daemon: u32 },
124    /// `BadHandshake` variant.
125    #[error("protocol: malformed handshake")]
126    BadHandshake,
127
128    #[error("protocol: frame too large ({size} > {max})")]
129    FrameTooLarge { size: usize, max: usize },
130    /// `Shutdown` variant.
131    #[error("daemon: shutting down")]
132    Shutdown,
133    /// `UnknownOp` variant.
134    #[error("op: unknown opcode {0:?}")]
135    UnknownOp(String),
136
137    #[error("op: bad args for {op}: {reason}")]
138    BadArgs { op: String, reason: String },
139    /// `NotConnected` variant.
140    #[error("client: not connected to daemon")]
141    NotConnected,
142    /// `Timeout` variant.
143    #[error("client: timed out after {0:?}")]
144    Timeout(std::time::Duration),
145    /// `Other` variant.
146    #[error("{0}")]
147    Other(String),
148}
149
150impl DaemonError {
151    /// `other` — see implementation.
152    pub fn other<S: Into<String>>(msg: S) -> Self {
153        Self::Other(msg.into())
154    }
155}
156
157/// Run the daemon to completion. Used by `zshrs --daemon` mode.
158///
159/// Acquires the singleton lock, opens IPC socket, sets up logging, runs the accept loop
160/// until SIGTERM/SIGINT or a `daemon stop` IPC op.
161pub fn run() -> Result<()> {
162    let paths = paths::CachePaths::resolve()?;
163    paths.ensure_dirs()?;
164
165    // Seed zshrs.toml + zshrs-daemon.toml + zshrs-recorder.toml with defaults BEFORE log::init so
166    // the new [log] level directive can be picked up on first run.
167    // Idempotent — never overwrites user edits.
168    if let Err(e) = paths.ensure_default_configs() {
169        eprintln!("zshrs-daemon: failed to seed default configs: {e}");
170    }
171
172    // Logging next so subsequent setup is observable.
173    let _log_guard = log::init(&paths)?;
174
175    tracing::info!(version = env!("CARGO_PKG_VERSION"), "zshrs-daemon starting");
176
177    // First-pass diagnostics — gated to TRACE so they don't flood
178    // the log at default INFO. Bump `[log] level = "trace"` in
179    // zshrs-daemon.toml (or `ZSHRS_LOG=trace`) to see them.
180    let pid = std::process::id();
181    let cwd = std::env::current_dir()
182        .map(|p| p.display().to_string())
183        .unwrap_or_else(|_| "?".to_string());
184    let zshrs_home = std::env::var("ZSHRS_HOME").unwrap_or_default();
185    tracing::trace!(
186        pid,
187        cwd,
188        zshrs_home = if zshrs_home.is_empty() { "<unset>" } else { &zshrs_home },
189        root = %paths.root.display(),
190        socket = %paths.socket.display(),
191        pid_file = %paths.pid_file.display(),
192        log = %paths.log.display(),
193        daemon_toml = %paths.daemon_config_path().display(),
194        shell_toml = %paths.shell_config_path().display(),
195        catalog_db = %paths.catalog_db.display(),
196        history_db = %paths.history_db.display(),
197        cache_db = %paths.cache_db.display(),
198        images_dir = %paths.images.display(),
199        artifacts_dir = %paths.artifacts_dir.display(),
200        snapshots_dir = %paths.snapshots_dir.display(),
201        replay_dir = %paths.replay_dir.display(),
202        "daemon: resolved environment"
203    );
204
205    // Singleton enforcement. Holds the lock for daemon lifetime.
206    let _pid_lock = pidlock::acquire(&paths)?;
207    tracing::trace!(pid_file = %paths.pid_file.display(), pid, "daemon: pidlock acquired");
208
209    // Spin up tokio runtime + run server.
210    let rt = tokio::runtime::Builder::new_multi_thread()
211        .enable_all()
212        .thread_name("zshrs-daemon")
213        .build()?;
214
215    let result = rt.block_on(server::serve(paths.clone()));
216
217    tracing::info!("zshrs-daemon exiting");
218
219    result
220}