mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
## Why `codex-utils-pty` and `codex-windows-sandbox` were the remaining crates in `codex-rs` that still overrode the workspace's Rust 2024 edition. Moving them forward in a separate PR keeps the baseline edition update isolated from the follow-on Bazel clippy workflow in #15955, while making linting and formatting behavior consistent with the rest of the workspace. This PR also needs Cargo and Bazel to agree on the edition for `codex-windows-sandbox`. Without the Bazel-side sync, the experimental Bazel app-server builds fail once they compile `windows-sandbox-rs`. ## What changed - switch `codex-rs/utils/pty` and `codex-rs/windows-sandbox-rs` to `edition = "2024"` - update `codex-utils-pty` callsites and tests to use the collapsed `if let` form that Clippy expects under the new edition - fix the Rust 2024 fallout in `windows-sandbox-rs`, including the reserved `gen` identifier, `unsafe extern` requirements, and new Clippy findings that surfaced under the edition bump - keep the edition bump separate from a larger unsafe cleanup by temporarily allowing `unsafe_op_in_unsafe_fn` in the Windows entrypoint modules that now report it under Rust 2024 - update `codex-rs/windows-sandbox-rs/BUILD.bazel` to `crate_edition = "2024"` so Bazel compiles the crate with the same edition as Cargo --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/15954). * #15976 * #15955 * __->__ #15954
482 lines
14 KiB
Rust
482 lines
14 KiB
Rust
use std::collections::HashMap;
|
|
#[cfg(unix)]
|
|
use std::fs::File;
|
|
use std::io::ErrorKind;
|
|
#[cfg(unix)]
|
|
use std::os::fd::AsRawFd;
|
|
#[cfg(unix)]
|
|
use std::os::fd::FromRawFd;
|
|
#[cfg(unix)]
|
|
use std::os::fd::RawFd;
|
|
#[cfg(unix)]
|
|
use std::os::unix::process::CommandExt;
|
|
use std::path::Path;
|
|
#[cfg(unix)]
|
|
use std::process::Command as StdCommand;
|
|
#[cfg(unix)]
|
|
use std::process::Stdio;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex as StdMutex;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use portable_pty::CommandBuilder;
|
|
#[cfg(not(windows))]
|
|
use portable_pty::native_pty_system;
|
|
use tokio::sync::mpsc;
|
|
use tokio::sync::oneshot;
|
|
use tokio::task::JoinHandle;
|
|
|
|
use crate::process::ChildTerminator;
|
|
use crate::process::ProcessHandle;
|
|
use crate::process::PtyHandles;
|
|
use crate::process::PtyMasterHandle;
|
|
use crate::process::SpawnedProcess;
|
|
use crate::process::TerminalSize;
|
|
|
|
/// Returns true when ConPTY support is available (Windows only).
|
|
#[cfg(windows)]
|
|
pub fn conpty_supported() -> bool {
|
|
crate::win::conpty_supported()
|
|
}
|
|
|
|
/// Returns true when ConPTY support is available (non-Windows always true).
|
|
#[cfg(not(windows))]
|
|
pub fn conpty_supported() -> bool {
|
|
true
|
|
}
|
|
|
|
struct PtyChildTerminator {
|
|
killer: Box<dyn portable_pty::ChildKiller + Send + Sync>,
|
|
#[cfg(unix)]
|
|
process_group_id: Option<u32>,
|
|
}
|
|
|
|
impl ChildTerminator for PtyChildTerminator {
|
|
fn kill(&mut self) -> std::io::Result<()> {
|
|
#[cfg(unix)]
|
|
if let Some(process_group_id) = self.process_group_id {
|
|
// Match the pipe backend's hard-kill behavior so descendant
|
|
// processes from interactive shells/REPLs do not survive shutdown.
|
|
// Also try the direct child killer in case the cached PGID is stale.
|
|
let process_group_kill_result =
|
|
crate::process_group::kill_process_group(process_group_id);
|
|
let child_kill_result = self.killer.kill();
|
|
return match child_kill_result {
|
|
Ok(()) => Ok(()),
|
|
Err(err) if err.kind() == ErrorKind::NotFound => process_group_kill_result,
|
|
Err(err) => process_group_kill_result.or(Err(err)),
|
|
};
|
|
}
|
|
|
|
self.killer.kill()
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
struct RawPidTerminator {
|
|
process_group_id: u32,
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
impl ChildTerminator for RawPidTerminator {
|
|
fn kill(&mut self) -> std::io::Result<()> {
|
|
crate::process_group::kill_process_group(self.process_group_id)
|
|
}
|
|
}
|
|
|
|
fn platform_native_pty_system() -> Box<dyn portable_pty::PtySystem + Send> {
|
|
#[cfg(windows)]
|
|
{
|
|
Box::new(crate::win::ConPtySystem::default())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
native_pty_system()
|
|
}
|
|
}
|
|
|
|
/// Spawn a process attached to a PTY, returning handles for stdin, split output, and exit.
|
|
pub async fn spawn_process(
|
|
program: &str,
|
|
args: &[String],
|
|
cwd: &Path,
|
|
env: &HashMap<String, String>,
|
|
arg0: &Option<String>,
|
|
size: TerminalSize,
|
|
) -> Result<SpawnedProcess> {
|
|
spawn_process_with_inherited_fds(program, args, cwd, env, arg0, size, &[]).await
|
|
}
|
|
|
|
/// Spawn a process attached to a PTY, preserving any inherited file
|
|
/// descriptors listed in `inherited_fds` across exec on Unix.
|
|
pub async fn spawn_process_with_inherited_fds(
|
|
program: &str,
|
|
args: &[String],
|
|
cwd: &Path,
|
|
env: &HashMap<String, String>,
|
|
arg0: &Option<String>,
|
|
size: TerminalSize,
|
|
inherited_fds: &[i32],
|
|
) -> Result<SpawnedProcess> {
|
|
if program.is_empty() {
|
|
anyhow::bail!("missing program for PTY spawn");
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
let _ = inherited_fds;
|
|
|
|
#[cfg(unix)]
|
|
if !inherited_fds.is_empty() {
|
|
return spawn_process_preserving_fds(program, args, cwd, env, arg0, size, inherited_fds)
|
|
.await;
|
|
}
|
|
|
|
spawn_process_portable(program, args, cwd, env, arg0, size).await
|
|
}
|
|
|
|
async fn spawn_process_portable(
|
|
program: &str,
|
|
args: &[String],
|
|
cwd: &Path,
|
|
env: &HashMap<String, String>,
|
|
arg0: &Option<String>,
|
|
size: TerminalSize,
|
|
) -> Result<SpawnedProcess> {
|
|
let pty_system = platform_native_pty_system();
|
|
let pair = pty_system.openpty(size.into())?;
|
|
|
|
let mut command_builder = CommandBuilder::new(arg0.as_ref().unwrap_or(&program.to_string()));
|
|
command_builder.cwd(cwd);
|
|
command_builder.env_clear();
|
|
for arg in args {
|
|
command_builder.arg(arg);
|
|
}
|
|
for (key, value) in env {
|
|
command_builder.env(key, value);
|
|
}
|
|
|
|
let mut child = pair.slave.spawn_command(command_builder)?;
|
|
#[cfg(unix)]
|
|
// portable-pty establishes the spawned PTY child as a new session leader on
|
|
// Unix, so PID == PGID and we can reuse the pipe backend's process-group
|
|
// hard-kill semantics for descendants.
|
|
let process_group_id = child.process_id();
|
|
let killer = child.clone_killer();
|
|
|
|
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
|
let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(128);
|
|
let (_stderr_tx, stderr_rx) = mpsc::channel::<Vec<u8>>(1);
|
|
let mut reader = pair.master.try_clone_reader()?;
|
|
let reader_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || {
|
|
let mut buf = [0u8; 8_192];
|
|
loop {
|
|
match reader.read(&mut buf) {
|
|
Ok(0) => break,
|
|
Ok(n) => {
|
|
let _ = stdout_tx.blocking_send(buf[..n].to_vec());
|
|
}
|
|
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
|
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
continue;
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
});
|
|
|
|
let writer = pair.master.take_writer()?;
|
|
let writer = Arc::new(tokio::sync::Mutex::new(writer));
|
|
let writer_handle: JoinHandle<()> = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
async move {
|
|
while let Some(bytes) = writer_rx.recv().await {
|
|
let mut guard = writer.lock().await;
|
|
use std::io::Write;
|
|
let _ = guard.write_all(&bytes);
|
|
let _ = guard.flush();
|
|
}
|
|
}
|
|
});
|
|
|
|
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
|
let exit_status = Arc::new(AtomicBool::new(false));
|
|
let wait_exit_status = Arc::clone(&exit_status);
|
|
let exit_code = Arc::new(StdMutex::new(None));
|
|
let wait_exit_code = Arc::clone(&exit_code);
|
|
let wait_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || {
|
|
let code = match child.wait() {
|
|
Ok(status) => status.exit_code() as i32,
|
|
Err(_) => -1,
|
|
};
|
|
wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
if let Ok(mut guard) = wait_exit_code.lock() {
|
|
*guard = Some(code);
|
|
}
|
|
let _ = exit_tx.send(code);
|
|
});
|
|
|
|
let handles = PtyHandles {
|
|
_slave: if cfg!(windows) {
|
|
Some(pair.slave)
|
|
} else {
|
|
None
|
|
},
|
|
_master: PtyMasterHandle::Resizable(pair.master),
|
|
};
|
|
|
|
let handle = ProcessHandle::new(
|
|
writer_tx,
|
|
Box::new(PtyChildTerminator {
|
|
killer,
|
|
#[cfg(unix)]
|
|
process_group_id,
|
|
}),
|
|
reader_handle,
|
|
Vec::new(),
|
|
writer_handle,
|
|
wait_handle,
|
|
exit_status,
|
|
exit_code,
|
|
Some(handles),
|
|
);
|
|
|
|
Ok(SpawnedProcess {
|
|
session: handle,
|
|
stdout_rx,
|
|
stderr_rx,
|
|
exit_rx,
|
|
})
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
async fn spawn_process_preserving_fds(
|
|
program: &str,
|
|
args: &[String],
|
|
cwd: &Path,
|
|
env: &HashMap<String, String>,
|
|
arg0: &Option<String>,
|
|
size: TerminalSize,
|
|
inherited_fds: &[RawFd],
|
|
) -> Result<SpawnedProcess> {
|
|
let (master, slave) = open_unix_pty(size)?;
|
|
let mut command = StdCommand::new(program);
|
|
if let Some(arg0) = arg0 {
|
|
command.arg0(arg0);
|
|
}
|
|
command.current_dir(cwd);
|
|
command.env_clear();
|
|
for arg in args {
|
|
command.arg(arg);
|
|
}
|
|
for (key, value) in env {
|
|
command.env(key, value);
|
|
}
|
|
|
|
// The child should see one terminal on all three stdio streams. Cloning
|
|
// the slave fd gives us three owned handles to the same PTY slave device
|
|
// so Command can wire them up independently as stdin/stdout/stderr.
|
|
let stdin = slave.try_clone()?;
|
|
let stdout = slave.try_clone()?;
|
|
let stderr = slave.try_clone()?;
|
|
let inherited_fds = inherited_fds.to_vec();
|
|
|
|
unsafe {
|
|
command
|
|
.stdin(Stdio::from(stdin))
|
|
.stdout(Stdio::from(stdout))
|
|
.stderr(Stdio::from(stderr))
|
|
.pre_exec(move || {
|
|
for signo in &[
|
|
libc::SIGCHLD,
|
|
libc::SIGHUP,
|
|
libc::SIGINT,
|
|
libc::SIGQUIT,
|
|
libc::SIGTERM,
|
|
libc::SIGALRM,
|
|
] {
|
|
libc::signal(*signo, libc::SIG_DFL);
|
|
}
|
|
|
|
let empty_set: libc::sigset_t = std::mem::zeroed();
|
|
libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut());
|
|
|
|
if libc::setsid() == -1 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
|
|
// stdin now refers to the PTY slave, so make that fd the
|
|
// controlling terminal for the child's new session. stdout and
|
|
// stderr point at clones of the same slave device.
|
|
#[allow(clippy::cast_lossless)]
|
|
if libc::ioctl(0, libc::TIOCSCTTY as _, 0) == -1 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
|
|
close_inherited_fds_except(&inherited_fds);
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
let mut child = command.spawn()?;
|
|
drop(slave);
|
|
let process_group_id = child.id();
|
|
|
|
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
|
let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(128);
|
|
let (_stderr_tx, stderr_rx) = mpsc::channel::<Vec<u8>>(1);
|
|
let mut reader = master.try_clone()?;
|
|
let reader_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || {
|
|
let mut buf = [0u8; 8_192];
|
|
loop {
|
|
match std::io::Read::read(&mut reader, &mut buf) {
|
|
Ok(0) => break,
|
|
Ok(n) => {
|
|
let _ = stdout_tx.blocking_send(buf[..n].to_vec());
|
|
}
|
|
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
|
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
|
|
std::thread::sleep(Duration::from_millis(5));
|
|
continue;
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
});
|
|
|
|
let writer = Arc::new(tokio::sync::Mutex::new(master.try_clone()?));
|
|
let writer_handle: JoinHandle<()> = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
async move {
|
|
while let Some(bytes) = writer_rx.recv().await {
|
|
let mut guard = writer.lock().await;
|
|
use std::io::Write;
|
|
let _ = guard.write_all(&bytes);
|
|
let _ = guard.flush();
|
|
}
|
|
}
|
|
});
|
|
|
|
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
|
let exit_status = Arc::new(AtomicBool::new(false));
|
|
let wait_exit_status = Arc::clone(&exit_status);
|
|
let exit_code = Arc::new(StdMutex::new(None));
|
|
let wait_exit_code = Arc::clone(&exit_code);
|
|
let wait_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || {
|
|
let code = match child.wait() {
|
|
Ok(status) => status.code().unwrap_or(-1),
|
|
Err(_) => -1,
|
|
};
|
|
wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
if let Ok(mut guard) = wait_exit_code.lock() {
|
|
*guard = Some(code);
|
|
}
|
|
let _ = exit_tx.send(code);
|
|
});
|
|
|
|
let handles = PtyHandles {
|
|
_slave: None,
|
|
_master: PtyMasterHandle::Opaque {
|
|
raw_fd: master.as_raw_fd(),
|
|
_handle: Box::new(master),
|
|
},
|
|
};
|
|
|
|
let handle = ProcessHandle::new(
|
|
writer_tx,
|
|
Box::new(RawPidTerminator { process_group_id }),
|
|
reader_handle,
|
|
Vec::new(),
|
|
writer_handle,
|
|
wait_handle,
|
|
exit_status,
|
|
exit_code,
|
|
Some(handles),
|
|
);
|
|
|
|
Ok(SpawnedProcess {
|
|
session: handle,
|
|
stdout_rx,
|
|
stderr_rx,
|
|
exit_rx,
|
|
})
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn open_unix_pty(size: TerminalSize) -> Result<(File, File)> {
|
|
let mut master: RawFd = -1;
|
|
let mut slave: RawFd = -1;
|
|
let mut size = libc::winsize {
|
|
ws_row: size.rows,
|
|
ws_col: size.cols,
|
|
ws_xpixel: 0,
|
|
ws_ypixel: 0,
|
|
};
|
|
let winp = std::ptr::addr_of_mut!(size);
|
|
|
|
let result = unsafe {
|
|
libc::openpty(
|
|
&mut master,
|
|
&mut slave,
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
winp,
|
|
)
|
|
};
|
|
if result != 0 {
|
|
anyhow::bail!("failed to openpty: {:?}", std::io::Error::last_os_error());
|
|
}
|
|
|
|
set_cloexec(master)?;
|
|
set_cloexec(slave)?;
|
|
|
|
Ok(unsafe { (File::from_raw_fd(master), File::from_raw_fd(slave)) })
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn set_cloexec(fd: RawFd) -> std::io::Result<()> {
|
|
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
|
|
if flags == -1 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
|
|
if result == -1 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) fn close_inherited_fds_except(preserved_fds: &[RawFd]) {
|
|
if let Ok(dir) = std::fs::read_dir("/dev/fd") {
|
|
let mut fds = Vec::new();
|
|
for entry in dir {
|
|
let num = entry
|
|
.ok()
|
|
.map(|entry| entry.file_name())
|
|
.and_then(|name| name.into_string().ok())
|
|
.and_then(|name| name.parse::<RawFd>().ok());
|
|
if let Some(num) = num {
|
|
if num <= 2 || preserved_fds.contains(&num) {
|
|
continue;
|
|
}
|
|
// Keep CLOEXEC descriptors open so std::process can still use
|
|
// its internal exec-error pipe to report spawn failures.
|
|
let flags = unsafe { libc::fcntl(num, libc::F_GETFD) };
|
|
if flags == -1 || flags & libc::FD_CLOEXEC != 0 {
|
|
continue;
|
|
}
|
|
fds.push(num);
|
|
}
|
|
}
|
|
for fd in fds {
|
|
unsafe {
|
|
libc::close(fd);
|
|
}
|
|
}
|
|
}
|
|
}
|