Skip to main content

xpty/
lib.rs

1//! Cross-platform async-ready PTY interface.
2//!
3//! This crate provides a cross platform API for working with the
4//! pseudo terminal (pty) interfaces provided by the system.
5//! Unlike other crates in this space, this crate provides a set
6//! of traits that allow selecting from different implementations
7//! at runtime.
8//!
9//! Forked from [portable-pty](https://github.com/wezterm/wezterm/tree/main/pty)
10//! (part of wezterm).
11//!
12//! ```no_run
13//! use xpty::{CommandBuilder, PtySize, native_pty_system, PtySystem};
14//!
15//! // Use the native pty implementation for the system
16//! let pty_system = native_pty_system();
17//!
18//! // Create a new pty
19//! let mut pair = pty_system.openpty(PtySize {
20//!     rows: 24,
21//!     cols: 80,
22//!     pixel_width: 0,
23//!     pixel_height: 0,
24//! })?;
25//!
26//! // Spawn a shell into the pty
27//! let cmd = CommandBuilder::new("bash");
28//! let child = pair.slave.spawn_command(cmd)?;
29//!
30//! // Read and parse output from the pty with reader
31//! let mut reader = pair.master.try_clone_reader()?;
32//!
33//! // Send data to the pty by writing to the master
34//! writeln!(pair.master.take_writer()?, "ls -l\r\n")?;
35//! # Ok::<(), xpty::Error>(())
36//! ```
37//!
38pub mod error;
39pub use error::{Error, Result};
40
41pub mod cmdbuilder;
42pub use cmdbuilder::CommandBuilder;
43
44#[cfg(unix)]
45pub mod unix;
46#[cfg(windows)]
47pub mod win;
48
49#[cfg(feature = "serial")]
50pub mod serial;
51
52use downcast_rs::{impl_downcast, Downcast};
53#[cfg(feature = "serde_support")]
54use serde::{Deserialize, Serialize};
55use std::io::Result as IoResult;
56
57/// Represents the size of the visible display area in the pty.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
60pub struct PtySize {
61    /// The number of lines of text
62    pub rows: u16,
63    /// The number of columns of text
64    pub cols: u16,
65    /// The width of a cell in pixels.  Note that some systems never
66    /// fill this value and ignore it.
67    pub pixel_width: u16,
68    /// The height of a cell in pixels.  Note that some systems never
69    /// fill this value and ignore it.
70    pub pixel_height: u16,
71}
72
73impl Default for PtySize {
74    fn default() -> Self {
75        PtySize {
76            rows: 24,
77            cols: 80,
78            pixel_width: 0,
79            pixel_height: 0,
80        }
81    }
82}
83
84/// Represents the master/control end of the pty.
85///
86/// All methods on this trait are cross-platform. Platform-specific
87/// extensions are available via [`MasterPtyExt`] (unix only).
88pub trait MasterPty: Downcast + Send {
89    /// Inform the kernel and thus the child process that the window resized.
90    fn resize(&self, size: PtySize) -> Result<()>;
91    /// Retrieves the size of the pty as known by the kernel.
92    fn get_size(&self) -> Result<PtySize>;
93    /// Obtain a readable handle; output from the slave(s) is readable
94    /// via this stream.
95    fn try_clone_reader(&self) -> Result<Box<dyn std::io::Read + Send>>;
96    /// Obtain a writable handle; writing to it will send data to the
97    /// slave end.  Dropping the writer will send EOF to the slave end.
98    /// It is invalid to take the writer more than once.
99    fn take_writer(&self) -> Result<Box<dyn std::io::Write + Send>>;
100
101    /// If applicable, return the local process id of the process group
102    /// or session leader.  Returns `None` on non-Unix platforms.
103    fn process_group_leader(&self) -> Option<i32> {
104        None
105    }
106
107    /// If applicable, return the raw file descriptor of the master pty.
108    /// Returns `None` on non-Unix platforms.
109    fn as_raw_fd(&self) -> Option<i32> {
110        None
111    }
112
113    /// Returns the TTY device name (e.g., `/dev/pts/0`).
114    /// Returns `None` on non-Unix platforms.
115    fn tty_name(&self) -> Option<std::path::PathBuf> {
116        None
117    }
118}
119impl_downcast!(MasterPty);
120
121/// Unix-specific extensions for [`MasterPty`].
122///
123/// Provides access to termios settings, which requires platform-specific types.
124#[cfg(unix)]
125pub trait MasterPtyExt {
126    /// If applicable, return the termios associated with the stream.
127    fn get_termios(&self) -> Option<nix::sys::termios::Termios> {
128        None
129    }
130}
131
132/// Represents a child process spawned into the pty.
133pub trait Child: std::fmt::Debug + ChildKiller + Downcast + Send {
134    /// Poll the child to see if it has completed.  Does not block.
135    fn try_wait(&mut self) -> IoResult<Option<ExitStatus>>;
136    /// Blocks execution until the child process has completed.
137    fn wait(&mut self) -> IoResult<ExitStatus>;
138    /// Returns the process identifier of the child process, if applicable.
139    fn process_id(&self) -> Option<u32>;
140    /// Returns the process handle of the child process (Windows only).
141    #[cfg(windows)]
142    fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle>;
143}
144impl_downcast!(Child);
145
146/// Represents the ability to signal a Child to terminate.
147pub trait ChildKiller: std::fmt::Debug + Downcast + Send {
148    /// Terminate the child process.
149    fn kill(&mut self) -> IoResult<()>;
150
151    /// Clone an object that can be split out from the Child in order
152    /// to send it signals independently from a thread that may be
153    /// blocked in `.wait`.
154    fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync>;
155}
156impl_downcast!(ChildKiller);
157
158/// Represents the slave side of a pty.
159/// Can be used to spawn processes into the pty.
160pub trait SlavePty: Send {
161    /// Spawns the command specified by the provided CommandBuilder.
162    fn spawn_command(&self, cmd: CommandBuilder) -> Result<Box<dyn Child + Send + Sync>>;
163}
164
165/// Represents the exit status of a child process.
166#[derive(Debug, Clone)]
167pub struct ExitStatus {
168    code: u32,
169    signal: Option<String>,
170}
171
172impl ExitStatus {
173    /// Construct an ExitStatus from a process return code.
174    pub fn with_exit_code(code: u32) -> Self {
175        Self { code, signal: None }
176    }
177
178    /// Construct an ExitStatus from a signal name.
179    pub fn with_signal(signal: &str) -> Self {
180        Self {
181            code: 1,
182            signal: Some(signal.to_string()),
183        }
184    }
185
186    /// Returns true if the status indicates successful completion.
187    pub fn success(&self) -> bool {
188        self.signal.is_none() && self.code == 0
189    }
190
191    /// Returns the exit code.
192    pub fn exit_code(&self) -> u32 {
193        self.code
194    }
195
196    /// Returns the signal name if present.
197    pub fn signal(&self) -> Option<&str> {
198        self.signal.as_deref()
199    }
200}
201
202impl From<std::process::ExitStatus> for ExitStatus {
203    fn from(status: std::process::ExitStatus) -> ExitStatus {
204        #[cfg(unix)]
205        {
206            use std::os::unix::process::ExitStatusExt;
207
208            if let Some(signal) = status.signal() {
209                let signame = unsafe { libc::strsignal(signal) };
210                let signal = if signame.is_null() {
211                    format!("Signal {}", signal)
212                } else {
213                    let signame = unsafe { std::ffi::CStr::from_ptr(signame) };
214                    signame.to_string_lossy().to_string()
215                };
216
217                return ExitStatus {
218                    code: status.code().map(|c| c as u32).unwrap_or(1),
219                    signal: Some(signal),
220                };
221            }
222        }
223
224        let code =
225            status
226                .code()
227                .map(|c| c as u32)
228                .unwrap_or_else(|| if status.success() { 0 } else { 1 });
229
230        ExitStatus { code, signal: None }
231    }
232}
233
234impl std::fmt::Display for ExitStatus {
235    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
236        if self.success() {
237            write!(fmt, "Success")
238        } else {
239            match &self.signal {
240                Some(sig) => write!(fmt, "Terminated by {}", sig),
241                None => write!(fmt, "Exited with code {}", self.code),
242            }
243        }
244    }
245}
246
247/// A pair of master and slave PTY handles.
248pub struct PtyPair {
249    // slave is listed first so that it is dropped first.
250    // The drop order is stable and specified by rust rfc 1857
251    pub slave: Box<dyn SlavePty>,
252    pub master: Box<dyn MasterPty + Send>,
253}
254
255/// The `PtySystem` trait allows an application to work with multiple
256/// possible Pty implementations at runtime.
257pub trait PtySystem: Downcast {
258    /// Create a new Pty instance with the window size set to the specified
259    /// dimensions.  Returns a (master, slave) Pty pair.
260    fn openpty(&self, size: PtySize) -> Result<PtyPair>;
261}
262impl_downcast!(PtySystem);
263
264impl Child for std::process::Child {
265    fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> {
266        std::process::Child::try_wait(self).map(|s| s.map(Into::into))
267    }
268
269    fn wait(&mut self) -> IoResult<ExitStatus> {
270        std::process::Child::wait(self).map(Into::into)
271    }
272
273    fn process_id(&self) -> Option<u32> {
274        Some(self.id())
275    }
276
277    #[cfg(windows)]
278    fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
279        Some(std::os::windows::io::AsRawHandle::as_raw_handle(self))
280    }
281}
282
283#[derive(Debug)]
284struct ProcessSignaller {
285    pid: Option<u32>,
286
287    #[cfg(windows)]
288    handle: Option<filedescriptor::OwnedHandle>,
289}
290
291#[cfg(windows)]
292impl ChildKiller for ProcessSignaller {
293    fn kill(&mut self) -> IoResult<()> {
294        if let Some(handle) = &self.handle {
295            use std::os::windows::io::AsRawHandle;
296            unsafe {
297                if windows_sys::Win32::System::Threading::TerminateProcess(
298                    handle.as_raw_handle(),
299                    127,
300                ) == 0
301                {
302                    return Err(std::io::Error::last_os_error());
303                }
304            }
305        }
306        Ok(())
307    }
308    fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
309        Box::new(Self {
310            pid: self.pid,
311            handle: self.handle.as_ref().and_then(|h| h.try_clone().ok()),
312        })
313    }
314}
315
316#[cfg(unix)]
317impl ChildKiller for ProcessSignaller {
318    fn kill(&mut self) -> IoResult<()> {
319        if let Some(pid) = self.pid {
320            let result = unsafe { libc::kill(pid as i32, libc::SIGHUP) };
321            if result != 0 {
322                return Err(std::io::Error::last_os_error());
323            }
324        }
325        Ok(())
326    }
327
328    fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
329        Box::new(Self { pid: self.pid })
330    }
331}
332
333impl ChildKiller for std::process::Child {
334    fn kill(&mut self) -> IoResult<()> {
335        #[cfg(unix)]
336        {
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            for attempt in 0..5 {
343                if attempt > 0 {
344                    std::thread::sleep(std::time::Duration::from_millis(50));
345                }
346                if let Ok(Some(_)) = self.try_wait() {
347                    return Ok(());
348                }
349            }
350        }
351
352        std::process::Child::kill(self)
353    }
354
355    #[cfg(windows)]
356    fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
357        use std::os::windows::io::AsRawHandle;
358        struct RawDup(std::os::windows::io::RawHandle);
359        impl AsRawHandle for RawDup {
360            fn as_raw_handle(&self) -> std::os::windows::io::RawHandle {
361                self.0
362            }
363        }
364
365        Box::new(ProcessSignaller {
366            pid: self.process_id(),
367            handle: Child::as_raw_handle(self)
368                .as_ref()
369                .and_then(|h| filedescriptor::OwnedHandle::dup(&RawDup(*h)).ok()),
370        })
371    }
372
373    #[cfg(unix)]
374    fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
375        Box::new(ProcessSignaller {
376            pid: self.process_id(),
377        })
378    }
379}
380
381/// Returns a `NativePtySystem` for the current platform.
382///
383/// If you need a trait object (e.g., for runtime dispatch or mock injection),
384/// use [`native_pty_system_boxed`] instead.
385pub fn native_pty_system() -> NativePtySystem {
386    NativePtySystem::default()
387}
388
389/// Returns the native PTY system as a boxed trait object.
390///
391/// Useful when you need runtime polymorphism, e.g., swapping in a mock
392/// implementation for testing.
393pub fn native_pty_system_boxed() -> Box<dyn PtySystem + Send> {
394    Box::new(NativePtySystem::default())
395}
396
397#[cfg(unix)]
398pub type NativePtySystem = unix::UnixPtySystem;
399#[cfg(windows)]
400pub type NativePtySystem = win::conpty::ConPtySystem;