use std::collections::HashMap; use std::io::ErrorKind; use std::path::Path; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::time::Duration; use anyhow::Result; #[cfg(not(windows))] use portable_pty::native_pty_system; use portable_pty::CommandBuilder; use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio::task::JoinHandle; use crate::process::ChildTerminator; use crate::process::ProcessHandle; use crate::process::PtyHandles; use crate::process::SpawnedProcess; use crate::process::TerminalSize; /// Returns true when ConPTY support is available (Windows only). #[cfg(windows)] pub fn conpty_supported() -> bool { crate::win::conpty_supported() } /// Returns true when ConPTY support is available (non-Windows always true). #[cfg(not(windows))] pub fn conpty_supported() -> bool { true } struct PtyChildTerminator { killer: Box, #[cfg(unix)] process_group_id: Option, } impl ChildTerminator for PtyChildTerminator { fn kill(&mut self) -> std::io::Result<()> { #[cfg(unix)] if let Some(process_group_id) = self.process_group_id { // Match the pipe backend's hard-kill behavior so descendant // processes from interactive shells/REPLs do not survive shutdown. // Also try the direct child killer in case the cached PGID is stale. let process_group_kill_result = crate::process_group::kill_process_group(process_group_id); let child_kill_result = self.killer.kill(); return match child_kill_result { Ok(()) => Ok(()), Err(err) if err.kind() == ErrorKind::NotFound => process_group_kill_result, Err(err) => process_group_kill_result.or(Err(err)), }; } self.killer.kill() } } fn platform_native_pty_system() -> Box { #[cfg(windows)] { Box::new(crate::win::ConPtySystem::default()) } #[cfg(not(windows))] { native_pty_system() } } /// Spawn a process attached to a PTY, returning handles for stdin, merged output, and exit. pub async fn spawn_process( program: &str, args: &[String], cwd: &Path, env: &HashMap, arg0: &Option, size: TerminalSize, ) -> Result { if program.is_empty() { anyhow::bail!("missing program for PTY spawn"); } let pty_system = platform_native_pty_system(); let pair = pty_system.openpty(size.into())?; let mut command_builder = CommandBuilder::new(arg0.as_ref().unwrap_or(&program.to_string())); command_builder.cwd(cwd); command_builder.env_clear(); for arg in args { command_builder.arg(arg); } for (key, value) in env { command_builder.env(key, value); } let mut child = pair.slave.spawn_command(command_builder)?; let pid = child.process_id(); #[cfg(unix)] // portable-pty establishes the spawned PTY child as a new session leader on // Unix, so PID == PGID and we can reuse the pipe backend's process-group // hard-kill semantics for descendants. let process_group_id = child.process_id(); let killer = child.clone_killer(); let (writer_tx, mut writer_rx) = mpsc::channel::>(128); let (output_tx, output_rx) = mpsc::channel::>(256); let mut reader = pair.master.try_clone_reader()?; let reader_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || { let mut buf = [0u8; 8_192]; loop { match reader.read(&mut buf) { Ok(0) => break, Ok(n) => { if output_tx.blocking_send(buf[..n].to_vec()).is_err() { break; } } Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, Err(ref e) if e.kind() == ErrorKind::WouldBlock => { std::thread::sleep(Duration::from_millis(5)); continue; } Err(_) => break, } } }); let writer = pair.master.take_writer()?; let writer = Arc::new(tokio::sync::Mutex::new(writer)); let writer_handle: JoinHandle<()> = tokio::spawn({ let writer = Arc::clone(&writer); async move { while let Some(bytes) = writer_rx.recv().await { let mut guard = writer.lock().await; use std::io::Write; let _ = guard.write_all(&bytes); let _ = guard.flush(); } } }); let (exit_tx, exit_rx) = oneshot::channel::(); let exit_status = Arc::new(AtomicBool::new(false)); let wait_exit_status = Arc::clone(&exit_status); let exit_code = Arc::new(StdMutex::new(None)); let wait_exit_code = Arc::clone(&exit_code); let wait_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || { let code = match child.wait() { Ok(status) => status.exit_code() as i32, Err(_) => -1, }; wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst); if let Ok(mut guard) = wait_exit_code.lock() { *guard = Some(code); } let _ = exit_tx.send(code); }); let handles = PtyHandles { _slave: if cfg!(windows) { Some(pair.slave) } else { None }, _master: pair.master, }; let handle = ProcessHandle::new( writer_tx, pid, Box::new(PtyChildTerminator { killer, #[cfg(unix)] process_group_id, }), reader_handle, Vec::new(), writer_handle, wait_handle, exit_status, exit_code, Some(handles), ); Ok(SpawnedProcess { session: handle, output_rx, exit_rx, }) }