From 774da7371e563bda9990f46814d20c0fe8ca8148 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:30:51 -0400 Subject: [PATCH] Chunk terminal output below MTU --- src/pty.rs | 23 +++++++++++++---- tests/integration_smoke.rs | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/pty.rs b/src/pty.rs index 2dd64cc..924f550 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -7,6 +7,12 @@ use std::sync::{Arc, Mutex}; use std::thread; use tokio::sync::mpsc; +// Keep live terminal output comfortably below common path MTUs after Dosh's +// protocol header, AEAD tag, UDP/IP headers, and bincode framing. Full-screen +// TUIs often write several KiB on the first draw; sending that as one UDP +// datagram can fragment and vanish, leaving only a blank alternate screen. +const PTY_OUTPUT_CHUNK_BYTES: usize = 1024; + /// Backing for a PTY master held by the server. /// /// `Owned` means this process spawned the shell as a child and is responsible @@ -264,11 +270,13 @@ fn spawn_reader_thread( break; } Ok(n) => { - let _ = tx.send(PtyOutput { - session: reader_session.clone(), - bytes: buf[..n].to_vec(), - exited: false, - }); + for chunk in buf[..n].chunks(PTY_OUTPUT_CHUNK_BYTES) { + let _ = tx.send(PtyOutput { + session: reader_session.clone(), + bytes: chunk.to_vec(), + exited: false, + }); + } } Err(_) => { let _ = tx.send(PtyOutput { @@ -297,4 +305,9 @@ mod tests { assert!(!terminfo_available("definitely-not-a-real-terminal-xyz")); assert!(!terminfo_available("")); } + + #[test] + fn pty_output_chunk_size_stays_mtu_safe() { + assert!(PTY_OUTPUT_CHUNK_BYTES <= 1200); + } } diff --git a/tests/integration_smoke.rs b/tests/integration_smoke.rs index 77af004..e85c4a5 100644 --- a/tests/integration_smoke.rs +++ b/tests/integration_smoke.rs @@ -1382,6 +1382,57 @@ fn tui_control_sequences_survive_transport_verbatim() { } } +#[test] +fn large_tui_paint_is_delivered_in_mtu_safe_frames() { + let dir = tempfile::tempdir().unwrap(); + let port = free_udp_port(); + let config = write_server_config(&dir, port); + let mut server = start_server(&dir, &config); + let (socket, bootstrap, ok) = direct_attach(&config, port, "read-write"); + + let input = Input { + bytes: b"printf '\\033[?1049h'; yes DOSH_TUI_BIG_PAINT | head -n 300 | tr -d '\\n'; printf '\\033[?1049l'\n".to_vec(), + }; + send_encrypted( + &socket, + port, + PacketKind::Input, + ok.client_id, + 2, + 0, + &bootstrap.session_key, + &protocol::to_body(&input).unwrap(), + ); + + let mut text = String::new(); + let mut saw_split_frame = false; + let deadline = std::time::Instant::now() + Duration::from_secs(3); + while std::time::Instant::now() < deadline && text.matches("DOSH_TUI_BIG_PAINT").count() < 200 + { + if let Some((_header, frame)) = recv_frame(&socket, &bootstrap.session_key) { + if frame.bytes.len() <= 1400 && frame.bytes.len() >= 900 { + saw_split_frame = true; + } + text.push_str(&String::from_utf8_lossy(&frame.bytes)); + } + } + + let _ = server.kill(); + let _ = server.wait(); + + assert!( + text.contains("\x1b[?1049h") && text.matches("DOSH_TUI_BIG_PAINT").count() >= 200, + "large alternate-screen paint did not survive transport, enter_alt={} markers={} bytes={}", + text.contains("\x1b[?1049h"), + text.matches("DOSH_TUI_BIG_PAINT").count(), + text.len() + ); + assert!( + saw_split_frame, + "expected large TUI paint to be split into MTU-safe frames" + ); +} + #[test] fn resume_snapshot_preserves_alternate_screen_mode() { let dir = tempfile::tempdir().unwrap();