1use libc::TIOCSCTTY;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::ffi::{OsStr, OsString};
5use std::os::fd::AsRawFd as _;
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8use tokio::fs::File;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10
11#[derive(Debug, Serialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Output {
14 Pid(u32),
15 Stdout(Vec<u8>),
16 Error(String),
17 Terminated(Option<i32>),
18}
19
20#[derive(Debug, Deserialize, Eq, PartialEq)]
21pub enum Input {
22 Data(Vec<u8>),
23 Resize((usize, usize)),
24 Terminate,
25}
26
27const BUF_SIZE: usize = 8192;
28
29fn set_term_size(
30 fd: i32,
31 term_size: (usize, usize),
32) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
33 let ws = libc::winsize {
34 ws_row: u16::try_from(term_size.1)?,
35 ws_col: u16::try_from(term_size.0)?,
36 ws_xpixel: 0,
37 ws_ypixel: 0,
38 };
39
40 if unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &ws) } != 0 {
41 return Err("ioctl".into());
42 }
43
44 Ok(())
45}
46
47pub struct Command {
48 pid: Option<u32>,
49 program: OsString,
50 args: Vec<OsString>,
51 env: BTreeMap<OsString, OsString>,
52 current_dir: Option<PathBuf>,
53 in_tx: async_channel::Sender<Input>,
54 in_rx: async_channel::Receiver<Input>,
55 out_tx: async_channel::Sender<Output>,
56 out_rx: async_channel::Receiver<Output>,
57 terminal_id: String,
58 terminal_size: (usize, usize),
59}
60
61impl Command {
62 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
63 let (in_tx, in_rx) = async_channel::bounded(BUF_SIZE);
64 let (out_tx, out_rx) = async_channel::bounded(BUF_SIZE);
65 Self {
66 pid: None,
67 program: program.as_ref().to_os_string(),
68 args: <_>::default(),
69 env: <_>::default(),
70 current_dir: None,
71 in_tx,
72 in_rx,
73 out_tx,
74 out_rx,
75 terminal_id: "screen-256color".to_string(),
76 terminal_size: (80, 24),
77 }
78 }
79 pub fn in_tx(&self) -> async_channel::Sender<Input> {
80 self.in_tx.clone()
81 }
82 pub fn out_rx(&self) -> async_channel::Receiver<Output> {
83 self.out_rx.clone()
84 }
85 pub fn terminal_id<S: Into<String>>(mut self, terminal_id: S) -> Self {
86 self.terminal_id = terminal_id.into();
87 self
88 }
89 pub fn terminal_size(mut self, terminal_size: (usize, usize)) -> Self {
90 self.terminal_size = terminal_size;
91 self
92 }
93 pub fn args<I, S>(mut self, args: I) -> Self
94 where
95 I: IntoIterator<Item = S>,
96 S: AsRef<OsStr>,
97 {
98 self.args = args
99 .into_iter()
100 .map(|s| s.as_ref().to_os_string())
101 .collect();
102 self
103 }
104 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
105 self.args.push(arg.as_ref().to_os_string());
106 self
107 }
108 pub fn envs<I, K, V>(mut self, env: I) -> Self
109 where
110 I: IntoIterator<Item = (K, V)>,
111 K: AsRef<OsStr>,
112 V: AsRef<OsStr>,
113 {
114 self.env = env
115 .into_iter()
116 .map(|(k, v)| (k.as_ref().to_os_string(), v.as_ref().to_os_string()))
117 .collect();
118 self
119 }
120 pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, value: V) -> Self {
121 self.env
122 .insert(key.as_ref().to_os_string(), value.as_ref().to_os_string());
123 self
124 }
125 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
126 self.current_dir = Some(current_dir.as_ref().to_path_buf());
127 self
128 }
129 #[allow(clippy::too_many_arguments)]
130 pub async fn run(self) {
131 let out_tx = self.out_tx.clone();
132 match self.run_subprocess().await {
133 Ok(v) => {
134 out_tx.send(Output::Terminated(v)).await.ok();
135 }
136 Err(e) => {
137 out_tx.send(Output::Error(e.to_string())).await.ok();
138 }
139 }
140 }
141
142 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
143 async fn run_subprocess(
144 mut self,
145 ) -> Result<Option<i32>, Box<dyn std::error::Error + Send + Sync + 'static>> {
146 let win_size = rustix::termios::Winsize {
147 ws_col: self.terminal_size.0.try_into()?,
148 ws_row: self.terminal_size.1.try_into()?,
149 ws_xpixel: 0,
150 ws_ypixel: 0,
151 };
152 let pty = rustix_openpty::openpty(None, Some(&win_size))?;
153 let (master, slave) = (pty.controller, pty.user);
154
155 let master_fd = master.as_raw_fd();
156 let slave_fd = slave.as_raw_fd();
157
158 if let Ok(mut termios) = rustix::termios::tcgetattr(&master) {
159 termios
161 .input_modes
162 .set(rustix::termios::InputModes::IUTF8, true);
163 let _ = rustix::termios::tcsetattr(
164 &master,
165 rustix::termios::OptionalActions::Now,
166 &termios,
167 );
168 }
169
170 let mut builder = tokio::process::Command::new(&self.program);
171
172 if let Some(ref current_dir) = self.current_dir {
173 builder.current_dir(current_dir);
174 }
175
176 builder
177 .args(&self.args)
178 .envs(&self.env)
179 .env("COLUMNS", self.terminal_size.0.to_string())
180 .env("LINES", self.terminal_size.1.to_string())
181 .env("TERM", &self.terminal_id);
182
183 builder.stdin(slave.try_clone()?);
184 builder.stderr(slave.try_clone()?);
185 builder.stdout(slave);
186
187 unsafe {
188 builder.pre_exec(move || {
189 let err = libc::setsid();
190 if err == -1 {
191 return Err(std::io::Error::new(
192 std::io::ErrorKind::Other,
193 "Failed to set session id",
194 ));
195 }
196
197 let res = libc::ioctl(slave_fd, TIOCSCTTY as _, 0);
198 if res == -1 {
199 return Err(std::io::Error::new(
200 std::io::ErrorKind::Other,
201 format!("Failed to set controlling terminal: {}", res),
202 ));
203 }
204
205 libc::close(slave_fd);
206 libc::close(master_fd);
207
208 libc::signal(libc::SIGCHLD, libc::SIG_DFL);
209 libc::signal(libc::SIGHUP, libc::SIG_DFL);
210 libc::signal(libc::SIGINT, libc::SIG_DFL);
211 libc::signal(libc::SIGQUIT, libc::SIG_DFL);
212 libc::signal(libc::SIGTERM, libc::SIG_DFL);
213 libc::signal(libc::SIGALRM, libc::SIG_DFL);
214
215 Ok(())
216 });
217 }
218
219 let mut child = builder.spawn()?;
220
221 let pid = child.id().ok_or("unable to get child pid")?;
222
223 self.out_tx.send(Output::Pid(pid)).await?;
224 self.pid = Some(pid);
225
226 let mut stdout = File::from_std(std::fs::File::from(master));
227
228 let mut stdin = stdout.try_clone().await?;
229
230 let tx_stdout = self.out_tx.clone();
231
232 let fut_out = tokio::spawn(async move {
233 let mut buf = [0u8; BUF_SIZE];
234 while let Ok(b) = stdout.read(&mut buf).await {
235 if b == 0 {
236 break;
237 }
238 if tx_stdout
239 .send(Output::Stdout(buf[..b].to_vec()))
240 .await
241 .is_err()
242 {
243 break;
244 }
245 }
246 });
247
248 let fut_in = tokio::spawn(async move {
249 while let Ok(input) = self.in_rx.recv().await {
250 let mut data = match input {
251 Input::Data(d) => d,
252 Input::Resize(size) => {
253 set_term_size(stdin.as_raw_fd(), size).ok();
254 bmart::process::kill_pstree_with_signal(
255 pid,
256 bmart::process::Signal::SIGWINCH,
257 true,
258 );
259 continue;
260 }
261 Input::Terminate => {
262 break;
263 }
264 };
265 if data == [0x0a] {
267 data[0] = 0x0d;
268 }
269 if stdin.write_all(&data).await.is_err() {
270 break;
271 }
272 }
273 });
274
275 let result = child.wait().await?;
276
277 fut_out.abort();
278 fut_in.abort();
279
280 let exit_code = result.code();
281
282 Ok(exit_code)
283 }
284}
285
286impl Drop for Command {
287 fn drop(&mut self) {
288 if let Some(pid) = self.pid {
289 tokio::spawn(bmart::process::kill_pstree(
290 pid,
291 Some(Duration::from_secs(1)),
292 true,
293 ));
294 }
295 }
296}