darq 0.1.0

darq CLI + TUI — autonomous issue → PR pipeline with SAT and a learning loop.
Documentation
//! Daemon lifecycle management.
//!
//! Handles PID file, socket path, daemon detection, and cleanup.

use std::path::PathBuf;

/// Get the darq home directory (~/.darq/).
pub fn darq_home() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".darq")
}

/// Get the daemon socket path.
pub fn socket_path() -> PathBuf {
    darq_home().join("daemon.sock")
}

/// Get the daemon PID file path.
pub fn pid_path() -> PathBuf {
    darq_home().join("daemon.pid")
}

/// Check if the daemon is running by checking PID file and socket.
pub fn is_daemon_running() -> bool {
    let pid_file = pid_path();
    let sock_file = socket_path();

    if !pid_file.exists() || !sock_file.exists() {
        return false;
    }

    // Read PID file
    let pid_str = match std::fs::read_to_string(&pid_file) {
        Ok(s) => s.trim().to_string(),
        Err(_) => return false,
    };

    let pid: u32 = match pid_str.parse() {
        Ok(p) => p,
        Err(_) => return false,
    };

    // Check if process is alive
    #[cfg(unix)]
    {
        // Send signal 0 — doesn't actually signal, just checks if process exists
        let result = unsafe { libc::kill(pid as i32, 0) };
        result == 0
    }

    #[cfg(not(unix))]
    {
        // On non-unix, just check if socket exists and is connectable
        let _ = pid;
        sock_file.exists()
    }
}

/// Write the PID file.
pub fn write_pid_file() -> std::io::Result<()> {
    let pid_file = pid_path();
    if let Some(parent) = pid_file.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let pid = std::process::id();
    std::fs::write(&pid_file, pid.to_string())
}

/// Remove the PID file.
pub fn remove_pid_file() {
    let _ = std::fs::remove_file(pid_path());
}

/// Remove stale socket file.
pub fn remove_socket_file() {
    let _ = std::fs::remove_file(socket_path());
}

/// Clean up all daemon files (socket + PID).
pub fn cleanup() {
    remove_pid_file();
    remove_socket_file();
}

/// Ensure the ~/.darq directory exists.
pub fn ensure_home_dir() -> std::io::Result<()> {
    std::fs::create_dir_all(darq_home())
}

/// Get the PID of the running daemon (if any).
pub fn daemon_pid() -> Option<u32> {
    let pid_file = pid_path();
    let pid_str = std::fs::read_to_string(&pid_file).ok()?;
    pid_str.trim().parse().ok()
}

/// Spawn the daemon as a background process.
///
/// Launches `darq daemon start` which detects --background flag and daemonizes.
pub async fn spawn_daemon(config_path: &str) -> anyhow::Result<()> {
    ensure_home_dir()?;

    // If socket exists but daemon is not running, clean up stale socket
    if socket_path().exists() && !is_daemon_running() {
        tracing::warn!("stale socket found, cleaning up");
        cleanup();
    }

    // Spawn daemon process with full detachment
    let exe = std::env::current_exe()?;
    let log_path = darq_home().join("daemon.log");
    let log_file = std::fs::File::create(&log_path)?;
    let mut cmd = std::process::Command::new(exe);
    cmd.arg("--config")
        .arg(config_path)
        .arg("daemon")
        .arg("start")
        .arg("--background")
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(log_file);

    // On Unix, create new session (full daemonization)
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;

        // SAFETY:
        //
        // This uses the standard Unix double-fork daemonization pattern:
        // 1. parent fork()s → child inherits parent's fd table
        // 2. child calls setsid() → creates new session, detaches from controlling terminal
        //   - setsid() is async-signal-safe (only syscall, no locks)
        // 3. grandchild fork()s (optional, done by exec) → prevents acquiring a new controlling terminal
        //
        // pre_exec runs in the child process AFTER fork() but BEFORE exec().
        // At this point the child has its own address space with no parent threads.
        // libc::setsid() creates a new session with no controlling terminal.
        //
        // This is the canonical Unix daemon pattern (see Stevens & Rago, APUE §13.3).
        unsafe {
            cmd.pre_exec(|| {
                libc::setsid();
                Ok(())
            });
        }
    }

    cmd.spawn()?;

    // Wait for socket to appear (up to 5 seconds)
    let sock = socket_path();
    for _ in 0..50 {
        if sock.exists() {
            // Small delay to let socket start listening
            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
            return Ok(());
        }
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    }

    anyhow::bail!("daemon did not start within 5 seconds")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_paths_are_under_home() {
        let home = darq_home();
        assert!(socket_path().starts_with(&home));
        assert!(pid_path().starts_with(&home));
    }

    #[test]
    fn test_socket_filename() {
        assert_eq!(socket_path().file_name().unwrap(), "daemon.sock");
    }

    #[test]
    fn test_pid_filename() {
        assert_eq!(pid_path().file_name().unwrap(), "daemon.pid");
    }

    #[test]
    fn test_write_and_remove_pid() {
        ensure_home_dir().unwrap();
        write_pid_file().unwrap();
        assert!(pid_path().exists());
        assert_eq!(daemon_pid(), Some(std::process::id()));

        remove_pid_file();
        assert!(!pid_path().exists());
        assert_eq!(daemon_pid(), None);
    }
}