#[cfg(unix)]
mod client;
#[cfg(unix)]
mod idle;
#[cfg(unix)]
mod notify;
#[cfg(unix)]
mod paths;
#[cfg(unix)]
mod protocol;
#[cfg(unix)]
mod server;
#[cfg(unix)]
pub mod www;
#[cfg(unix)]
pub use client::{
flush as client_flush, ping, read_pid, response_found, response_messages, response_ok,
response_uuid, response_value, shutdown, Client,
};
#[cfg(unix)]
pub use notify::{subscribe, watch, EventHub, Notice};
#[cfg(unix)]
pub use paths::{
daemon_dir, events_socket_path, http_port_path, pid_path, socket_path, tick_socket_path,
www_dir,
};
#[cfg(unix)]
pub use protocol::{is_tick_socket_request, ok_empty, Request, Response};
#[cfg(unix)]
pub use server::run as run_server;
use std::path::Path;
use std::process::{Command, Stdio};
use crate::error::{Error, Result};
use crate::home::UnifierHome;
pub fn ensure_running(home: &UnifierHome) -> Result<()> {
if is_running(home) {
return Ok(());
}
start(home, false)
}
pub fn is_running(home: &UnifierHome) -> bool {
#[cfg(unix)]
{
Client::is_running(home)
}
#[cfg(not(unix))]
{
let _ = home;
false
}
}
pub fn start(home: &UnifierHome, foreground: bool) -> Result<()> {
#[cfg(not(unix))]
{
let _ = (home, foreground);
return Err(Error::msg("hot daemon requires a Unix platform"));
}
#[cfg(unix)]
{
if Client::is_running(home) {
return Err(Error::msg("daemon is already running"));
}
home.ensure()?;
std::fs::create_dir_all(crate::daemon::paths::daemon_dir(home))?;
if foreground {
return run_server(home.clone());
}
let exe = std::env::current_exe()?;
let mut cmd = Command::new(exe);
cmd.arg("daemon").arg("run");
if let Some(p) = home.global_path().to_str() {
cmd.args(["--home", p]);
}
if let Some(name) = home.chroot_name() {
cmd.args(["--chroot", name]);
}
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
if libc::setsid() == -1 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EPERM) {
return Err(err);
}
}
Ok(())
});
}
let child = cmd.spawn()?;
wait_for_socket(home, child.id())?;
Ok(())
}
}
pub fn stop(home: &UnifierHome) -> Result<()> {
#[cfg(not(unix))]
{
let _ = home;
return Err(Error::msg("hot daemon requires a Unix platform"));
}
#[cfg(unix)]
{
shutdown(home)
}
}
pub fn status(home: &UnifierHome) -> Result<()> {
#[cfg(not(unix))]
{
let _ = home;
println!("daemon: unavailable (requires Unix)");
return Ok(());
}
#[cfg(unix)]
{
if Client::is_running(home) {
let pid = read_pid(home)?.unwrap_or(0);
println!("daemon: running (pid {pid})");
println!("socket: {}", socket_path(home).display());
println!("events: {}", events_socket_path(home).display());
println!("tick: {}", tick_socket_path(home).display());
if let Some(url) = crate::daemon::www::base_url(home) {
println!("www: {url}");
}
} else {
println!("daemon: stopped");
}
Ok(())
}
}
pub fn flush(home: &UnifierHome) -> Result<()> {
#[cfg(not(unix))]
{
let _ = home;
return Err(Error::msg("hot daemon requires a Unix platform"));
}
#[cfg(unix)]
{
let dirty = client_flush(home)?;
if dirty {
println!("flushed dirty state to disk");
} else {
println!("nothing to flush");
}
Ok(())
}
}
pub fn gc(dry_run: bool) -> Result<()> {
#[cfg(not(unix))]
{
let _ = dry_run;
return Err(Error::msg("hot daemon requires a Unix platform"));
}
#[cfg(unix)]
{
let self_pid = std::process::id();
let mut killed = 0usize;
let mut skipped = 0usize;
let proc = std::fs::read_dir("/proc").map_err(|e| Error::msg(e.to_string()))?;
for entry in proc.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.chars().all(|c| c.is_ascii_digit()) {
continue;
}
let pid: u32 = match name.parse() {
Ok(p) if p != self_pid => p,
_ => continue,
};
let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).unwrap_or_default();
if cmdline.is_empty() {
continue;
}
let args: Vec<&str> = cmdline
.split(|&b| b == 0)
.filter(|a| !a.is_empty())
.filter_map(|a| std::str::from_utf8(a).ok())
.collect();
if !is_unifier_daemon_run(&args) {
continue;
}
let Some(home) = home_from_args(&args) else {
skipped += 1;
continue;
};
if Path::new(home).is_dir() {
skipped += 1;
continue;
}
if dry_run {
println!("would kill pid={pid} home={home} (missing)");
killed += 1;
continue;
}
match send_sigterm(pid) {
Ok(()) => {
println!("killed pid={pid} home={home} (missing)");
killed += 1;
}
Err(e) => eprintln!("failed to kill pid={pid}: {e}"),
}
}
if dry_run {
println!("daemon gc dry-run: {killed} orphan(s), {skipped} kept");
} else {
println!("daemon gc: killed {killed} orphan(s), kept {skipped}");
}
Ok(())
}
}
#[cfg(unix)]
fn is_unifier_daemon_run(args: &[&str]) -> bool {
let has_unifier = args
.iter()
.any(|a| a.ends_with("unifier") || *a == "unifier");
let mut saw_daemon = false;
let mut saw_run = false;
for a in args {
if *a == "daemon" {
saw_daemon = true;
} else if saw_daemon && *a == "run" {
saw_run = true;
}
}
has_unifier && saw_daemon && saw_run
}
#[cfg(unix)]
fn home_from_args<'a>(args: &[&'a str]) -> Option<&'a str> {
let mut i = 0usize;
while i < args.len() {
if args[i] == "--home" {
return args.get(i + 1).copied();
}
if let Some(rest) = args[i].strip_prefix("--home=") {
return Some(rest);
}
i += 1;
}
None
}
#[cfg(unix)]
fn send_sigterm(pid: u32) -> Result<()> {
let status = Command::new("kill")
.args(["-TERM", &pid.to_string()])
.status()
.map_err(|e| Error::msg(format!("spawn kill: {e}")))?;
if status.success() {
Ok(())
} else {
Err(Error::msg(format!("kill -TERM {pid} failed ({status})")))
}
}
#[cfg(unix)]
fn wait_for_socket(home: &UnifierHome, _pid: u32) -> Result<()> {
let path = socket_path(home);
for _ in 0..100 {
if path.exists() && Client::is_running(home) {
return ping(home);
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(Error::msg("daemon failed to start"))
}