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::{
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
41pub fn ensure_running(home: &UnifierHome) -> Result<()> {
43 if is_running(home) {
44 return Ok(());
45 }
46 start(home, false)
47}
48
49pub 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
62pub 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 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 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
119pub 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
133pub 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
160pub 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
180pub 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}