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;