portable_pty/
lib.rs

1//! This crate provides a cross platform API for working with the
2//! psuedo terminal (pty) interfaces provided by the system.
3//! Unlike other crates in this space, this crate provides a set
4//! of traits that allow selecting from different implementations
5//! at runtime.
6//! This crate is part of [wezterm](https://github.com/wez/wezterm).
7//!
8//! ```no_run
9//! use portable_pty::{CommandBuilder, PtySize, native_pty_system, PtySystem};
10//! use anyhow::Error;
11//!
12//! // Use the native pty implementation for the system
13//! let pty_system = native_pty_system();
14//!
15//! // Create a new pty
16//! let mut pair = pty_system.openpty(PtySize {
17//!     rows: 24,
18//!     cols: 80,
19//!     // Not all systems support pixel_width, pixel_height,
20//!     // but it is good practice to set it to something
21//!     // that matches the size of the selected font.  That
22//!     // is more complex than can be shown here in this
23//!     // brief example though!
24//!     pixel_width: 0,
25//!     pixel_height: 0,
26//! })?;
27//!
28//! // Spawn a shell into the pty
29//! let cmd = CommandBuilder::new("bash");
30//! let child = pair.slave.spawn_command(cmd)?;
31//!
32//! // Read and parse output from the pty with reader
33//! let mut reader = pair.master.try_clone_reader()?;
34//!
35//! // Send data to the pty by writing to the master
36//! writeln!(pair.master, "ls -l\r\n")?;
37//! # Ok::<(), Error>(())
38//! ```
39//!
40//! ## ssh2
41//!
42//! If the `ssh` feature is enabled, this crate exposes an
43//! `ssh::SshSession` type that can wrap an established ssh
44//! session with an implementation of `PtySystem`, allowing
45//! you to use the same pty interface with remote ptys.
46use anyhow::Error;
47use downcast_rs::{impl_downcast, Downcast};
48#[cfg(unix)]
49use libc;
50#[cfg(feature = "serde_support")]
51use serde_derive::*;
52use std::io::Result as IoResult;
53#[cfg(windows)]
54use std::os::windows::prelude::{AsRawHandle, RawHandle};
55
56pub mod cmdbuilder;
57pub use cmdbuilder::CommandBuilder;
58
59#[cfg(unix)]
60pub mod unix;
61#[cfg(windows)]
62pub mod win;
63
64#[cfg(feature = "ssh")]
65pub mod ssh;
66
67pub mod serial;
68
69/// Represents the size of the visible display area in the pty
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
72pub struct PtySize {
73  /// The number of lines of text
74  pub rows: u16,
75  /// The number of columns of text
76  pub cols: u16,
77  /// The width of a cell in pixels.  Note that some systems never
78  /// fill this value and ignore it.
79  pub pixel_width: u16,
80  /// The height of a cell in pixels.  Note that some systems never
81  /// fill this value and ignore it.
82  pub pixel_height: u16,
83}
84
85impl Default for PtySize {
86  fn default() -> Self {
87    PtySize {
88      rows: 24,
89      cols: 80,
90      pixel_width: 0,
91      pixel_height: 0,
92    }
93  }
94}
95
96/// Represents the master/control end of the pty
97pub trait MasterPty: std::io::Write {
98  /// Inform the kernel and thus the child process that the window resized.
99  /// It will update the winsize information maintained by the kernel,
100  /// and generate a signal for the child to notice and update its state.
101  fn resize(&self, size: PtySize) -> Result<(), Error>;
102  /// Retrieves the size of the pty as known by the kernel
103  fn get_size(&self) -> Result<PtySize, Error>;
104  /// Obtain a readable handle; output from the slave(s) is readable
105  /// via this stream.
106  fn try_clone_reader(&self) -> Result<Box<dyn std::io::Read + Send>, Error>;
107  /// Obtain a writable handle; writing to it will send data to the
108  /// slave end.  This is equivalent to the Write impl on MasterPty
109  /// itself, but allows splitting it off into a separate object.
110  fn try_clone_writer(&self) -> Result<Box<dyn std::io::Write + Send>, Error>;
111
112  /// If applicable to the type of the tty, return the local process id
113  /// of the process group or session leader
114  #[cfg(unix)]
115  fn process_group_leader(&self) -> Option<libc::pid_t>;
116}
117
118/// Represents a child process spawned into the pty.
119/// This handle can be used to wait for or terminate that child process.
120pub trait Child: std::fmt::Debug + ChildKiller {
121  /// Poll the child to see if it has completed.
122  /// Does not block.
123  /// Returns None if the child has not yet terminated,
124  /// else returns its exit status.
125  fn try_wait(&mut self) -> IoResult<Option<ExitStatus>>;
126  /// Blocks execution until the child process has completed,
127  /// yielding its exit status.
128  fn wait(&mut self) -> IoResult<ExitStatus>;
129  /// Returns the process identifier of the child process,
130  /// if applicable
131  fn process_id(&self) -> Option<u32>;
132  /// Returns the process handle of the child process, if applicable.
133  /// Only available on Windows.
134  #[cfg(windows)]
135  fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle>;
136}
137
138/// Represents the ability to signal a Child to terminate
139pub trait ChildKiller: std::fmt::Debug {
140  /// Terminate the child process
141  fn kill(&mut self) -> IoResult<()>;
142
143  /// Clone an object that can be split out from the Child in order
144  /// to send it signals independently from a thread that may be
145  /// blocked in `.wait`.
146  fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync>;
147}
148
149/// Represents the slave side of a pty.
150/// Can be used to spawn processes into the pty.
151pub trait SlavePty {
152  /// Spawns the command specified by the provided CommandBuilder
153  fn spawn_command(
154    &self,
155    cmd: CommandBuilder,
156  ) -> Result<Box<dyn Child + Send + Sync>, Error>;
157}
158
159/// Represents the exit status of a child process.
160#[derive(Debug, Clone)]
161pub struct ExitStatus {
162  code: u32,
163  signal: Option<String>,
164}
165
166impl ExitStatus {
167  /// Construct an ExitStatus from a process return code
168  pub fn with_exit_code(code: u32) -> Self {
169    Self { code, signal: None }
170  }
171
172  /// Construct an ExitStatus from a signal name
173  pub fn with_signal(signal: &str) -> Self {
174    Self {
175      code: 1,
176      signal: Some(signal.to_string()),
177    }
178  }
179
180  /// Returns true if the status indicates successful completion
181  pub fn success(&self) -> bool {
182    match self.signal {
183      None => self.code == 0,
184      Some(_) => false,
185    }
186  }
187
188  /// Returns the exit code that this ExitStatus was constructed with
189  pub fn exit_code(&self) -> u32 {
190    self.code
191  }
192}
193
194impl From<std::process::ExitStatus> for ExitStatus {
195  fn from(status: std::process::ExitStatus) -> ExitStatus {
196    #[cfg(unix)]
197    {
198      use std::os::unix::process::ExitStatusExt;
199
200      if let Some(signal) = status.signal() {
201        let signame = unsafe { libc::strsignal(signal) };
202        let signal = if signame.is_null() {
203          format!("Signal {}", signal)
204        } else {
205          let signame = unsafe { std::ffi::CStr::from_ptr(signame) };
206          signame.to_string_lossy().to_string()
207        };
208
209        return ExitStatus {
210          code: status.code().map(|c| c as u32).unwrap_or(1),
211          signal: Some(signal),
212        };
213      }
214    }
215
216    let code = status.code().map(|c| c as u32).unwrap_or_else(|| {
217      if status.success() {
218        0
219      } else {
220        1
221      }
222    });
223
224    ExitStatus { code, signal: None }
225  }
226}
227
228impl std::fmt::Display for ExitStatus {
229  fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
230    if self.success() {
231      write!(fmt, "Success")
232    } else {
233      match &self.signal {
234        Some(sig) => write!(fmt, "Terminated by {}", sig),
235        None => write!(fmt, "Exited with code {}", self.code),
236      }
237    }
238  }
239}
240
241pub struct PtyPair {
242  // slave is listed first so that it is dropped first.
243  // The drop order is stable and specified by rust rfc 1857
244  pub slave: Box<dyn SlavePty + Send>,
245  pub master: Box<dyn MasterPty + Send>,
246}
247
248/// The `PtySystem` trait allows an application to work with multiple
249/// possible Pty implementations at runtime.  This is important on
250/// Windows systems which have a variety of implementations.
251pub trait PtySystem: Downcast {
252  /// Create a new Pty instance with the window size set to the specified
253  /// dimensions.  Returns a (master, slave) Pty pair.  The master side
254  /// is used to drive the slave side.
255  fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair>;
256}
257impl_downcast!(PtySystem);
258
259impl Child for std::process::Child {
260  fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> {
261    std::process::Child::try_wait(self).map(|s| match s {
262      Some(s) => Some(s.into()),
263      None => None,
264    })
265  }
266
267  fn wait(&mut self) -> IoResult<ExitStatus> {
268    std::process::Child::wait(self).map(Into::into)
269  }
270
271  fn process_id(&self) -> Option<u32> {
272    Some(self.id())
273  }
274
275  #[cfg(windows)]
276  fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
277    Some(std::os::windows::io::AsRawHandle::as_raw_handle(self))
278  }
279}
280
281#[derive(Debug)]
282struct ProcessSignaller {
283  pid: Option<u32>,
284
285  #[cfg(windows)]
286  handle: Option<filedescriptor::OwnedHandle>,
287}
288
289#[cfg(windows)]
290impl ChildKiller for ProcessSignaller {
291  fn kill(&mut self) -> IoResult<()> {
292    if let Some(handle) = &self.handle {
293      unsafe {
294        if winapi::um::processthreadsapi::TerminateProcess(
295          handle.as_raw_handle() as _,
296          127,
297        ) == 0
298        {
299          return Err(std::io::Error::last_os_error());
300        }
301      }
302    }
303    Ok(())
304  }
305  fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
306    Box::new(Self {
307      pid: self.pid,
308      handle: self.handle.as_ref().and_then(|h| h.try_clone().ok()),
309    })
310  }
311}
312
313#[cfg(unix)]
314impl ChildKiller for ProcessSignaller {
315  fn kill(&mut self) -> IoResult<()> {
316    if let Some(pid) = self.pid {
317      let result = unsafe { libc::kill(pid as i32, libc::SIGHUP) };
318      if result != 0 {
319        return Err(std::io::Error::last_os_error());
320      }
321    }
322    Ok(())
323  }
324
325  fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
326    Box::new(Self { pid: self.pid })
327  }
328}
329
330impl ChildKiller for std::process::Child {
331  fn kill(&mut self) -> IoResult<()> {
332    #[cfg(unix)]
333    {
334      // On unix, we send the SIGHUP signal instead of trying to kill
335      // the process. The default behavior of a process receiving this
336      // signal is to be killed unless it configured a signal handler.
337      let result = unsafe { libc::kill(self.id() as i32, libc::SIGHUP) };
338      if result != 0 {
339        return Err(std::io::Error::last_os_error());
340      }
341
342      // We successfully delivered SIGHUP, but the semantics of Child::kill
343      // are that on success the process is dead or shortly about to
344      // terminate.  Since SIGUP doesn't guarantee termination, we
345      // give the process a bit of a grace period to shutdown or do whatever
346      // it is doing in its signal handler befre we proceed with the
347      // full on kill.
348      for attempt in 0..5 {
349        if attempt > 0 {
350          std::thread::sleep(std::time::Duration::from_millis(50));
351        }
352
353        if let Ok(Some(_)) = self.try_wait() {
354          // It completed, so report success!
355          return Ok(());
356        }
357      }
358
359      // it's still alive after a grace period, so proceed with a kill
360    }
361
362    std::process::Child::kill(self)
363  }
364
365  #[cfg(windows)]
366  fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
367    struct RawDup(RawHandle);
368    impl AsRawHandle for RawDup {
369      fn as_raw_handle(&self) -> RawHandle {
370        self.0
371      }
372    }
373
374    Box::new(ProcessSignaller {
375      pid: self.process_id(),
376      handle: Child::as_raw_handle(self)
377        .as_ref()
378        .and_then(|h| filedescriptor::OwnedHandle::dup(&RawDup(*h)).ok()),
379    })
380  }
381
382  #[cfg(unix)]
383  fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
384    Box::new(ProcessSignaller {
385      pid: self.process_id(),
386    })
387  }
388}
389
390pub fn native_pty_system() -> Box<dyn PtySystem> {
391  Box::new(NativePtySystem::default())
392}
393
394#[cfg(unix)]
395pub type NativePtySystem = unix::UnixPtySystem;
396#[cfg(windows)]
397pub type NativePtySystem = win::conpty::ConPtySystem;