aynur 0.1.1

A minimal pm2-like process guardian for local Rust binaries
use crate::app::AppConfig;
use crate::paths::AynurPaths;
use anyhow::Context;
use std::fs::OpenOptions;
use std::os::unix::process::CommandExt;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::Sender;
use std::thread;

#[derive(Debug)]
pub struct ExitEvent {
    pub name: String,
    pub pid: u32,
}

pub fn spawn_app(
    paths: &AynurPaths,
    config: &AppConfig,
    event_sender: Sender<ExitEvent>,
) -> anyhow::Result<u32> {
    let stdout = OpenOptions::new()
        .create(true)
        .append(true)
        .open(paths.stdout_log_path(&config.name))
        .with_context(|| format!("failed to open stdout log for app '{}'", config.name))?;
    let stderr = OpenOptions::new()
        .create(true)
        .append(true)
        .open(paths.stderr_log_path(&config.name))
        .with_context(|| format!("failed to open stderr log for app '{}'", config.name))?;

    let mut command = Command::new(&config.binary_path);
    command
        .args(&config.args)
        .current_dir(&config.working_directory)
        .env_clear()
        .envs(&config.env)
        .stdin(Stdio::null())
        .stdout(Stdio::from(stdout))
        .stderr(Stdio::from(stderr));
    unsafe {
        command.pre_exec(|| {
            if libc::setpgid(0, 0) != 0 {
                return Err(std::io::Error::last_os_error());
            }
            Ok(())
        });
    }

    let child = command
        .spawn()
        .with_context(|| format!("failed to spawn app '{}'", config.name))?;
    let pid = child.id();
    watch_child(config.name.clone(), pid, child, event_sender);
    Ok(pid)
}

pub fn terminate_process_group(pid: u32) -> anyhow::Result<()> {
    send_signal_to_process_group(pid, libc::SIGTERM)
}

pub fn kill_process_group(pid: u32) -> anyhow::Result<()> {
    send_signal_to_process_group(pid, libc::SIGKILL)
}

fn send_signal_to_process_group(pid: u32, signal: libc::c_int) -> anyhow::Result<()> {
    let process_group = -(pid as libc::pid_t);
    let result = unsafe { libc::kill(process_group, signal) };
    if result != 0 {
        let error = std::io::Error::last_os_error();
        if error.raw_os_error() == Some(libc::ESRCH) {
            return Ok(());
        }
        anyhow::bail!(
            "failed to send signal {} to process group {}: {}",
            signal,
            pid,
            error
        );
    }
    Ok(())
}

fn watch_child(name: String, pid: u32, mut child: Child, event_sender: Sender<ExitEvent>) {
    thread::spawn(move || {
        let _ = child.wait();
        let _ = event_sender.send(ExitEvent { name, pid });
    });
}