fix(agor-pty): implement real PTY resize (TIOCSWINSZ via portable-pty)

Was only updating cached dimensions without calling PTY resize.
Shell thought terminal was wrong size → double prompts, escape code leaks.

- Session stores master PTY handle (Arc<Mutex<Box<dyn MasterPty>>>)
- resize() calls master.resize(PtySize) → issues TIOCSWINSZ
- Reader task no longer owns master handle (uses cloned reader only)
This commit is contained in:
Hibryda 2026-03-20 03:29:53 +01:00
parent 0f7024ec8f
commit 621e1c5c8c
2 changed files with 18 additions and 18 deletions

View file

@ -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 {

View file

@ -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<Mutex<_>>` 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<dyn Write + Send>`.
writer: Arc<Mutex<Box<dyn IoWrite + Send>>>,
/// Broadcast channel — all subscribers receive raw output chunks.
/// Master PTY handle — needed for resize (TIOCSWINSZ).
master: Arc<Mutex<Box<dyn MasterPty + Send>>>,
pub tx: broadcast::Sender<Vec<u8>>,
/// false once the child process exits.
pub alive: Arc<AtomicBool>,
/// 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<Mutex<Option<i32>>>,
}
@ -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::<i32>));
// 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<dyn MasterPty + Send> = 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<i32>),
mut child: Box<dyn portable_pty::Child + Send>,
_master: Box<dyn portable_pty::MasterPty + Send>,
) {
let mut buf = [0u8; 4096];
loop {