1#[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
38pub fn ensure_running(home: &UnifierHome) -> Result<()> {
40 if is_running(home) {
41 return Ok(());
42 }
43 start(home, false)
44}
45
46pub 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
59pub 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
99pub 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
113pub 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
139pub 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
159pub 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}