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:
@ -34,7 +34,11 @@ pub type PtyChunk = Vec<u8>;
|
||||
/// Thread-safe; cloned `Arc<PtyBridge>` is held in [`crate::state::AppState`].
|
||||
#[derive(Default)]
|
||||
pub struct PtyBridge {
|
||||
channels: Mutex<HashMap<SessionId, Channel<PtyChunk>>>,
|
||||
/// Per session: a monotonically-increasing **generation** plus the current
|
||||
/// output channel. The generation distinguishes successive (re-)attaches so a
|
||||
/// superseded pump thread can't tear down the channel of the attach that
|
||||
/// replaced it (see [`PtyBridge::unregister_if`]).
|
||||
channels: Mutex<HashMap<SessionId, (u64, Channel<PtyChunk>)>>,
|
||||
}
|
||||
|
||||
impl PtyBridge {
|
||||
@ -46,21 +50,39 @@ impl PtyBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the output channel for a session (called when a terminal is
|
||||
/// opened, from a `#[tauri::command]` that receives the `Channel` argument).
|
||||
pub fn register(&self, session: SessionId, channel: Channel<PtyChunk>) {
|
||||
/// Registers (or replaces) the output channel for a session and returns the
|
||||
/// **generation** of this registration. Each call for a session bumps the
|
||||
/// generation, so the caller's pump thread can later tear down *only its own*
|
||||
/// registration via [`unregister_if`](Self::unregister_if).
|
||||
pub fn register(&self, session: SessionId, channel: Channel<PtyChunk>) -> u64 {
|
||||
if let Ok(mut map) = self.channels.lock() {
|
||||
map.insert(session, channel);
|
||||
let gen = map.get(&session).map_or(0, |(g, _)| g.wrapping_add(1));
|
||||
map.insert(session, (gen, channel));
|
||||
gen
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a session's channel (terminal closed).
|
||||
/// Removes a session's channel unconditionally (terminal explicitly closed).
|
||||
pub fn unregister(&self, session: &SessionId) {
|
||||
if let Ok(mut map) = self.channels.lock() {
|
||||
map.remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a session's channel **only if** `gen` is still the current
|
||||
/// generation. A pump thread calls this when its output stream ends: if the
|
||||
/// session has since been re-attached (newer generation), this is a no-op, so
|
||||
/// the dying thread never unregisters the live channel that superseded it.
|
||||
pub fn unregister_if(&self, session: &SessionId, gen: u64) {
|
||||
if let Ok(mut map) = self.channels.lock() {
|
||||
if matches!(map.get(session), Some((g, _)) if *g == gen) {
|
||||
map.remove(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards a chunk of output bytes to a session's channel.
|
||||
///
|
||||
/// Returns `true` if the chunk was delivered, `false` if no channel is
|
||||
@ -71,7 +93,7 @@ impl PtyBridge {
|
||||
return false;
|
||||
};
|
||||
match map.get(session) {
|
||||
Some(channel) => channel.send(chunk).is_ok(),
|
||||
Some((_, channel)) => channel.send(chunk).is_ok(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user