use super::collect_output_until_exit; use super::combine_spawned_output; use super::find_python; use super::wait_for_output_contains; use crate::TerminalSize; use crate::spawn_pty_process; use std::collections::HashMap; use std::path::Path; const READY_MARKER: &str = "__CODEX_CHILD_READY__"; const VALUE_MARKER: &str = "__CODEX_CHILD_VALUE__"; struct WindowsShell { name: &'static str, program: String, args: Vec, child_command: String, } fn find_powershell() -> Option { ["pwsh.exe", "powershell.exe"] .into_iter() .find_map(|candidate| { std::process::Command::new(candidate) .args(["-NoLogo", "-NoProfile", "-Command", "exit 0"]) .status() .ok() .filter(std::process::ExitStatus::success) .map(|_| candidate.to_string()) }) } fn utf8_hex(value: &str) -> String { value .as_bytes() .iter() .map(|byte| format!("{byte:02x}")) .collect::>() .join("") } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn conpty_delivers_input_to_foreground_children() -> anyhow::Result<()> { let Some(python) = find_python() else { eprintln!("python not found; skipping ConPTY input test"); return Ok(()); }; let code = format!( "print('__CODEX_CHILD_'+'READY__', flush=True); value=input(); print('{VALUE_MARKER}'+value.encode('utf-8').hex(), flush=True)" ); let expected = "cafeé 漢字"; let expected_marker = format!("{VALUE_MARKER}{}", utf8_hex(expected)); let mut shells = vec![WindowsShell { name: "cmd", program: std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()), args: vec!["/D".to_string(), "/Q".to_string()], child_command: format!("\"{}\" -u -c \"{code}\"", python.replace('"', "\"\"")), }]; if let Some(program) = find_powershell() { shells.push(WindowsShell { name: "PowerShell", program, args: vec!["-NoLogo".to_string(), "-NoProfile".to_string()], child_command: format!("& '{}' -u -c \"{code}\"", python.replace('\'', "''")), }); } let env: HashMap = std::env::vars().collect(); for shell in shells { let spawned = spawn_pty_process( &shell.program, &shell.args, Path::new("."), &env, /*arg0*/ &None, TerminalSize::default(), ) .await?; let (session, mut output_rx, exit_rx) = combine_spawned_output(spawned); let writer = session.writer_sender(); writer .send(format!("{}\n", shell.child_command).into_bytes()) .await?; wait_for_output_contains(&mut output_rx, READY_MARKER, /*timeout_ms*/ 10_000) .await .map_err(|err| anyhow::anyhow!("{} child did not become ready: {err}", shell.name))?; writer .send(format!("{expected}X\u{8}\n").into_bytes()) .await?; let mut output = wait_for_output_contains(&mut output_rx, &expected_marker, /*timeout_ms*/ 10_000) .await .map_err(|err| { anyhow::anyhow!("{} child received incorrect input: {err}", shell.name) })?; writer.send(b"exit 0\n".to_vec()).await?; let (remaining, exit_code) = collect_output_until_exit(output_rx, exit_rx, /*timeout_ms*/ 10_000).await; output.extend_from_slice(&remaining); assert_eq!( exit_code, 0, "{} did not exit cleanly: {:?}", shell.name, String::from_utf8_lossy(&output) ); } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn conpty_ctrl_c_interrupts_powershell_foreground_child() -> anyhow::Result<()> { let Some(program) = find_powershell() else { return Ok(()); }; let args = vec!["-NoLogo".to_string(), "-NoProfile".to_string()]; let env: HashMap = std::env::vars().collect(); let spawned = spawn_pty_process( &program, &args, Path::new("."), &env, /*arg0*/ &None, TerminalSize::default(), ) .await?; let (session, mut output_rx, exit_rx) = combine_spawned_output(spawned); let writer = session.writer_sender(); writer.send(b"ping.exe -4 -t localhost\n".to_vec()).await?; wait_for_output_contains(&mut output_rx, "127.0.0.1", /*timeout_ms*/ 10_000).await?; writer.send(vec![0x03]).await?; tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; writer.send(b"cmd.exe /D /C ver\n".to_vec()).await?; let mut output = wait_for_output_contains( &mut output_rx, "Microsoft Windows", /*timeout_ms*/ 10_000, ) .await?; writer.send(b"exit 0\n".to_vec()).await?; let (remaining, exit_code) = collect_output_until_exit(output_rx, exit_rx, /*timeout_ms*/ 10_000).await; output.extend_from_slice(&remaining); assert_eq!( exit_code, 0, "PowerShell did not resume after Ctrl-C: {:?}", String::from_utf8_lossy(&output) ); Ok(()) }