use std::path::{Path, PathBuf};
use std::process::Child;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::{Mutex, MutexGuard};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
static ASK_CHILD_PGID: AtomicI32 = AtomicI32::new(0);
static ASK_INTERRUPTED: AtomicBool = AtomicBool::new(false);
static SIGINT_MUTEX: Mutex<()> = Mutex::new(());
pub fn ask_interrupted() -> bool {
ASK_INTERRUPTED.load(Ordering::SeqCst)
}
extern "C" fn forward_sigint_to_child(_sig: libc::c_int) {
let pgid = ASK_CHILD_PGID.load(Ordering::SeqCst);
if pgid > 0 {
unsafe {
libc::killpg(pgid, libc::SIGINT);
}
}
ASK_INTERRUPTED.store(true, Ordering::SeqCst);
}
#[must_use = "dropping the guard immediately uninstalls the SIGINT handler"]
pub struct SigintForwarder {
prev: libc::sighandler_t,
_guard: MutexGuard<'static, ()>,
}
impl SigintForwarder {
pub fn install(pgid: u32) -> Self {
let guard = SIGINT_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
debug_assert_eq!(
ASK_CHILD_PGID.load(Ordering::SeqCst),
0,
"SIGINT_MUTEX held but ASK_CHILD_PGID != 0; guard lifetime invariant violated"
);
ASK_INTERRUPTED.store(false, Ordering::SeqCst);
ASK_CHILD_PGID.store(pgid as i32, Ordering::SeqCst);
let prev = unsafe {
libc::signal(
libc::SIGINT,
forward_sigint_to_child as *const () as libc::sighandler_t,
)
};
if prev == libc::SIG_IGN {
unsafe {
libc::signal(libc::SIGINT, libc::SIG_IGN);
}
ASK_CHILD_PGID.store(0, Ordering::SeqCst);
} else if prev == libc::SIG_ERR {
eprintln!(
"fno-agents: failed to install SIGINT handler; Ctrl-C will not forward to the child"
);
}
Self {
prev,
_guard: guard,
}
}
}
impl Drop for SigintForwarder {
fn drop(&mut self) {
let restore = if self.prev == libc::SIG_ERR {
libc::SIG_DFL
} else {
self.prev
};
unsafe {
libc::signal(libc::SIGINT, restore);
}
ASK_CHILD_PGID.store(0, Ordering::SeqCst);
}
}
pub fn kill_pgrp(pid: u32, sig: libc::c_int) {
unsafe {
let pgid = libc::getpgid(pid as libc::pid_t);
if pgid > 0 {
libc::killpg(pgid, sig);
}
}
}
pub fn wait_with_grace(pid: u32, child: &mut Child, grace_sec: f64) -> (i32, bool) {
let deadline = Instant::now() + Duration::from_secs_f64(grace_sec);
loop {
match child.try_wait() {
Ok(Some(status)) => return (status.code().unwrap_or(-1), false),
Ok(None) => {
if Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(25));
}
Err(_) => break,
}
}
kill_pgrp(pid, libc::SIGTERM);
let sigterm_deadline = Instant::now() + Duration::from_secs(5);
loop {
match child.try_wait() {
Ok(Some(status)) => return (status.code().unwrap_or(-1), false),
Ok(None) => {
if Instant::now() >= sigterm_deadline {
break;
}
std::thread::sleep(Duration::from_millis(25));
}
Err(_) => break,
}
}
kill_pgrp(pid, libc::SIGKILL);
let sigkill_deadline = Instant::now() + Duration::from_secs(2);
loop {
match child.try_wait() {
Ok(Some(status)) => return (status.code().unwrap_or(-1), true),
Ok(None) => {
if Instant::now() >= sigkill_deadline {
break;
}
std::thread::sleep(Duration::from_millis(25));
}
Err(_) => break,
}
}
(-9, true)
}
pub fn open_tee(log_path: &Path) -> std::io::Result<std::fs::File> {
if let Some(parent) = log_path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
}
pub struct AskWatchdog {
timed_out: std::sync::Arc<AtomicBool>,
done_tx: Option<Sender<()>>,
handle: Option<JoinHandle<()>>,
}
impl AskWatchdog {
pub fn spawn(pid: u32, timeout: Option<Duration>) -> Self {
let timed_out = std::sync::Arc::new(AtomicBool::new(false));
let watchdog_timeout = timeout.filter(|d| !d.is_zero());
let (done_tx, done_rx) = channel::<()>();
let handle = watchdog_timeout.map(|d| {
let pid_for_wd = pid;
let timed_out_for_wd = timed_out.clone();
std::thread::spawn(move || {
use std::sync::mpsc::RecvTimeoutError;
match done_rx.recv_timeout(d) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => {
return;
}
Err(RecvTimeoutError::Timeout) => {
timed_out_for_wd.store(true, Ordering::SeqCst);
kill_pgrp(pid_for_wd, libc::SIGTERM);
}
}
match done_rx.recv_timeout(Duration::from_secs(2)) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => {}
Err(RecvTimeoutError::Timeout) => {
kill_pgrp(pid_for_wd, libc::SIGKILL);
}
}
})
});
Self {
timed_out,
done_tx: Some(done_tx),
handle,
}
}
pub fn cancel(&mut self) {
self.done_tx.take();
}
pub fn join(&mut self) {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
pub fn timed_out(&self) -> bool {
self.timed_out.load(Ordering::SeqCst)
}
}
pub fn resolve_ask_cwd(cwd_param: Option<&str>) -> PathBuf {
match cwd_param {
Some(c) => match std::fs::canonicalize(c) {
Ok(p) => p,
Err(e) => {
let p = PathBuf::from(c);
let resolved = if p.is_absolute() {
p
} else {
std::env::current_dir().map(|d| d.join(&p)).unwrap_or(p)
};
eprintln!(
"fno-agents: could not canonicalize --cwd {:?} ({}); recording {:?}",
c, e, resolved
);
resolved
}
},
None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
}
}