Files
codex/codex-rs/utils/pty/src/pty.rs
Ruslan Nigmatullin 9dbf1a14d3 cr
2026-03-06 11:43:18 -08:00

193 lines
5.9 KiB
Rust

use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
use anyhow::Result;
#[cfg(not(windows))]
use portable_pty::native_pty_system;
use portable_pty::CommandBuilder;
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::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()
}
}
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> {
if program.is_empty() {
anyhow::bail!("missing program for PTY spawn");
}
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: 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,
})
}