use std::collections::VecDeque;
use std::ffi::OsString;
use std::fmt;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use portable_pty::{ChildKiller, CommandBuilder, MasterPty, PtySize, native_pty_system};
pub type Notify = Arc<dyn Fn() + Send + Sync>;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ExitStatus {
code: u32,
signal: Option<String>,
}
impl ExitStatus {
pub fn with_code(code: u32) -> ExitStatus {
ExitStatus { code, signal: None }
}
pub fn with_signal(signal: impl Into<String>) -> ExitStatus {
ExitStatus {
code: 1,
signal: Some(signal.into()),
}
}
pub fn code(&self) -> u32 {
self.code
}
pub fn signal(&self) -> Option<&str> {
self.signal.as_deref()
}
pub fn success(&self) -> bool {
self.code == 0 && self.signal.is_none()
}
}
impl fmt::Display for ExitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.signal {
Some(signal) => write!(f, "ended by signal {signal}"),
None => write!(f, "exited with code {}", self.code),
}
}
}
impl From<portable_pty::ExitStatus> for ExitStatus {
fn from(status: portable_pty::ExitStatus) -> ExitStatus {
match status.signal() {
Some(signal) => ExitStatus::with_signal(signal),
None => ExitStatus::with_code(status.exit_code()),
}
}
}
pub trait PtyHost {
fn start(&mut self, columns: u16, rows: u16) -> io::Result<()>;
fn write(&mut self, bytes: &[u8]) -> io::Result<()>;
fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()>;
fn read(&mut self) -> Vec<u8>;
fn set_notify(&mut self, notify: Notify);
fn exit_status(&mut self) -> Option<ExitStatus>;
fn kill(&mut self) -> io::Result<()>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Command {
program: OsString,
args: Vec<OsString>,
env: Vec<(OsString, OsString)>,
cwd: Option<PathBuf>,
}
impl Command {
pub fn new(program: impl Into<OsString>) -> Command {
Command {
program: program.into(),
args: Vec::new(),
env: Vec::new(),
cwd: None,
}
}
#[must_use]
pub fn arg(mut self, arg: impl Into<OsString>) -> Command {
self.args.push(arg.into());
self
}
#[must_use]
pub fn args<I: IntoIterator<Item = S>, S: Into<OsString>>(mut self, args: I) -> Command {
self.args.extend(args.into_iter().map(Into::into));
self
}
#[must_use]
pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Command {
self.env.push((key.into(), value.into()));
self
}
#[must_use]
pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Command {
self.cwd = Some(dir.into());
self
}
pub fn program(&self) -> &OsString {
&self.program
}
fn builder(&self) -> CommandBuilder {
let mut builder = CommandBuilder::new(&self.program);
builder.args(&self.args);
builder.env("TERM", "xterm-256color");
for (key, value) in &self.env {
builder.env(key, value);
}
match &self.cwd {
Some(dir) => builder.cwd(dir),
None => {
if let Ok(dir) = std::env::current_dir() {
builder.cwd(dir);
}
}
}
builder
}
}
impl From<&str> for Command {
fn from(program: &str) -> Command {
Command::new(program)
}
}
impl From<String> for Command {
fn from(program: String) -> Command {
Command::new(program)
}
}
impl<S: Into<OsString>, const N: usize> From<[S; N]> for Command {
fn from(parts: [S; N]) -> Command {
Command::from_parts(parts)
}
}
impl<S: Into<OsString>> From<Vec<S>> for Command {
fn from(parts: Vec<S>) -> Command {
Command::from_parts(parts)
}
}
impl Command {
fn from_parts<S: Into<OsString>>(parts: impl IntoIterator<Item = S>) -> Command {
let mut parts = parts.into_iter().map(Into::into);
let program = parts.next().unwrap_or_default();
Command::new(program).args(parts)
}
}
const MAX_BUFFERED: usize = 16 * 1024 * 1024;
const MAX_INPUT: usize = 1024 * 1024;
const KILL_GRACE: Duration = Duration::from_secs(1);
const WAIT_TURN: Duration = Duration::from_millis(20);
#[derive(Default)]
struct Output {
buffer: VecDeque<u8>,
eof: bool,
paused: bool,
closed: bool,
hung_up: Option<Instant>,
status: Option<ExitStatus>,
notify: Option<Notify>,
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn notify(shared: &Mutex<Output>) {
let notify = lock(shared).notify.clone();
if let Some(notify) = notify {
notify();
}
}
pub struct LocalPty {
command: Command,
shared: Arc<Mutex<Output>>,
master: Option<Box<dyn MasterPty + Send>>,
writer: Option<mpsc::Sender<Vec<u8>>>,
queued: Arc<AtomicUsize>,
killer: Option<Box<dyn ChildKiller + Send + Sync>>,
backpressure: Option<usize>,
}
impl LocalPty {
pub fn new(command: impl Into<Command>) -> LocalPty {
LocalPty {
command: command.into(),
shared: Arc::default(),
master: None,
writer: None,
queued: Arc::default(),
killer: None,
backpressure: None,
}
}
#[must_use]
pub fn backpressure(mut self, bytes: usize) -> LocalPty {
self.backpressure = Some(bytes.max(1));
self
}
fn size(columns: u16, rows: u16) -> PtySize {
PtySize {
rows: rows.max(1),
cols: columns.max(1),
pixel_width: 0,
pixel_height: 0,
}
}
}
impl PtyHost for LocalPty {
fn start(&mut self, columns: u16, rows: u16) -> io::Result<()> {
if self.master.is_some() {
return Ok(());
}
let pty = native_pty_system()
.openpty(LocalPty::size(columns, rows))
.map_err(io::Error::other)?;
let mut child = pty
.slave
.spawn_command(self.command.builder())
.map_err(io::Error::other)?;
drop(pty.slave);
let mut reader = pty.master.try_clone_reader().map_err(io::Error::other)?;
let mut writer = pty.master.take_writer().map_err(io::Error::other)?;
let (input, keys) = mpsc::channel::<Vec<u8>>();
let queued = Arc::clone(&self.queued);
thread::spawn(move || {
for bytes in keys {
if writer
.write_all(&bytes)
.and_then(|()| writer.flush())
.is_err()
{
queued.store(0, Ordering::SeqCst);
break;
}
queued.fetch_sub(bytes.len(), Ordering::SeqCst);
}
});
self.writer = Some(input);
self.killer = Some(child.clone_killer());
self.master = Some(pty.master);
let shared = Arc::clone(&self.shared);
let limit = self.backpressure;
thread::spawn(move || {
let mut chunk = [0u8; 65536];
loop {
if let Some(limit) = limit
&& !wait_for_room(&shared, limit)
{
break;
}
let read = match reader.read(&mut chunk) {
Ok(0) | Err(_) => break,
Ok(read) => read,
};
keep(&mut lock(&shared), &chunk[..read], limit.is_none());
notify(&shared);
}
lock(&shared).eof = true;
notify(&shared);
});
let shared = Arc::clone(&self.shared);
let leader = child.process_id();
thread::spawn(move || {
let mut killed = false;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break ExitStatus::from(status),
Ok(None) => {}
Err(error) => break ExitStatus::with_signal(error.to_string()),
}
let hung_up = lock(&shared).hung_up;
if !killed && hung_up.is_some_and(|at| at.elapsed() >= KILL_GRACE) {
if let Some(leader) = leader {
kill_group(leader);
}
killed = true;
}
thread::sleep(WAIT_TURN);
};
let mut waited = 0;
while waited < 50 {
let (eof, paused) = {
let output = lock(&shared);
(output.eof, output.paused)
};
if eof {
break;
}
if !paused {
waited += 1;
}
thread::sleep(Duration::from_millis(10));
}
lock(&shared).status = Some(status);
notify(&shared);
});
Ok(())
}
fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
if self.writer.is_some() && self.queued.load(Ordering::SeqCst) >= MAX_INPUT {
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"the program is not reading its input",
));
}
match &self.writer {
Some(writer) => {
self.queued.fetch_add(bytes.len(), Ordering::SeqCst);
writer.send(bytes.to_vec()).map_err(|_| {
self.queued.store(0, Ordering::SeqCst);
io::Error::new(io::ErrorKind::BrokenPipe, "the program has gone")
})
}
None => Err(io::Error::new(
io::ErrorKind::NotConnected,
"the program has not started",
)),
}
}
fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
match &self.master {
Some(master) => master
.resize(LocalPty::size(columns, rows))
.map_err(io::Error::other),
None => Ok(()),
}
}
fn read(&mut self) -> Vec<u8> {
lock(&self.shared).buffer.drain(..).collect()
}
fn set_notify(&mut self, notify: Notify) {
let waiting = {
let mut output = lock(&self.shared);
output.notify = Some(notify.clone());
!output.buffer.is_empty() || output.status.is_some()
};
if waiting {
notify();
}
}
fn exit_status(&mut self) -> Option<ExitStatus> {
let output = lock(&self.shared);
if output.buffer.is_empty() {
output.status.clone()
} else {
None
}
}
fn kill(&mut self) -> io::Result<()> {
{
let mut output = lock(&self.shared);
if output.status.is_some() {
return Ok(());
}
if self.killer.is_some() {
output.hung_up.get_or_insert_with(Instant::now);
}
}
match &mut self.killer {
Some(killer) => killer.kill(),
None => Ok(()),
}
}
}
impl Drop for LocalPty {
fn drop(&mut self) {
lock(&self.shared).closed = true;
let _ = self.kill();
}
}
#[cfg(unix)]
#[allow(unsafe_code)]
fn kill_group(leader: u32) {
if let Ok(group) = libc::pid_t::try_from(leader) {
unsafe {
libc::killpg(group, libc::SIGKILL);
}
}
}
#[cfg(not(unix))]
fn kill_group(_leader: u32) {}
fn keep(output: &mut Output, bytes: &[u8], drop_oldest: bool) {
output.buffer.extend(bytes);
if drop_oldest {
let over = output.buffer.len().saturating_sub(MAX_BUFFERED);
output.buffer.drain(..over);
}
}
fn wait_for_room(shared: &Mutex<Output>, limit: usize) -> bool {
loop {
{
let mut output = lock(shared);
if output.closed {
return false;
}
output.paused = output.buffer.len() >= limit;
if !output.paused {
return true;
}
}
thread::sleep(Duration::from_millis(5));
}
}
#[derive(Default)]
struct Replay {
script: Vec<u8>,
script_exit: Option<ExitStatus>,
pending: Vec<u8>,
status: Option<ExitStatus>,
started: Option<(u16, u16)>,
written: Vec<u8>,
sizes: Vec<(u16, u16)>,
killed: bool,
notify: Option<Notify>,
}
#[derive(Default)]
pub struct ReplayHost {
state: Arc<Mutex<Replay>>,
}
impl ReplayHost {
pub fn new() -> ReplayHost {
ReplayHost::default()
}
#[must_use]
pub fn output(self, bytes: impl AsRef<[u8]>) -> ReplayHost {
lock(&self.state).script.extend_from_slice(bytes.as_ref());
self
}
#[must_use]
pub fn exit(self, status: ExitStatus) -> ReplayHost {
lock(&self.state).script_exit = Some(status);
self
}
pub fn handle(&self) -> ReplayHandle {
ReplayHandle {
state: Arc::clone(&self.state),
}
}
}
#[derive(Clone)]
pub struct ReplayHandle {
state: Arc<Mutex<Replay>>,
}
impl ReplayHandle {
fn notify(&self) {
let notify = lock(&self.state).notify.clone();
if let Some(notify) = notify {
notify();
}
}
pub fn feed(&self, bytes: impl AsRef<[u8]>) {
{
let mut state = lock(&self.state);
if state.started.is_some() {
state.pending.extend_from_slice(bytes.as_ref());
} else {
state.script.extend_from_slice(bytes.as_ref());
}
}
self.notify();
}
pub fn exit(&self, status: ExitStatus) {
{
let mut state = lock(&self.state);
if state.started.is_some() {
state.status = Some(status);
} else {
state.script_exit = Some(status);
}
}
self.notify();
}
pub fn written(&self) -> Vec<u8> {
lock(&self.state).written.clone()
}
pub fn take_written(&self) -> Vec<u8> {
std::mem::take(&mut lock(&self.state).written)
}
pub fn started(&self) -> Option<(u16, u16)> {
lock(&self.state).started
}
pub fn sizes(&self) -> Vec<(u16, u16)> {
lock(&self.state).sizes.clone()
}
pub fn killed(&self) -> bool {
lock(&self.state).killed
}
}
impl PtyHost for ReplayHost {
fn start(&mut self, columns: u16, rows: u16) -> io::Result<()> {
{
let mut state = lock(&self.state);
if state.started.is_some() {
return Ok(());
}
state.started = Some((columns, rows));
let script = std::mem::take(&mut state.script);
state.pending.extend_from_slice(&script);
state.status = state.script_exit.take();
}
self.handle().notify();
Ok(())
}
fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
lock(&self.state).written.extend_from_slice(bytes);
Ok(())
}
fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
lock(&self.state).sizes.push((columns, rows));
Ok(())
}
fn read(&mut self) -> Vec<u8> {
std::mem::take(&mut lock(&self.state).pending)
}
fn set_notify(&mut self, notify: Notify) {
let waiting = {
let mut state = lock(&self.state);
state.notify = Some(notify.clone());
!state.pending.is_empty() || state.status.is_some()
};
if waiting {
notify();
}
}
fn exit_status(&mut self) -> Option<ExitStatus> {
let state = lock(&self.state);
if state.pending.is_empty() {
state.status.clone()
} else {
None
}
}
fn kill(&mut self) -> io::Result<()> {
let mut state = lock(&self.state);
state.killed = true;
if state.status.is_none() && state.started.is_some() {
state.status = Some(ExitStatus::with_signal("Killed"));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backpressure_never_drops_output_past_the_legacy_limit() {
let chunk = vec![b'x'; 1 << 20];
let mut held = Output::default();
for _ in 0..(MAX_BUFFERED / chunk.len() + 4) {
keep(&mut held, &chunk, false);
}
assert_eq!(held.buffer.len(), MAX_BUFFERED + 4 * chunk.len());
let mut dropped = Output::default();
for _ in 0..(MAX_BUFFERED / chunk.len() + 4) {
keep(&mut dropped, &chunk, true);
}
assert_eq!(dropped.buffer.len(), MAX_BUFFERED);
}
}