Skip to main content

unifier/daemon/
mod.rs

1//! Hot in-memory daemon lifecycle and client dispatch.
2
3#[cfg(unix)]
4mod client;
5#[cfg(unix)]
6mod idle;
7#[cfg(unix)]
8mod notify;
9#[cfg(unix)]
10mod paths;
11#[cfg(unix)]
12mod protocol;
13#[cfg(unix)]
14mod server;
15#[cfg(unix)]
16pub mod www;
17
18#[cfg(unix)]
19pub use client::{
20    flush as client_flush, ping, read_pid, response_found, response_messages, response_ok,
21    response_uuid, response_value, shutdown, Client,
22};
23#[cfg(unix)]
24pub use notify::{subscribe, watch, EventHub, Notice};
25#[cfg(unix)]
26pub use paths::{daemon_dir, events_socket_path, http_port_path, pid_path, socket_path, www_dir};
27#[cfg(unix)]
28pub use protocol::{ok_empty, Request, Response};
29#[cfg(unix)]
30pub use server::run as run_server;
31
32use std::path::Path;
33use std::process::{Command, Stdio};
34
35use crate::error::{Error, Result};
36use crate::home::UnifierHome;
37
38/// Start the daemon if it is not already running (Gradle-style invocation).
39pub fn ensure_running(home: &UnifierHome) -> Result<()> {
40    if is_running(home) {
41        return Ok(());
42    }
43    start(home, false)
44}
45
46/// Whether a hot daemon is running for this store root.
47pub fn is_running(home: &UnifierHome) -> bool {
48    #[cfg(unix)]
49    {
50        Client::is_running(home)
51    }
52    #[cfg(not(unix))]
53    {
54        let _ = home;
55        false
56    }
57}
58
59/// Start the hot daemon. With `foreground`, blocks in the current process (for tests).
60pub fn start(home: &UnifierHome, foreground: bool) -> Result<()> {
61    #[cfg(not(unix))]
62    {
63        let _ = (home, foreground);
64        return Err(Error::msg("hot daemon requires a Unix platform"));
65    }
66
67    #[cfg(unix)]
68    {
69        if Client::is_running(home) {
70            return Err(Error::msg("daemon is already running"));
71        }
72
73        home.ensure()?;
74        std::fs::create_dir_all(crate::daemon::paths::daemon_dir(home))?;
75
76        if foreground {
77            return run_server(home.clone());
78        }
79
80        let exe = std::env::current_exe()?;
81        let mut cmd = Command::new(exe);
82        cmd.arg("daemon").arg("run");
83        if let Some(p) = home.global_path().to_str() {
84            cmd.args(["--home", p]);
85        }
86        if let Some(name) = home.chroot_name() {
87            cmd.args(["--chroot", name]);
88        }
89        cmd.stdin(Stdio::null())
90            .stdout(Stdio::null())
91            .stderr(Stdio::null());
92
93        let child = cmd.spawn()?;
94        wait_for_socket(home, child.id())?;
95        Ok(())
96    }
97}
98
99/// Stop the daemon, flushing dirty state first.
100pub fn stop(home: &UnifierHome) -> Result<()> {
101    #[cfg(not(unix))]
102    {
103        let _ = home;
104        return Err(Error::msg("hot daemon requires a Unix platform"));
105    }
106
107    #[cfg(unix)]
108    {
109        shutdown(home)
110    }
111}
112
113/// Print daemon status to stdout.
114pub fn status(home: &UnifierHome) -> Result<()> {
115    #[cfg(not(unix))]
116    {
117        let _ = home;
118        println!("daemon: unavailable (requires Unix)");
119        return Ok(());
120    }
121
122    #[cfg(unix)]
123    {
124        if Client::is_running(home) {
125            let pid = read_pid(home)?.unwrap_or(0);
126            println!("daemon: running (pid {pid})");
127            println!("socket: {}", socket_path(home).display());
128            println!("events: {}", events_socket_path(home).display());
129            if let Some(url) = crate::daemon::www::base_url(home) {
130                println!("www: {url}");
131            }
132        } else {
133            println!("daemon: stopped");
134        }
135        Ok(())
136    }
137}
138
139/// Flush dirty in-memory state to disk via the running daemon.
140pub fn flush(home: &UnifierHome) -> Result<()> {
141    #[cfg(not(unix))]
142    {
143        let _ = home;
144        return Err(Error::msg("hot daemon requires a Unix platform"));
145    }
146
147    #[cfg(unix)]
148    {
149        let dirty = client_flush(home)?;
150        if dirty {
151            println!("flushed dirty state to disk");
152        } else {
153            println!("nothing to flush");
154        }
155        Ok(())
156    }
157}
158
159/// Kill orphan `unifier daemon run` processes whose `--home` directory is gone.
160pub fn gc(dry_run: bool) -> Result<()> {
161    #[cfg(not(unix))]
162    {
163        let _ = dry_run;
164        return Err(Error::msg("hot daemon requires a Unix platform"));
165    }
166
167    #[cfg(unix)]
168    {
169        let self_pid = std::process::id();
170        let mut killed = 0usize;
171        let mut skipped = 0usize;
172        let proc = std::fs::read_dir("/proc").map_err(|e| Error::msg(e.to_string()))?;
173        for entry in proc.flatten() {
174            let name = entry.file_name();
175            let name = name.to_string_lossy();
176            if !name.chars().all(|c| c.is_ascii_digit()) {
177                continue;
178            }
179            let pid: u32 = match name.parse() {
180                Ok(p) if p != self_pid => p,
181                _ => continue,
182            };
183            let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).unwrap_or_default();
184            if cmdline.is_empty() {
185                continue;
186            }
187            let args: Vec<&str> = cmdline
188                .split(|&b| b == 0)
189                .filter(|a| !a.is_empty())
190                .filter_map(|a| std::str::from_utf8(a).ok())
191                .collect();
192            if !is_unifier_daemon_run(&args) {
193                continue;
194            }
195            let Some(home) = home_from_args(&args) else {
196                skipped += 1;
197                continue;
198            };
199            if Path::new(home).is_dir() {
200                skipped += 1;
201                continue;
202            }
203            if dry_run {
204                println!("would kill pid={pid} home={home} (missing)");
205                killed += 1;
206                continue;
207            }
208            match send_sigterm(pid) {
209                Ok(()) => {
210                    println!("killed pid={pid} home={home} (missing)");
211                    killed += 1;
212                }
213                Err(e) => eprintln!("failed to kill pid={pid}: {e}"),
214            }
215        }
216        if dry_run {
217            println!("daemon gc dry-run: {killed} orphan(s), {skipped} kept");
218        } else {
219            println!("daemon gc: killed {killed} orphan(s), kept {skipped}");
220        }
221        Ok(())
222    }
223}
224
225#[cfg(unix)]
226fn is_unifier_daemon_run(args: &[&str]) -> bool {
227    let has_unifier = args.iter().any(|a| a.ends_with("unifier") || *a == "unifier");
228    let mut saw_daemon = false;
229    let mut saw_run = false;
230    for a in args {
231        if *a == "daemon" {
232            saw_daemon = true;
233        } else if saw_daemon && *a == "run" {
234            saw_run = true;
235        }
236    }
237    has_unifier && saw_daemon && saw_run
238}
239
240#[cfg(unix)]
241fn home_from_args<'a>(args: &[&'a str]) -> Option<&'a str> {
242    let mut i = 0usize;
243    while i < args.len() {
244        if args[i] == "--home" {
245            return args.get(i + 1).copied();
246        }
247        if let Some(rest) = args[i].strip_prefix("--home=") {
248            return Some(rest);
249        }
250        i += 1;
251    }
252    None
253}
254
255#[cfg(unix)]
256fn send_sigterm(pid: u32) -> Result<()> {
257    let status = Command::new("kill")
258        .args(["-TERM", &pid.to_string()])
259        .status()
260        .map_err(|e| Error::msg(format!("spawn kill: {e}")))?;
261    if status.success() {
262        Ok(())
263    } else {
264        Err(Error::msg(format!("kill -TERM {pid} failed ({status})")))
265    }
266}
267
268#[cfg(unix)]
269fn wait_for_socket(home: &UnifierHome, _pid: u32) -> Result<()> {
270    let path = socket_path(home);
271    for _ in 0..100 {
272        if path.exists() && Client::is_running(home) {
273            return ping(home);
274        }
275        std::thread::sleep(std::time::Duration::from_millis(50));
276    }
277    Err(Error::msg("daemon failed to start"))
278}