Files
codex/codex-rs/git-utils/src/git_process.rs
T
Eric Traut fbc169827e Suppress Windows console windows for background subprocesses (#49164)
## Why

Background helpers launched from a detached Windows process can allocate console windows. Job Object launches and containment fallbacks need to preserve console suppression as well.

## What changed

- Add `codex_utils_process::background_command` to set `CREATE_NO_WINDOW` on Windows, and use it across background subprocess launches, including Git, plugins, authentication, diagnostics, and sandbox helpers.
- Centralize console suppression in Job Object launches. Keep `CREATE_NO_WINDOW` when retrying without containment, and clear `CREATE_SUSPENDED` so the fallback child can run.
- Preserve console inheritance for interactive shell launches and interactive AWS authentication refreshes.

## Testing

Add Windows regression tests covering console-free launches from detached parents through standard and Tokio commands, stdout/stderr capture, successful containment, Job Object creation and assignment failures, and sandbox setup refresh helpers.

GitOrigin-RevId: b2c238fa7d4f9fbd868e5adf7d4be9958ff01e6c
2026-09-29 04:38:37 +00:00

98 lines
2.3 KiB
Rust

use std::process::Output;
use std::process::Stdio;
use std::time::Duration;
use codex_protocol::shell_environment::scrub_non_inheritable_env_vars;
#[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)> {
scrub_non_inheritable_env_vars(command.as_std_mut());
#[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) = JobObject::spawn_background(command).ok()?;
#[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;