claude-smart 0.2.9

Cross-platform Claude Code smart session manager
//! POSIX (macOS + Linux/WSL) implementations of `Launcher` and `ProcCheck`.
//!
//! `PosixLauncher` — foreground job-control supervisor:
//!   - Opens `/dev/tty` to get a handle for `tcsetpgrp`.
//!   - `pre_exec`: `setsid()` (or `setpgid(0,0)` fallback) so the child becomes
//!     its own process-group leader.
//!   - `SIG_IGN` SIGTTOU around the `tcsetpgrp` write (otherwise the supervisor
//!     stops itself if it is not already the foreground process group).
//!   - `tcsetpgrp(tty_fd, child_pgid)` hands the terminal to claude; the kernel
//!     then delivers Ctrl-C / Ctrl-Z / SIGWINCH *directly* to claude's foreground
//!     pgrp — the supervisor installs NO SIGINT/SIGTERM handler and just blocks
//!     in `wait()`.
//!   - On exit: `tcsetpgrp(tty_fd, parent_pgid)` reclaims the tty and SIGTTOU is
//!     restored.
//!
//! The "is PID a live claude/node?" check is platform-agnostic and lives in
//! `proc_check::SysinfoProcCheck` (no external `ps` spawn), so this module only
//! provides the POSIX launcher.

use std::collections::HashMap;
use std::ffi::OsString;
use std::io;
use std::os::unix::process::CommandExt; // pre_exec
use std::process::{Command, ExitStatus};
use std::time::{SystemTime, UNIX_EPOCH};

use super::launcher::{ChildHandle, Launcher};

// ─── PosixLauncher ────────────────────────────────────────────────────────────

/// POSIX foreground supervisor.  See module-level doc for the full protocol.
#[derive(Default)]
pub struct PosixLauncher;

impl Launcher for PosixLauncher {
    fn run_foreground(
        &self,
        sid: &str,
        cli: &[OsString],
        env: &HashMap<OsString, OsString>,
    ) -> io::Result<(ExitStatus, ChildHandle)> {
        use std::os::unix::io::AsFd;

        use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
        use nix::unistd::{setpgid, tcgetpgrp, tcsetpgrp, Pid};

        let born = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs() as i64)
            .unwrap_or(0);

        // Open the controlling tty for the tcsetpgrp handoff. If there is no tty
        // (piped/headless), skip job control and just inherit fds — the child
        // still runs in the foreground of whatever it inherited.
        let tty = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/tty")
            .ok();

        // Resolve the launch command: CLAUDE_SMART_CLAUDE_BIN env > config.json
        // `launchCommand` > "claude". out[0] is the binary; out[1..] are tokens
        // prepended to the claude-style argv (multi-token, e.g. `npx happy`).
        let launch = crate::config::resolve_launch_command();
        let (bin, prefix) = launch.split_first().expect("resolver returns ≥1 token");
        let mut cmd = Command::new(bin);
        cmd.args(prefix);
        cmd.args(cli);
        for (k, v) in env {
            cmd.env(k, v);
        }
        // stdin/stdout/stderr inherited by default (never piped) — the child must
        // own the real tty for a usable interactive session.

        // pre_exec runs in the child between fork and exec: make the child its own
        // process-group leader so the kernel delivers Ctrl-C/Ctrl-Z to its pgrp
        // (not the supervisor). setpgid(0,0) — NOT setsid (which would drop the
        // controlling tty we need for the tcsetpgrp grant-back).
        unsafe {
            cmd.pre_exec(|| {
                let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
                Ok(())
            });
        }

        let child = cmd.spawn()?;
        let pid = child.id();
        let child_pgid = Pid::from_raw(pid as i32);

        // Write the pidfile NOW, while claude is alive — the limit-switch hook
        // fires mid-session and reads `<sid>.pid` to stamp the sentinel's born.
        let _ = crate::platform::pid::write_pid_file(&crate::paths::pid_file(sid), pid, born);

        // Parent also sets the child's pgid (race-free: whoever wins, the child
        // lands in its own group). Idempotent.
        let _ = setpgid(child_pgid, child_pgid);

        // Hand the terminal to the child's pgrp. tcsetpgrp from a background pgrp
        // raises SIGTTOU (would stop the supervisor) — ignore it across the
        // handoff, then restore.
        let saved_ttou = if tty.is_some() {
            unsafe {
                let ign = SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty());
                sigaction(Signal::SIGTTOU, &ign).ok()
            }
        } else {
            None
        };

        let parent_pgid = tty.as_ref().and_then(|t| {
            let fd = t.as_fd();
            let prev = tcgetpgrp(fd).ok();
            let _ = tcsetpgrp(fd, child_pgid);
            prev
        });

        // The supervisor installs NO SIGINT/SIGTERM handler: with the child in the
        // foreground pgrp, the kernel routes keyboard signals straight to claude.
        // We just block in wait().
        let status = wait_for(child)?;

        // Reclaim the terminal for the relaunch loop, then restore SIGTTOU.
        if let (Some(t), Some(prev)) = (tty.as_ref(), parent_pgid) {
            let _ = tcsetpgrp(t.as_fd(), prev);
        }
        if let Some(prev) = saved_ttou {
            unsafe {
                let _ = sigaction(Signal::SIGTTOU, &prev);
            }
        }

        Ok((status, ChildHandle { pid, born }))
    }
}

/// Wait for `child`, retrying on EINTR-interrupted waits.
fn wait_for(mut child: std::process::Child) -> io::Result<ExitStatus> {
    loop {
        match child.wait() {
            Ok(status) => return Ok(status),
            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        }
    }
}

// The "is PID a live claude/node?" check lives in `proc_check::SysinfoProcCheck`,
// wired as `PlatformProcCheck` for every target (no external `ps` spawn).