use std::process::ExitStatus;
use std::time::Duration;
use super::combine_cleanup_results;
const TERMINATION_GRACE_PERIOD: Duration = Duration::from_secs(5);
pub(super) struct ProcessTree {
process_group: i32,
terminal: Option<TerminalForeground>,
signals: TerminationSignals,
}
pub(super) async fn spawn(
command: &mut tokio::process::Command,
) -> std::io::Result<(tokio::process::Child, ProcessTree)> {
let signals = TerminationSignals::new()?;
let terminal_owner = terminal_foreground_owner()?;
command.process_group(0);
let mut child = command.spawn()?;
let process_group = child.id().ok_or_else(|| {
std::io::Error::other("coding-agent process exited before Relay could supervise it")
})? as i32;
let terminal = match terminal_owner {
Some(owner) => match TerminalForeground::acquire(owner, process_group) {
Ok(terminal) => Some(terminal),
Err(error) => {
terminate_group(process_group, &mut child);
let _ = child.wait().await;
return Err(std::io::Error::new(
error.kind(),
format!(
"failed to give the coding agent foreground terminal ownership: {error}"
),
));
}
},
None => None,
};
Ok((
child,
ProcessTree {
process_group,
terminal,
signals,
},
))
}
pub(super) async fn wait(
tree: &mut ProcessTree,
child: &mut tokio::process::Child,
) -> std::io::Result<ExitStatus> {
let mut termination_deadline = None;
loop {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
if tree.resume_foreground_child()? {
tokio::task::yield_now().await;
continue;
}
if let Some(status) = handle_stopped_child(tree, child)? {
return Ok(status);
}
if termination_deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
tree.terminate(child)?;
return child.wait().await;
}
tokio::select! {
_ = tokio::time::sleep(Duration::from_millis(25)) => {}
signal = tree.signals.recv() => {
let signal = signal?;
if termination_deadline.is_some() {
tree.terminate(child)?;
return child.wait().await;
}
tree.forward_signal(signal, child)?;
termination_deadline = Some(tokio::time::Instant::now() + TERMINATION_GRACE_PERIOD);
}
}
}
}
fn handle_stopped_child(
tree: &mut ProcessTree,
child: &mut tokio::process::Child,
) -> std::io::Result<Option<ExitStatus>> {
if tree.terminal.is_none() || !child_is_stopped(tree.process_group)? {
return Ok(None);
}
tree.restore_terminal()?;
tree.stop_supervisor_group()?;
if let Some(status) = child.try_wait()? {
return Ok(Some(status));
}
if let Some(terminal) = tree.terminal.as_mut() {
terminal.resume_after_supervisor()?;
}
Ok(None)
}
impl ProcessTree {
pub(super) fn restore_terminal(&mut self) -> std::io::Result<()> {
self.terminal
.as_mut()
.map_or(Ok(()), TerminalForeground::restore)
}
pub(super) fn terminate(&mut self, child: &mut tokio::process::Child) -> std::io::Result<()> {
if unsafe { libc::kill(-self.process_group, libc::SIGKILL) } == 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
let _ = child.start_kill();
Ok(())
} else {
Err(error)
}
}
fn forward_signal(
&mut self,
signal: i32,
child: &mut tokio::process::Child,
) -> std::io::Result<()> {
if unsafe { libc::kill(-self.process_group, signal) } == 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
child.start_kill()
} else {
Err(error)
}
}
fn stop_supervisor_group(&self) -> std::io::Result<()> {
let Some(terminal) = &self.terminal else {
return Ok(());
};
if unsafe { libc::kill(-terminal.owner_process_group, libc::SIGSTOP) } == -1 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
fn resume_foreground_child(&mut self) -> std::io::Result<bool> {
let Some(terminal) = self.terminal.as_mut() else {
return Ok(false);
};
if terminal.active || terminal_process_group()? != terminal.owner_process_group {
return Ok(false);
}
terminal.activate()?;
Ok(true)
}
}
struct TerminalForeground {
owner_process_group: i32,
child_process_group: i32,
active: bool,
}
impl TerminalForeground {
fn acquire(owner_process_group: i32, child_process_group: i32) -> std::io::Result<Self> {
let mut terminal = Self {
owner_process_group,
child_process_group,
active: false,
};
terminal.activate()?;
Ok(terminal)
}
fn activate(&mut self) -> std::io::Result<()> {
set_terminal_process_group(self.child_process_group)?;
self.active = true;
if let Err(error) = self.continue_child() {
return combine_cleanup_results([
("continue foreground coding-agent group", Err(error)),
("restore foreground terminal", self.restore()),
]);
}
Ok(())
}
fn resume_after_supervisor(&mut self) -> std::io::Result<()> {
if terminal_process_group()? == self.owner_process_group {
self.activate()
} else {
self.continue_child()
}
}
fn continue_child(&self) -> std::io::Result<()> {
if unsafe { libc::kill(-self.child_process_group, libc::SIGCONT) } == -1 {
let error = std::io::Error::last_os_error();
if error.raw_os_error() != Some(libc::ESRCH) {
return Err(error);
}
}
Ok(())
}
fn restore(&mut self) -> std::io::Result<()> {
if !self.active {
return Ok(());
}
set_terminal_process_group(self.owner_process_group)?;
self.active = false;
Ok(())
}
}
struct TerminationSignals {
hangup: tokio::signal::unix::Signal,
interrupt: tokio::signal::unix::Signal,
quit: tokio::signal::unix::Signal,
terminate: tokio::signal::unix::Signal,
}
impl TerminationSignals {
fn new() -> std::io::Result<Self> {
use tokio::signal::unix::{SignalKind, signal};
Ok(Self {
hangup: signal(SignalKind::hangup())?,
interrupt: signal(SignalKind::interrupt())?,
quit: signal(SignalKind::quit())?,
terminate: signal(SignalKind::terminate())?,
})
}
async fn recv(&mut self) -> std::io::Result<i32> {
let signal = tokio::select! {
signal = self.hangup.recv() => signal.map(|()| libc::SIGHUP),
signal = self.interrupt.recv() => signal.map(|()| libc::SIGINT),
signal = self.quit.recv() => signal.map(|()| libc::SIGQUIT),
signal = self.terminate.recv() => signal.map(|()| libc::SIGTERM),
};
signal.ok_or_else(|| std::io::Error::other("transparent-run signal receiver closed"))
}
}
fn terminal_foreground_owner() -> std::io::Result<Option<i32>> {
if unsafe { libc::isatty(libc::STDIN_FILENO) } == 0 {
return Ok(None);
}
let owner_process_group = unsafe { libc::getpgrp() };
let foreground_process_group = terminal_process_group()?;
if foreground_process_group != owner_process_group {
return Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"Relay is not the terminal foreground process; bring the transparent run to the foreground or redirect its standard input",
));
}
Ok(Some(owner_process_group))
}
fn terminal_process_group() -> std::io::Result<i32> {
let foreground_process_group = unsafe { libc::tcgetpgrp(libc::STDIN_FILENO) };
if foreground_process_group == -1 {
Err(std::io::Error::last_os_error())
} else {
Ok(foreground_process_group)
}
}
fn set_terminal_process_group(process_group: i32) -> std::io::Result<()> {
let mut blocked = std::mem::MaybeUninit::<libc::sigset_t>::uninit();
let mut previous = std::mem::MaybeUninit::<libc::sigset_t>::uninit();
let mask_result = unsafe {
libc::sigemptyset(blocked.as_mut_ptr());
libc::sigaddset(blocked.as_mut_ptr(), libc::SIGTTOU);
libc::pthread_sigmask(libc::SIG_BLOCK, blocked.as_ptr(), previous.as_mut_ptr())
};
if mask_result != 0 {
return Err(std::io::Error::from_raw_os_error(mask_result));
}
let foreground_result = unsafe { libc::tcsetpgrp(libc::STDIN_FILENO, process_group) };
let foreground_error = (foreground_result == -1).then(std::io::Error::last_os_error);
let restore_result = unsafe {
libc::pthread_sigmask(
libc::SIG_SETMASK,
previous.assume_init_ref(),
std::ptr::null_mut(),
)
};
combine_cleanup_results([
(
"set terminal foreground process group",
foreground_error.map_or(Ok(()), Err),
),
(
"restore supervisor signal mask",
if restore_result == 0 {
Ok(())
} else {
Err(std::io::Error::from_raw_os_error(restore_result))
},
),
])
}
fn child_is_stopped(pid: i32) -> std::io::Result<bool> {
let mut info = std::mem::MaybeUninit::<libc::siginfo_t>::zeroed();
let result = unsafe {
libc::waitid(
libc::P_PID,
pid as _,
info.as_mut_ptr(),
libc::WSTOPPED | libc::WNOHANG | libc::WNOWAIT,
)
};
if result == -1 {
let error = std::io::Error::last_os_error();
return if error.raw_os_error() == Some(libc::ECHILD) {
Ok(false)
} else {
Err(error)
};
}
Ok(unsafe { info.assume_init().si_pid() } == pid)
}
fn terminate_group(process_group: i32, child: &mut tokio::process::Child) {
if unsafe { libc::kill(-process_group, libc::SIGKILL) } == -1 {
let _ = child.start_kill();
}
}