diff --git a/agor-pty/src/daemon.rs b/agor-pty/src/daemon.rs index 502b38d..cbf9d81 100644 --- a/agor-pty/src/daemon.rs +++ b/agor-pty/src/daemon.rs @@ -314,7 +314,9 @@ async fn handle_message( let mut st = state.lock().await; match st.sessions.get_mut(&session_id) { Some(sess) => { - sess.note_resize(cols, rows); + if let Err(e) = sess.resize(cols, rows).await { + log::warn!("resize {session_id}: {e}"); + } } None => { let _ = out_tx.try_send(DaemonMessage::Error { diff --git a/agor-pty/src/session.rs b/agor-pty/src/session.rs index 0529c94..9abbe2a 100644 --- a/agor-pty/src/session.rs +++ b/agor-pty/src/session.rs @@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize}; use tokio::sync::{broadcast, Mutex}; use crate::protocol::SessionInfo; @@ -12,9 +12,6 @@ use crate::protocol::SessionInfo; const OUTPUT_CHANNEL_CAP: usize = 256; /// A live (or recently exited) PTY session. -/// -/// All fields that cross await points are either `Send + Sync` or wrapped in -/// `Arc>` so the Session itself is `Send`. pub struct Session { pub id: String, pub pid: u32, @@ -23,14 +20,11 @@ pub struct Session { pub cols: u16, pub rows: u16, pub created_at: u64, - /// Used to write input into the PTY master. `Box`. writer: Arc>>, - /// Broadcast channel — all subscribers receive raw output chunks. + /// Master PTY handle — needed for resize (TIOCSWINSZ). + master: Arc>>, pub tx: broadcast::Sender>, - /// false once the child process exits. pub alive: Arc, - /// Last known exit code (set by the reader task on child exit). - /// Public for callers that poll exit state after SessionClosed is received. #[allow(dead_code)] pub exit_code: Arc>>, } @@ -57,11 +51,14 @@ impl Session { .map_err(|e| format!("PTY write for {}: {e}", self.id)) } - /// Update cached dimensions after a resize. The actual TIOCSWINSZ is issued - /// by the daemon before calling this. - pub fn note_resize(&mut self, cols: u16, rows: u16) { + /// Resize the PTY (issues TIOCSWINSZ) and update cached dimensions. + pub async fn resize(&mut self, cols: u16, rows: u16) -> Result<(), String> { + let master = self.master.lock().await; + master.resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 }) + .map_err(|e| format!("PTY resize for {}: {e}", self.id))?; self.cols = cols; self.rows = rows; + Ok(()) } /// Return a new receiver subscribed to this session's broadcast output. @@ -157,13 +154,15 @@ impl SessionManager { let alive = Arc::new(AtomicBool::new(true)); let exit_code = Arc::new(Mutex::new(None::)); - // Spawn the blocking reader task. It takes ownership of `pair.master` - // (via `_master`) so the PTY file descriptor stays open. + // Keep a reference to the master for resize operations. + // The reader task gets the master handle to keep the PTY fd alive. + let master_for_session: Box = pair.master; + + // Spawn the blocking reader task. let tx_clone = tx.clone(); let alive_clone = alive.clone(); let exit_code_clone = exit_code.clone(); let id_clone = id.clone(); - let _master = pair.master; // keep PTY fd alive inside the task tokio::task::spawn_blocking(move || { read_pty_output( reader, @@ -173,7 +172,6 @@ impl SessionManager { id_clone, on_exit, child, - _master, ); }); @@ -186,6 +184,7 @@ impl SessionManager { rows, created_at: unix_now(), writer: Arc::new(Mutex::new(writer)), + master: Arc::new(Mutex::new(master_for_session)), tx, alive, exit_code, @@ -244,7 +243,6 @@ fn read_pty_output( id: String, on_exit: impl FnOnce(String, Option), mut child: Box, - _master: Box, ) { let mut buf = [0u8; 4096]; loop {