pty_exec/
lib.rs

1//! ```rust
2//! use std::os::fd::{AsRawFd, FromRawFd};
3//! use pty_exec::Pty;
4//!
5//! // spawn Pty
6//! let pty = Pty::spawn(move |_fd, res| {
7//!     println!("-> {}", res.unwrap());
8//! }, move |fd| {
9//!     println!("-> {fd} died");
10//! })?;
11//!
12//! // (optional) create new pty, this maintains the on_read and on_death callbacks
13//! let pty = unsafe { Pty::from_raw_fd(pty.as_raw_fd()) };
14//!
15//! // write to original pty with new pty from_raw_fd
16//! pty.write("echo 'Hello, World'\r")?;
17//!
18//! pty.kill();
19//! ```
20
21pub mod error;
22mod unix;
23
24pub use error::PtyError;
25use std::error::Error;
26use std::os::fd::{FromRawFd, AsRawFd, RawFd};
27use crate::unix::window::WindowSize;
28
29/// Pty struct that encapsulates pid of our tty
30/// _DOES NOT_ close pty on drop() _ONLY_ on Pty::kill()
31/// this is so that a pty process can outlive this struct
32pub struct Pty {
33    pid: RawFd
34}
35
36impl Pty {
37    /// Spawns a new pty,
38    /// on_read: callback called when there is something to read
39    /// on_death: callback called when there the pty dies
40    pub fn spawn<F, G>(on_read: F, on_death: G) -> Result<Pty, Box<dyn Error>>
41        where
42            F: FnMut(RawFd, Result<String, Box<dyn Error>>) + Send + 'static,
43            G: FnMut(RawFd) + Send + 'static
44    {
45        let master = unix::pty::spawn()?;
46        unix::pty::poll(master, on_read, on_death)?;
47
48        Ok(Pty { pid: master })
49    }
50
51    /// write to pty
52    pub fn write(&self, s: &str) -> Result<(), Box<dyn Error>> {
53        unix::pty::write(self.pid, s.as_bytes())
54    }
55
56    /// resize pty with syscall
57    pub fn resize(&self, window_size: WindowSize) -> Result<(), Box<dyn Error>> {
58        unix::pty::resize(self.pid, window_size)
59    }
60
61    /// kill pty
62    pub fn kill(&self) {
63        unix::pty::kill(self.pid)
64    }
65}
66
67impl FromRawFd for Pty {
68    unsafe fn from_raw_fd(fd: RawFd) -> Self {
69        Pty { pid: fd }
70    }
71}
72
73impl AsRawFd for Pty {
74    fn as_raw_fd(&self) -> RawFd {
75        self.pid
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use std::time::Duration;
82    use std::sync::{Arc, Mutex};
83    use super::*;
84
85    #[test]
86    fn spawn() -> Result<(), Box<dyn Error>> {
87        let read_buf = Arc::new(Mutex::new(String::new()));
88        let die_buf = Arc::new(Mutex::new(String::new()));
89
90        let (read_buf_async, die_buf_async) = (read_buf.clone(), die_buf.clone());
91
92        // spawn Pty
93        let pty = Pty::spawn(move |_fd, res| {
94            read_buf_async.lock().unwrap().push_str(res.unwrap().as_str());
95        }, move |fd| {
96            die_buf_async.lock().unwrap().push_str(format!("{fd} dead").as_str());
97        })?;
98        std::thread::sleep(Duration::from_millis(100));
99
100        // create new pty, this maintains the on_read and on_death callbacks
101        let pty = unsafe { Pty::from_raw_fd(pty.as_raw_fd()) };
102        // write to original pty with new pty from_raw_fd
103        pty.write("echo 'Hello, World'\r")?;
104        std::thread::sleep(Duration::from_millis(100));
105
106        pty.kill();
107        std::thread::sleep(Duration::from_millis(100));
108
109        // read_buf are effected whether using Pty::spawn or Pty::from_raw_fd() on a
110        // pre-existing spawned pty
111        assert!(read_buf.lock().unwrap().contains("echo 'Hello, World'"));
112        assert_eq!(die_buf.lock().unwrap().as_str(), format!("{} dead", pty.as_raw_fd()).as_str());
113
114        Ok(())
115    }
116}