#![warn(clippy::pedantic)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::struct_field_names)]
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "ixd",
version = env!("CARGO_PKG_VERSION"),
about = "Background daemon that watches directories for changes and rebuilds the index.",
after_help = "EXAMPLES:\n \
ixd /path/to/repo\n \
ixd --daemon /path/to/repo # detach and run in the background\n \
ixd /project-a /project-b /project-c\n\n\
DOCS:\n \
https://github.com/moeshawky/ix/blob/main/docs/DAEMON-RUNBOOK.md\n \
https://github.com/moeshawky/ix/blob/main/docs/.ixd.toml.md"
)]
struct Cli {
#[arg(default_value = ".", value_name = "PATH")]
paths: Vec<PathBuf>,
#[arg(long)]
daemon: bool,
#[arg(long)]
stop: bool,
}
#[cfg(unix)]
fn main() -> ix::error::Result<()> {
let cli = Cli::parse();
let roots = resolve_roots(&cli.paths)?;
if cli.stop {
stop_daemons(&roots);
return Ok(());
}
if cli.daemon {
daemonize()?;
}
ix::daemon::run_many(&roots)
}
#[cfg(not(unix))]
fn main() {
eprintln!("ixd: the daemon is not supported on this platform");
std::process::exit(1);
}
#[cfg(unix)]
fn resolve_roots(paths: &[PathBuf]) -> ix::error::Result<Vec<PathBuf>> {
if paths.is_empty() {
return Ok(Vec::new());
}
let cwd = std::env::current_dir().map_err(|e| {
ix::error::Error::Config(format!("ixd: cannot determine current directory: {e}"))
})?;
paths
.iter()
.map(|p| {
if p.is_absolute() {
Ok(p.clone())
} else {
Ok(cwd.join(p))
}
})
.collect()
}
#[cfg(unix)]
fn daemonize() -> ix::error::Result<()> {
use nix::unistd::{ForkResult, fork, setsid};
use std::fs::OpenOptions;
use std::os::fd::AsRawFd;
let cfg = |ctx: &str, e: nix::errno::Errno| {
ix::error::Error::Config(format!("daemonize: {ctx}: {e}"))
};
match unsafe { fork() }.map_err(|e| cfg("fork", e))? {
ForkResult::Parent { .. } => std::process::exit(0),
ForkResult::Child => {}
}
setsid().map_err(|e| cfg("setsid", e))?;
match unsafe { fork() }.map_err(|e| cfg("fork", e))? {
ForkResult::Parent { .. } => std::process::exit(0),
ForkResult::Child => {}
}
let _ = std::env::set_current_dir("/");
let devnull = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
.map_err(|e| ix::error::Error::Config(format!("daemonize: cannot open /dev/null: {e}")))?;
let raw = devnull.as_raw_fd();
for fd in [0_i32, 1, 2] {
let rc = unsafe { libc::dup2(raw, fd) };
if rc < 0 {
return Err(ix::error::Error::Config(format!(
"daemonize: cannot redirect fd {fd} to /dev/null"
)));
}
}
std::mem::forget(devnull);
Ok(())
}
#[cfg(unix)]
fn stop_daemons(roots: &[PathBuf]) {
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
use std::collections::HashSet;
use std::thread::sleep;
use std::time::Duration;
let mut stopped_pids = HashSet::new();
for root in roots {
let ix_dir = root.join(".ix");
let beacon = if let Ok(b) = ix::format::Beacon::read_from(&ix_dir) {
b
} else {
let mut current = root.clone();
let mut found = None;
while current.pop() {
let candidate = current.join(".ix");
if let Ok(b) = ix::format::Beacon::read_from(&candidate) {
found = Some(b);
break;
}
}
if let Some(b) = found {
b
} else {
println!("ixd: no running daemon found for {}", root.display());
continue;
}
};
if !beacon.is_live() {
println!(
"ixd: no running daemon found for {} (stale beacon from PID {})",
root.display(),
beacon.pid
);
continue;
}
let pid = beacon.pid;
if stopped_pids.insert(pid) {
println!("ixd: stopping daemon (PID {pid}) for {}...", root.display());
let nix_pid = Pid::from_raw(pid);
if let Err(e) = kill(nix_pid, Some(Signal::SIGTERM)) {
eprintln!("ixd: failed to signal PID {pid}: {e}");
continue;
}
let mut exited = false;
for _ in 0..50 {
sleep(Duration::from_millis(100));
if kill(nix_pid, None).is_err() {
exited = true;
break;
}
}
if exited {
println!("ixd: daemon (PID {pid}) stopped.");
} else {
eprintln!(
"ixd: warning: daemon (PID {pid}) did not exit within 5s; sending SIGKILL..."
);
let _ = kill(nix_pid, Some(Signal::SIGKILL));
println!("ixd: daemon (PID {pid}) killed.");
}
}
}
}