use core::fmt; use std::io; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::Mutex as StdMutex; use anyhow::anyhow; use portable_pty::MasterPty; use portable_pty::PtySize; use portable_pty::SlavePty; use tokio::sync::broadcast; use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio::task::AbortHandle; use tokio::task::JoinHandle; pub(crate) trait ChildTerminator: Send + Sync { fn kill(&mut self) -> io::Result<()>; } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TerminalSize { pub rows: u16, pub cols: u16, } impl Default for TerminalSize { fn default() -> Self { Self { rows: 24, cols: 80 } } } impl From for PtySize { fn from(value: TerminalSize) -> Self { Self { rows: value.rows, cols: value.cols, pixel_width: 0, pixel_height: 0, } } } pub struct PtyHandles { pub _slave: Option>, pub _master: Box, } impl fmt::Debug for PtyHandles { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PtyHandles").finish() } } /// Handle for driving an interactive process (PTY or pipe). pub struct ProcessHandle { writer_tx: StdMutex>>>, killer: StdMutex>>, reader_handle: StdMutex>>, reader_abort_handles: StdMutex>, writer_handle: StdMutex>>, wait_handle: StdMutex>>, exit_status: Arc, exit_code: Arc>>, // PtyHandles must be preserved because the process will receive Control+C if the // slave is closed _pty_handles: StdMutex>, } impl fmt::Debug for ProcessHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ProcessHandle").finish() } } impl ProcessHandle { #[allow(clippy::too_many_arguments)] pub(crate) fn new( writer_tx: mpsc::Sender>, killer: Box, reader_handle: JoinHandle<()>, reader_abort_handles: Vec, writer_handle: JoinHandle<()>, wait_handle: JoinHandle<()>, exit_status: Arc, exit_code: Arc>>, pty_handles: Option, ) -> Self { Self { writer_tx: StdMutex::new(Some(writer_tx)), killer: StdMutex::new(Some(killer)), reader_handle: StdMutex::new(Some(reader_handle)), reader_abort_handles: StdMutex::new(reader_abort_handles), writer_handle: StdMutex::new(Some(writer_handle)), wait_handle: StdMutex::new(Some(wait_handle)), exit_status, exit_code, _pty_handles: StdMutex::new(pty_handles), } } /// Returns a channel sender for writing raw bytes to the child stdin. pub fn writer_sender(&self) -> mpsc::Sender> { if let Ok(writer_tx) = self.writer_tx.lock() { if let Some(writer_tx) = writer_tx.as_ref() { return writer_tx.clone(); } } let (writer_tx, writer_rx) = mpsc::channel(1); drop(writer_rx); writer_tx } /// True if the child process has exited. pub fn has_exited(&self) -> bool { self.exit_status.load(std::sync::atomic::Ordering::SeqCst) } /// Returns the exit code if known. pub fn exit_code(&self) -> Option { self.exit_code.lock().ok().and_then(|guard| *guard) } /// Resize the PTY in character cells. pub fn resize(&self, size: TerminalSize) -> anyhow::Result<()> { let handles = self ._pty_handles .lock() .map_err(|_| anyhow!("failed to lock PTY handles"))?; let handles = handles .as_ref() .ok_or_else(|| anyhow!("process is not attached to a PTY"))?; handles._master.resize(size.into()) } /// Close the child's stdin channel. pub fn close_stdin(&self) { if let Ok(mut writer_tx) = self.writer_tx.lock() { writer_tx.take(); } } /// Attempts to kill the child while leaving the reader/writer tasks alive /// so callers can still drain output until EOF. pub fn request_terminate(&self) { if let Ok(mut killer_opt) = self.killer.lock() { if let Some(mut killer) = killer_opt.take() { let _ = killer.kill(); } } } /// Attempts to kill the child and abort helper tasks. pub fn terminate(&self) { self.request_terminate(); if let Ok(mut h) = self.reader_handle.lock() { if let Some(handle) = h.take() { handle.abort(); } } if let Ok(mut handles) = self.reader_abort_handles.lock() { for handle in handles.drain(..) { handle.abort(); } } if let Ok(mut h) = self.writer_handle.lock() { if let Some(handle) = h.take() { handle.abort(); } } if let Ok(mut h) = self.wait_handle.lock() { if let Some(handle) = h.take() { handle.abort(); } } } } impl Drop for ProcessHandle { fn drop(&mut self) { self.terminate(); } } /// Combine split stdout/stderr receivers into a single broadcast receiver. pub fn combine_output_receivers( mut stdout_rx: mpsc::Receiver>, mut stderr_rx: mpsc::Receiver>, ) -> broadcast::Receiver> { let (combined_tx, combined_rx) = broadcast::channel(256); tokio::spawn(async move { let mut stdout_open = true; let mut stderr_open = true; loop { tokio::select! { stdout = stdout_rx.recv(), if stdout_open => match stdout { Some(chunk) => { let _ = combined_tx.send(chunk); } None => { stdout_open = false; } }, stderr = stderr_rx.recv(), if stderr_open => match stderr { Some(chunk) => { let _ = combined_tx.send(chunk); } None => { stderr_open = false; } }, else => break, } } }); combined_rx } /// Return value from PTY or pipe spawn helpers. #[derive(Debug)] pub struct SpawnedProcess { pub session: ProcessHandle, pub stdout_rx: mpsc::Receiver>, pub stderr_rx: mpsc::Receiver>, pub exit_rx: oneshot::Receiver, }