Files
codex/codex-rs/utils/process/src/background_command_tests.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

55 lines
2.2 KiB
Rust

//! Verifies detached background children stay console-free through std and Tokio launches.
use super::background_command;
use pretty_assertions::assert_eq;
use std::os::windows::process::CommandExt;
use std::process::Command;
#[tokio::test]
async fn background_commands_have_no_console_under_detached_parent() {
const TEST_NAME: &str = "tests::background_commands_have_no_console_under_detached_parent";
const PHASE: &str = "CODEX_TEST_BACKGROUND_COMMAND_PHASE";
let executable = std::env::current_exe().expect("test executable");
match std::env::var(PHASE).as_deref() {
Ok("probe") => {
#[link(name = "kernel32")]
unsafe extern "system" {
#[link_name = "GetConsoleWindow"]
fn get_console_window() -> isize;
}
assert_eq!(unsafe { get_console_window() }, 0);
println!("background stdout");
eprintln!("background stderr");
}
Ok("detached") => {
for asynchronous in [false, true] {
let mut command = background_command(&executable);
command
.args(["--exact", TEST_NAME, "--nocapture"])
.env(PHASE, "probe");
let output = if asynchronous {
tokio::process::Command::from(command).output().await
} else {
command.output()
}
.expect("run background command");
assert!(output.status.success(), "probe failed: {output:?}");
assert!(String::from_utf8_lossy(&output.stdout).contains("background stdout"));
assert!(String::from_utf8_lossy(&output.stderr).contains("background stderr"));
}
}
_ => {
let output = Command::new(executable)
.args(["--exact", TEST_NAME, "--nocapture"])
.env(PHASE, "detached")
.creation_flags(/*flags*/ 0x0000_0008) // DETACHED_PROCESS
.output()
.expect("spawn detached parent");
assert!(
output.status.success(),
"detached parent failed: {output:?}"
);
}
}
}