mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Terminate Windows process trees with job objects (#34624)
## Why Terminating a Windows execution session must also stop child processes, while a normal root-process exit must continue to allow background descendants to run. ## What changed - Assign Windows pipe, ConPTY, and sandbox processes to job objects and terminate the job when a session is cancelled, times out, or is explicitly stopped. - Preserve descendants when the root process exits normally. - Attach ConPTY and sandbox processes to their jobs atomically at creation; keep root-process termination as a fallback where job setup is unavailable. ## Testing Added Windows coverage for descendant termination and preservation across pipe, ConPTY, capture, cancellation, and legacy sandbox execution paths. GitOrigin-RevId: 8f831f2fc4caaa7b79ce842a3ed7192bd02dd3b4
This commit is contained in:
1 parent
ded4eacdbd
commit
9b33613db6
16 files changed
+946
-241
No files matched your search
@@ -26,9 +26,15 @@ use crate::process::exit_code_from_status;
|
||||
#[cfg(target_os = "linux")]
|
||||
use libc;
|
||||
|
||||
#[cfg(windows)]
|
||||
enum WindowsChildTerminator {
|
||||
Job(Arc<crate::win::JobObject>),
|
||||
Process(u32),
|
||||
}
|
||||
|
||||
struct PipeChildTerminator {
|
||||
#[cfg(windows)]
|
||||
pid: u32,
|
||||
windows: WindowsChildTerminator,
|
||||
#[cfg(unix)]
|
||||
process_group_id: u32,
|
||||
}
|
||||
@@ -58,7 +64,10 @@ impl ChildTerminator for PipeChildTerminator {
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
kill_process(self.pid)
|
||||
match &self.windows {
|
||||
WindowsChildTerminator::Job(job) => job.terminate(),
|
||||
WindowsChildTerminator::Process(pid) => kill_process(*pid),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
@@ -109,6 +118,8 @@ enum PipeStdinMode {
|
||||
Null,
|
||||
}
|
||||
|
||||
/// On Windows, process-tree containment is best-effort because Tokio returns
|
||||
/// only after the root process starts, so job assignment cannot be atomic.
|
||||
async fn spawn_process_with_stdin_mode(
|
||||
program: &str,
|
||||
args: &[String],
|
||||
@@ -165,12 +176,37 @@ async fn spawn_process_with_stdin_mode(
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
let job = crate::win::JobObject::create().map(Arc::new);
|
||||
let mut child = command.spawn()?;
|
||||
let pid = child
|
||||
#[cfg(windows)]
|
||||
let windows_terminator = {
|
||||
// Accept the small race: a descendant created between spawn and
|
||||
// assignment is not guaranteed to join the job and can escape termination.
|
||||
let pid = child
|
||||
.id()
|
||||
.ok_or_else(|| io::Error::other("missing child pid"))?;
|
||||
let assigned_job = job.and_then(|job| {
|
||||
let process_handle = child
|
||||
.raw_handle()
|
||||
.ok_or_else(|| io::Error::other("missing child process handle"))?;
|
||||
job.assign_process(process_handle)?;
|
||||
Ok(job)
|
||||
});
|
||||
match assigned_job {
|
||||
Ok(job) => WindowsChildTerminator::Job(job),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Windows pipe process tree containment unavailable for pid {pid}: {err}"
|
||||
);
|
||||
WindowsChildTerminator::Process(pid)
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(unix)]
|
||||
let process_group_id = child
|
||||
.id()
|
||||
.ok_or_else(|| io::Error::other("missing child pid"))?;
|
||||
#[cfg(unix)]
|
||||
let process_group_id = pid;
|
||||
|
||||
let stdin = child.stdin.take();
|
||||
let stdout = child.stdout.take();
|
||||
@@ -225,9 +261,24 @@ async fn spawn_process_with_stdin_mode(
|
||||
let wait_exit_status = Arc::clone(&exit_status);
|
||||
let exit_code = Arc::new(StdMutex::new(None));
|
||||
let wait_exit_code = Arc::clone(&exit_code);
|
||||
#[cfg(windows)]
|
||||
let wait_job = match &windows_terminator {
|
||||
WindowsChildTerminator::Job(job) => Some(Arc::clone(job)),
|
||||
WindowsChildTerminator::Process(_) => None,
|
||||
};
|
||||
let wait_handle: JoinHandle<()> = tokio::spawn(async move {
|
||||
let code = match child.wait().await {
|
||||
Ok(status) => exit_code_from_status(status),
|
||||
Ok(status) => {
|
||||
#[cfg(windows)]
|
||||
if let Some(job) = wait_job
|
||||
&& let Err(err) = job.preserve_descendants()
|
||||
{
|
||||
log::warn!(
|
||||
"Windows pipe failed to preserve descendants after root exit: {err}"
|
||||
);
|
||||
}
|
||||
exit_code_from_status(status)
|
||||
}
|
||||
Err(_) => -1,
|
||||
};
|
||||
wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
@@ -241,7 +292,7 @@ async fn spawn_process_with_stdin_mode(
|
||||
writer_tx,
|
||||
Box::new(PipeChildTerminator {
|
||||
#[cfg(windows)]
|
||||
pid,
|
||||
windows: windows_terminator,
|
||||
#[cfg(unix)]
|
||||
process_group_id,
|
||||
}),
|
||||
@@ -306,3 +357,7 @@ pub async fn spawn_process_no_stdin(
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
#[path = "pipe_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user