mod buffer;
mod child;
mod pty;
mod signals;
use std::ffi::OsStr;
use std::io;
pub use buffer::PtyBuffer;
pub use child::{UnixPtyChild, spawn_child};
pub use pty::{UnixPtyMaster, open_slave};
pub use signals::{
PtySignalEvent, SignalHandle, is_sigchld, is_sigwinch, on_window_change, sigchld, sigwinch,
start_signal_handler,
};
use crate::config::PtyConfig;
use crate::error::{PtyError, Result};
use crate::traits::PtySystem;
async fn open_master_with_retry() -> Result<(UnixPtyMaster, String)> {
const ATTEMPTS: u32 = 10;
let mut last_err = PtyError::Create(io::Error::other("openpt was never attempted"));
for attempt in 0..ATTEMPTS {
match UnixPtyMaster::open() {
Ok(pair) => return Ok(pair),
Err(e) => {
last_err = e;
if attempt + 1 < ATTEMPTS {
tokio::time::sleep(std::time::Duration::from_millis(
2 * u64::from(attempt + 1),
))
.await;
}
}
}
}
Err(last_err)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UnixPtySystem;
impl PtySystem for UnixPtySystem {
type Master = UnixPtyMaster;
type Child = UnixPtyChild;
async fn spawn<S, I>(
program: S,
args: I,
config: &PtyConfig,
) -> Result<(Self::Master, Self::Child)>
where
S: AsRef<OsStr> + Send,
I: IntoIterator + Send,
I::Item: AsRef<OsStr>,
{
let (master, slave_path) = open_master_with_retry().await?;
#[cfg(target_os = "macos")]
let master = {
let mut master = master;
master.start_read_drain()?;
master
};
let slave_fd = open_slave(&slave_path)?;
let window_size = config.window_size.into();
master.set_window_size(window_size)?;
let child = spawn_child(slave_fd, program, args, config).await?;
Ok((master, child))
}
}
pub type NativePtySystem = UnixPtySystem;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn spawn_shell() {
let config = PtyConfig::default();
let result = UnixPtySystem::spawn_shell(&config).await;
if let Ok((mut master, mut child)) = result {
assert!(master.is_open());
assert!(child.is_running());
child.kill().ok();
master.close().ok();
}
}
#[tokio::test]
async fn spawn_echo() {
let config = PtyConfig::default();
let result = UnixPtySystem::spawn("echo", ["hello"], &config).await;
if let Ok((mut master, mut child)) = result {
let status = child.wait().await;
assert!(status.is_ok());
master.close().ok();
}
}
#[tokio::test]
async fn spawn_succeeds_with_default_config() {
let config = PtyConfig::default();
let (mut master, mut child) = UnixPtySystem::spawn("/bin/sh", ["-c", "exit 0"], &config)
.await
.expect("default-config spawn must succeed (EPERM regression)");
let status = child.wait().await.expect("wait");
assert_eq!(status, crate::traits::ExitStatus::Exited(0));
master.close().ok();
}
#[tokio::test]
async fn allocation_retry_happy_path() {
let (master, _slave_path) = open_master_with_retry().await.expect("allocate");
assert!(master.is_open());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn try_wait_reports_real_exit_status_under_multi_thread() {
let config = PtyConfig::default();
let (mut master, mut child) = UnixPtySystem::spawn("/bin/sh", ["-c", "exit 7"], &config)
.await
.expect("spawn");
let mut status = None;
for _ in 0..500 {
match child.try_wait() {
Ok(Some(s)) => {
status = Some(s);
break;
}
Ok(None) => tokio::time::sleep(std::time::Duration::from_millis(10)).await,
Err(e) => panic!("try_wait errored (reaper race?): {e:?}"),
}
}
assert_eq!(
status,
Some(crate::traits::ExitStatus::Exited(7)),
"try_wait did not report the real exit status"
);
master.close().ok();
}
}