| use std::io::{Read, Write}; |
| use std::sync::atomic::{AtomicBool, Ordering}; |
| use std::sync::{Arc, Mutex}; |
| use std::thread; |
| use std::time::{Duration, Instant}; |
|
|
| use portable_pty::{native_pty_system, ChildKiller, MasterPty, PtySize}; |
| use tauri::ipc::{Channel, Response}; |
|
|
| use super::shell_init; |
|
|
| const POLL_INTERVAL: Duration = Duration::from_millis(4); |
| const BURST_MAX_AGE: Duration = Duration::from_millis(16); |
| const BURST_THRESHOLD: usize = 64 * 1024; |
| const READ_BUF: usize = 16 * 1024; |
| |
| |
| |
| |
| const MAX_PENDING: usize = 4 * 1024 * 1024; |
| |
| |
| const OVERFLOW_NOTICE: &[u8] = |
| b"\x1bc\x1b[2m[terax: dropped output due to backpressure]\x1b[0m\r\n"; |
|
|
| pub struct Session { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[cfg(windows)] |
| _job: Option<super::job::PtyJob>, |
| pub killer: Mutex<Box<dyn ChildKiller + Send + Sync>>, |
| pub writer: Mutex<Box<dyn Write + Send>>, |
| pub master: Mutex<Box<dyn MasterPty + Send>>, |
| } |
|
|
| impl Drop for Session { |
| fn drop(&mut self) { |
| |
| |
| |
| |
| if let Ok(mut k) = self.killer.lock() { |
| let _ = k.kill(); |
| } |
| } |
| } |
| static SPAWN_LOCK: Mutex<()> = Mutex::new(()); |
|
|
| pub fn spawn( |
| cols: u16, |
| rows: u16, |
| cwd: Option<String>, |
| on_data: Channel<Response>, |
| on_exit: Channel<i32>, |
| ) -> Result<(Arc<Session>, PtySize), String> { |
| let _spawn_guard = SPAWN_LOCK.lock().unwrap(); |
|
|
| let pty_system = native_pty_system(); |
| let size = PtySize { |
| rows, |
| cols, |
| pixel_width: 0, |
| pixel_height: 0, |
| }; |
| let pair = pty_system.openpty(size).map_err(|e| e.to_string())?; |
|
|
| let cmd = shell_init::build_command(cwd)?; |
| let mut child = pair.slave.spawn_command(cmd).map_err(|e| e.to_string())?; |
| drop(pair.slave); |
|
|
| let killer = child.clone_killer(); |
| let mut reader = pair.master.try_clone_reader().map_err(|e| e.to_string())?; |
| let writer = pair.master.take_writer().map_err(|e| e.to_string())?; |
|
|
| #[cfg(windows)] |
| let job = match child.process_id() { |
| Some(pid) => match super::job::PtyJob::create_for(pid) { |
| Ok(j) => Some(j), |
| Err(e) => { |
| log::warn!("pty job-object setup failed for pid={pid}: {e}"); |
| None |
| } |
| }, |
| None => None, |
| }; |
|
|
| let session = Arc::new(Session { |
| #[cfg(windows)] |
| _job: job, |
| killer: Mutex::new(killer), |
| writer: Mutex::new(writer), |
| master: Mutex::new(pair.master), |
| }); |
|
|
| let pending: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::with_capacity(READ_BUF))); |
| let done = Arc::new(AtomicBool::new(false)); |
| let spawn_at = Instant::now(); |
|
|
| let pending_r = pending.clone(); |
| let reader_thread = thread::Builder::new() |
| .name("terax-pty-reader".into()) |
| .spawn(move || { |
| let mut buf = [0u8; READ_BUF]; |
| let mut dropped_bytes: u64 = 0; |
| let mut logged_first = false; |
| loop { |
| match reader.read(&mut buf) { |
| Ok(0) => break, |
| Ok(n) => { |
| if !logged_first { |
| logged_first = true; |
| log::info!("pty first byte after {}ms", spawn_at.elapsed().as_millis()); |
| } |
| let mut g = pending_r.lock().unwrap(); |
| if g.len() + n > MAX_PENDING { |
| |
| |
| |
| dropped_bytes += g.len() as u64; |
| g.clear(); |
| g.extend_from_slice(OVERFLOW_NOTICE); |
| } |
| g.extend_from_slice(&buf[..n]); |
| } |
| Err(e) => { |
| |
| |
| |
| log::debug!("pty reader ended: {e}"); |
| break; |
| } |
| } |
| } |
| if dropped_bytes > 0 { |
| log::warn!("pty backpressure: dropped {dropped_bytes} bytes (cap {MAX_PENDING})"); |
| } |
| }) |
| .expect("spawn pty reader thread"); |
|
|
| let on_data_flush = on_data.clone(); |
| let pending_f = pending.clone(); |
| let done_f = done.clone(); |
| thread::Builder::new() |
| .name("terax-pty-flusher".into()) |
| .spawn(move || { |
| let mut idle = true; |
| let mut batch_started = Instant::now(); |
| loop { |
| thread::sleep(POLL_INTERVAL); |
| let chunk = { |
| let mut g = pending_f.lock().unwrap(); |
| if g.is_empty() { |
| idle = true; |
| if done_f.load(Ordering::Acquire) { |
| break; |
| } |
| continue; |
| } |
| if idle { |
| |
| |
| idle = false; |
| } else if g.len() < BURST_THRESHOLD && batch_started.elapsed() < BURST_MAX_AGE { |
| |
| continue; |
| } |
| batch_started = Instant::now(); |
| std::mem::take(&mut *g) |
| }; |
| |
| if let Err(e) = on_data_flush.send(Response::new(chunk)) { |
| log::debug!("pty flusher exiting, channel closed: {e}"); |
| break; |
| } |
| } |
| }) |
| .expect("spawn pty flusher thread"); |
|
|
| let on_data_exit = on_data; |
| let pending_e = pending; |
| let done_e = done; |
| thread::Builder::new() |
| .name("terax-pty-waiter".into()) |
| .spawn(move || { |
| let code = match child.wait() { |
| Ok(status) => status.exit_code() as i32, |
| Err(e) => { |
| log::warn!("pty child wait failed: {e}"); |
| -1 |
| } |
| }; |
| |
| |
| if let Err(e) = reader_thread.join() { |
| log::error!("pty reader thread panicked: {e:?}"); |
| } |
| let tail = std::mem::take(&mut *pending_e.lock().unwrap()); |
| if !tail.is_empty() { |
| if let Err(e) = on_data_exit.send(Response::new(tail)) { |
| log::debug!("pty final-data send failed (channel closed): {e}"); |
| } |
| } |
| done_e.store(true, Ordering::Release); |
| if let Err(e) = on_exit.send(code) { |
| log::debug!("pty exit send failed (channel closed): {e}"); |
| } |
| }) |
| .expect("spawn pty waiter thread"); |
|
|
| Ok((session, size)) |
| } |
|
|