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