sesh_shared/
pty.rs

1use anyhow::{anyhow, Context, Result};
2use std::{
3    ffi::OsStr,
4    io,
5    os::unix::{
6        io::{FromRawFd, RawFd},
7        process::CommandExt,
8    },
9    process::{Command, Stdio},
10    ptr,
11    time::Duration,
12};
13use tokio::fs::File;
14
15use crate::{error::CResult, term::Size};
16
17const PTY_ERR: &str = "[pty.rs] Failed to open pty";
18const PRG_ERR: &str = "[pty.rs] Failed to spawn shell";
19
20pub struct Pty {
21    /// Master FD
22    fd: RawFd,
23    /// R/W access to the PTY
24    file: File,
25    /// Pid of the child process
26    pid: i32,
27    kill_on_drop: bool,
28}
29
30pub struct PtyBuilder {
31    inner: Command,
32    daemonize: bool,
33}
34
35impl PtyBuilder {
36    pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
37        self.inner.arg(arg);
38        self
39    }
40
41    pub fn args<I, S>(mut self, args: I) -> Self
42    where
43        I: IntoIterator<Item = S>,
44        S: AsRef<OsStr>,
45    {
46        self.inner.args(args);
47        self
48    }
49
50    pub fn env_clear(mut self) -> Self {
51        self.inner.env_clear();
52        self
53    }
54
55    pub fn env<K, V>(mut self, key: K, val: V) -> Self
56    where
57        K: AsRef<OsStr>,
58        V: AsRef<OsStr>,
59    {
60        self.inner.env(key, val);
61        self
62    }
63
64    pub fn envs<I, K, V>(mut self, vars: I) -> Self
65    where
66        I: IntoIterator<Item = (K, V)>,
67        K: AsRef<OsStr>,
68        V: AsRef<OsStr>,
69    {
70        self.inner.envs(vars);
71        self
72    }
73
74    pub fn daemonize(mut self) -> Self {
75        self.daemonize = true;
76        self
77    }
78
79    pub fn kill_on_drop(mut self) -> Self {
80        self.daemonize = false;
81        self
82    }
83
84    pub fn set_daemonize(&mut self, daemonize: bool) {
85        self.daemonize = daemonize;
86    }
87
88    pub fn current_dir<P: AsRef<std::path::Path>>(mut self, dir: P) -> Self {
89        self.inner.current_dir(dir);
90        self
91    }
92
93    pub fn spawn(self, size: &Size) -> Result<Pty> {
94        let (master, slave) = Pty::open(size)?;
95
96        let mut cmd = self.inner;
97
98        cmd.stdin(unsafe { Stdio::from_raw_fd(slave) })
99            .stdout(unsafe { Stdio::from_raw_fd(slave) })
100            .stderr(unsafe { Stdio::from_raw_fd(slave) });
101
102        unsafe {
103            cmd.pre_exec(Pty::pre_exec);
104        }
105        cmd.spawn().map_err(|_| anyhow!(PRG_ERR)).and_then(|e| {
106            let pty = Pty {
107                fd: master,
108                file: unsafe { File::from_raw_fd(master) },
109                pid: e.id() as i32,
110                kill_on_drop: !self.daemonize,
111            };
112
113            pty.resize(size)?;
114
115            Ok(pty)
116        })
117    }
118}
119
120impl Pty {
121    pub fn builder(program: impl AsRef<str>) -> PtyBuilder {
122        PtyBuilder {
123            inner: Command::new(program.as_ref()),
124            daemonize: false,
125        }
126    }
127
128    pub fn spawn(program: &str, args: Vec<String>, size: &Size) -> Result<Pty> {
129        Pty::builder(program).args(args).spawn(size)
130    }
131
132    pub fn daemonize(&mut self) {
133        self.kill_on_drop = false;
134    }
135
136    pub fn pid(&self) -> i32 {
137        self.pid
138    }
139
140    pub fn file(&self) -> &File {
141        &self.file
142    }
143
144    pub fn fd(&self) -> RawFd {
145        self.fd
146    }
147
148    /// Resizes the child pty.
149    pub fn resize(&self, size: &Size) -> Result<()> {
150        unsafe {
151            libc::ioctl(
152                self.fd,
153                libc::TIOCSWINSZ,
154                &libc::winsize {
155                    ws_row: size.rows,
156                    ws_col: size.cols,
157                    ws_xpixel: 0,
158                    ws_ypixel: 0,
159                },
160            )
161            .to_result()
162            .map(|_| ())
163            .context(PTY_ERR)
164        }
165    }
166
167    /// Creates a pty with the given size and returns the (master, slave)
168    /// file descriptors attached to it.
169    pub fn open(size: &Size) -> Result<(RawFd, RawFd)> {
170        let mut master = 0;
171        let mut slave = 0;
172
173        unsafe {
174            #[cfg(target_arch = "aarch64")]
175            libc::openpty(
176                &mut master,
177                &mut slave,
178                ptr::null_mut(),
179                ptr::null_mut(),
180                &mut size.into(),
181            )
182            .to_result()
183            .context(PTY_ERR)?;
184            #[cfg(not(target_arch = "aarch64"))]
185            libc::openpty(
186                &mut master,
187                &mut slave,
188                ptr::null_mut(),
189                ptr::null_mut(),
190                &size.into(),
191            )
192            .to_result()
193            .context(PTY_ERR)?;
194
195            // Configure master to be non blocking
196            let current_config = libc::fcntl(master, libc::F_GETFL, 0)
197                .to_result()
198                .context(PTY_ERR)?;
199
200            libc::fcntl(master, libc::F_SETFL, current_config)
201                .to_result()
202                .context(PTY_ERR)?;
203        }
204
205        Ok((master, slave))
206    }
207
208    // Runs between fork and exec calls
209    fn pre_exec() -> io::Result<()> {
210        unsafe {
211            if libc::getpid() == 0 {
212                std::process::exit(0);
213            }
214            // Create a new process group, this process being the master
215            libc::setsid().to_result().map_err(|e| {
216                io::Error::new(
217                    io::ErrorKind::Other,
218                    format!("Failed to create process group: {}", e),
219                )
220            })?;
221
222            // Set this process as the controling terminal
223            libc::ioctl(0, libc::TIOCSCTTY, 1)
224                .to_result()
225                .map_err(|e| {
226                    io::Error::new(
227                        io::ErrorKind::Other,
228                        format!("Failed to set controlling terminal: {}", e),
229                    )
230                })?;
231        }
232
233        Ok(())
234    }
235}
236
237/// Handle cleanup automatically
238impl Drop for Pty {
239    fn drop(&mut self) {
240        unsafe {
241            if self.kill_on_drop {
242                let fd = self.fd;
243                let pid = self.pid;
244                // Close file descriptor
245                libc::close(fd);
246                // Kill the owned processed when the Pty is dropped
247                libc::kill(pid, libc::SIGTERM);
248                std::thread::sleep(Duration::from_millis(5));
249
250                let mut status = 0;
251                // make sure the process has exited
252                libc::waitpid(pid, &mut status, libc::WNOHANG);
253
254                // if it hasn't exited, force kill it and clean up the zombie process
255                if status <= 0 {
256                    // The process exists but hasn't changed state, or there was an error
257                    libc::kill(pid, libc::SIGKILL);
258                    libc::waitpid(pid, &mut status, 0);
259                }
260            }
261        }
262    }
263}