use std::process::ExitStatus;
pub(super) struct ProcessTree {
job: AgentJob,
}
pub(super) async fn spawn(
command: &mut tokio::process::Command,
) -> std::io::Result<(tokio::process::Child, ProcessTree)> {
let job = AgentJob::create()?;
command.creation_flags(windows_sys::Win32::System::Threading::CREATE_SUSPENDED);
let mut child = command.spawn()?;
if let Err(error) = job.assign(&child) {
abort_spawn(&job, &mut child).await;
return Err(error);
}
if let Err(error) = resume_suspended_process(child.id().ok_or_else(|| {
std::io::Error::other("coding-agent process exited before Relay could resume it")
})?) {
abort_spawn(&job, &mut child).await;
return Err(error);
}
Ok((child, ProcessTree { job }))
}
pub(super) async fn wait(
_tree: &mut ProcessTree,
child: &mut tokio::process::Child,
) -> std::io::Result<ExitStatus> {
child.wait().await
}
impl ProcessTree {
pub(super) fn restore_terminal(&mut self) -> std::io::Result<()> {
Ok(())
}
pub(super) fn terminate(&mut self, _child: &mut tokio::process::Child) -> std::io::Result<()> {
self.job.terminate()
}
}
struct AgentJob {
handle: windows_sys::Win32::Foundation::HANDLE,
}
const WINDOWS_JOB_OBJECT_LIMIT_KILL_ON_CLOSE: u32 = 0x0000_2000;
unsafe impl Send for AgentJob {}
unsafe impl Sync for AgentJob {}
impl AgentJob {
fn create() -> std::io::Result<Self> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::JobObjects::{
CreateJobObjectW, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, SetInformationJobObject,
};
let handle = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
if handle.is_null() {
return Err(last_windows_error(
"failed to create coding-agent Job Object",
));
}
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
limits.BasicLimitInformation.LimitFlags = WINDOWS_JOB_OBJECT_LIMIT_KILL_ON_CLOSE;
let configured = unsafe {
SetInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
std::ptr::from_ref(&limits).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if configured == 0 {
let error = last_windows_error("failed to configure coding-agent Job Object cleanup");
unsafe { CloseHandle(handle) };
return Err(error);
}
Ok(Self { handle })
}
fn assign(&self, child: &tokio::process::Child) -> std::io::Result<()> {
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
let child_handle = child.raw_handle().ok_or_else(|| {
std::io::Error::other("coding-agent process exited before Job Object assignment")
})?;
if unsafe { AssignProcessToJobObject(self.handle, child_handle.cast()) } == 0 {
Err(last_windows_error(&format!(
"failed to assign coding-agent process {} to its Job Object; the current Windows Job Object may reject nested assignment",
child.id().unwrap_or_default()
)))
} else {
Ok(())
}
}
fn terminate(&self) -> std::io::Result<()> {
use windows_sys::Win32::System::JobObjects::TerminateJobObject;
if unsafe { TerminateJobObject(self.handle, 1) } == 0 {
Err(last_windows_error(
"failed to terminate coding-agent Job Object",
))
} else {
Ok(())
}
}
}
async fn abort_spawn(job: &AgentJob, child: &mut tokio::process::Child) {
let _ = job.terminate();
let _ = child.start_kill();
let _ = child.wait().await;
}
fn resume_suspended_process(process_id: u32) -> std::io::Result<()> {
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
};
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return Err(last_windows_error(
"failed to enumerate the suspended coding-agent thread",
));
}
let mut entry = THREADENTRY32 {
dwSize: std::mem::size_of::<THREADENTRY32>() as u32,
..THREADENTRY32::default()
};
let mut has_entry = unsafe { Thread32First(snapshot, &mut entry) } != 0;
while has_entry {
if entry.th32OwnerProcessID == process_id {
let result = resume_thread(entry.th32ThreadID);
unsafe { CloseHandle(snapshot) };
return result;
}
has_entry = unsafe { Thread32Next(snapshot, &mut entry) } != 0;
}
unsafe { CloseHandle(snapshot) };
Err(std::io::Error::other(format!(
"could not find the suspended primary thread for coding-agent process {process_id}"
)))
}
fn resume_thread(thread_id: u32) -> std::io::Result<()> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenThread, ResumeThread, THREAD_SUSPEND_RESUME};
let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, thread_id) };
if thread.is_null() {
return Err(last_windows_error(
"failed to open the suspended coding-agent primary thread",
));
}
let mut previous_count = unsafe { ResumeThread(thread) };
while previous_count > 1 && previous_count != u32::MAX {
previous_count = unsafe { ResumeThread(thread) };
}
let result = match previous_count {
u32::MAX => Err(last_windows_error(
"failed to resume the Job-owned coding-agent process",
)),
0 => Err(std::io::Error::other(
"coding-agent primary thread was not suspended before Job Object assignment",
)),
_ => Ok(()),
};
unsafe { CloseHandle(thread) };
result
}
fn last_windows_error(context: &str) -> std::io::Error {
let source = std::io::Error::last_os_error();
std::io::Error::new(source.kind(), format!("{context}: {source}"))
}
impl Drop for AgentJob {
fn drop(&mut self) {
use windows_sys::Win32::Foundation::CloseHandle;
unsafe { CloseHandle(self.handle) };
}
}