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 fd: RawFd,
23 file: File,
25 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 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 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 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 fn pre_exec() -> io::Result<()> {
210 unsafe {
211 if libc::getpid() == 0 {
212 std::process::exit(0);
213 }
214 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 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
237impl 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 libc::close(fd);
246 libc::kill(pid, libc::SIGTERM);
248 std::thread::sleep(Duration::from_millis(5));
249
250 let mut status = 0;
251 libc::waitpid(pid, &mut status, libc::WNOHANG);
253
254 if status <= 0 {
256 libc::kill(pid, libc::SIGKILL);
258 libc::waitpid(pid, &mut status, 0);
259 }
260 }
261 }
262 }
263}