diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 795723f9d3..7d82c5ab80 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2065,6 +2065,7 @@ impl CodexMessageProcessor { let exec_params = ExecParams { command, cwd: cwd.clone(), + stdin: None, expiration, capture_policy, env, diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index b72c84e906..e066eade1d 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -232,6 +232,7 @@ impl CommandExecManager { command, cwd, env, + stdin: _stdin, expiration, sandbox: _sandbox, arg0, @@ -738,6 +739,7 @@ mod tests { vec!["cmd".to_string()], AbsolutePathBuf::current_dir().expect("current dir"), HashMap::new(), + None, /*network*/ None, ExecExpiration::DefaultTimeout, codex_core::exec::ExecCapturePolicy::ShellTool, @@ -850,6 +852,7 @@ mod tests { vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], AbsolutePathBuf::current_dir().expect("current dir"), HashMap::new(), + None, /*network*/ None, ExecExpiration::Cancellation(CancellationToken::new()), codex_core::exec::ExecCapturePolicy::ShellTool, diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index f8b61796b5..aeba145d34 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -1,5 +1,6 @@ use std::fs::File; use std::future::Future; +use std::io::Read; use std::path::Path; use std::path::PathBuf; @@ -94,7 +95,16 @@ pub fn arg0_dispatch() -> Option { let argv1 = args.next().unwrap_or_default(); if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 { - let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned)); + let patch_arg = match args.next() { + Some(arg) => arg.to_str().map(str::to_owned), + None => { + let mut patch = String::new(); + match std::io::stdin().read_to_string(&mut patch) { + Ok(_) if !patch.is_empty() => Some(patch), + _ => None, + } + } + }; let exit_code = match patch_arg { Some(patch_arg) => { let mut stdout = std::io::stdout(); diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 0ad2f36382..caa2800f77 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -171,6 +171,7 @@ async fn run_command_under_sandbox( command: command_vec, cwd: &cwd_clone, env_map, + stdin: None, timeout_ms: None, use_private_desktop: config.permissions.windows_sandbox_private_desktop, proxy_enforced: false, diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index cd0895a8b5..355742a92b 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -5597,6 +5597,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { ] }, cwd: turn_context.cwd.clone(), + stdin: None, expiration: timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index 30879fc82a..f482eba19b 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -123,6 +123,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid ] }, cwd: turn_context.cwd.clone(), + stdin: None, expiration: expiration_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index af39657e00..7c15f53d08 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -83,6 +83,7 @@ pub const IO_DRAIN_TIMEOUT_MS: u64 = 2_000; // 2 s should be plenty for local pi pub struct ExecParams { pub command: Vec, pub cwd: AbsolutePathBuf, + pub stdin: Option>, pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, pub env: HashMap, @@ -265,6 +266,7 @@ pub fn build_exec_request( command, cwd, mut env, + stdin, expiration, capture_policy, network, @@ -291,6 +293,7 @@ pub fn build_exec_request( cwd, env, additional_permissions: None, + stdin, }; let options = ExecOptions { expiration, @@ -349,6 +352,7 @@ pub(crate) async fn execute_exec_request( command, cwd, env, + stdin, network, expiration, capture_policy, @@ -365,6 +369,7 @@ pub(crate) async fn execute_exec_request( let params = ExecParams { command, cwd, + stdin, expiration, capture_policy, env, @@ -472,6 +477,7 @@ async fn exec_windows_sandbox( command, cwd, mut env, + stdin, network, expiration, capture_policy, @@ -538,6 +544,7 @@ async fn exec_windows_sandbox( command, cwd: &cwd, env_map: env, + stdin, timeout_ms, use_private_desktop: windows_sandbox_private_desktop, proxy_enforced, @@ -554,6 +561,7 @@ async fn exec_windows_sandbox( command, &cwd, env, + stdin, timeout_ms, &additional_deny_write_paths, windows_sandbox_private_desktop, @@ -822,6 +830,7 @@ async fn exec( command, cwd, mut env, + stdin, network, arg0, expiration, @@ -852,6 +861,7 @@ async fn exec( network: None, stdio_policy: StdioPolicy::RedirectForShellTool, env, + stdin, }) .await?; if let Some(after_spawn) = after_spawn { diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 334757e53f..921780bf5e 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -265,6 +265,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> { ExecParams { command, cwd: codex_utils_absolute_path::AbsolutePathBuf::current_dir()?, + stdin: None, expiration: 1.into(), capture_policy: ExecCapturePolicy::FullBuffer, env, @@ -305,6 +306,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p "printf hello; sleep 30 &".to_string(), ], cwd: codex_utils_absolute_path::AbsolutePathBuf::current_dir()?, + stdin: None, expiration: 1.into(), capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), @@ -356,6 +358,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result ExecParams { command, cwd: cwd.clone(), + stdin: None, expiration: 1.into(), capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), @@ -926,6 +929,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> let params = ExecParams { command, cwd, + stdin: None, expiration: 500.into(), capture_policy: ExecCapturePolicy::ShellTool, env, @@ -985,6 +989,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { let params = ExecParams { command, cwd: cwd.clone(), + stdin: None, expiration: ExecExpiration::Cancellation(cancel_token), capture_policy: ExecCapturePolicy::ShellTool, env, diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 95f107be9c..d2dfe35303 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -72,6 +72,7 @@ where network, stdio_policy, env, + stdin: None, }) .await } diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 2377dbcaf4..cf06a1b771 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -38,6 +38,7 @@ pub struct ExecRequest { pub command: Vec, pub cwd: AbsolutePathBuf, pub env: HashMap, + pub stdin: Option>, pub network: Option, pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, @@ -57,6 +58,7 @@ impl ExecRequest { command: Vec, cwd: AbsolutePathBuf, env: HashMap, + stdin: Option>, network: Option, expiration: ExecExpiration, capture_policy: ExecCapturePolicy, @@ -72,6 +74,7 @@ impl ExecRequest { command, cwd, env, + stdin, network, expiration, capture_policy, @@ -94,6 +97,7 @@ impl ExecRequest { command, cwd, mut env, + stdin, network, sandbox, windows_sandbox_level, @@ -121,6 +125,7 @@ impl ExecRequest { command, cwd, env, + stdin, network, expiration, capture_policy, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index cc59f65c83..502490849c 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -43,6 +43,7 @@ pub async fn spawn_command_under_seatbelt( network, stdio_policy, env, + stdin: None, }) .await } diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index 575a2f777c..798568271d 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -45,6 +45,7 @@ pub(crate) struct SpawnChildRequest<'a> { pub network: Option<&'a NetworkProxy>, pub stdio_policy: StdioPolicy, pub env: HashMap, + pub stdin: Option>, } pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io::Result { @@ -57,6 +58,7 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io network, stdio_policy, mut env, + stdin, } = request; trace!( @@ -105,11 +107,15 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io match stdio_policy { StdioPolicy::RedirectForShellTool => { - // Do not create a file descriptor for stdin because otherwise some - // commands may hang forever waiting for input. For example, ripgrep has - // a heuristic where it may try to read from stdin as explained here: - // https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103 - cmd.stdin(Stdio::null()); + if stdin.is_some() { + cmd.stdin(Stdio::piped()); + } else { + // Do not create a file descriptor for stdin because otherwise some + // commands may hang forever waiting for input. For example, ripgrep has + // a heuristic where it may try to read from stdin as explained here: + // https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103 + cmd.stdin(Stdio::null()); + } cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); } @@ -121,5 +127,16 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io } } - cmd.kill_on_drop(true).spawn() + let mut child = cmd.kill_on_drop(true).spawn()?; + if let Some(stdin) = stdin { + let Some(mut child_stdin) = child.stdin.take() else { + return Err(std::io::Error::other("child stdin was not piped")); + }; + tokio::spawn(async move { + use tokio::io::AsyncWriteExt as _; + let _ = child_stdin.write_all(&stdin).await; + let _ = child_stdin.shutdown().await; + }); + } + Ok(child) } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index e0a7703879..6fd4516699 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -162,6 +162,7 @@ pub(crate) async fn execute_user_shell_command( command: exec_command.clone(), cwd: cwd.clone(), env: exec_env_map, + stdin: None, network: turn_context.network.clone(), // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we // should use that instead of an "arbitrarily large" timeout here. diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 970afd6f33..bb820f3b3d 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -96,6 +96,7 @@ impl ShellHandler { ExecParams { command: params.command.clone(), cwd: turn_context.resolve_path(params.workdir.clone()), + stdin: None, expiration: params.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), @@ -151,6 +152,7 @@ impl ShellCommandHandler { Ok(ExecParams { command, cwd: turn_context.resolve_path(params.workdir.clone()), + stdin: None, expiration: params.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index f3acf13d53..00982d7d0f 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1053,6 +1053,7 @@ impl JsReplManager { cwd: turn.cwd.clone(), env, additional_permissions: None, + stdin: None, }; let options = ExecOptions { expiration: ExecExpiration::DefaultTimeout, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index b2b7a73f8f..c37d273d28 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -29,6 +29,8 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxCommand; +#[cfg(target_os = "windows")] +use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; @@ -75,6 +77,7 @@ impl ApplyPatchRuntime { Ok(Self::build_sandbox_command_with_program( req, codex_windows_sandbox::resolve_current_exe_for_launch(codex_home, "codex.exe"), + /*pass_patch_via_stdin*/ true, )) } @@ -84,7 +87,9 @@ impl ApplyPatchRuntime { codex_self_exe: Option<&PathBuf>, ) -> Result { let exe = Self::resolve_apply_patch_program(codex_self_exe)?; - Ok(Self::build_sandbox_command_with_program(req, exe)) + Ok(Self::build_sandbox_command_with_program( + req, exe, /*pass_patch_via_stdin*/ true, + )) } #[cfg(not(target_os = "windows"))] @@ -97,17 +102,33 @@ impl ApplyPatchRuntime { .map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}"))) } - fn build_sandbox_command_with_program(req: &ApplyPatchRequest, exe: PathBuf) -> SandboxCommand { + fn build_sandbox_command_with_program( + req: &ApplyPatchRequest, + exe: PathBuf, + pass_patch_via_stdin: bool, + ) -> SandboxCommand { + let (args, stdin) = if pass_patch_via_stdin { + ( + vec![CODEX_CORE_APPLY_PATCH_ARG1.to_string()], + Some(req.action.patch.clone().into_bytes()), + ) + } else { + ( + vec![ + CODEX_CORE_APPLY_PATCH_ARG1.to_string(), + req.action.patch.clone(), + ], + None, + ) + }; SandboxCommand { program: exe.into_os_string(), - args: vec![ - CODEX_CORE_APPLY_PATCH_ARG1.to_string(), - req.action.patch.clone(), - ], - cwd: req.action.cwd.clone(), + args, + cwd: req.action.cwd.to_path_buf(), // Run apply_patch with a minimal environment for determinism and to avoid leaks. env: HashMap::new(), additional_permissions: req.additional_permissions.clone(), + stdin, } } @@ -241,7 +262,17 @@ impl ToolRuntime for ApplyPatchRuntime { } #[cfg(target_os = "windows")] - let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?; + let command = { + let exe = codex_windows_sandbox::resolve_current_exe_for_launch( + &ctx.turn.config.codex_home, + "codex.exe", + ); + Self::build_sandbox_command_with_program( + req, + exe, + matches!(attempt.sandbox, SandboxType::WindowsRestrictedToken), + ) + }; #[cfg(not(target_os = "windows"))] let command = Self::build_sandbox_command(req, ctx.turn.codex_self_exe.as_ref())?; let options = ExecOptions { diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 5ea14b8a8f..ff1844e537 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -35,6 +35,7 @@ pub(crate) fn build_sandbox_command( cwd: cwd.clone(), env: env.clone(), additional_permissions, + stdin: None, }) } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 47d1b62539..7f6ac0d5d9 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -124,6 +124,7 @@ pub(super) async fn try_run_zsh_fork( command, cwd: sandbox_cwd, env: sandbox_env, + stdin: _stdin, network: sandbox_network, expiration: _sandbox_expiration, capture_policy: _capture_policy, @@ -713,6 +714,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { command: self.command.clone(), cwd: self.cwd.clone(), env: exec_env, + stdin: None, network: self.network.clone(), expiration: ExecExpiration::Cancellation(cancel_rx), capture_policy: ExecCapturePolicy::ShellTool, @@ -837,6 +839,7 @@ impl CoreShellCommandExecutor { cwd: workdir.clone(), env, additional_permissions, + stdin: None, }; let options = ExecOptions { expiration: ExecExpiration::DefaultTimeout, diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index 2cc280e1e4..14421cc751 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -64,6 +64,7 @@ fn test_exec_request( command, cwd, env, + None, network, ExecExpiration::DefaultTimeout, ExecCapturePolicy::ShellTool, diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 4ed413735a..dbb4ffa171 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -69,6 +69,7 @@ pub struct SandboxCommand { pub cwd: AbsolutePathBuf, pub env: HashMap, pub additional_permissions: Option, + pub stdin: Option>, } #[derive(Debug)] @@ -76,6 +77,7 @@ pub struct SandboxExecRequest { pub command: Vec, pub cwd: AbsolutePathBuf, pub env: HashMap, + pub stdin: Option>, pub network: Option, pub sandbox: SandboxType, pub windows_sandbox_level: WindowsSandboxLevel, @@ -244,6 +246,7 @@ impl SandboxManager { command: argv, cwd: command.cwd, env: command.env, + stdin: command.stdin, network: network.cloned(), sandbox, windows_sandbox_level, diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 39531f13b8..e86f4ac13f 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -83,6 +83,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() cwd: cwd.clone(), env: HashMap::new(), additional_permissions: None, + stdin: None, }, policy: &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, @@ -135,6 +136,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { write: Some(Vec::new()), }), }), + stdin: None, }, policy: &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, @@ -189,6 +191,7 @@ fn transform_additional_permissions_preserves_denied_entries() { }), ..Default::default() }), + stdin: None, }, policy: &SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, @@ -259,6 +262,7 @@ fn transform_linux_seccomp_request( cwd: cwd.clone(), env: HashMap::new(), additional_permissions: None, + stdin: None, }, policy: &SandboxPolicy::DangerFullAccess, file_system_policy: &FileSystemSandboxPolicy::unrestricted(), diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index 327425bd07..33c9322229 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -9,6 +9,7 @@ pub struct ElevatedSandboxCaptureRequest<'a> { pub command: Vec, pub cwd: &'a Path, pub env_map: HashMap, + pub stdin: Option>, pub timeout_ms: Option, pub use_private_desktop: bool, pub proxy_enforced: bool, @@ -32,6 +33,7 @@ mod windows_impl { use crate::ipc_framed::OutputStream; use crate::ipc_framed::SpawnRequest; use crate::ipc_framed::decode_bytes; + use crate::ipc_framed::encode_bytes; use crate::ipc_framed::read_frame; use crate::ipc_framed::write_frame; use crate::logging::log_failure; @@ -234,6 +236,7 @@ mod windows_impl { command, cwd, mut env_map, + stdin, timeout_ms, use_private_desktop, proxy_enforced, @@ -422,13 +425,26 @@ mod windows_impl { cap_sids, timeout_ms, tty: false, - stdin_open: false, + stdin_open: stdin.is_some(), use_private_desktop, }), }, }; write_frame(&mut pipe_write, &spawn_request)?; read_spawn_ready(&mut pipe_read)?; + if let Some(stdin) = stdin { + write_frame( + &mut pipe_write, + &FramedMessage { + version: 1, + message: Message::Stdin { + payload: crate::ipc_framed::StdinPayload { + data_b64: encode_bytes(&stdin), + }, + }, + }, + )?; + } drop(pipe_write); let mut stdout = Vec::new(); diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index dd8f23d00f..cd54cc6cb2 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -234,6 +234,8 @@ mod windows_impl { use std::collections::HashMap; use std::ffi::c_void; use std::io; + use std::io::Write; + use std::os::windows::io::FromRawHandle; use std::path::Path; use std::path::PathBuf; use std::ptr; @@ -311,6 +313,7 @@ mod windows_impl { command, cwd, env_map, + None, timeout_ms, &[], use_private_desktop, @@ -325,6 +328,7 @@ mod windows_impl { command: Vec, cwd: &Path, mut env_map: HashMap, + stdin: Option>, timeout_ms: Option, additional_deny_write_paths: &[PathBuf], use_private_desktop: bool, @@ -479,11 +483,19 @@ mod windows_impl { unsafe { CloseHandle(in_r); - // Close the parent's stdin write end so the child sees EOF immediately. - CloseHandle(in_w); CloseHandle(out_w); CloseHandle(err_w); } + if let Some(stdin) = stdin { + let mut stdin_write = unsafe { std::fs::File::from_raw_handle(in_w as _) }; + stdin_write.write_all(&stdin)?; + drop(stdin_write); + } else { + // Close the parent's stdin write end so the child sees EOF immediately. + unsafe { + CloseHandle(in_w); + } + } let (tx_out, rx_out) = std::sync::mpsc::channel::>(); let (tx_err, rx_err) = std::sync::mpsc::channel::>(); @@ -694,6 +706,7 @@ mod stub { _command: Vec, _cwd: &Path, _env_map: HashMap, + _stdin: Option>, _timeout_ms: Option, _use_private_desktop: bool, ) -> Result {