mirror of
https://github.com/openai/codex.git
synced 2026-05-22 20:14:17 +00:00
Compare commits
4 Commits
fcoury/usa
...
codex/dont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
724faf95da | ||
|
|
b7c1f602b0 | ||
|
|
8618410ae2 | ||
|
|
a4abf76e8d |
1
.bazelrc
1
.bazelrc
@@ -95,6 +95,7 @@ build:clippy --@rules_rust//rust/settings:clippy.toml=//codex-rs:clippy.toml
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=-Dwarnings
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::await_holding_invalid_type
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::await_holding_lock
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::disallowed_methods
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::expect_used
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::identity_op
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_clamp
|
||||
|
||||
20
codex-rs/Cargo.lock
generated
20
codex-rs/Cargo.lock
generated
@@ -2041,6 +2041,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-core",
|
||||
"codex-managed-process",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-utils-cli",
|
||||
@@ -2238,6 +2239,7 @@ dependencies = [
|
||||
"codex-features",
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
"codex-managed-process",
|
||||
"codex-mcp",
|
||||
"codex-mcp-server",
|
||||
"codex-memories-write",
|
||||
@@ -2508,6 +2510,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-hooks",
|
||||
"codex-login",
|
||||
"codex-managed-process",
|
||||
"codex-mcp",
|
||||
"codex-memories-read",
|
||||
"codex-model-provider",
|
||||
@@ -2627,6 +2630,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-hooks",
|
||||
"codex-login",
|
||||
"codex-managed-process",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
@@ -2690,6 +2694,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-managed-process",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2755,6 +2760,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-file-system",
|
||||
"codex-managed-process",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-test-binary-support",
|
||||
@@ -3101,6 +3107,15 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-managed-process"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp"
|
||||
version = "0.0.0"
|
||||
@@ -3589,6 +3604,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"codex-managed-process",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -3631,6 +3647,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"codex-managed-process",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"once_cell",
|
||||
@@ -3703,6 +3720,7 @@ name = "codex-stdio-to-uds"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-managed-process",
|
||||
"codex-uds",
|
||||
"codex-utils-cargo-bin",
|
||||
"pretty_assertions",
|
||||
@@ -3807,6 +3825,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
"codex-managed-process",
|
||||
"codex-mcp",
|
||||
"codex-message-history",
|
||||
"codex-model-provider",
|
||||
@@ -4083,6 +4102,7 @@ dependencies = [
|
||||
name = "codex-utils-sleep-inhibitor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-managed-process",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
"tracing",
|
||||
|
||||
@@ -65,6 +65,7 @@ members = [
|
||||
"models-manager",
|
||||
"network-proxy",
|
||||
"ollama",
|
||||
"managed-process",
|
||||
"process-hardening",
|
||||
"protocol",
|
||||
"realtime-webrtc",
|
||||
@@ -192,6 +193,7 @@ codex-ollama = { path = "ollama" }
|
||||
codex-otel = { path = "otel" }
|
||||
codex-plugin = { path = "plugin" }
|
||||
codex-model-provider = { path = "model-provider" }
|
||||
codex-managed-process = { path = "managed-process" }
|
||||
codex-process-hardening = { path = "process-hardening" }
|
||||
codex-protocol = { path = "protocol" }
|
||||
codex-realtime-webrtc = { path = "realtime-webrtc" }
|
||||
@@ -433,6 +435,7 @@ rust = {}
|
||||
[workspace.lints.clippy]
|
||||
await_holding_invalid_type = "deny"
|
||||
await_holding_lock = "deny"
|
||||
disallowed_methods = "deny"
|
||||
expect_used = "deny"
|
||||
identity_op = "deny"
|
||||
manual_clamp = "deny"
|
||||
|
||||
@@ -177,6 +177,7 @@ impl PidBackend {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let child = match command.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
|
||||
@@ -164,6 +164,7 @@ async fn install_latest_standalone() -> Result<()> {
|
||||
.await
|
||||
.context("failed to read standalone Codex updater")?;
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = Command::new("/bin/sh")
|
||||
.arg("-s")
|
||||
.stdin(Stdio::piped())
|
||||
|
||||
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::ChildStdin;
|
||||
use std::process::ChildStdout;
|
||||
use std::process::Command;
|
||||
@@ -68,6 +67,8 @@ use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::config::Config;
|
||||
use codex_managed_process::CommandExt;
|
||||
use codex_managed_process::ManagedChild;
|
||||
use codex_otel::OtelProvider;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -431,7 +432,7 @@ enum Endpoint {
|
||||
}
|
||||
|
||||
struct BackgroundAppServer {
|
||||
process: Child,
|
||||
process: ManagedChild,
|
||||
url: String,
|
||||
}
|
||||
|
||||
@@ -489,7 +490,7 @@ impl BackgroundAppServer {
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.with_context(|| format!("failed to start `{}` app-server", codex_bin.display()))?;
|
||||
|
||||
Ok(Self { process, url })
|
||||
@@ -535,6 +536,10 @@ fn serve(codex_bin: &Path, config_overrides: &[String], listen: &str, kill: bool
|
||||
}
|
||||
cmdline.push_str(&format!(" app-server --listen {}", shell_quote(listen)));
|
||||
|
||||
#[allow(
|
||||
clippy::disallowed_methods,
|
||||
reason = "ManagedChild would terminate this intentionally detached app-server."
|
||||
)]
|
||||
let child = Command::new("nohup")
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
@@ -1377,7 +1382,7 @@ fn parse_dynamic_tools_arg(dynamic_tools: &Option<String>) -> Result<Option<Vec<
|
||||
|
||||
enum ClientTransport {
|
||||
Stdio {
|
||||
child: Child,
|
||||
child: ManagedChild,
|
||||
stdin: Option<ChildStdin>,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
},
|
||||
@@ -1449,7 +1454,7 @@ impl CodexClient {
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.with_context(|| format!("failed to start `{codex_bin_display}` app-server"))?;
|
||||
|
||||
let stdin = codex_app_server
|
||||
|
||||
@@ -206,6 +206,7 @@ impl McpProcess {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut process = cmd
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
|
||||
@@ -396,6 +396,7 @@ pub(super) async fn spawn_websocket_server_with_args(
|
||||
.stderr(Stdio::piped())
|
||||
.env("CODEX_HOME", codex_home)
|
||||
.env("RUST_LOG", "warn");
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut process = cmd
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
|
||||
@@ -85,6 +85,7 @@ codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-
|
||||
[dev-dependencies]
|
||||
assert_cmd = { workspace = true }
|
||||
assert_matches = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
predicates = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -491,6 +491,7 @@ async fn spawn_debug_sandbox_child(
|
||||
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
cmd.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
|
||||
@@ -277,6 +277,7 @@ fn track_descendants(kq: libc::c_int, root_pid: i32) -> HashSet<i32> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_managed_process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
@@ -293,7 +294,7 @@ mod tests {
|
||||
let mut child = Command::new("/bin/sleep")
|
||||
.arg("5")
|
||||
.stdin(Stdio::null())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.expect("failed to spawn child process");
|
||||
|
||||
let child_pid = child.id() as i32;
|
||||
@@ -322,7 +323,7 @@ mod tests {
|
||||
let mut child = Command::new("/bin/sleep")
|
||||
.arg("0.1")
|
||||
.stdin(Stdio::null())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.expect("failed to spawn child process");
|
||||
|
||||
let child_pid = child.id() as i32;
|
||||
@@ -353,7 +354,7 @@ mod tests {
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.expect("failed to spawn bash");
|
||||
|
||||
let output = child.wait_with_output().unwrap().stdout;
|
||||
|
||||
@@ -89,6 +89,7 @@ fn start_log_stream() -> Option<Child> {
|
||||
|
||||
const PREDICATE: &str = r#"(((processID == 0) AND (senderImagePath CONTAINS "/Sandbox")) OR (subsystem == "com.apple.sandbox.reporting"))"#;
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
tokio::process::Command::new("log")
|
||||
.args(["stream", "--style", "ndjson", "--predicate", PREDICATE])
|
||||
.stdin(Stdio::null())
|
||||
|
||||
@@ -5,14 +5,40 @@ await-holding-invalid-types = [
|
||||
"tokio::sync::RwLockReadGuard",
|
||||
"tokio::sync::RwLockWriteGuard",
|
||||
]
|
||||
disallowed-methods = [
|
||||
{ path = "ratatui::style::Color::Rgb", reason = "Use ANSI colors, which work better in various terminal themes." },
|
||||
{ path = "ratatui::style::Color::Indexed", reason = "Use ANSI colors, which work better in various terminal themes." },
|
||||
{ path = "ratatui::style::Stylize::white", reason = "Avoid hardcoding white; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." },
|
||||
{ path = "ratatui::style::Stylize::black", reason = "Avoid hardcoding black; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." },
|
||||
{ path = "ratatui::style::Stylize::yellow", reason = "Avoid yellow; prefer other colors in `tui/styles.md`." },
|
||||
]
|
||||
|
||||
# Increase the size threshold for result_large_err to accommodate
|
||||
# richer error variants.
|
||||
large-error-threshold = 256
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "std::process::Command::spawn"
|
||||
reason = "Don't leak processes on Drop."
|
||||
replacement = "codex_managed_process::CommandExt::spawn_managed"
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "tokio::process::Command::spawn"
|
||||
reason = "Don't leak processes on Drop."
|
||||
replacement = "codex_managed_process::TokioCommandExt::spawn_managed"
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "portable_pty::SlavePty::spawn_command"
|
||||
reason = "Don't leak processes on Drop."
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "ratatui::style::Color::Rgb"
|
||||
reason = "Use ANSI colors, which work better in various terminal themes."
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "ratatui::style::Color::Indexed"
|
||||
reason = "Use ANSI colors, which work better in various terminal themes."
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "ratatui::style::Stylize::white"
|
||||
reason = "Avoid hardcoding white; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background."
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "ratatui::style::Stylize::black"
|
||||
reason = "Avoid hardcoding black; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background."
|
||||
|
||||
[[disallowed-methods]]
|
||||
path = "ratatui::style::Stylize::yellow"
|
||||
reason = "Avoid yellow; prefer other colors in `tui/styles.md`."
|
||||
|
||||
@@ -22,6 +22,7 @@ codex-exec-server = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::process::Output;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_managed_process::CommandExt;
|
||||
pub(super) fn git_remote_revision(
|
||||
source: &str,
|
||||
ref_name: Option<&str>,
|
||||
@@ -172,7 +173,7 @@ fn run_git_command_with_timeout(
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.map_err(|err| format!("failed to run {context}: {err}"))?;
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::process::Output;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_managed_process::CommandExt;
|
||||
use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC;
|
||||
use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_METRIC;
|
||||
use reqwest::Client;
|
||||
@@ -527,7 +528,7 @@ fn run_git_command_with_timeout(
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.map_err(|err| format!("failed to run {context}: {err}"))?;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
@@ -131,6 +131,7 @@ codex-shell-escalation = { workspace = true }
|
||||
[dev-dependencies]
|
||||
assert_cmd = { workspace = true }
|
||||
assert_matches = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-test-binary-support = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
|
||||
@@ -122,5 +122,6 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
cmd.kill_on_drop(true).spawn()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! `cargo test --test live_cli -- --ignored` provided they set a valid `OPENAI_API_KEY`.
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use codex_managed_process::CommandExt;
|
||||
use predicates::prelude::*;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
@@ -60,7 +61,7 @@ fn run_live(prompt: &str) -> (assert_cmd::assert::Assert, TempDir) {
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd.spawn().expect("failed to spawn codex-rs");
|
||||
let mut child = cmd.spawn_managed().expect("failed to spawn codex-rs");
|
||||
|
||||
// Send the terminating newline so Session::run exits after the first turn.
|
||||
child
|
||||
|
||||
@@ -2099,6 +2099,7 @@ async fn start_streamable_http_test_server(
|
||||
if let Some(expected_token) = expected_token {
|
||||
command.env("MCP_EXPECT_BEARER", expected_token);
|
||||
}
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
wait_for_local_streamable_http_server(&mut child, &server_url, Duration::from_secs(5)).await?;
|
||||
|
||||
@@ -11,6 +11,7 @@ workspace = true
|
||||
anyhow.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-app-server-protocol.workspace = true
|
||||
codex-managed-process = { workspace = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::process::Child;
|
||||
use std::process::ChildStdin;
|
||||
use std::process::ChildStdout;
|
||||
use std::process::Command;
|
||||
@@ -33,6 +32,8 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_managed_process::CommandExt;
|
||||
use codex_managed_process::ManagedChild;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::Output;
|
||||
@@ -42,7 +43,7 @@ use crate::state::ReaderEvent;
|
||||
use crate::state::State;
|
||||
|
||||
pub struct AppServerClient {
|
||||
child: Child,
|
||||
child: ManagedChild,
|
||||
stdin: Arc<Mutex<Option<ChildStdin>>>,
|
||||
stdout: Option<BufReader<ChildStdout>>,
|
||||
next_request_id: AtomicI64,
|
||||
@@ -68,7 +69,7 @@ impl AppServerClient {
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.with_context(|| format!("failed to start `{codex_bin}` app-server"))?;
|
||||
|
||||
let stdin = child
|
||||
|
||||
@@ -20,6 +20,7 @@ codex-app-server-protocol = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-file-system = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
@@ -82,6 +82,7 @@ impl ExecServerClient {
|
||||
pub(crate) async fn connect_stdio_command(
|
||||
args: StdioExecServerConnectArgs,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = stdio_command_process(&args.command)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_managed_process::ManagedTokioChild;
|
||||
use codex_managed_process::TokioCommandExt;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
@@ -17,6 +19,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::FileSystemSandboxContext;
|
||||
@@ -249,12 +252,26 @@ async fn run_command(
|
||||
request_json: Vec<u8>,
|
||||
) -> Result<FsHelperPayload, JSONRPCErrorError> {
|
||||
let mut child = spawn_command(command)?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| internal_error("failed to open fs sandbox helper stdin".to_string()))?;
|
||||
stdin.write_all(&request_json).await.map_err(io_error)?;
|
||||
stdin.shutdown().await.map_err(io_error)?;
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
if let Err(err) = child.kill_and_wait().await {
|
||||
warn!("failed to kill fs sandbox helper after missing stdin: {err}");
|
||||
}
|
||||
return Err(internal_error(
|
||||
"failed to open fs sandbox helper stdin".to_string(),
|
||||
));
|
||||
};
|
||||
if let Err(err) = stdin.write_all(&request_json).await {
|
||||
if let Err(kill_err) = child.kill_and_wait().await {
|
||||
warn!("failed to kill fs sandbox helper after stdin write failed: {kill_err}");
|
||||
}
|
||||
return Err(io_error(err));
|
||||
}
|
||||
if let Err(err) = stdin.shutdown().await {
|
||||
if let Err(kill_err) = child.kill_and_wait().await {
|
||||
warn!("failed to kill fs sandbox helper after stdin shutdown failed: {kill_err}");
|
||||
}
|
||||
return Err(io_error(err));
|
||||
}
|
||||
drop(stdin);
|
||||
|
||||
let output = child.wait_with_output().await.map_err(io_error)?;
|
||||
@@ -280,7 +297,7 @@ fn spawn_command(
|
||||
arg0,
|
||||
..
|
||||
}: SandboxExecRequest,
|
||||
) -> Result<tokio::process::Child, JSONRPCErrorError> {
|
||||
) -> Result<ManagedTokioChild, JSONRPCErrorError> {
|
||||
let Some((program, args)) = argv.split_first() else {
|
||||
return Err(invalid_request("fs sandbox command was empty".to_string()));
|
||||
};
|
||||
@@ -298,7 +315,7 @@ fn spawn_command(
|
||||
command.stdin(std::process::Stdio::piped());
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
command.stderr(std::process::Stdio::piped());
|
||||
command.spawn().map_err(io_error)
|
||||
command.spawn_managed().map_err(io_error)
|
||||
}
|
||||
|
||||
fn io_error(err: std::io::Error) -> JSONRPCErrorError {
|
||||
|
||||
@@ -76,6 +76,7 @@ where
|
||||
child.kill_on_drop(true);
|
||||
child.env("CODEX_HOME", codex_home.path());
|
||||
child.envs(env);
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = child.spawn()?;
|
||||
|
||||
let websocket_url = read_listen_url_from_stdout(&mut child).await?;
|
||||
|
||||
@@ -89,6 +89,7 @@ fn run_delayed_output_after_exit_parent(release_path: &Path) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
match Command::new(current_exe)
|
||||
.arg(DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG)
|
||||
.arg(release_path)
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn spawn_command_under_sandbox(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
child.kill_on_drop(true).spawn()
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(crate) async fn run_command(
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = match command.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
|
||||
@@ -60,6 +60,7 @@ pub fn notify_hook(argv: Vec<String>) -> Hook {
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
match command.spawn() {
|
||||
Ok(_) => HookResult::Success,
|
||||
Err(err) => HookResult::FailedContinue(err.into()),
|
||||
|
||||
6
codex-rs/managed-process/BUILD.bazel
Normal file
6
codex-rs/managed-process/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "managed-process",
|
||||
crate_name = "codex_managed_process",
|
||||
)
|
||||
19
codex-rs/managed-process/Cargo.toml
Normal file
19
codex-rs/managed-process/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "codex-managed-process"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["process"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
libc = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
74
codex-rs/managed-process/src/drop_bomb.rs
Normal file
74
codex-rs/managed-process/src/drop_bomb.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::ffi::OsString;
|
||||
use std::panic::Location;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DebugDropBomb {
|
||||
armed: bool,
|
||||
program: OsString,
|
||||
spawn_location: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl DebugDropBomb {
|
||||
pub(crate) fn new(program: OsString, spawn_location: &'static Location<'static>) -> Self {
|
||||
Self {
|
||||
armed: true,
|
||||
program,
|
||||
spawn_location,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn defuse(&mut self) {
|
||||
self.armed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DebugDropBomb {
|
||||
fn drop(&mut self) {
|
||||
if !self.armed {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
panic!(
|
||||
"managed Tokio child for {:?} spawned at {} dropped without explicit teardown",
|
||||
self.program, self.spawn_location
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
tracing::error!(
|
||||
program = ?self.program,
|
||||
spawn_location = %self.spawn_location,
|
||||
"managed Tokio child dropped without explicit teardown"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defused_bomb_drops() {
|
||||
let mut bomb = DebugDropBomb::new("test".into(), Location::caller());
|
||||
bomb.defuse();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
#[should_panic(expected = "dropped without explicit teardown")]
|
||||
fn armed_bomb_panics_in_debug() {
|
||||
drop(DebugDropBomb::new("test".into(), Location::caller()));
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[test]
|
||||
fn armed_bomb_does_not_panic_in_release() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
drop(DebugDropBomb::new("test".into(), Location::caller()));
|
||||
});
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
8
codex-rs/managed-process/src/lib.rs
Normal file
8
codex-rs/managed-process/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Child process helpers that keep process lifetime ownership explicit.
|
||||
|
||||
pub(crate) mod drop_bomb;
|
||||
mod sync;
|
||||
mod tokio;
|
||||
|
||||
pub use sync::*;
|
||||
pub use tokio::*;
|
||||
203
codex-rs/managed-process/src/sync.rs
Normal file
203
codex-rs/managed-process/src/sync.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::panic::Location;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Output;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use tracing::trace;
|
||||
use tracing::warn;
|
||||
|
||||
const DROP_WAIT_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
const DROP_WAIT_POLL_INTERVAL: Duration = Duration::from_millis(10);
|
||||
|
||||
/// Extends [`Command`] with Codex-owned child process spawning.
|
||||
///
|
||||
/// Implementations must return a child handle that makes ownership behavior explicit instead of
|
||||
/// relying on [`Child`]'s no-op drop behavior.
|
||||
pub trait CommandExt {
|
||||
/// Spawn the command and return a managed direct-child handle.
|
||||
#[track_caller]
|
||||
fn spawn_managed(&mut self) -> io::Result<ManagedChild>;
|
||||
}
|
||||
|
||||
impl CommandExt for Command {
|
||||
#[track_caller]
|
||||
fn spawn_managed(&mut self) -> io::Result<ManagedChild> {
|
||||
let program = self.get_program().to_os_string();
|
||||
let spawn_location = Location::caller();
|
||||
#[allow(
|
||||
clippy::disallowed_methods,
|
||||
reason = "ManagedChild wraps the raw child handle here."
|
||||
)]
|
||||
self.spawn().map(|child| ManagedChild {
|
||||
child: Some(child),
|
||||
program,
|
||||
spawn_location,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Child`] that best-effort terminates and reaps its direct child process on drop.
|
||||
///
|
||||
/// This handle manages only the direct process represented by [`Child`]. Transitive children need
|
||||
/// their own process-group or process-tree lifetime policy.
|
||||
#[derive(Debug)]
|
||||
pub struct ManagedChild {
|
||||
child: Option<Child>,
|
||||
program: OsString,
|
||||
spawn_location: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl ManagedChild {
|
||||
/// Wait for this child to exit and collect its captured output.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn wait_with_output(mut self) -> io::Result<Output> {
|
||||
self.child
|
||||
.take()
|
||||
.expect("managed child is present until consumed")
|
||||
.wait_with_output()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ManagedChild {
|
||||
type Target = Child;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.child
|
||||
.as_ref()
|
||||
.expect("managed child is present until consumed")
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ManagedChild {
|
||||
#[expect(clippy::expect_used)]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.child
|
||||
.as_mut()
|
||||
.expect("managed child is present until consumed")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ManagedChild {
|
||||
fn drop(&mut self) {
|
||||
let Some(child) = self.child.as_mut() else {
|
||||
// `wait_with_output` takes ownership of the child before this destructor runs.
|
||||
return;
|
||||
};
|
||||
let pid = child.id();
|
||||
if let Err(error) = child.kill() {
|
||||
warn!(
|
||||
pid,
|
||||
program = ?self.program,
|
||||
spawn_location = %self.spawn_location,
|
||||
reason = %error,
|
||||
"failed to kill managed child process during drop"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
match wait_for_exit(child, DROP_WAIT_TIMEOUT) {
|
||||
Ok(true) => trace!(
|
||||
pid,
|
||||
program = ?self.program,
|
||||
spawn_location = %self.spawn_location,
|
||||
"managed child process exited during drop"
|
||||
),
|
||||
Ok(false) => warn!(
|
||||
pid,
|
||||
program = ?self.program,
|
||||
spawn_location = %self.spawn_location,
|
||||
"timed out waiting for managed child process to exit during drop"
|
||||
),
|
||||
Err(error) => warn!(
|
||||
pid,
|
||||
program = ?self.program,
|
||||
spawn_location = %self.spawn_location,
|
||||
reason = %error,
|
||||
"failed to wait for managed child process during drop"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait up to `timeout` for a child to exit and be reaped.
|
||||
///
|
||||
/// Returns `Ok(false)` if the child is still running at the deadline.
|
||||
fn wait_for_exit(child: &mut Child, timeout: Duration) -> io::Result<bool> {
|
||||
// `Child::try_wait` reaps a finished child but never blocks, so enforce the timeout here.
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
if child.try_wait()?.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
return Ok(false);
|
||||
}
|
||||
thread::sleep(DROP_WAIT_POLL_INTERVAL.min(remaining));
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Expand these process tests to cover Windows.
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use std::process::Stdio;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn waits_for_short_lived_managed_child() -> io::Result<()> {
|
||||
let status = short_lived_command().spawn_managed()?.wait()?;
|
||||
|
||||
assert!(status.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_terminates_direct_child() -> io::Result<()> {
|
||||
let child = long_lived_command().spawn_managed()?;
|
||||
let pid = child.id();
|
||||
|
||||
drop(child);
|
||||
|
||||
assert!(!process_exists(pid));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_timeout_path_returns_without_hanging() -> io::Result<()> {
|
||||
let mut child = long_lived_command().spawn_managed()?;
|
||||
let exited = wait_for_exit(&mut child, Duration::ZERO)?;
|
||||
|
||||
assert!(!exited);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn short_lived_command() -> Command {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "exit 0"]);
|
||||
command
|
||||
}
|
||||
|
||||
fn long_lived_command() -> Command {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "sleep 30"]);
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::null());
|
||||
command.stderr(Stdio::null());
|
||||
command
|
||||
}
|
||||
|
||||
fn process_exists(pid: u32) -> bool {
|
||||
// SAFETY: `kill` with signal 0 performs existence/permission checks only.
|
||||
unsafe { libc::kill(pid.cast_signed(), 0) == 0 }
|
||||
}
|
||||
}
|
||||
202
codex-rs/managed-process/src/tokio.rs
Normal file
202
codex-rs/managed-process/src/tokio.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::panic::Location;
|
||||
use std::process::ExitStatus;
|
||||
use std::process::Output;
|
||||
|
||||
use ::tokio::process::Child;
|
||||
use ::tokio::process::Command;
|
||||
|
||||
use crate::drop_bomb::DebugDropBomb;
|
||||
|
||||
/// Extends Tokio [`Command`] with Codex-owned child process spawning.
|
||||
///
|
||||
/// Implementations must return a child handle that requires callers to explicitly wait for or
|
||||
/// terminate the spawned process.
|
||||
pub trait TokioCommandExt {
|
||||
/// Spawn the command and return a managed direct-child handle.
|
||||
#[track_caller]
|
||||
fn spawn_managed(&mut self) -> io::Result<ManagedTokioChild>;
|
||||
}
|
||||
|
||||
impl TokioCommandExt for Command {
|
||||
#[track_caller]
|
||||
fn spawn_managed(&mut self) -> io::Result<ManagedTokioChild> {
|
||||
let program = self.as_std().get_program().to_os_string();
|
||||
let spawn_location = Location::caller();
|
||||
// Prefer the explicit terminal methods below; this is only a fallback if ownership escapes.
|
||||
self.kill_on_drop(true);
|
||||
#[allow(
|
||||
clippy::disallowed_methods,
|
||||
reason = "ManagedTokioChild wraps the raw child handle here."
|
||||
)]
|
||||
self.spawn()
|
||||
.map(|child| ManagedTokioChild::new(child, program, spawn_location))
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tokio [`Child`] that requires explicit asynchronous teardown via [`Self::wait`],
|
||||
/// [`Self::wait_with_output`], or [`Self::kill_and_wait`].
|
||||
///
|
||||
/// Violating this requirement panics in debug builds.
|
||||
#[derive(Debug)]
|
||||
pub struct ManagedTokioChild {
|
||||
// This only becomes `None` in consuming terminal methods such as `wait_with_output`.
|
||||
child: Option<Child>,
|
||||
drop_bomb: DebugDropBomb,
|
||||
}
|
||||
|
||||
impl ManagedTokioChild {
|
||||
fn new(child: Child, program: OsString, spawn_location: &'static Location<'static>) -> Self {
|
||||
Self {
|
||||
child: Some(child),
|
||||
drop_bomb: DebugDropBomb::new(program, spawn_location),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for this child to exit.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub async fn wait(mut self) -> io::Result<ExitStatus> {
|
||||
let result = self
|
||||
.child
|
||||
.as_mut()
|
||||
.expect("managed Tokio child is present until consumed")
|
||||
.wait()
|
||||
.await;
|
||||
self.drop_bomb.defuse();
|
||||
result
|
||||
}
|
||||
|
||||
/// Wait for this child to exit and collect its captured output.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub async fn wait_with_output(mut self) -> io::Result<Output> {
|
||||
let result = self
|
||||
.child
|
||||
.take()
|
||||
.expect("managed Tokio child is present until consumed")
|
||||
.wait_with_output()
|
||||
.await;
|
||||
self.drop_bomb.defuse();
|
||||
result
|
||||
}
|
||||
|
||||
/// Kill this child and wait for it to exit.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub async fn kill_and_wait(mut self) -> io::Result<()> {
|
||||
let result = self
|
||||
.child
|
||||
.as_mut()
|
||||
.expect("managed Tokio child is present until consumed")
|
||||
.kill()
|
||||
.await;
|
||||
self.drop_bomb.defuse();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ManagedTokioChild {
|
||||
type Target = Child;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.child
|
||||
.as_ref()
|
||||
.expect("managed Tokio child is present until consumed")
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ManagedTokioChild {
|
||||
#[expect(clippy::expect_used)]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.child
|
||||
.as_mut()
|
||||
.expect("managed Tokio child is present until consumed")
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Expand these process tests to cover Windows.
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use std::process::Stdio;
|
||||
|
||||
use ::tokio::time::Duration;
|
||||
use ::tokio::time::sleep;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn waits_for_short_lived_managed_child() -> io::Result<()> {
|
||||
let status = short_lived_command().spawn_managed()?.wait().await?;
|
||||
|
||||
assert!(status.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn waits_for_managed_child_output() -> io::Result<()> {
|
||||
let output = output_command().spawn_managed()?.wait_with_output().await?;
|
||||
|
||||
assert_eq!(output.stdout, b"managed\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn kill_and_wait_terminates_direct_child() -> io::Result<()> {
|
||||
let child = long_lived_command().spawn_managed()?;
|
||||
let pid = child.id();
|
||||
|
||||
child.kill_and_wait().await?;
|
||||
|
||||
assert!(!process_exists(pid.expect("child should have a PID")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "dropped without explicit teardown")]
|
||||
async fn drop_without_teardown_panics_in_debug() {
|
||||
drop(long_lived_command().spawn_managed().expect("spawn child"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "dropped without explicit teardown")]
|
||||
async fn cancelled_wait_panics_in_debug() {
|
||||
let child = long_lived_command().spawn_managed().expect("spawn child");
|
||||
let wait = child.wait();
|
||||
::tokio::pin!(wait);
|
||||
::tokio::select! {
|
||||
_ = sleep(Duration::from_millis(10)) => {}
|
||||
_ = &mut wait => panic!("long-lived child should not exit first"),
|
||||
}
|
||||
}
|
||||
|
||||
fn short_lived_command() -> Command {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "exit 0"]);
|
||||
command
|
||||
}
|
||||
|
||||
fn output_command() -> Command {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "printf 'managed\n'"]);
|
||||
command.stdout(Stdio::piped());
|
||||
command
|
||||
}
|
||||
|
||||
fn long_lived_command() -> Command {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "sleep 30"]);
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::null());
|
||||
command.stderr(Stdio::null());
|
||||
command
|
||||
}
|
||||
|
||||
fn process_exists(pid: u32) -> bool {
|
||||
// SAFETY: `kill` with signal 0 performs existence/permission checks only.
|
||||
unsafe { libc::kill(pid.cast_signed(), 0) == 0 }
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ impl McpProcess {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut process = cmd
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
|
||||
@@ -199,6 +199,7 @@ pub(crate) async fn spawn_streamable_http_server() -> anyhow::Result<(Child, Str
|
||||
|
||||
let bind_addr = format!("127.0.0.1:{port}");
|
||||
let base_url = format!("http://{bind_addr}");
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = Command::new(streamable_http_server_bin()?)
|
||||
.kill_on_drop(true)
|
||||
.env("MCP_STREAMABLE_HTTP_BIND_ADDR", &bind_addr)
|
||||
@@ -225,6 +226,7 @@ impl Drop for ExecServerProcess {
|
||||
/// Starts a local exec-server and connects an initialized `ExecServerClient`.
|
||||
pub(crate) async fn spawn_exec_server() -> anyhow::Result<ExecServerProcess> {
|
||||
let codex_home = TempDir::new()?;
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
|
||||
.args(["exec-server", "--listen", "ws://127.0.0.1:0"])
|
||||
.stdin(Stdio::null())
|
||||
|
||||
@@ -14,6 +14,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
|
||||
@@ -12,6 +12,8 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_managed_process::CommandExt;
|
||||
|
||||
const SYSTEM_BWRAP_PROGRAM: &str = "bwrap";
|
||||
const MISSING_BWRAP_WARNING: &str = concat!(
|
||||
"Codex could not find bubblewrap on PATH. ",
|
||||
@@ -83,7 +85,7 @@ fn system_bwrap_has_user_namespace_access(system_bwrap_path: &Path, timeout: Dur
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(_) => return true,
|
||||
|
||||
@@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_managed_process::CommandExt;
|
||||
use codex_managed_process::ManagedChild;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -103,7 +105,7 @@ fn encoded_parser_script() -> &'static str {
|
||||
}
|
||||
|
||||
struct PowershellParserProcess {
|
||||
child: Child,
|
||||
child: ManagedChild,
|
||||
stdin: ChildStdin,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
// Request ids are monotonic within one child process so the caller can detect protocol
|
||||
@@ -124,7 +126,7 @@ impl PowershellParserProcess {
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
.spawn_managed()?;
|
||||
let stdin = match take_child_stdin(&mut child) {
|
||||
Ok(stdin) => stdin,
|
||||
Err(error) => {
|
||||
|
||||
@@ -345,6 +345,7 @@ async fn handle_escalate_session_with_policy(
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = command.spawn()?;
|
||||
let exit_status = tokio::select! {
|
||||
status = child.wait() => status?,
|
||||
|
||||
@@ -28,6 +28,7 @@ tokio = { workspace = true, features = [
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Context;
|
||||
use codex_managed_process::CommandExt;
|
||||
use codex_uds::UnixListener;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -77,7 +78,7 @@ async fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
|
||||
.stdin(Stdio::from(stdin))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.context("failed to spawn codex-stdio-to-uds")?;
|
||||
|
||||
let mut child_stdout = child.stdout.take().context("missing child stdout")?;
|
||||
|
||||
@@ -43,6 +43,7 @@ codex-feedback = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-managed-process = { workspace = true }
|
||||
codex-message-history = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
//! no reusable clipboard abstraction. Image paste lives in `clipboard_paste`.
|
||||
|
||||
use base64::Engine;
|
||||
use codex_managed_process::CommandExt;
|
||||
use std::io::Write;
|
||||
|
||||
/// Maximum raw bytes we will base64-encode into an OSC 52 sequence.
|
||||
@@ -271,7 +272,7 @@ fn wsl_clipboard_copy(text: &str) -> Result<(), String> {
|
||||
"-Command",
|
||||
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; $ErrorActionPreference = 'Stop'; $text = [Console]::In.ReadToEnd(); Set-Clipboard -Value $text",
|
||||
])
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.map_err(|e| format!("failed to spawn powershell.exe: {e}"))?;
|
||||
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
@@ -326,7 +327,7 @@ fn tmux_clipboard_copy(text: &str) -> Result<(), String> {
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.spawn_managed()
|
||||
.map_err(|e| format!("failed to spawn tmux: {e}"))?;
|
||||
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
|
||||
@@ -147,6 +147,7 @@ async fn spawn_process_with_stdin_mode(
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = command.spawn()?;
|
||||
let pid = child
|
||||
.id()
|
||||
|
||||
@@ -158,6 +158,7 @@ async fn spawn_process_portable(
|
||||
command_builder.env(key, value);
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = pair.slave.spawn_command(command_builder)?;
|
||||
#[cfg(unix)]
|
||||
// portable-pty establishes the spawned PTY child as a new session leader on
|
||||
@@ -322,6 +323,7 @@ async fn spawn_process_preserving_fds(
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
let mut child = command.spawn()?;
|
||||
drop(slave);
|
||||
let process_group_id = child.id();
|
||||
|
||||
@@ -14,6 +14,7 @@ tracing = { workspace = true }
|
||||
core-foundation = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
codex-managed-process = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use codex_managed_process::CommandExt as _;
|
||||
use codex_managed_process::ManagedChild;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use tracing::warn;
|
||||
@@ -25,7 +26,7 @@ enum InhibitState {
|
||||
Inactive,
|
||||
Active {
|
||||
backend: LinuxBackend,
|
||||
child: Child,
|
||||
child: ManagedChild,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ impl Drop for LinuxSleepInhibitor {
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_backend(backend: LinuxBackend) -> Result<Child, std::io::Error> {
|
||||
fn spawn_backend(backend: LinuxBackend) -> Result<ManagedChild, std::io::Error> {
|
||||
// Ensure the helper receives SIGTERM when the original parent dies.
|
||||
// `parent_pid` is captured before spawn and checked in `pre_exec` to avoid
|
||||
// the fork/exec race where the parent exits before PDEATHSIG is armed.
|
||||
@@ -222,7 +223,7 @@ fn spawn_backend(backend: LinuxBackend) -> Result<Child, std::io::Error> {
|
||||
});
|
||||
}
|
||||
|
||||
command.spawn()
|
||||
command.spawn_managed()
|
||||
}
|
||||
|
||||
fn child_exited(error: &std::io::Error) -> bool {
|
||||
|
||||
@@ -163,6 +163,7 @@ fn spawn_read_acl_helper(payload: &Payload, _log: &mut File) -> Result<()> {
|
||||
let payload_json = serde_json::to_vec(&read_payload)?;
|
||||
let payload_b64 = BASE64.encode(payload_json);
|
||||
let exe = std::env::current_exe().context("locate setup helper")?;
|
||||
#[allow(clippy::disallowed_methods, reason = "Grandfathered-in usage.")]
|
||||
Command::new(&exe)
|
||||
.arg(payload_b64)
|
||||
.stdin(Stdio::null())
|
||||
|
||||
Reference in New Issue
Block a user