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:
@ -163,11 +163,11 @@ pub async fn open_terminal(
|
||||
let session_id = output.session.id;
|
||||
|
||||
// (2) Register the xterm output channel for this session.
|
||||
state.pty_bridge.register(session_id, on_output);
|
||||
let gen = state.pty_bridge.register(session_id, on_output);
|
||||
|
||||
// (3) Subscribe to the PTY's byte stream and pump it to the channel. The
|
||||
// stream is a blocking iterator, so it runs on a dedicated OS thread; it
|
||||
// ends when the PTY hits EOF (process exit) or the bridge channel is gone.
|
||||
// ends when the PTY hits EOF (process exit) or this attach is superseded.
|
||||
let handle = PtyHandle { session_id };
|
||||
match state.pty_port.subscribe_output(&handle) {
|
||||
Ok(stream) => {
|
||||
@ -178,8 +178,9 @@ pub async fn open_terminal(
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Stream ended (process exited): drop the channel registration.
|
||||
bridge.unregister(&session_id);
|
||||
// Stream ended: drop the channel only if still ours (a re-attach
|
||||
// may have superseded this generation — don't tear down its channel).
|
||||
bridge.unregister_if(&session_id, gen);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@ -283,10 +284,12 @@ pub fn reattach_terminal(
|
||||
.map_err(|e| ErrorDto::from(AppError::from(e)))?;
|
||||
|
||||
// (2) Register the new output channel for this session, replacing any stale
|
||||
// one from a previous attach.
|
||||
state.pty_bridge.register(sid, on_output);
|
||||
// one from a previous attach (and bumping the generation).
|
||||
let gen = state.pty_bridge.register(sid, on_output);
|
||||
|
||||
// (3) Subscribe afresh to the live byte stream and pump it to the channel.
|
||||
// The fresh subscription supersedes the previous attach's, so its pump thread
|
||||
// ends and stops double-delivering this session's bytes.
|
||||
match state.pty_port.subscribe_output(&handle) {
|
||||
Ok(stream) => {
|
||||
let bridge: std::sync::Arc<PtyBridge> = std::sync::Arc::clone(&state.pty_bridge);
|
||||
@ -296,7 +299,7 @@ pub fn reattach_terminal(
|
||||
break;
|
||||
}
|
||||
}
|
||||
bridge.unregister(&sid);
|
||||
bridge.unregister_if(&sid, gen);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@ -767,11 +770,11 @@ pub async fn launch_agent(
|
||||
let session_id = output.session.id;
|
||||
|
||||
// Register the xterm output channel for this session.
|
||||
state.pty_bridge.register(session_id, on_output);
|
||||
let gen = state.pty_bridge.register(session_id, on_output);
|
||||
|
||||
// Subscribe to the PTY's byte stream and pump it to the channel.
|
||||
// The stream is a blocking iterator; it runs on a dedicated OS thread and
|
||||
// ends when the PTY hits EOF or the bridge channel is gone.
|
||||
// ends when the PTY hits EOF or this attach is superseded.
|
||||
let handle = PtyHandle { session_id };
|
||||
match state.pty_port.subscribe_output(&handle) {
|
||||
Ok(stream) => {
|
||||
@ -782,7 +785,7 @@ pub async fn launch_agent(
|
||||
break;
|
||||
}
|
||||
}
|
||||
bridge.unregister(&session_id);
|
||||
bridge.unregister_if(&session_id, gen);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user