From 4bd3b03a3cd80886bdb41e654c411cebb3123899 Mon Sep 17 00:00:00 2001 From: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Date: Sat, 1 Aug 2026 22:11:47 -0400 Subject: [PATCH] fix(terminal): recover attach publication race Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> --- desktop/src-tauri/src/terminal_runtime.rs | 86 +++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/desktop/src-tauri/src/terminal_runtime.rs b/desktop/src-tauri/src/terminal_runtime.rs index f82d7e3e2..dcf861e23 100644 --- a/desktop/src-tauri/src/terminal_runtime.rs +++ b/desktop/src-tauri/src/terminal_runtime.rs @@ -5,14 +5,14 @@ use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; use buzz_terminal::context::{context_vars, GuiContext}; -use buzz_terminal::damage::Style; +use buzz_terminal::damage::{Frame, Style}; use buzz_terminal::{Fences, SharedTerminal, Size, Terminal, Viewport}; use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize}; use serde::{Deserialize, Serialize}; use tauri::ipc::Channel; use uuid::Uuid; -use crate::terminal_transport::{FramePublisher, Publication, SubscriptionId}; +use crate::terminal_transport::{FramePublisher, OfferError, Publication, SubscriptionId}; const MAX_LIVE_SESSIONS: usize = 20; const MAX_INPUT_BYTES: usize = 1024 * 1024; @@ -316,6 +316,20 @@ fn pty_size(columns: u16, rows: u16, pixel_width: u16, pixel_height: u16) -> Pty } } +fn offer_capture( + publisher: &Mutex, + frame: Frame, + snapshot: impl FnOnce() -> Frame, +) -> Option { + let offered = publisher.lock().ok()?.offer(frame); + match offered { + Ok(publication) => publication, + Err(OfferError::PendingFrameMustBeSnapshot) => { + publisher.lock().ok()?.offer(snapshot()).ok().flatten() + } + } +} + #[tauri::command] pub(crate) fn terminal_attach( request: AttachRequest, @@ -473,10 +487,9 @@ pub(crate) fn terminal_attach( if frame.is_empty() { continue; } - let publication = reader_publisher - .lock() - .ok() - .and_then(|mut publisher| publisher.offer(frame).ok().flatten()); + let publication = offer_capture(&reader_publisher, frame, || { + reader_terminal.snapshot(&mut snapshot_encoder) + }); if let Some(publication) = publication { let subscription = publication.subscription_id; let result = wire_publication(publication).and_then(|mut message| { @@ -720,6 +733,67 @@ mod tests { } } + fn marker_frame(marker: usize, full: bool) -> Frame { + Frame { + rows: vec![RowFrame { + line: marker, + spans: Vec::new(), + }], + cursor: CursorFrame { + line: 0, + column: 0, + visible: true, + }, + full, + viewport: Viewport { + generation: 0, + columns: 80, + screen_lines: 24, + }, + } + } + + fn assert_post_snapshot_capture_survives_attach(mut publisher: FramePublisher) { + assert!(!publisher.requires_snapshot()); + let bootstrap = marker_frame(1, true); + let post_snapshot_incremental = marker_frame(42, false); + let subscription = SubscriptionId::new(); + publisher.attach(subscription, bootstrap).unwrap(); + let publisher = Mutex::new(publisher); + + assert_eq!( + offer_capture(&publisher, post_snapshot_incremental, || marker_frame( + 42, true + )), + None + ); + + let successor = publisher + .lock() + .unwrap() + .acknowledge(subscription, 1) + .expect("post-snapshot PTY output was lost"); + assert_eq!(successor.frame.rows[0].line, 42); + assert!(successor.frame.full); + } + + #[test] + fn initial_attach_retains_output_captured_after_its_bootstrap_snapshot() { + let viewport = marker_frame(0, true).viewport; + let publisher = FramePublisher::new(viewport); + assert_post_snapshot_capture_survives_attach(publisher); + } + + #[test] + fn reattach_retains_output_captured_after_its_bootstrap_snapshot() { + let viewport = marker_frame(0, true).viewport; + let mut publisher = FramePublisher::new(viewport); + let old = SubscriptionId::new(); + publisher.attach(old, marker_frame(0, true)).unwrap(); + assert!(publisher.acknowledge(old, 1).is_none()); + assert_post_snapshot_capture_survives_attach(publisher); + } + #[test] fn mapper_expands_ascii_runs_without_unicode_classification() { let message = wire_publication(publication(vec![Span {