fix(terminals): stop PTY output duplication on re-attach
Each open/reattach spawned a pump thread feeding pty_bridge[session]. The broadcast hub added a subscriber per attach without dropping the previous one, and the stale pump thread kept delivering (its send_output still succeeded against the new channel) — so every byte was delivered twice (more after further re-attaches). Accumulated on each tab/layout/agent switch; window resize made it glaring. - Broadcast::subscribe() is now single-consumer: a new subscription supersedes the previous one, ending the old pump thread's stream. - PtyBridge gains a per-session generation; register() returns it and unregister_if(session, gen) only removes when still current, so a dying superseded thread can't tear down the channel that replaced it. - All three attach sites (open/reattach/agent launch) use unregister_if. Tests: 6 added (generation guard + single-consumer broadcast). Workspace green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -88,3 +88,47 @@ fn register_same_session_twice_replaces_channel() {
|
||||
assert!(first.lock().unwrap().is_empty(), "old channel no longer used");
|
||||
assert_eq!(second.lock().unwrap().as_slice(), &[vec![9]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_returns_monotonic_generation_per_session() {
|
||||
let bridge = PtyBridge::new();
|
||||
let session = sid();
|
||||
let g0 = bridge.register(session, capturing_channel(Arc::new(Mutex::new(Vec::new()))));
|
||||
let g1 = bridge.register(session, capturing_channel(Arc::new(Mutex::new(Vec::new()))));
|
||||
assert_eq!(g0, 0);
|
||||
assert_eq!(g1, 1, "re-attaching the same session bumps the generation");
|
||||
}
|
||||
|
||||
/// The regression guard for on-resize output duplication: a superseded attach's
|
||||
/// pump thread (older generation) must NOT tear down the channel of the
|
||||
/// re-attach that replaced it.
|
||||
#[test]
|
||||
fn unregister_if_is_a_noop_for_a_superseded_generation() {
|
||||
let bridge = PtyBridge::new();
|
||||
let session = sid();
|
||||
let old = Arc::new(Mutex::new(Vec::new()));
|
||||
let new = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let old_gen = bridge.register(session, capturing_channel(Arc::clone(&old)));
|
||||
let _new_gen = bridge.register(session, capturing_channel(Arc::clone(&new)));
|
||||
|
||||
// The stale (old) pump thread ends and tries to clean up with its own gen.
|
||||
bridge.unregister_if(&session, old_gen);
|
||||
|
||||
// The current channel survives and still delivers — no duplication, no drop.
|
||||
assert_eq!(bridge.active_sessions(), 1, "live re-attach must not be removed");
|
||||
assert!(bridge.send_output(&session, vec![7]));
|
||||
assert_eq!(new.lock().unwrap().as_slice(), &[vec![7]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unregister_if_removes_when_generation_is_current() {
|
||||
let bridge = PtyBridge::new();
|
||||
let session = sid();
|
||||
let gen = bridge.register(session, capturing_channel(Arc::new(Mutex::new(Vec::new()))));
|
||||
|
||||
// The current attach's pump thread ends (PTY EOF): it owns the live channel.
|
||||
bridge.unregister_if(&session, gen);
|
||||
assert_eq!(bridge.active_sessions(), 0);
|
||||
assert!(!bridge.send_output(&session, vec![1]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user