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