mirror of
https://github.com/wez/wezterm.git
synced 2024-12-28 07:55:03 +03:00
3f6ff534d3
Since removing the regular periodic background tasks, we're now prone to not noticing child processes exiting. This commit explicictly schedules a thread to do that on Windows so that we can close a tab as soon as it exits.
254 lines
8.5 KiB
Rust
254 lines
8.5 KiB
Rust
//! This crate provides a cross platform API for working with the
|
|
//! psuedo terminal (pty) interfaces provided by the system.
|
|
//! Unlike other crates in this space, this crate provides a set
|
|
//! of traits that allow selecting from different implementations
|
|
//! at runtime.
|
|
//! This crate is part of [wezterm](https://github.com/wez/wezterm).
|
|
//!
|
|
//! ```no_run
|
|
//! use portable_pty::{CommandBuilder, PtySize, native_pty_system, PtySystem};
|
|
//! use anyhow::Error;
|
|
//!
|
|
//! // Use the native pty implementation for the system
|
|
//! let pty_system = native_pty_system();
|
|
//!
|
|
//! // Create a new pty
|
|
//! let mut pair = pty_system.openpty(PtySize {
|
|
//! rows: 24,
|
|
//! cols: 80,
|
|
//! // Not all systems support pixel_width, pixel_height,
|
|
//! // but it is good practice to set it to something
|
|
//! // that matches the size of the selected font. That
|
|
//! // is more complex than can be shown here in this
|
|
//! // brief example though!
|
|
//! pixel_width: 0,
|
|
//! pixel_height: 0,
|
|
//! })?;
|
|
//!
|
|
//! // Spawn a shell into the pty
|
|
//! let cmd = CommandBuilder::new("bash");
|
|
//! let child = pair.slave.spawn_command(cmd)?;
|
|
//!
|
|
//! // Read and parse output from the pty with reader
|
|
//! let mut reader = pair.master.try_clone_reader()?;
|
|
//!
|
|
//! // Send data to the pty by writing to the master
|
|
//! writeln!(pair.master, "ls -l\r\n")?;
|
|
//! # Ok::<(), Error>(())
|
|
//! ```
|
|
//!
|
|
//! ## ssh2
|
|
//!
|
|
//! If the `ssh` feature is enabled, this crate exposes an
|
|
//! `ssh::SshSession` type that can wrap an established ssh
|
|
//! session with an implementation of `PtySystem`, allowing
|
|
//! you to use the same pty interface with remote ptys.
|
|
use anyhow::Error;
|
|
#[cfg(unix)]
|
|
use libc;
|
|
#[cfg(feature = "serde_support")]
|
|
use serde_derive::*;
|
|
use std::io::Result as IoResult;
|
|
|
|
pub mod cmdbuilder;
|
|
pub use cmdbuilder::CommandBuilder;
|
|
|
|
#[cfg(unix)]
|
|
pub mod unix;
|
|
#[cfg(windows)]
|
|
pub mod win;
|
|
|
|
#[cfg(feature = "ssh")]
|
|
pub mod ssh;
|
|
|
|
pub mod serial;
|
|
|
|
/// Represents the size of the visible display area in the pty
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
|
pub struct PtySize {
|
|
/// The number of lines of text
|
|
pub rows: u16,
|
|
/// The number of columns of text
|
|
pub cols: u16,
|
|
/// The width of a cell in pixels. Note that some systems never
|
|
/// fill this value and ignore it.
|
|
pub pixel_width: u16,
|
|
/// The height of a cell in pixels. Note that some systems never
|
|
/// fill this value and ignore it.
|
|
pub pixel_height: u16,
|
|
}
|
|
|
|
impl Default for PtySize {
|
|
fn default() -> Self {
|
|
PtySize {
|
|
rows: 24,
|
|
cols: 80,
|
|
pixel_width: 0,
|
|
pixel_height: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents the master/control end of the pty
|
|
pub trait MasterPty: std::io::Write {
|
|
/// Inform the kernel and thus the child process that the window resized.
|
|
/// It will update the winsize information maintained by the kernel,
|
|
/// and generate a signal for the child to notice and update its state.
|
|
fn resize(&self, size: PtySize) -> Result<(), Error>;
|
|
/// Retrieves the size of the pty as known by the kernel
|
|
fn get_size(&self) -> Result<PtySize, Error>;
|
|
/// Obtain a readable handle; output from the slave(s) is readable
|
|
/// via this stream.
|
|
fn try_clone_reader(&self) -> Result<Box<dyn std::io::Read + Send>, Error>;
|
|
/// Obtain a writable handle; writing to it will send data to the
|
|
/// slave end. This is equivalent to the Write impl on MasterPty
|
|
/// itself, but allows splitting it off into a separate object.
|
|
fn try_clone_writer(&self) -> Result<Box<dyn std::io::Write + Send>, Error>;
|
|
|
|
/// If applicable to the type of the tty, return the local process id
|
|
/// of the process group or session leader
|
|
#[cfg(unix)]
|
|
fn process_group_leader(&self) -> Option<libc::pid_t>;
|
|
}
|
|
|
|
/// Represents a child process spawned into the pty.
|
|
/// This handle can be used to wait for or terminate that child process.
|
|
pub trait Child: std::fmt::Debug {
|
|
/// Poll the child to see if it has completed.
|
|
/// Does not block.
|
|
/// Returns None if the child has not yet terminated,
|
|
/// else returns its exit status.
|
|
fn try_wait(&mut self) -> IoResult<Option<ExitStatus>>;
|
|
/// Terminate the child process
|
|
fn kill(&mut self) -> IoResult<()>;
|
|
/// Blocks execution until the child process has completed,
|
|
/// yielding its exit status.
|
|
fn wait(&mut self) -> IoResult<ExitStatus>;
|
|
/// Returns the process identifier of the child process,
|
|
/// if applicable
|
|
fn process_id(&self) -> Option<u32>;
|
|
/// Returns the process handle of the child process, if applicable.
|
|
/// Only available on Windows.
|
|
#[cfg(windows)]
|
|
fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle>;
|
|
}
|
|
|
|
/// Represents the slave side of a pty.
|
|
/// Can be used to spawn processes into the pty.
|
|
pub trait SlavePty {
|
|
/// Spawns the command specified by the provided CommandBuilder
|
|
fn spawn_command(&self, cmd: CommandBuilder) -> Result<Box<dyn Child + Send + Sync>, Error>;
|
|
}
|
|
|
|
/// Represents the exit status of a child process.
|
|
/// This is rather anemic in the current version of this crate,
|
|
/// holding only an indicator of success or failure.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExitStatus {
|
|
successful: bool,
|
|
}
|
|
|
|
impl ExitStatus {
|
|
/// Construct an ExitStatus from a process return code
|
|
pub fn with_exit_code(code: u32) -> Self {
|
|
Self {
|
|
successful: code == 0,
|
|
}
|
|
}
|
|
|
|
pub fn success(&self) -> bool {
|
|
self.successful
|
|
}
|
|
}
|
|
|
|
impl From<std::process::ExitStatus> for ExitStatus {
|
|
fn from(status: std::process::ExitStatus) -> ExitStatus {
|
|
ExitStatus {
|
|
successful: status.success(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct PtyPair {
|
|
// slave is listed first so that it is dropped first.
|
|
// The drop order is stable and specified by rust rfc 1857
|
|
pub slave: Box<dyn SlavePty + Send>,
|
|
pub master: Box<dyn MasterPty + Send>,
|
|
}
|
|
|
|
/// The `PtySystem` trait allows an application to work with multiple
|
|
/// possible Pty implementations at runtime. This is important on
|
|
/// Windows systems which have a variety of implementations.
|
|
pub trait PtySystem {
|
|
/// Create a new Pty instance with the window size set to the specified
|
|
/// dimensions. Returns a (master, slave) Pty pair. The master side
|
|
/// is used to drive the slave side.
|
|
fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair>;
|
|
}
|
|
|
|
impl Child for std::process::Child {
|
|
fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> {
|
|
std::process::Child::try_wait(self).map(|s| match s {
|
|
Some(s) => Some(s.into()),
|
|
None => None,
|
|
})
|
|
}
|
|
|
|
fn kill(&mut self) -> IoResult<()> {
|
|
#[cfg(unix)]
|
|
{
|
|
// On unix, we send the SIGHUP signal instead of trying to kill
|
|
// the process. The default behavior of a process receiving this
|
|
// signal is to be killed unless it configured a signal handler.
|
|
let result = unsafe { libc::kill(self.id() as i32, libc::SIGHUP) };
|
|
if result != 0 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
|
|
// We successfully delivered SIGHUP, but the semantics of Child::kill
|
|
// are that on success the process is dead or shortly about to
|
|
// terminate. Since SIGUP doesn't guarantee termination, we
|
|
// give the process a bit of a grace period to shutdown or do whatever
|
|
// it is doing in its signal handler befre we proceed with the
|
|
// full on kill.
|
|
for attempt in 0..5 {
|
|
if attempt > 0 {
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
}
|
|
|
|
if let Ok(Some(_)) = self.try_wait() {
|
|
// It completed, so report success!
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// it's still alive after a grace period, so proceed with a kill
|
|
}
|
|
|
|
std::process::Child::kill(self)
|
|
}
|
|
|
|
fn wait(&mut self) -> IoResult<ExitStatus> {
|
|
std::process::Child::wait(self).map(Into::into)
|
|
}
|
|
|
|
fn process_id(&self) -> Option<u32> {
|
|
Some(self.id())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
|
|
Some(std::os::windows::io::AsRawHandle::as_raw_handle(self))
|
|
}
|
|
}
|
|
|
|
pub fn native_pty_system() -> Box<dyn PtySystem> {
|
|
Box::new(NativePtySystem::default())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub type NativePtySystem = unix::UnixPtySystem;
|
|
#[cfg(windows)]
|
|
pub type NativePtySystem = win::conpty::ConPtySystem;
|