| #[cfg(windows)] |
| use std::collections::HashMap; |
| #[cfg(windows)] |
| use std::path::Path; |
| use std::process::Stdio; |
| use std::time::Duration; |
|
|
| #[cfg(windows)] |
| use codex_protocol::config_types::WindowsSandboxLevel; |
| #[cfg(windows)] |
| use codex_protocol::models::PermissionProfile; |
| #[cfg(windows)] |
| use codex_sandboxing::SandboxExecRequest; |
| #[cfg(windows)] |
| use codex_sandboxing::SandboxType; |
| #[cfg(windows)] |
| use codex_utils_path_uri::PathUri; |
| use pretty_assertions::assert_eq; |
| #[cfg(windows)] |
| use tokio::io::AsyncReadExt; |
| use tokio::io::AsyncWriteExt; |
| use tokio::process::Command; |
|
|
| use super::drain_helper_stderr; |
| use super::read_helper_response; |
| use super::reap_helper_after_response; |
| #[cfg(windows)] |
| use super::run_command; |
| #[cfg(windows)] |
| use crate::fs_helper::FsHelperPayload; |
| #[cfg(windows)] |
| use crate::protocol::FsReadFileResponse; |
| #[cfg(windows)] |
| use crate::protocol::FsWriteFileResponse; |
|
|
| #[tokio::test(start_paused = true)] |
| async fn filesystem_operation_is_not_limited_by_helper_response_deadline() { |
| let (reader, mut writer) = tokio::io::duplex( 256); |
| let response = tokio::spawn(async move { read_helper_response(reader).await }); |
|
|
| tokio::task::yield_now().await; |
| tokio::time::advance(Duration::from_secs( 31)).await; |
| tokio::task::yield_now().await; |
| assert!( |
| !response.is_finished(), |
| "a filesystem operation must not time out before the helper responds" |
| ); |
|
|
| writer |
| .write_all(b"completed after the old deadline\n") |
| .await |
| .expect("helper response"); |
|
|
| assert_eq!( |
| response |
| .await |
| .expect("response task") |
| .expect("unbounded filesystem operation"), |
| b"completed after the old deadline\n" |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn noisy_failing_helper_preserves_exit_status_and_bounded_stderr() { |
| #[cfg(unix)] |
| let mut command = { |
| let mut command = Command::new("sh"); |
| command.arg("-c").arg( |
| "printf 'expected helper diagnostic' >&2; i=0; while [ \"$i\" -lt 1024 ]; do printf '%0128d' 0 >&2; i=$((i + 1)); done; exit 7", |
| ); |
| command |
| }; |
| #[cfg(windows)] |
| let mut command = { |
| let system_root = std::env::var("SystemRoot").expect("Windows system root"); |
| let powershell = Path::new(&system_root) |
| .join("System32") |
| .join("WindowsPowerShell") |
| .join("v1.0") |
| .join("powershell.exe"); |
| let mut command = Command::new(powershell); |
| command |
| .arg("-NoProfile") |
| .arg("-Command") |
| .arg("[Console]::Error.Write('expected helper diagnostic' + ('x' * 131072)); exit 7"); |
| command |
| }; |
| command.stdout(Stdio::null()); |
| command.stderr(Stdio::piped()); |
| command.kill_on_drop(true); |
| let mut child = command.spawn().expect("noisy helper process"); |
| let stderr = drain_helper_stderr(&mut child); |
|
|
| let error = tokio::time::timeout( |
| Duration::from_secs( 8), |
| reap_helper_after_response(child, stderr), |
| ) |
| .await |
| .expect("helper stderr must be drained during bounded cleanup") |
| .expect_err("nonzero helper exit should fail after its stderr pipe fills"); |
|
|
| assert!(error.message.contains('7'), "{}", error.message); |
| assert!( |
| error.message.contains("expected helper diagnostic"), |
| "{}", |
| error.message |
| ); |
| assert!(error.message.len() < 4400, "{}", error.message.len()); |
| } |
|
|
| #[tokio::test] |
| async fn helper_stderr_is_drained_before_the_response() { |
| #[cfg(unix)] |
| let mut command = { |
| let mut command = Command::new("sh"); |
| command.arg("-c").arg( |
| "printf 'expected pre-response diagnostic' >&2; i=0; while [ \"$i\" -lt 1024 ]; do printf '%0128d' 0 >&2; i=$((i + 1)); done; printf 'completed after noisy stderr\\n'", |
| ); |
| command |
| }; |
| #[cfg(windows)] |
| let mut command = { |
| let system_root = std::env::var("SystemRoot").expect("Windows system root"); |
| let powershell = Path::new(&system_root) |
| .join("System32") |
| .join("WindowsPowerShell") |
| .join("v1.0") |
| .join("powershell.exe"); |
| let mut command = Command::new(powershell); |
| command.arg("-NoProfile").arg("-Command").arg( |
| "[Console]::Error.Write('expected pre-response diagnostic' + ('x' * 131072)); [Console]::Out.WriteLine('completed after noisy stderr')", |
| ); |
| command |
| }; |
| command.stdout(Stdio::piped()); |
| command.stderr(Stdio::piped()); |
| command.kill_on_drop(true); |
| let mut child = command.spawn().expect("noisy helper process"); |
| let stdout = child.stdout.take().expect("helper stdout"); |
| let stderr = drain_helper_stderr(&mut child); |
|
|
| let response = tokio::time::timeout( |
| Duration::from_secs( 2), |
| read_helper_response(stdout), |
| ) |
| .await |
| .expect("helper stderr must be drained before awaiting its response") |
| .expect("helper response"); |
|
|
| assert_eq!(response.trim_ascii_end(), b"completed after noisy stderr"); |
| reap_helper_after_response(child, stderr) |
| .await |
| .expect("noisy helper should be cleaned up after its response"); |
| } |
|
|
| #[cfg(windows)] |
| #[tokio::test] |
| async fn completed_windows_image_read_does_not_wait_for_a_stuck_helper() { |
| let directory = tempfile::tempdir().expect("temporary directory"); |
| let path = directory.path().join("image.png"); |
| let operations = [ |
| ( |
| r#"[IO.File]::WriteAllText($env:CODEX_FS_HELPER_TEST_PATH, 'image') |
| [Console]::Out.WriteLine('{"status":"ok","payload":{"operation":"fs/writeFile","response":{}}}')"#, |
| FsHelperPayload::WriteFile(FsWriteFileResponse {}), |
| ), |
| ( |
| r#"$data = [Convert]::ToBase64String([IO.File]::ReadAllBytes($env:CODEX_FS_HELPER_TEST_PATH)) |
| [Console]::Out.WriteLine('{"status":"ok","payload":{"operation":"fs/readFile","response":{"dataBase64":"' + $data + '"}}}')"#, |
| FsHelperPayload::ReadFile(FsReadFileResponse { |
| data_base64: "aW1hZ2U=".to_string(), |
| }), |
| ), |
| ]; |
|
|
| for (operation, expected) in operations { |
| let script = format!( |
| "[Console]::In.ReadLine() | Out-Null\n{operation}\n[Console]::Out.Flush()\n[Threading.Thread]::Sleep(30000)" |
| ); |
| let command = powershell_command(&script, &path).expect("PowerShell helper command"); |
| let result = tokio::time::timeout( |
| Duration::from_secs( 8), |
| run_command(command, b"{}".to_vec()), |
| ) |
| .await |
| .expect("the completed operation must not wait for helper termination") |
| .expect("helper response"); |
|
|
| assert_eq!(result, expected); |
| } |
| assert_eq!(std::fs::read(&path).expect("created file"), b"image"); |
| } |
|
|
| #[cfg(windows)] |
| #[tokio::test] |
| async fn duplicated_windows_file_handle_survives_bounded_helper_cleanup() { |
| let directory = tempfile::tempdir().expect("temporary directory"); |
| let path = directory.path().join("image.png"); |
| std::fs::write(&path, b"image").expect("image file"); |
| let command = powershell_command( |
| r#"[Console]::In.ReadLine() | Out-Null |
| $file = [IO.File]::OpenRead($env:CODEX_FS_HELPER_TEST_PATH) |
| $handle = $file.SafeFileHandle.DangerousGetHandle().ToInt64() |
| [Console]::Out.WriteLine('{"status":"ok","payload":{"operation":"fs/open","response":{"processId":' + $PID + ',"fileHandle":' + $handle + '}}}') |
| [Console]::Out.Flush() |
| [Threading.Thread]::Sleep(30000)"#, |
| &path, |
| ) |
| .expect("PowerShell helper command"); |
|
|
| let mut file = tokio::time::timeout( |
| Duration::from_secs( 8), |
| crate::sandboxed_file_open::open( |
| command, |
| PathUri::from_host_native_path(&path).expect("image path URI"), |
| ), |
| ) |
| .await |
| .expect("the opened file must not wait for helper termination") |
| .expect("duplicated file handle"); |
| let mut data = Vec::new(); |
| file.read_to_end(&mut data).await.expect("image contents"); |
|
|
| assert_eq!(data, b"image"); |
| } |
|
|
| #[cfg(windows)] |
| fn powershell_command(script: &str, path: &Path) -> anyhow::Result<SandboxExecRequest> { |
| let system_root = std::env::var("SystemRoot")?; |
| let powershell = Path::new(&system_root) |
| .join("System32") |
| .join("WindowsPowerShell") |
| .join("v1.0") |
| .join("powershell.exe"); |
| let cwd = PathUri::from_host_native_path(std::env::current_dir()?)?; |
|
|
| Ok(SandboxExecRequest { |
| command: vec![ |
| powershell.to_string_lossy().into_owned(), |
| "-NoProfile".to_string(), |
| "-Command".to_string(), |
| script.to_string(), |
| ], |
| cwd: cwd.clone(), |
| sandbox_policy_cwd: cwd, |
| env: HashMap::from([ |
| ("SystemRoot".to_string(), system_root), |
| ( |
| "CODEX_FS_HELPER_TEST_PATH".to_string(), |
| path.to_string_lossy().into_owned(), |
| ), |
| ]), |
| network: None, |
| network_environment_id: None, |
| sandbox: SandboxType::None, |
| windows_sandbox_level: WindowsSandboxLevel::Disabled, |
| windows_sandbox_private_desktop: false, |
| permission_profile: PermissionProfile::Disabled, |
| arg0: None, |
| }) |
| } |
|
|