mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Make local child process launch settings explicit (#46660)
## Why The native macOS backend cannot inspect every setting or callback on a Tokio command. A shared, constrained launch API makes the supported settings explicit for both backends. ## What changed - Replace `spawn_child` with `codex_utils_pty::Command`, enforcing an explicitly supplied environment, piped stdio, and kill-on-drop behavior. - Add `ProcessMode::Inherit` and `ProcessMode::NewGroup`, honoring the selected mode in native macOS and Tokio spawning. - Share the `Child` wrapper across platforms and migrate local MCP server launching to the new API, retaining its new process group and Windows suspended-spawn support. ## Testing Adapt the existing macOS spawn compatibility and child lifecycle tests to the new API. Add a regression test that checks inherited and new process groups with both backends. GitOrigin-RevId: 3e7a401659e3a8a94a13c4ff58d67bdcd2eea33d
This commit is contained in:
@@ -6,6 +6,7 @@ use std::future::Future;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_utils_pty::Command;
|
||||
use futures::FutureExt;
|
||||
use rmcp::service::RoleClient;
|
||||
use rmcp::service::RxJsonRpcMessage;
|
||||
@@ -15,14 +16,13 @@ use rmcp::transport::async_rw::AsyncRwTransport;
|
||||
use tokio::process::ChildStderr;
|
||||
use tokio::process::ChildStdin;
|
||||
use tokio::process::ChildStdout;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::bounded_stdio_transport::BoundedStdioTransport;
|
||||
use crate::protocol_mode::McpProtocolMode;
|
||||
use codex_utils_pty::Child;
|
||||
|
||||
pub(super) struct LocalStdioTransport {
|
||||
child: Child,
|
||||
child: Box<Child>,
|
||||
transport: StdioTransport,
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl LocalStdioTransport {
|
||||
program_name: String,
|
||||
protocol_mode: McpProtocolMode,
|
||||
) -> io::Result<(Self, Option<ChildStderr>)> {
|
||||
let mut child = codex_utils_pty::spawn_child(command)?;
|
||||
let mut child = command.spawn()?;
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
@@ -55,7 +55,13 @@ impl LocalStdioTransport {
|
||||
StdioTransport::V20260728(BoundedStdioTransport::new(stdin, stdout, program_name))
|
||||
}
|
||||
};
|
||||
Ok((Self { child, transport }, stderr))
|
||||
Ok((
|
||||
Self {
|
||||
child: Box::new(child),
|
||||
transport,
|
||||
},
|
||||
stderr,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn id(&self) -> Option<u32> {
|
||||
|
||||
@@ -38,6 +38,8 @@ use codex_exec_server::ExecProcess;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_pty::Command;
|
||||
use codex_utils_pty::ProcessMode;
|
||||
#[cfg(unix)]
|
||||
use codex_utils_pty::process_group::kill_process_group;
|
||||
#[cfg(unix)]
|
||||
@@ -50,7 +52,6 @@ use rmcp::service::TxJsonRpcMessage;
|
||||
use rmcp::transport::Transport;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::Instant;
|
||||
use tracing::info;
|
||||
@@ -276,13 +277,8 @@ impl LocalStdioServerLauncher {
|
||||
|
||||
let build_command = || {
|
||||
let mut command = Command::new(&resolved_program);
|
||||
command
|
||||
.current_dir(&cwd)
|
||||
.env_clear()
|
||||
.envs(&envs)
|
||||
.args(&args);
|
||||
#[cfg(unix)]
|
||||
command.process_group(0);
|
||||
command.current_dir(&cwd).envs(&envs).args(&args);
|
||||
command.process_mode(ProcessMode::NewGroup);
|
||||
command
|
||||
};
|
||||
#[cfg(windows)]
|
||||
@@ -292,7 +288,7 @@ impl LocalStdioServerLauncher {
|
||||
#[cfg(windows)]
|
||||
let job = match codex_utils_pty::JobObject::create_without_breakaway() {
|
||||
Ok(job) => {
|
||||
job.prepare_suspended_spawn(&mut command);
|
||||
command.prepare_suspended_spawn(&job);
|
||||
Some(job)
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -1,34 +1,59 @@
|
||||
//! Uniform child-process API for local subprocesses, with platform-specific spawning.
|
||||
//! Local child ownership and platform selection, independent of process transports.
|
||||
//!
|
||||
//! Commands must use the launcher's cleared environment, process group, and default
|
||||
//! argv[0]. Both implementations expose Tokio stdio handles and kill on drop.
|
||||
//! Every child exposes Tokio stdio handles. Native children retain their PID until
|
||||
//! reaped, including when a wait is cancelled or the async runtime shuts down.
|
||||
|
||||
use std::io;
|
||||
use std::process::Stdio;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
use tokio::process::Command;
|
||||
use tokio::process::ChildStderr;
|
||||
use tokio::process::ChildStdin;
|
||||
use tokio::process::ChildStdout;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "macos_child.rs"]
|
||||
mod macos;
|
||||
pub(super) mod macos;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::Child;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub use tokio::process::Child;
|
||||
/// A local subprocess with owned stdio and cancellation-safe exit handling.
|
||||
pub struct Child {
|
||||
pub(super) inner: ChildKind,
|
||||
pub stdin: Option<ChildStdin>,
|
||||
pub stdout: Option<ChildStdout>,
|
||||
pub stderr: Option<ChildStderr>,
|
||||
}
|
||||
|
||||
pub fn spawn(mut command: Command) -> io::Result<Child> {
|
||||
command
|
||||
.kill_on_drop(true)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
pub(super) enum ChildKind {
|
||||
Tokio(tokio::process::Child),
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Child::spawn(command)
|
||||
Native(macos::NativeChild),
|
||||
}
|
||||
|
||||
impl Child {
|
||||
pub fn id(&self) -> Option<u32> {
|
||||
match &self.inner {
|
||||
ChildKind::Tokio(child) => child.id(),
|
||||
#[cfg(target_os = "macos")]
|
||||
ChildKind::Native(child) => child.id(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
command.spawn()
|
||||
|
||||
/// Close retained stdin and wait without giving up ownership on cancellation.
|
||||
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
|
||||
self.stdin.take();
|
||||
match &mut self.inner {
|
||||
ChildKind::Tokio(child) => child.wait().await,
|
||||
#[cfg(target_os = "macos")]
|
||||
ChildKind::Native(child) => child.wait().await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill the direct child and reap it. Process-tree policy belongs to the caller.
|
||||
pub async fn kill(&mut self) -> io::Result<()> {
|
||||
self.stdin.take();
|
||||
match &mut self.inner {
|
||||
ChildKind::Tokio(child) => child.kill().await,
|
||||
#[cfg(target_os = "macos")]
|
||||
ChildKind::Native(child) => child.kill().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
codex-rs/utils/pty/src/child_command.rs
Normal file
118
codex-rs/utils/pty/src/child_command.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
//! Explicit local launch settings shared by native and Tokio process creation.
|
||||
//!
|
||||
//! The wrapped Tokio command is private: callers cannot install callbacks or
|
||||
//! change settings that the native backend cannot inspect. Children receive only
|
||||
//! explicitly supplied environment variables, piped stdio, and kill-on-drop.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
|
||||
use crate::child::Child;
|
||||
use crate::child::ChildKind;
|
||||
|
||||
/// Relationship between a child and its parent's process group.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ProcessMode {
|
||||
Inherit,
|
||||
NewGroup,
|
||||
}
|
||||
|
||||
/// A local command whose complete launch contract is known to both backends.
|
||||
pub struct Command {
|
||||
inner: tokio::process::Command,
|
||||
process_mode: ProcessMode,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(program: impl AsRef<OsStr>) -> Self {
|
||||
let mut inner = tokio::process::Command::new(program);
|
||||
inner
|
||||
.env_clear()
|
||||
.kill_on_drop(true)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
Self {
|
||||
inner,
|
||||
process_mode: ProcessMode::Inherit,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
|
||||
self.inner.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> &mut Self {
|
||||
self.inner.args(args);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
|
||||
self.inner.env(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn envs<K, V>(&mut self, env: impl IntoIterator<Item = (K, V)>) -> &mut Self
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>,
|
||||
{
|
||||
self.inner.envs(env);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn current_dir(&mut self, cwd: impl AsRef<Path>) -> &mut Self {
|
||||
self.inner.current_dir(cwd);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn process_mode(&mut self, mode: ProcessMode) -> &mut Self {
|
||||
self.process_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Preserve Job Object assignment before the child begins executing on Windows.
|
||||
#[cfg(windows)]
|
||||
pub fn prepare_suspended_spawn(&mut self, job: &crate::JobObject) {
|
||||
job.prepare_suspended_spawn(&mut self.inner);
|
||||
}
|
||||
|
||||
/// Launch with native macOS path handling and the existing compatibility fallback.
|
||||
pub fn spawn(mut self) -> io::Result<Child> {
|
||||
#[cfg(unix)]
|
||||
if let ProcessMode::NewGroup = self.process_mode {
|
||||
self.inner.process_group(/*pgroup*/ 0);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let command = self.inner.as_std();
|
||||
let program = command.get_program();
|
||||
if Path::new(program).is_relative()
|
||||
&& !program.is_empty()
|
||||
&& let Some((child, stdin, stdout, stderr)) =
|
||||
crate::child::macos::NativeChild::spawn(command, self.process_mode)?
|
||||
{
|
||||
return Ok(Child {
|
||||
inner: ChildKind::Native(child),
|
||||
stdin: Some(stdin),
|
||||
stdout: Some(stdout),
|
||||
stderr: Some(stderr),
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut child = self.inner.spawn()?;
|
||||
Ok(Child {
|
||||
stdin: child.stdin.take(),
|
||||
stdout: child.stdout.take(),
|
||||
stderr: child.stderr.take(),
|
||||
inner: ChildKind::Tokio(child),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
#[path = "macos_child_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,6 +1,8 @@
|
||||
mod child;
|
||||
pub use child::Child;
|
||||
pub use child::spawn as spawn_child;
|
||||
mod child_command;
|
||||
pub use child_command::Command;
|
||||
pub use child_command::ProcessMode;
|
||||
pub mod pipe;
|
||||
mod process;
|
||||
pub mod process_group;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Rust falls back to fork for a historical relative-path/cwd bug in Apple's
|
||||
//! `posix_spawnp`. Calling `posix_spawn` directly avoids that wrapper. This module only
|
||||
//! accepts the launcher's cleared-environment command shape, with piped stdio,
|
||||
//! a new process group, and default `argv[0]`. Bare commands search the child's
|
||||
//! an explicit process-group mode, and default `argv[0]`. Bare commands search the child's
|
||||
//! PATH. Unsuccessful searches and executable files without shebangs retain the
|
||||
//! existing launcher. Each native child owns its PID until it has been reaped.
|
||||
|
||||
@@ -15,81 +15,16 @@ use std::os::fd::FromRawFd;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::Path;
|
||||
use std::process::ExitStatus;
|
||||
use std::ptr;
|
||||
|
||||
use tokio::process::Child as TokioChild;
|
||||
use tokio::process::ChildStderr;
|
||||
use tokio::process::ChildStdin;
|
||||
use tokio::process::ChildStdout;
|
||||
use tokio::process::Command;
|
||||
use tokio::signal::unix::Signal;
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tokio::signal::unix::signal;
|
||||
|
||||
/// Matches Tokio's child API while keeping native spawning private to macOS.
|
||||
pub struct Child {
|
||||
inner: ChildKind,
|
||||
pub stdin: Option<ChildStdin>,
|
||||
pub stdout: Option<ChildStdout>,
|
||||
pub stderr: Option<ChildStderr>,
|
||||
}
|
||||
|
||||
enum ChildKind {
|
||||
Tokio(TokioChild),
|
||||
Native(NativeChild),
|
||||
}
|
||||
|
||||
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> {
|
||||
let program = command.as_std().get_program();
|
||||
if Path::new(program).is_relative()
|
||||
&& !program.is_empty()
|
||||
&& let Some((child, stdin, stdout, stderr)) = NativeChild::spawn(command.as_std())?
|
||||
{
|
||||
return Ok(Self {
|
||||
inner: ChildKind::Native(child),
|
||||
stdin: Some(stdin),
|
||||
stdout: Some(stdout),
|
||||
stderr: Some(stderr),
|
||||
});
|
||||
}
|
||||
let mut child = command.spawn()?;
|
||||
Ok(Self {
|
||||
stdin: child.stdin.take(),
|
||||
stdout: child.stdout.take(),
|
||||
stderr: child.stderr.take(),
|
||||
inner: ChildKind::Tokio(child),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<u32> {
|
||||
match &self.inner {
|
||||
ChildKind::Tokio(child) => child.id(),
|
||||
ChildKind::Native(child) => child.id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
|
||||
self.stdin.take();
|
||||
match &mut self.inner {
|
||||
ChildKind::Tokio(child) => child.wait().await,
|
||||
ChildKind::Native(child) => child.wait().await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn kill(&mut self) -> io::Result<()> {
|
||||
self.stdin.take();
|
||||
match &mut self.inner {
|
||||
ChildKind::Tokio(child) => child.kill().await,
|
||||
ChildKind::Native(child) => child.kill().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// libc does not expose this Apple extension. It is available since macOS 10.15,
|
||||
// before Codex's minimum supported macOS version (12).
|
||||
unsafe extern "C" {
|
||||
@@ -101,7 +36,7 @@ unsafe extern "C" {
|
||||
|
||||
/// Owns a child PID until reaping, so cancellation cannot lose or reuse it.
|
||||
/// Dropping a live child kills it and reaps it independently of the Tokio runtime.
|
||||
struct NativeChild {
|
||||
pub(crate) struct NativeChild {
|
||||
pid: Option<libc::pid_t>,
|
||||
status: Option<ExitStatus>,
|
||||
sigchld: Signal,
|
||||
@@ -111,8 +46,9 @@ impl NativeChild {
|
||||
/// Spawns the MCP command without changing its executable path or `argv[0]`.
|
||||
/// The caller must clear inherited environment variables before setting the
|
||||
/// child's environment, because only explicit command entries are copied.
|
||||
fn spawn(
|
||||
pub(crate) fn spawn(
|
||||
command: &std::process::Command,
|
||||
process_mode: crate::ProcessMode,
|
||||
) -> io::Result<Option<(Self, ChildStdin, ChildStdout, ChildStderr)>> {
|
||||
let program = c_string(command.get_program())?;
|
||||
let search_path = !program.as_bytes().contains(&b'/');
|
||||
@@ -180,10 +116,16 @@ impl NativeChild {
|
||||
target as i32,
|
||||
))?;
|
||||
}
|
||||
cvt(libc::posix_spawnattr_setpgroup(
|
||||
&mut attrs.0,
|
||||
/*pgroup*/ 0,
|
||||
))?;
|
||||
let group_flags = match process_mode {
|
||||
crate::ProcessMode::Inherit => 0,
|
||||
crate::ProcessMode::NewGroup => {
|
||||
cvt(libc::posix_spawnattr_setpgroup(
|
||||
&mut attrs.0,
|
||||
/*pgroup*/ 0,
|
||||
))?;
|
||||
libc::POSIX_SPAWN_SETPGROUP
|
||||
}
|
||||
};
|
||||
let mut defaults = 0;
|
||||
cvt_errno(libc::sigemptyset(&mut defaults))?;
|
||||
cvt_errno(libc::sigaddset(&mut defaults, libc::SIGPIPE))?;
|
||||
@@ -192,7 +134,7 @@ impl NativeChild {
|
||||
// than introducing a different policy with CLOEXEC_DEFAULT.
|
||||
cvt(libc::posix_spawnattr_setflags(
|
||||
&mut attrs.0,
|
||||
(libc::POSIX_SPAWN_SETPGROUP | libc::POSIX_SPAWN_SETSIGDEF) as _,
|
||||
(group_flags | libc::POSIX_SPAWN_SETSIGDEF) as _,
|
||||
))?;
|
||||
let mut spawn = |executable: &CString| {
|
||||
libc::posix_spawn(
|
||||
@@ -255,7 +197,7 @@ impl NativeChild {
|
||||
Ok(Some((child, stdin, stdout, stderr)))
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<u32> {
|
||||
pub(crate) fn id(&self) -> Option<u32> {
|
||||
self.pid.map(|pid| pid as u32)
|
||||
}
|
||||
|
||||
@@ -289,7 +231,7 @@ impl NativeChild {
|
||||
|
||||
/// Waits without transferring child ownership into the future, so callers
|
||||
/// may cancel a wait and then wait again or kill the same child.
|
||||
async fn wait(&mut self) -> io::Result<ExitStatus> {
|
||||
pub(crate) async fn wait(&mut self) -> io::Result<ExitStatus> {
|
||||
loop {
|
||||
match self.try_wait() {
|
||||
Ok(Some(status)) => return Ok(status),
|
||||
@@ -305,7 +247,7 @@ impl NativeChild {
|
||||
}
|
||||
|
||||
/// Sends SIGKILL if still owned, then waits for the child to be reaped.
|
||||
async fn kill(&mut self) -> io::Result<()> {
|
||||
pub(crate) async fn kill(&mut self) -> io::Result<()> {
|
||||
if let Some(pid) = self.pid {
|
||||
// SAFETY: An unreaped child retains its PID, even after it exits.
|
||||
let result = unsafe { libc::kill(pid, libc::SIGKILL) };
|
||||
@@ -409,7 +351,3 @@ impl Drop for Attributes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "macos_child_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
//! Regression coverage for native spawn compatibility, descriptor inheritance, and reaping.
|
||||
|
||||
use super::*;
|
||||
use crate::child::ChildKind;
|
||||
use crate::child::macos::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::ptr;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
async fn native_output(command: Command) -> anyhow::Result<std::process::Output> {
|
||||
let mut child = crate::spawn_child(command)?;
|
||||
let mut child = command.spawn()?;
|
||||
assert!(matches!(child.inner, ChildKind::Native(_)));
|
||||
drop(child.stdin.take());
|
||||
let mut stdout = child.stdout.take().expect("piped stdout");
|
||||
let mut stderr = child.stderr.take().expect("piped stderr");
|
||||
let mut stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("piped stdout"))?;
|
||||
let mut stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("piped stderr"))?;
|
||||
let mut output = Vec::new();
|
||||
let mut diagnostic = Vec::new();
|
||||
let (_, _, status) = tokio::try_join!(
|
||||
@@ -57,11 +71,11 @@ async fn bare_script_search_matches_child_path_and_preserves_script_spelling() -
|
||||
let mut command = Command::new(&program);
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.env_clear()
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.env("PATH", path)
|
||||
.env("MCP_TEST", "kept")
|
||||
.arg("spaces ; literal $arg");
|
||||
let expected = command.output().await?;
|
||||
let expected = command.inner.output().await?;
|
||||
assert_eq!(native_output(command).await?, expected);
|
||||
}
|
||||
Ok(())
|
||||
@@ -70,8 +84,10 @@ async fn bare_script_search_matches_child_path_and_preserves_script_spelling() -
|
||||
#[tokio::test]
|
||||
async fn bare_executable_uses_default_path_and_preserves_argv0() -> anyhow::Result<()> {
|
||||
let mut command = Command::new("sh");
|
||||
command.env_clear().args(["-c", "printf '%s' \"$0\""]);
|
||||
let expected = command.output().await?;
|
||||
command
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.args(["-c", "printf '%s' \"$0\""]);
|
||||
let expected = command.inner.output().await?;
|
||||
assert_eq!(native_output(command).await?, expected);
|
||||
Ok(())
|
||||
}
|
||||
@@ -88,15 +104,15 @@ async fn relative_script_preserves_paths_stdio_environment_and_process_group() -
|
||||
"#!/bin/sh\nread -r input\nprintf '%s\\n' \"$0\" \"$1\" \"$2\" \"$MCP_TEST\" \"$input\"\nprintf diagnostic >&2\nexit 23\n",
|
||||
)?;
|
||||
fs::set_permissions(script, fs::Permissions::from_mode(0o755))?;
|
||||
let mut command = std::process::Command::new("./link/../server");
|
||||
let mut command = Command::new("./link/../server");
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.env_clear()
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.env("MCP_TEST", "kept")
|
||||
.arg("spaces ; literal $arg")
|
||||
.arg(std::ffi::OsString::from_vec(b"raw-\xff".to_vec()));
|
||||
let (mut child, mut stdin, mut stdout, mut stderr) =
|
||||
NativeChild::spawn(&command)?.expect("native child");
|
||||
NativeChild::spawn(command.inner.as_std(), command.process_mode)?.expect("native child");
|
||||
let pid = child.id().expect("live PID") as libc::pid_t;
|
||||
// SAFETY: getpgid only inspects the live child, which waits for input below.
|
||||
assert_eq!(unsafe { libc::getpgid(pid) }, pid);
|
||||
@@ -127,13 +143,13 @@ async fn native_executable_preserves_argv0() -> anyhow::Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
symlink("/bin/sh", root.path().join("shell"))?;
|
||||
let program = Path::new("./shell");
|
||||
let mut command = std::process::Command::new(program);
|
||||
let mut command = Command::new(program);
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.env_clear()
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.args(["-c", "printf '%s' \"$0\""]);
|
||||
let (mut child, stdin, mut stdout, _stderr) =
|
||||
NativeChild::spawn(&command)?.expect("native child");
|
||||
NativeChild::spawn(command.inner.as_std(), command.process_mode)?.expect("native child");
|
||||
drop(stdin);
|
||||
let mut output = Vec::new();
|
||||
stdout.read_to_end(&mut output).await?;
|
||||
@@ -146,10 +162,12 @@ async fn native_executable_preserves_argv0() -> anyhow::Result<()> {
|
||||
async fn cancelled_wait_can_still_kill_and_reap_child() -> anyhow::Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
symlink("/bin/cat", root.path().join("server"))?;
|
||||
let mut command = std::process::Command::new("./server");
|
||||
command.current_dir(root.path()).env_clear();
|
||||
let mut command = Command::new("./server");
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.process_mode(ProcessMode::NewGroup);
|
||||
let (mut child, _stdin, _stdout, _stderr) =
|
||||
NativeChild::spawn(&command)?.expect("native child");
|
||||
NativeChild::spawn(command.inner.as_std(), command.process_mode)?.expect("native child");
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(20), child.wait())
|
||||
.await
|
||||
@@ -172,26 +190,30 @@ async fn descriptor_inheritance_matches_command() -> anyhow::Result<()> {
|
||||
] {
|
||||
// SAFETY: Duplicate a harmless descriptor with the requested inheritance flag.
|
||||
let fd = unsafe { libc::fcntl(file.as_raw_fd(), operation, 200) };
|
||||
cvt_errno(fd)?;
|
||||
assert!(fd >= 0);
|
||||
// SAFETY: fcntl returned a new owned descriptor.
|
||||
let _sentinel = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
let mut command = std::process::Command::new("./shell");
|
||||
let mut command = Command::new("./shell");
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.env_clear()
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.env("SENTINEL", fd.to_string())
|
||||
.args([
|
||||
"-c",
|
||||
"if [ -e /dev/fd/\"$SENTINEL\" ]; then printf inherited; else printf closed; fi",
|
||||
]);
|
||||
let (mut child, stdin, mut stdout, _stderr) =
|
||||
NativeChild::spawn(&command)?.expect("native child");
|
||||
NativeChild::spawn(command.inner.as_std(), command.process_mode)?
|
||||
.expect("native child");
|
||||
drop(stdin);
|
||||
let mut output = String::new();
|
||||
stdout.read_to_string(&mut output).await?;
|
||||
assert!(child.wait().await?.success());
|
||||
assert_eq!(output, expected);
|
||||
assert_eq!(command.output()?.stdout, output.as_bytes());
|
||||
assert_eq!(
|
||||
command.inner.as_std_mut().output()?.stdout,
|
||||
output.as_bytes()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -210,12 +232,10 @@ async fn launch_failures_preserve_os_errors() -> anyhow::Result<()> {
|
||||
] {
|
||||
let mut command = Command::new(program);
|
||||
command
|
||||
.env_clear()
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.env("PATH", root.path())
|
||||
.current_dir(cwd);
|
||||
let error = crate::spawn_child(command)
|
||||
.err()
|
||||
.expect("spawn should fail");
|
||||
let error = command.spawn().err().expect("spawn should fail");
|
||||
assert_eq!(error.raw_os_error(), Some(errno));
|
||||
}
|
||||
Ok(())
|
||||
@@ -231,9 +251,9 @@ async fn executable_text_without_shebang_retains_command_fallback() -> anyhow::R
|
||||
let mut command = Command::new(program);
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.env_clear()
|
||||
.process_mode(ProcessMode::NewGroup)
|
||||
.env("PATH", ".");
|
||||
let mut child = crate::spawn_child(command)?;
|
||||
let mut child = command.spawn()?;
|
||||
let mut output = Vec::new();
|
||||
child
|
||||
.stdout
|
||||
@@ -256,9 +276,11 @@ fn dropping_after_runtime_shutdown_kills_and_reaps_child() -> anyhow::Result<()>
|
||||
.build()?;
|
||||
let (child, stdin, _stdout, _stderr) = runtime
|
||||
.block_on(async {
|
||||
let mut command = std::process::Command::new("./server");
|
||||
command.current_dir(root.path()).env_clear();
|
||||
NativeChild::spawn(&command)
|
||||
let mut command = Command::new("./server");
|
||||
command
|
||||
.current_dir(root.path())
|
||||
.process_mode(ProcessMode::NewGroup);
|
||||
NativeChild::spawn(command.inner.as_std(), command.process_mode)
|
||||
})?
|
||||
.expect("native child");
|
||||
let pid = child.id().expect("live PID") as libc::pid_t;
|
||||
@@ -286,3 +308,26 @@ fn dropping_after_runtime_shutdown_kills_and_reaps_child() -> anyhow::Result<()>
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_mode_is_preserved_by_both_backends() -> anyhow::Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
symlink("/bin/cat", root.path().join("server"))?;
|
||||
for program in ["./server", "/bin/cat"] {
|
||||
for mode in [ProcessMode::Inherit, ProcessMode::NewGroup] {
|
||||
let mut command = Command::new(program);
|
||||
command.current_dir(root.path()).process_mode(mode);
|
||||
let mut child = command.spawn()?;
|
||||
let pid = child.id().expect("live PID") as libc::pid_t;
|
||||
let expected = match mode {
|
||||
// SAFETY: getpgrp only inspects the current process.
|
||||
ProcessMode::Inherit => unsafe { libc::getpgrp() },
|
||||
ProcessMode::NewGroup => pid,
|
||||
};
|
||||
// SAFETY: The child is still owned and blocked on its stdin pipe.
|
||||
assert_eq!(unsafe { libc::getpgid(pid) }, expected);
|
||||
child.kill().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user