From f7cb13fe84657d7d1bfd35017548d3099c87aa59 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 12 Apr 2026 23:48:07 -0700 Subject: [PATCH] Fix shell timeout race on Windows --- .../tests/suite/v2/realtime_conversation.rs | 8 ++-- codex-rs/core/src/exec.rs | 15 ++++--- codex-rs/core/src/exec_tests.rs | 45 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs index f4c0f99ae3..d7934193ff 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -1888,10 +1888,10 @@ fn realtime_tool_ok_command() -> Vec { #[cfg(windows)] { vec![ - "powershell.exe".to_string(), - "-NoProfile".to_string(), - "-Command".to_string(), - "[Console]::Write('realtime-tool-ok')".to_string(), + "cmd.exe".to_string(), + "/D".to_string(), + "/C".to_string(), + "echo|set /p dummy=realtime-tool-ok".to_string(), ] } diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index af39657e00..cc33344265 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -1241,15 +1241,20 @@ async fn consume_output( }; tokio::pin!(expiration_wait); let (exit_status, timed_out) = tokio::select! { + biased; + _ = &mut expiration_wait => { + if let Some(exit_status) = child.try_wait()? { + (exit_status, false) + } else { + kill_child_process_group(&mut child)?; + child.start_kill()?; + (synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + TIMEOUT_CODE), true) + } + } status_result = child.wait() => { let exit_status = status_result?; (exit_status, false) } - _ = &mut expiration_wait => { - kill_child_process_group(&mut child)?; - child.start_kill()?; - (synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + TIMEOUT_CODE), true) - } _ = tokio::signal::ctrl_c() => { kill_child_process_group(&mut child)?; child.start_kill()?; diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 937a7d6f80..cfb25d9928 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -291,6 +291,51 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> { Ok(()) } +#[tokio::test] +async fn exec_expiration_observes_already_exited_child_before_timing_out() -> Result<()> { + #[cfg(windows)] + let command = vec![ + "cmd.exe".to_string(), + "/D".to_string(), + "/C".to_string(), + "echo|set /p dummy=hello".to_string(), + ]; + #[cfg(not(windows))] + let command = vec!["printf".to_string(), "hello".to_string()]; + + let cancel_token = CancellationToken::new(); + cancel_token.cancel(); + let output = exec( + ExecParams { + command, + cwd: codex_utils_absolute_path::AbsolutePathBuf::current_dir()?, + expiration: ExecExpiration::Cancellation(cancel_token), + capture_policy: ExecCapturePolicy::ShellTool, + env: std::env::vars().collect(), + network: None, + sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + justification: None, + arg0: None, + }, + SandboxType::None, + &SandboxPolicy::DangerFullAccess, + &FileSystemSandboxPolicy::unrestricted(), + /*windows_sandbox_filesystem_overrides*/ None, + NetworkSandboxPolicy::Enabled, + /*stdout_stream*/ None, + Some(Box::new(|| std::thread::sleep(Duration::from_secs(1)))), + ) + .await?; + + assert_eq!(output.stdout.from_utf8_lossy().text, "hello"); + assert_eq!(output.exit_status.code(), Some(0)); + assert!(!output.timed_out); + + Ok(()) +} + #[cfg(unix)] #[tokio::test] async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_pipe_open()