mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Terminate timed-out Git process trees (#36793)
## Why Timing out a Git metadata command must not leave helper processes running after the command wrapper exits. ## What changed - Run Git metadata commands in a dedicated process group on Unix and a Job Object on Windows so timeout cleanup terminates their full process trees. - Start Windows commands suspended, assign them to the Job Object, and then resume them so immediate descendants cannot escape containment. - Preserve descendants when a Git command completes normally, and retain the existing direct-spawn fallback if Windows Job Object setup fails. ## Testing Added cross-platform regression tests for cleanup both while the command wrapper is running and after it exits, plus Windows coverage for immediate-child Job Object containment. GitOrigin-RevId: 351851708e23ff06b89fe1894bd09a3558f67293
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
use std::process::Output;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(windows)]
|
||||
use codex_utils_pty::JobObject;
|
||||
#[cfg(unix)]
|
||||
use codex_utils_pty::process_group::kill_process_group;
|
||||
use tokio::process::Child;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
struct KillGitProcessTreeOnDrop {
|
||||
#[cfg(unix)]
|
||||
process_id: u32,
|
||||
#[cfg(windows)]
|
||||
job: Option<JobObject>,
|
||||
#[cfg(unix)]
|
||||
armed: bool,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for KillGitProcessTreeOnDrop {
|
||||
fn drop(&mut self) {
|
||||
if self.armed {
|
||||
let _ = kill_process_group(self.process_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_git_command(command: &mut Command) -> Option<(Child, KillGitProcessTreeOnDrop)> {
|
||||
#[cfg(unix)]
|
||||
command.process_group(0);
|
||||
command.kill_on_drop(true);
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
#[cfg(windows)]
|
||||
let (child, job) = match JobObject::create()
|
||||
.and_then(|job| job.spawn_contained(command).map(|child| (child, job)))
|
||||
{
|
||||
Ok((child, job)) => (child, Some(job)),
|
||||
Err(_) => {
|
||||
// A failed contained spawn leaves CREATE_SUSPENDED on the command.
|
||||
command.creation_flags(0);
|
||||
(command.spawn().ok()?, None)
|
||||
}
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
let child = command.spawn().ok()?;
|
||||
|
||||
let process_tree = KillGitProcessTreeOnDrop {
|
||||
#[cfg(unix)]
|
||||
process_id: child.id()?,
|
||||
#[cfg(windows)]
|
||||
job,
|
||||
#[cfg(unix)]
|
||||
armed: true,
|
||||
};
|
||||
|
||||
Some((child, process_tree))
|
||||
}
|
||||
|
||||
async fn wait_for_git_command_with_timeout_output(
|
||||
child: Child,
|
||||
process_tree: KillGitProcessTreeOnDrop,
|
||||
timeout_duration: Duration,
|
||||
) -> Option<Output> {
|
||||
#[cfg(unix)]
|
||||
let mut process_tree = process_tree;
|
||||
|
||||
let result = timeout(timeout_duration, child.wait_with_output()).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) => {
|
||||
#[cfg(windows)]
|
||||
if let Some(job) = &process_tree.job {
|
||||
job.preserve_descendants().ok()?;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
process_tree.armed = false;
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run_git_command_with_timeout_output(
|
||||
command: &mut Command,
|
||||
timeout_duration: Duration,
|
||||
) -> Option<Output> {
|
||||
let (child, process_tree) = spawn_git_command(command)?;
|
||||
wait_for_git_command_with_timeout_output(child, process_tree, timeout_duration).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "git_process_tests.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user