use std::fs;
use std::io::{self, ErrorKind, Read, Write};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::os::unix::net::{UnixListener, UnixStream};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;
use nix::fcntl::{Flock, FlockArg};
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
use crate::core::{LoopExit, Wake, run_loop};
use crate::frame::{read_frame, write_frame};
use crate::protocol::{Command, decode_command, encode_command, encode_event};
use crate::supervisor::Supervisor;
fn runtime_dir() -> PathBuf {
if let Ok(d) = std::env::var("FLEETCOM_RUNTIME_DIR") {
return PathBuf::from(d);
}
if let Ok(d) = std::env::var("XDG_RUNTIME_DIR")
&& !d.is_empty()
{
return PathBuf::from(d).join("fleetcom");
}
let uid = nix::unistd::getuid().as_raw();
std::env::temp_dir().join(format!("fleetcom-{uid}"))
}
fn socket_path() -> PathBuf {
runtime_dir().join("default.sock")
}
fn ensure_runtime_dir(dir: &Path) -> io::Result<()> {
match fs::symlink_metadata(dir) {
Ok(md) => {
if !md.file_type().is_dir() {
return Err(io::Error::new(
ErrorKind::AlreadyExists,
"runtime path exists but is not a directory",
));
}
if md.uid() != nix::unistd::getuid().as_raw() {
return Err(io::Error::new(
ErrorKind::PermissionDenied,
"runtime dir is not owned by this user",
));
}
if md.permissions().mode() & 0o077 != 0 {
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
Err(e) if e.kind() == ErrorKind::NotFound => fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(dir),
Err(e) => Err(e),
}
}
pub fn connect_or_autostart() -> io::Result<UnixStream> {
let path = socket_path();
if let Ok(s) = UnixStream::connect(&path) {
return Ok(s);
}
spawn_daemon()?;
for _ in 0..100 {
if let Ok(s) = UnixStream::connect(&path) {
return Ok(s);
}
thread::sleep(Duration::from_millis(10));
}
Err(io::Error::new(
ErrorKind::TimedOut,
"daemon did not come up",
))
}
fn spawn_daemon() -> io::Result<()> {
let exe = std::env::current_exe()?;
let dir = runtime_dir();
ensure_runtime_dir(&dir)?;
let log = fs::File::create(dir.join("daemon.log")).ok();
let mut cmd = std::process::Command::new(exe);
cmd.arg("--daemon")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(log.map(Stdio::from).unwrap_or_else(Stdio::null))
.process_group(0);
cmd.spawn()?;
Ok(())
}
pub fn run_kill() -> io::Result<()> {
let lock_path = runtime_dir().join("daemon.lock");
let Ok(file) = fs::OpenOptions::new()
.read(true)
.write(true)
.open(&lock_path)
else {
eprintln!("fleetcom: no daemon running");
return Ok(());
};
let mut file = match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(_held) => {
eprintln!("fleetcom: no daemon running");
return Ok(());
}
Err((file, _)) => file,
};
let mut pid_str = String::new();
file.read_to_string(&mut pid_str)?;
let Some(pid) = pid_str.trim().parse::<i32>().ok().filter(|p| *p > 0) else {
return kill_via_socket();
};
match kill(Pid::from_raw(pid), Signal::SIGTERM) {
Ok(()) | Err(nix::errno::Errno::ESRCH) => {}
Err(e) => return Err(io::Error::other(e)),
}
for _ in 0..200 {
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(_held) => return Ok(()),
Err((f, _)) => file = f,
}
thread::sleep(Duration::from_millis(50));
}
Err(io::Error::new(
ErrorKind::TimedOut,
"daemon did not exit after SIGTERM",
))
}
fn kill_via_socket() -> io::Result<()> {
let path = socket_path();
match UnixStream::connect(&path) {
Ok(mut s) => {
let (kind, payload) = encode_command(&Command::Shutdown);
write_frame(&mut s, kind, &payload)?;
let mut buf = [0u8; 256];
while s.read(&mut buf).map(|n| n > 0).unwrap_or(false) {}
Ok(())
}
Err(_) => {
eprintln!("fleetcom: no daemon running");
Ok(())
}
}
}
pub fn run_daemon() -> io::Result<()> {
let dir = runtime_dir();
ensure_runtime_dir(&dir)?; let path = dir.join("default.sock");
let lock_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false) .open(dir.join("daemon.lock"))?;
let mut lock = match Flock::lock(lock_file, FlockArg::LockExclusiveNonblock) {
Ok(l) => l,
Err(_) => return Ok(()), };
lock.set_len(0)?;
lock.write_all(std::process::id().to_string().as_bytes())?;
let _ = fs::remove_file(&path);
let listener = UnixListener::bind(&path)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
let base_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut sup = Supervisor::new(24, 80, base_dir);
let term = Arc::new(AtomicBool::new(false));
{
use signal_hook::consts::{SIGHUP, SIGINT, SIGTERM};
signal_hook::flag::register(SIGTERM, Arc::clone(&term))?;
signal_hook::flag::register(SIGINT, Arc::clone(&term))?;
signal_hook::flag::register(SIGHUP, Arc::clone(&term))?;
}
listener.set_nonblocking(true)?;
const IDLE_REAP: Duration = Duration::from_millis(100);
loop {
if term.load(Ordering::Relaxed) {
sup.apply(Command::Shutdown);
break;
}
match listener.accept() {
Ok((stream, _)) => {
stream.set_nonblocking(false)?;
if serve_client(&mut sup, stream, &term) == ServeOutcome::Shutdown {
break;
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock || transient_accept_error(&e) => {
sup.reap();
thread::sleep(IDLE_REAP);
}
Err(e) => {
eprintln!("fleetcom: accept failed, shutting down: {e}");
break;
}
}
}
let _ = fs::remove_file(&path);
Ok(())
}
fn transient_accept_error(e: &io::Error) -> bool {
use nix::errno::Errno;
matches!(
e.raw_os_error(),
Some(code) if code == Errno::EMFILE as i32
|| code == Errno::ENFILE as i32
|| code == Errno::EINTR as i32
|| code == Errno::ECONNABORTED as i32
)
}
#[derive(PartialEq)]
enum ServeOutcome {
Disconnected,
Shutdown,
}
fn serve_client(sup: &mut Supervisor, stream: UnixStream, stop: &AtomicBool) -> ServeOutcome {
let Ok(read) = stream.try_clone() else {
return ServeOutcome::Disconnected;
};
let (wake_tx, wake_rx) = channel::<Wake>();
sup.set_waker(wake_tx.clone());
thread::spawn(move || {
let mut read = read;
while let Ok((kind, payload)) = read_frame(&mut read) {
if let Some(cmd) = decode_command(kind, &payload)
&& wake_tx.send(Wake::Cmd(cmd)).is_err()
{
return;
}
}
let _ = wake_tx.send(Wake::Hangup);
});
let mut write = stream;
let _ = write.set_write_timeout(Some(Duration::from_secs(5)));
let outcome = run_loop(sup, &wake_rx, stop, |ev| {
let (kind, payload) = encode_event(ev);
write_frame(&mut write, kind, &payload).is_ok()
});
sup.clear_waker();
sup.clear_watch();
match outcome {
LoopExit::Shutdown => ServeOutcome::Shutdown,
LoopExit::ClientGone => ServeOutcome::Disconnected,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp(tag: &str) -> PathBuf {
let d =
std::env::temp_dir().join(format!("fleetcom_daemon_test_{tag}_{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn ensure_runtime_dir_rejects_symlink() {
let base = temp("symlink");
let target = base.join("target");
fs::create_dir(&target).unwrap();
let link = base.join("runtime");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(ensure_runtime_dir(&link).is_err());
let _ = fs::remove_dir_all(&base);
}
#[test]
fn ensure_runtime_dir_rejects_plain_file() {
let base = temp("file");
let path = base.join("runtime");
fs::write(&path, b"x").unwrap();
assert!(ensure_runtime_dir(&path).is_err());
let _ = fs::remove_dir_all(&base);
}
#[test]
fn transient_accept_errors_are_classified() {
use nix::errno::Errno;
for errno in [
Errno::EMFILE,
Errno::ENFILE,
Errno::EINTR,
Errno::ECONNABORTED,
] {
assert!(
transient_accept_error(&io::Error::from_raw_os_error(errno as i32)),
"{errno} should be transient"
);
}
assert!(!transient_accept_error(&io::Error::from_raw_os_error(
Errno::EBADF as i32
)));
assert!(!transient_accept_error(&io::Error::other("no raw errno")));
}
#[test]
fn ensure_runtime_dir_creates_private_dir() {
let base = temp("create");
let path = base.join("runtime");
ensure_runtime_dir(&path).unwrap();
let mode = fs::symlink_metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o700, "dir must be private");
ensure_runtime_dir(&path).unwrap();
let _ = fs::remove_dir_all(&base);
}
}