1use core::fmt;
2use std::io;
3#[cfg(unix)]
4use std::os::fd::RawFd;
5use std::process::ExitStatus;
6use std::sync::Arc;
7use std::sync::Mutex as StdMutex;
8use std::sync::atomic::AtomicBool;
9
10use anyhow::anyhow;
11use portable_pty::MasterPty;
12use portable_pty::PtySize;
13use portable_pty::SlavePty;
14use tokio::sync::broadcast;
15use tokio::sync::mpsc;
16use tokio::sync::oneshot;
17use tokio::sync::watch;
18use tokio::task::AbortHandle;
19use tokio::task::JoinHandle;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum ProcessSignal {
23 Interrupt,
24}
25
26pub(crate) fn unsupported_signal(signal: ProcessSignal) -> io::Error {
27 match signal {
28 ProcessSignal::Interrupt => io::Error::new(
29 io::ErrorKind::Unsupported,
30 "process interrupt is not supported by this process backend",
31 ),
32 }
33}
34
35pub(crate) fn exit_code_from_status(status: ExitStatus) -> i32 {
36 if let Some(code) = status.code() {
37 return code;
38 }
39
40 #[cfg(unix)]
41 {
42 use std::os::unix::process::ExitStatusExt;
43 if let Some(signal) = status.signal() {
44 return 128 + signal;
45 }
46 }
47
48 -1
49}
50
51pub(crate) trait ChildTerminator: Send + Sync {
52 fn signal(&mut self, signal: ProcessSignal) -> io::Result<()>;
53
54 fn kill(&mut self) -> io::Result<()>;
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct TerminalSize {
59 pub rows: u16,
60 pub cols: u16,
61}
62
63impl Default for TerminalSize {
64 fn default() -> Self {
65 Self { rows: 24, cols: 80 }
66 }
67}
68
69impl From<TerminalSize> for PtySize {
70 fn from(value: TerminalSize) -> Self {
71 Self {
72 rows: value.rows,
73 cols: value.cols,
74 pixel_width: 0,
75 pixel_height: 0,
76 }
77 }
78}
79
80#[cfg(unix)]
81pub(crate) trait PtyHandleKeepAlive: Send {}
82
83#[cfg(unix)]
84impl<T: Send + ?Sized> PtyHandleKeepAlive for T {}
85
86pub(crate) enum PtyMasterHandle {
87 Resizable(Box<dyn MasterPty + Send>),
88 #[cfg(unix)]
89 Opaque {
90 raw_fd: RawFd,
91 _handle: Box<dyn PtyHandleKeepAlive>,
92 },
93}
94
95pub struct PtyHandles {
96 pub _slave: Option<Box<dyn SlavePty + Send>>,
97 pub(crate) _master: PtyMasterHandle,
98}
99
100impl fmt::Debug for PtyHandles {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("PtyHandles").finish()
103 }
104}
105
106type ResizeFn = Box<dyn FnMut(TerminalSize) -> anyhow::Result<()> + Send>;
109
110pub struct ProcessHandle {
112 writer_tx: StdMutex<Option<mpsc::Sender<Vec<u8>>>>,
113 killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
114 reader_handle: StdMutex<Option<JoinHandle<()>>>,
115 reader_abort_handles: StdMutex<Vec<AbortHandle>>,
116 writer_handle: StdMutex<Option<JoinHandle<()>>>,
117 wait_handle: StdMutex<Option<JoinHandle<()>>>,
118 exit_status: Arc<AtomicBool>,
119 exit_code: Arc<StdMutex<Option<i32>>>,
120 _pty_handles: StdMutex<Option<PtyHandles>>,
123 resizer: StdMutex<Option<ResizeFn>>,
126}
127
128impl fmt::Debug for ProcessHandle {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.debug_struct("ProcessHandle").finish()
131 }
132}
133
134impl ProcessHandle {
135 #[allow(clippy::too_many_arguments)]
136 pub(crate) fn new(
137 writer_tx: mpsc::Sender<Vec<u8>>,
138 killer: Box<dyn ChildTerminator>,
139 reader_handle: JoinHandle<()>,
140 reader_abort_handles: Vec<AbortHandle>,
141 writer_handle: JoinHandle<()>,
142 wait_handle: JoinHandle<()>,
143 exit_status: Arc<AtomicBool>,
144 exit_code: Arc<StdMutex<Option<i32>>>,
145 pty_handles: Option<PtyHandles>,
146 resizer: Option<ResizeFn>,
147 ) -> Self {
148 Self {
149 writer_tx: StdMutex::new(Some(writer_tx)),
150 killer: StdMutex::new(Some(killer)),
151 reader_handle: StdMutex::new(Some(reader_handle)),
152 reader_abort_handles: StdMutex::new(reader_abort_handles),
153 writer_handle: StdMutex::new(Some(writer_handle)),
154 wait_handle: StdMutex::new(Some(wait_handle)),
155 exit_status,
156 exit_code,
157 _pty_handles: StdMutex::new(pty_handles),
158 resizer: StdMutex::new(resizer),
159 }
160 }
161
162 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
164 if let Ok(writer_tx) = self.writer_tx.lock()
165 && let Some(writer_tx) = writer_tx.as_ref()
166 {
167 return writer_tx.clone();
168 }
169
170 let (writer_tx, writer_rx) = mpsc::channel(1);
171 drop(writer_rx);
172 writer_tx
173 }
174
175 pub fn has_exited(&self) -> bool {
177 self.exit_status.load(std::sync::atomic::Ordering::SeqCst)
178 }
179
180 pub fn exit_code(&self) -> Option<i32> {
182 self.exit_code.lock().ok().and_then(|guard| *guard)
183 }
184
185 pub fn resize(&self, size: TerminalSize) -> anyhow::Result<()> {
187 {
188 let handles = self
189 ._pty_handles
190 .lock()
191 .map_err(|_| anyhow!("failed to lock PTY handles"))?;
192 if let Some(handles) = handles.as_ref() {
193 return match &handles._master {
194 PtyMasterHandle::Resizable(master) => master.resize(size.into()),
195 #[cfg(unix)]
196 PtyMasterHandle::Opaque { raw_fd, .. } => resize_raw_pty(*raw_fd, size),
197 };
198 }
199 }
200
201 let mut resizer = self
202 .resizer
203 .lock()
204 .map_err(|_| anyhow!("failed to lock PTY resizer"))?;
205 if let Some(resizer) = resizer.as_mut() {
206 resizer(size)
207 } else {
208 Err(anyhow!("process is not attached to a PTY"))
209 }
210 }
211
212 pub fn close_stdin(&self) {
214 if let Ok(mut writer_tx) = self.writer_tx.lock() {
215 writer_tx.take();
216 }
217 }
218
219 pub fn request_terminate(&self) {
222 if let Ok(mut killer_opt) = self.killer.lock()
223 && let Some(mut killer) = killer_opt.take()
224 {
225 let _ = killer.kill();
226 }
227 }
228
229 pub fn signal(&self, signal: ProcessSignal) -> io::Result<()> {
230 let Ok(mut killer_opt) = self.killer.lock() else {
231 return Ok(());
232 };
233 let Some(killer) = killer_opt.as_mut() else {
234 return Ok(());
235 };
236
237 killer.signal(signal)
238 }
239
240 pub fn terminate(&self) {
242 self.request_terminate();
243
244 if let Ok(mut h) = self.reader_handle.lock()
245 && let Some(handle) = h.take()
246 {
247 handle.abort();
248 }
249 if let Ok(mut handles) = self.reader_abort_handles.lock() {
250 for handle in handles.drain(..) {
251 handle.abort();
252 }
253 }
254 if let Ok(mut h) = self.writer_handle.lock()
255 && let Some(handle) = h.take()
256 {
257 handle.abort();
258 }
259 if let Ok(mut h) = self.wait_handle.lock()
260 && let Some(handle) = h.take()
261 {
262 handle.abort();
263 }
264 }
265}
266
267impl Drop for ProcessHandle {
268 fn drop(&mut self) {
269 self.terminate();
270 }
271}
272
273struct ClosureTerminator {
275 inner: Option<Box<dyn FnMut() + Send + Sync>>,
276}
277
278impl ChildTerminator for ClosureTerminator {
279 fn signal(&mut self, signal: ProcessSignal) -> io::Result<()> {
280 Err(unsupported_signal(signal))
281 }
282
283 fn kill(&mut self) -> io::Result<()> {
284 if let Some(inner) = self.inner.as_mut() {
285 (inner)();
286 }
287 Ok(())
288 }
289}
290
291#[cfg(unix)]
292fn resize_raw_pty(raw_fd: RawFd, size: TerminalSize) -> anyhow::Result<()> {
293 let mut winsize = libc::winsize {
294 ws_row: size.rows,
295 ws_col: size.cols,
296 ws_xpixel: 0,
297 ws_ypixel: 0,
298 };
299 let result = unsafe { libc::ioctl(raw_fd, libc::TIOCSWINSZ, &mut winsize) };
300 if result == -1 {
301 return Err(std::io::Error::last_os_error().into());
302 }
303 Ok(())
304}
305
306pub fn combine_output_receivers(
308 mut stdout_rx: mpsc::Receiver<Vec<u8>>,
309 mut stderr_rx: mpsc::Receiver<Vec<u8>>,
310) -> broadcast::Receiver<Vec<u8>> {
311 let (combined_tx, combined_rx) = broadcast::channel(256);
312 tokio::spawn(async move {
313 let mut stdout_open = true;
314 let mut stderr_open = true;
315
316 loop {
317 tokio::select! {
318 stdout = stdout_rx.recv(), if stdout_open => match stdout {
319 Some(chunk) => {
320 let _ = combined_tx.send(chunk);
321 }
322 None => {
323 stdout_open = false;
324 }
325 },
326 stderr = stderr_rx.recv(), if stderr_open => match stderr {
327 Some(chunk) => {
328 let _ = combined_tx.send(chunk);
329 }
330 None => {
331 stderr_open = false;
332 }
333 },
334 else => break,
335 }
336 }
337 });
338 combined_rx
339}
340
341#[derive(Debug)]
343pub struct SpawnedProcess {
344 pub session: ProcessHandle,
345 pub stdout_rx: mpsc::Receiver<Vec<u8>>,
346 pub stderr_rx: mpsc::Receiver<Vec<u8>>,
347 pub exit_rx: oneshot::Receiver<i32>,
348}
349
350pub struct ProcessDriver {
352 pub writer_tx: mpsc::Sender<Vec<u8>>,
353 pub stdout_rx: broadcast::Receiver<Vec<u8>>,
354 pub stderr_rx: Option<broadcast::Receiver<Vec<u8>>>,
355 pub exit_rx: oneshot::Receiver<i32>,
356 pub terminator: Option<Box<dyn FnMut() + Send + Sync>>,
357 pub writer_handle: Option<JoinHandle<()>>,
358 pub resizer: Option<ResizeFn>,
359}
360
361pub fn spawn_from_driver(driver: ProcessDriver) -> SpawnedProcess {
363 let ProcessDriver {
364 writer_tx,
365 stdout_rx: stdout_driver_rx,
366 stderr_rx: mut stderr_driver_rx,
367 exit_rx,
368 terminator,
369 writer_handle,
370 resizer,
371 } = driver;
372
373 let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(256);
374 let (stderr_tx, stderr_rx) = mpsc::channel::<Vec<u8>>(256);
375 let (exit_seen_tx, exit_seen_rx) = watch::channel(false);
376 let spawn_stream_reader =
377 |mut output_rx: broadcast::Receiver<Vec<u8>>,
378 output_tx: mpsc::Sender<Vec<u8>>,
379 mut exit_seen_rx: watch::Receiver<bool>| {
380 tokio::spawn(async move {
381 loop {
382 let recv_result = if *exit_seen_rx.borrow() {
383 output_rx.recv().await
392 } else {
393 tokio::select! {
394 _ = exit_seen_rx.changed() => {
395 continue;
396 }
397 result = output_rx.recv() => result,
398 }
399 };
400 match recv_result {
401 Ok(chunk) => {
402 if output_tx.send(chunk).await.is_err() {
403 break;
404 }
405 }
406 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
407 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
408 }
409 }
410 })
411 };
412 let reader_handle = spawn_stream_reader(stdout_driver_rx, stdout_tx, exit_seen_rx.clone());
413 let stderr_reader_handle = stderr_driver_rx
414 .take()
415 .map(|rx| spawn_stream_reader(rx, stderr_tx, exit_seen_rx));
416
417 let writer_handle = writer_handle.unwrap_or_else(|| tokio::spawn(async {}));
418
419 let (exit_tx, exit_rx_out) = oneshot::channel::<i32>();
420 let exit_status = Arc::new(AtomicBool::new(false));
421 let wait_exit_status = Arc::clone(&exit_status);
422 let exit_code = Arc::new(StdMutex::new(None));
423 let wait_exit_code = Arc::clone(&exit_code);
424 let wait_handle = tokio::spawn(async move {
425 let code = exit_rx.await.unwrap_or(-1);
426 wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst);
427 if let Ok(mut guard) = wait_exit_code.lock() {
428 *guard = Some(code);
429 }
430 let _ = exit_seen_tx.send(true);
431 let _ = exit_tx.send(code);
432 });
433
434 let handle = ProcessHandle::new(
435 writer_tx,
436 Box::new(ClosureTerminator { inner: terminator }),
437 reader_handle,
438 stderr_reader_handle
439 .map(|handle| handle.abort_handle())
440 .into_iter()
441 .collect(),
442 writer_handle,
443 wait_handle,
444 exit_status,
445 exit_code,
446 None,
447 resizer,
448 );
449
450 SpawnedProcess {
451 session: handle,
452 stdout_rx,
453 stderr_rx,
454 exit_rx: exit_rx_out,
455 }
456}