Move local child-process spawning into codex-utils-pty (#46659)

## What changed

Expose the shared `Child` type and `spawn_child` function from `codex-utils-pty`, and update the MCP stdio transport to use them. Move the existing macOS spawning implementation and its tests into the utility crate, preserving platform-specific spawning, piped stdio, and kill-on-drop behavior.

GitOrigin-RevId: 26c2fb2f22cdba907aa9d18cab7dc8c4ac1c909e
This commit is contained in:
Charlie Marsh
2026-09-19 15:00:41 +00:00
committed by copyberry
parent 78245b47af
commit d086e2752d
8 changed files with 29 additions and 26 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -5115,6 +5115,7 @@ dependencies = [
"portable-pty",
"pretty_assertions",
"shared_library",
"tempfile",
"tokio",
"winapi",
]

View File

@@ -12,7 +12,6 @@ mod http_client_adapter;
mod http_client_redirect;
mod http_headers;
mod in_process_transport;
mod local_child;
mod local_stdio_transport;
mod logging_client_handler;
mod oauth;

View File

@@ -18,12 +18,11 @@ use tokio::process::ChildStdout;
use tokio::process::Command;
use crate::bounded_stdio_transport::BoundedStdioTransport;
use crate::local_child;
use crate::local_child::LocalChild;
use crate::protocol_mode::McpProtocolMode;
use codex_utils_pty::Child;
pub(super) struct LocalStdioTransport {
child: LocalChild,
child: Child,
transport: StdioTransport,
}
@@ -40,7 +39,7 @@ impl LocalStdioTransport {
program_name: String,
protocol_mode: McpProtocolMode,
) -> io::Result<(Self, Option<ChildStderr>)> {
let mut child = local_child::spawn(command)?;
let mut child = codex_utils_pty::spawn_child(command)?;
let stdin = child
.stdin
.take()

View File

@@ -10,9 +10,10 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
portable-pty = { workspace = true }
tokio = { workspace = true, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync", "time"] }
tokio = { workspace = true, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] }
[dev-dependencies]
tempfile = { workspace = true }
pretty_assertions = { workspace = true }
[target.'cfg(windows)'.dependencies]

View File

@@ -1,4 +1,4 @@
//! Uniform child-process API for local MCP servers, with platform-specific spawning.
//! Uniform child-process API for local subprocesses, with platform-specific spawning.
//!
//! Commands must use the launcher's cleared environment, process group, and default
//! argv[0]. Both implementations expose Tokio stdio handles and kill on drop.
@@ -9,15 +9,15 @@ use std::process::Stdio;
use tokio::process::Command;
#[cfg(target_os = "macos")]
#[path = "macos_stdio.rs"]
#[path = "macos_child.rs"]
mod macos;
#[cfg(target_os = "macos")]
pub(super) use macos::LocalChild;
pub use macos::Child;
#[cfg(not(target_os = "macos"))]
pub(super) use tokio::process::Child as LocalChild;
pub use tokio::process::Child;
pub(super) fn spawn(mut command: Command) -> io::Result<LocalChild> {
pub fn spawn(mut command: Command) -> io::Result<Child> {
command
.kill_on_drop(true)
.stdin(Stdio::piped())
@@ -25,7 +25,7 @@ pub(super) fn spawn(mut command: Command) -> io::Result<LocalChild> {
.stderr(Stdio::piped());
#[cfg(target_os = "macos")]
{
LocalChild::spawn(command)
Child::spawn(command)
}
#[cfg(not(target_os = "macos"))]
{

View File

@@ -1,3 +1,6 @@
mod child;
pub use child::Child;
pub use child::spawn as spawn_child;
pub mod pipe;
mod process;
pub mod process_group;

View File

@@ -19,7 +19,7 @@ use std::path::Path;
use std::process::ExitStatus;
use std::ptr;
use tokio::process::Child;
use tokio::process::Child as TokioChild;
use tokio::process::ChildStderr;
use tokio::process::ChildStdin;
use tokio::process::ChildStdout;
@@ -29,19 +29,19 @@ use tokio::signal::unix::SignalKind;
use tokio::signal::unix::signal;
/// Matches Tokio's child API while keeping native spawning private to macOS.
pub(crate) struct LocalChild {
pub struct Child {
inner: ChildKind,
pub(crate) stdin: Option<ChildStdin>,
pub(crate) stdout: Option<ChildStdout>,
pub(crate) stderr: Option<ChildStderr>,
pub stdin: Option<ChildStdin>,
pub stdout: Option<ChildStdout>,
pub stderr: Option<ChildStderr>,
}
enum ChildKind {
Tokio(Child),
Tokio(TokioChild),
Native(NativeChild),
}
impl LocalChild {
impl Child {
/// Uses native spawning for relative paths and bare names, retaining Tokio's
/// fallback for unsuccessful PATH searches and executable text without a shebang.
pub(super) fn spawn(mut command: Command) -> io::Result<Self> {
@@ -66,14 +66,14 @@ impl LocalChild {
})
}
pub(crate) fn id(&self) -> Option<u32> {
pub fn id(&self) -> Option<u32> {
match &self.inner {
ChildKind::Tokio(child) => child.id(),
ChildKind::Native(child) => child.id(),
}
}
pub(crate) async fn wait(&mut self) -> io::Result<ExitStatus> {
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
self.stdin.take();
match &mut self.inner {
ChildKind::Tokio(child) => child.wait().await,
@@ -81,7 +81,7 @@ impl LocalChild {
}
}
pub(crate) async fn kill(&mut self) -> io::Result<()> {
pub async fn kill(&mut self) -> io::Result<()> {
self.stdin.take();
match &mut self.inner {
ChildKind::Tokio(child) => child.kill().await,
@@ -411,5 +411,5 @@ impl Drop for Attributes {
}
#[cfg(test)]
#[path = "macos_stdio_tests.rs"]
#[path = "macos_child_tests.rs"]
mod tests;

View File

@@ -11,7 +11,7 @@ use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
async fn native_output(command: Command) -> anyhow::Result<std::process::Output> {
let mut child = crate::local_child::spawn(command)?;
let mut child = crate::spawn_child(command)?;
assert!(matches!(child.inner, ChildKind::Native(_)));
drop(child.stdin.take());
let mut stdout = child.stdout.take().expect("piped stdout");
@@ -213,7 +213,7 @@ async fn launch_failures_preserve_os_errors() -> anyhow::Result<()> {
.env_clear()
.env("PATH", root.path())
.current_dir(cwd);
let error = crate::local_child::spawn(command)
let error = crate::spawn_child(command)
.err()
.expect("spawn should fail");
assert_eq!(error.raw_os_error(), Some(errno));
@@ -233,7 +233,7 @@ async fn executable_text_without_shebang_retains_command_fallback() -> anyhow::R
.current_dir(root.path())
.env_clear()
.env("PATH", ".");
let mut child = crate::local_child::spawn(command)?;
let mut child = crate::spawn_child(command)?;
let mut output = Vec::new();
child
.stdout