Skip to content

Commit f48d880

Browse files
pakrympakrym-oai
andauthored
Fix unified_exec on windows (#7620)
Fix unified_exec on windows Requires removal of PSUEDOCONSOLE_INHERIT_CURSOR flag so child processed don't attempt to wait for cursor position response (and timeout). https://github.com/wezterm/wezterm/compare/main...pakrym:wezterm:PSUEDOCONSOLE_INHERIT_CURSOR?expand=1 --------- Co-authored-by: pakrym-oai <pakrym@openai.com>
1 parent a8cbbdb commit f48d880

File tree

6 files changed

+128
-11
lines changed

6 files changed

+128
-11
lines changed

codex-rs/Cargo.lock

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ seccompiler = "0.5.0"
178178
sentry = "0.34.0"
179179
serde = "1"
180180
serde_json = "1"
181-
serde_yaml = "0.9"
182181
serde_with = "3.16"
182+
serde_yaml = "0.9"
183183
serial_test = "3.2.0"
184184
sha1 = "0.10.6"
185185
sha2 = "0.10"
@@ -288,6 +288,7 @@ opt-level = 0
288288
# Uncomment to debug local changes.
289289
# ratatui = { path = "../../ratatui" }
290290
crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" }
291+
portable-pty = { git = "https://github.com/pakrym/wezterm", branch = "PSUEDOCONSOLE_INHERIT_CURSOR" }
291292
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
292293

293294
# Uncomment to debug local changes.

codex-rs/core/tests/common/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,7 @@ macro_rules! skip_if_no_network {
374374
macro_rules! skip_if_windows {
375375
($return_value:expr $(,)?) => {{
376376
if cfg!(target_os = "windows") {
377-
println!(
378-
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
379-
);
377+
println!("Skipping test because it cannot execute on Windows.");
380378
return $return_value;
381379
}
382380
}};

codex-rs/core/tests/suite/unified_exec.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![cfg(not(target_os = "windows"))]
21
use std::collections::HashMap;
32
use std::ffi::OsStr;
43
use std::fs;
@@ -24,6 +23,7 @@ use core_test_support::responses::sse;
2423
use core_test_support::responses::start_mock_server;
2524
use core_test_support::skip_if_no_network;
2625
use core_test_support::skip_if_sandbox;
26+
use core_test_support::skip_if_windows;
2727
use core_test_support::test_codex::TestCodex;
2828
use core_test_support::test_codex::TestCodexHarness;
2929
use core_test_support::test_codex::test_codex;
@@ -155,6 +155,7 @@ fn collect_tool_outputs(bodies: &[Value]) -> Result<HashMap<String, ParsedUnifie
155155
async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
156156
skip_if_no_network!(Ok(()));
157157
skip_if_sandbox!(Ok(()));
158+
skip_if_windows!(Ok(()));
158159

159160
let builder = test_codex().with_config(|config| {
160161
config.include_apply_patch_tool = true;
@@ -279,6 +280,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
279280
async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
280281
skip_if_no_network!(Ok(()));
281282
skip_if_sandbox!(Ok(()));
283+
skip_if_windows!(Ok(()));
282284

283285
let server = start_mock_server().await;
284286

@@ -350,6 +352,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
350352
async fn unified_exec_resolves_relative_workdir() -> Result<()> {
351353
skip_if_no_network!(Ok(()));
352354
skip_if_sandbox!(Ok(()));
355+
skip_if_windows!(Ok(()));
353356

354357
let server = start_mock_server().await;
355358

@@ -427,6 +430,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
427430
async fn unified_exec_respects_workdir_override() -> Result<()> {
428431
skip_if_no_network!(Ok(()));
429432
skip_if_sandbox!(Ok(()));
433+
skip_if_windows!(Ok(()));
430434

431435
let server = start_mock_server().await;
432436

@@ -505,6 +509,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
505509
async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
506510
skip_if_no_network!(Ok(()));
507511
skip_if_sandbox!(Ok(()));
512+
skip_if_windows!(Ok(()));
508513

509514
let server = start_mock_server().await;
510515

@@ -591,6 +596,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
591596
async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
592597
skip_if_no_network!(Ok(()));
593598
skip_if_sandbox!(Ok(()));
599+
skip_if_windows!(Ok(()));
594600

595601
let server = start_mock_server().await;
596602

@@ -662,6 +668,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
662668
async fn unified_exec_emits_output_delta_for_write_stdin() -> Result<()> {
663669
skip_if_no_network!(Ok(()));
664670
skip_if_sandbox!(Ok(()));
671+
skip_if_windows!(Ok(()));
665672

666673
let server = start_mock_server().await;
667674

@@ -761,6 +768,7 @@ async fn unified_exec_emits_output_delta_for_write_stdin() -> Result<()> {
761768
async fn unified_exec_emits_begin_for_write_stdin() -> Result<()> {
762769
skip_if_no_network!(Ok(()));
763770
skip_if_sandbox!(Ok(()));
771+
skip_if_windows!(Ok(()));
764772

765773
let server = start_mock_server().await;
766774

@@ -857,6 +865,7 @@ async fn unified_exec_emits_begin_for_write_stdin() -> Result<()> {
857865
async fn unified_exec_emits_begin_event_for_write_stdin_requests() -> Result<()> {
858866
skip_if_no_network!(Ok(()));
859867
skip_if_sandbox!(Ok(()));
868+
skip_if_windows!(Ok(()));
860869

861870
let server = start_mock_server().await;
862871

@@ -978,6 +987,7 @@ async fn unified_exec_emits_begin_event_for_write_stdin_requests() -> Result<()>
978987
async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
979988
skip_if_no_network!(Ok(()));
980989
skip_if_sandbox!(Ok(()));
990+
skip_if_windows!(Ok(()));
981991

982992
let server = start_mock_server().await;
983993

@@ -1085,6 +1095,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
10851095
async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
10861096
skip_if_no_network!(Ok(()));
10871097
skip_if_sandbox!(Ok(()));
1098+
skip_if_windows!(Ok(()));
10881099

10891100
let server = start_mock_server().await;
10901101

@@ -1177,6 +1188,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
11771188
async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
11781189
skip_if_no_network!(Ok(()));
11791190
skip_if_sandbox!(Ok(()));
1191+
skip_if_windows!(Ok(()));
11801192

11811193
let server = start_mock_server().await;
11821194

@@ -1338,6 +1350,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
13381350
async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()> {
13391351
skip_if_no_network!(Ok(()));
13401352
skip_if_sandbox!(Ok(()));
1353+
skip_if_windows!(Ok(()));
13411354

13421355
let server = start_mock_server().await;
13431356

@@ -1442,6 +1455,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
14421455
async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
14431456
skip_if_no_network!(Ok(()));
14441457
skip_if_sandbox!(Ok(()));
1458+
skip_if_windows!(Ok(()));
14451459

14461460
let server = start_mock_server().await;
14471461

@@ -1553,6 +1567,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
15531567
async fn unified_exec_streams_after_lagged_output() -> Result<()> {
15541568
skip_if_no_network!(Ok(()));
15551569
skip_if_sandbox!(Ok(()));
1570+
skip_if_windows!(Ok(()));
15561571

15571572
let server = start_mock_server().await;
15581573

@@ -1684,6 +1699,7 @@ PY
16841699
async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
16851700
skip_if_no_network!(Ok(()));
16861701
skip_if_sandbox!(Ok(()));
1702+
skip_if_windows!(Ok(()));
16871703

16881704
let server = start_mock_server().await;
16891705

@@ -1790,6 +1806,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
17901806
async fn unified_exec_formats_large_output_summary() -> Result<()> {
17911807
skip_if_no_network!(Ok(()));
17921808
skip_if_sandbox!(Ok(()));
1809+
skip_if_windows!(Ok(()));
17931810

17941811
let server = start_mock_server().await;
17951812

@@ -1875,6 +1892,7 @@ PY
18751892
async fn unified_exec_runs_under_sandbox() -> Result<()> {
18761893
skip_if_no_network!(Ok(()));
18771894
skip_if_sandbox!(Ok(()));
1895+
skip_if_windows!(Ok(()));
18781896

18791897
let server = start_mock_server().await;
18801898

@@ -2067,11 +2085,83 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
20672085
Ok(())
20682086
}
20692087

2088+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2089+
async fn unified_exec_runs_on_all_platforms() -> Result<()> {
2090+
skip_if_no_network!(Ok(()));
2091+
skip_if_sandbox!(Ok(()));
2092+
2093+
let server = start_mock_server().await;
2094+
2095+
let mut builder = test_codex().with_config(|config| {
2096+
config.features.enable(Feature::UnifiedExec);
2097+
});
2098+
let TestCodex {
2099+
codex,
2100+
cwd,
2101+
session_configured,
2102+
..
2103+
} = builder.build(&server).await?;
2104+
2105+
let call_id = "uexec";
2106+
let args = serde_json::json!({
2107+
"cmd": "echo 'hello crossplat'",
2108+
});
2109+
2110+
let responses = vec![
2111+
sse(vec![
2112+
ev_response_created("resp-1"),
2113+
ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?),
2114+
ev_completed("resp-1"),
2115+
]),
2116+
sse(vec![
2117+
ev_assistant_message("msg-1", "done"),
2118+
ev_completed("resp-2"),
2119+
]),
2120+
];
2121+
mount_sse_sequence(&server, responses).await;
2122+
2123+
let session_model = session_configured.model.clone();
2124+
2125+
codex
2126+
.submit(Op::UserTurn {
2127+
items: vec![UserInput::Text {
2128+
text: "summarize large output".into(),
2129+
}],
2130+
final_output_json_schema: None,
2131+
cwd: cwd.path().to_path_buf(),
2132+
approval_policy: AskForApproval::Never,
2133+
sandbox_policy: SandboxPolicy::DangerFullAccess,
2134+
model: session_model,
2135+
effort: None,
2136+
summary: ReasoningSummary::Auto,
2137+
})
2138+
.await?;
2139+
2140+
wait_for_event(&codex, |event| matches!(event, EventMsg::TaskComplete(_))).await;
2141+
2142+
let requests = server.received_requests().await.expect("recorded requests");
2143+
assert!(!requests.is_empty(), "expected at least one POST request");
2144+
2145+
let bodies = requests
2146+
.iter()
2147+
.map(|req| req.body_json::<Value>().expect("request json"))
2148+
.collect::<Vec<_>>();
2149+
2150+
let outputs = collect_tool_outputs(&bodies)?;
2151+
let output = outputs.get(call_id).expect("missing output");
2152+
2153+
// TODO: Weaker match because windows produces control characters
2154+
assert_regex_match(".*hello crossplat.*", &output.output);
2155+
2156+
Ok(())
2157+
}
2158+
20702159
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
20712160
#[ignore]
20722161
async fn unified_exec_prunes_exited_sessions_first() -> Result<()> {
20732162
skip_if_no_network!(Ok(()));
20742163
skip_if_sandbox!(Ok(()));
2164+
skip_if_windows!(Ok(()));
20752165

20762166
let server = start_mock_server().await;
20772167

codex-rs/utils/pty/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ workspace = true
1010
[dependencies]
1111
anyhow = { workspace = true }
1212
portable-pty = { workspace = true }
13-
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] }
13+
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] }

codex-rs/utils/pty/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::fmt;
12
use std::collections::HashMap;
23
use std::io::ErrorKind;
34
use std::path::Path;
@@ -9,13 +10,20 @@ use std::time::Duration;
910
use anyhow::Result;
1011
use portable_pty::native_pty_system;
1112
use portable_pty::CommandBuilder;
13+
use portable_pty::MasterPty;
1214
use portable_pty::PtySize;
15+
use portable_pty::SlavePty;
1316
use tokio::sync::broadcast;
1417
use tokio::sync::mpsc;
1518
use tokio::sync::oneshot;
1619
use tokio::sync::Mutex as TokioMutex;
1720
use tokio::task::JoinHandle;
1821

22+
pub struct PtyPairWrapper {
23+
pub _slave: Option<Box<dyn SlavePty + Send>>,
24+
pub _master: Box<dyn MasterPty + Send>,
25+
}
26+
1927
#[derive(Debug)]
2028
pub struct ExecCommandSession {
2129
writer_tx: mpsc::Sender<Vec<u8>>,
@@ -26,6 +34,15 @@ pub struct ExecCommandSession {
2634
wait_handle: StdMutex<Option<JoinHandle<()>>>,
2735
exit_status: Arc<AtomicBool>,
2836
exit_code: Arc<StdMutex<Option<i32>>>,
37+
// PtyPair must be preserved because the process will receive Control+C if the
38+
// slave is closed
39+
_pair: StdMutex<PtyPairWrapper>,
40+
}
41+
42+
impl fmt::Debug for PtyPairWrapper {
43+
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
44+
Ok(())
45+
}
2946
}
3047

3148
impl ExecCommandSession {
@@ -39,6 +56,7 @@ impl ExecCommandSession {
3956
wait_handle: JoinHandle<()>,
4057
exit_status: Arc<AtomicBool>,
4158
exit_code: Arc<StdMutex<Option<i32>>>,
59+
pair: PtyPairWrapper,
4260
) -> (Self, broadcast::Receiver<Vec<u8>>) {
4361
let initial_output_rx = output_tx.subscribe();
4462
(
@@ -51,6 +69,7 @@ impl ExecCommandSession {
5169
wait_handle: StdMutex::new(Some(wait_handle)),
5270
exit_status,
5371
exit_code,
72+
_pair: StdMutex::new(pair),
5473
},
5574
initial_output_rx,
5675
)
@@ -192,6 +211,16 @@ pub async fn spawn_pty_process(
192211
let _ = exit_tx.send(code);
193212
});
194213

214+
let pair = PtyPairWrapper {
215+
_slave: if cfg!(windows) {
216+
// Keep the slave handle alive on Windows to prevent the process from receiving Control+C
217+
Some(pair.slave)
218+
} else {
219+
None
220+
},
221+
_master: pair.master,
222+
};
223+
195224
let (session, output_rx) = ExecCommandSession::new(
196225
writer_tx,
197226
output_tx,
@@ -201,6 +230,7 @@ pub async fn spawn_pty_process(
201230
wait_handle,
202231
exit_status,
203232
exit_code,
233+
pair,
204234
);
205235

206236
Ok(SpawnedPty {

0 commit comments

Comments
 (0)