mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(remote): don't replace a pane that could not be reached with a fresh shell
Reattaching a restored pane treated every failure other than silence as "the pane is gone on its machine" and spawned a fresh shell in its place. That includes the link never opening at all — the server refusing another session, a transport error — when the pane is still running over there. The tab lost its session, and the old shell was left orphaned on the remote daemon; one workspace had accumulated 53 shells for 17 panes. Only the daemon's explicit "no such pane" now means gone. For a remote pane, any other failure leaves it pending, and a pane that stands in for a running one retries on its own a few times (2s, 4s, 8s, 16s, 16s) before leaving it to Try Again. Local panes keep their previous behavior.
This commit is contained in:
+17
-1
@@ -1493,10 +1493,26 @@ impl TerminalView {
|
||||
log::warn!("attach to pane {id} went unanswered ({e:#}); spawning fresh");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e) if crate::terminal::attach_refused(&e) => {
|
||||
log::info!("pane {id} is gone on its machine ({e:#}); spawning fresh");
|
||||
None
|
||||
}
|
||||
// The daemon never got to say either way: the link to its
|
||||
// machine would not open, or broke before it answered. The pane
|
||||
// is most likely still running over there, so a fresh shell in
|
||||
// its place would orphan it — its scrollback, its agent session,
|
||||
// whatever it was doing — and put a stranger in its tab. Fail,
|
||||
// and let a retry reach the pane itself.
|
||||
Err(e) if !route.is_local() => {
|
||||
log::info!("pane {id} could not be reached to reattach ({e:#})");
|
||||
return Err(e.context(format!(
|
||||
"could not reach pane {id}, which may still be running"
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("pane {id} is gone ({e:#}); spawning fresh");
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
+22
-1
@@ -3886,7 +3886,28 @@ impl Tty7App {
|
||||
let parts = match parts {
|
||||
Ok(parts) => parts,
|
||||
Err(reason) => {
|
||||
pending.update(cx, |p, cx| p.fail(reason, cx));
|
||||
let retry = pending.update(cx, |p, cx| {
|
||||
p.fail(reason, cx);
|
||||
p.next_auto_retry()
|
||||
});
|
||||
if let Some(delay) = retry {
|
||||
let pending = pending.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor().timer(delay).await;
|
||||
let _ = this.update_in(cx, |app, window, cx| {
|
||||
let still_there = app.tabs.iter().any(|tab| {
|
||||
tab.pane.leaves().iter().any(|l| l.entity_id() == slot_id)
|
||||
});
|
||||
// Try Again got there first, or the tab is gone.
|
||||
if !still_there || !pending.read(cx).is_failed() {
|
||||
return;
|
||||
}
|
||||
pending.update(cx, |p, cx| p.retrying(cx));
|
||||
start_pane_spawn(pending.clone(), window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,6 +40,19 @@ pub struct PendingPane {
|
||||
pub machine: SharedString,
|
||||
pub state: PendingState,
|
||||
pub spawn: PendingSpawn,
|
||||
/// Retries made without anyone pressing Try Again. See
|
||||
/// [`PendingPane::next_auto_retry`].
|
||||
auto_retries: u32,
|
||||
}
|
||||
|
||||
/// How many times a pane standing in for one that may still be running asks
|
||||
/// again on its own before it leaves the question to Try Again.
|
||||
const AUTO_RETRY_LIMIT: u32 = 5;
|
||||
|
||||
/// 2s, 4s, 8s, 16s, 16s — about three quarters of a minute in all, which
|
||||
/// covers a reconnect without keeping a machine that is really gone busy.
|
||||
fn auto_retry_delay(restores: bool, made: u32) -> Option<Duration> {
|
||||
(restores && made < AUTO_RETRY_LIMIT).then(|| Duration::from_secs(1 << (made + 1).min(4)))
|
||||
}
|
||||
|
||||
impl EventEmitter<RetryRequested> for PendingPane {}
|
||||
@@ -55,9 +68,26 @@ impl PendingPane {
|
||||
machine: machine.into(),
|
||||
state: PendingState::Connecting,
|
||||
spawn,
|
||||
auto_retries: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// How long to wait before asking again on its own, or `None` once it has
|
||||
/// asked enough. Only a pane that stands in for one already running on its
|
||||
/// machine retries by itself: the failure that leaves it here is the link,
|
||||
/// not the pane, and the link usually comes back within seconds — the
|
||||
/// pane behind it has been running the whole time. A brand-new pane that
|
||||
/// failed has nothing waiting for it; Try Again is the user's call.
|
||||
pub fn next_auto_retry(&mut self) -> Option<Duration> {
|
||||
let delay = auto_retry_delay(self.spawn.restore_pane.is_some(), self.auto_retries)?;
|
||||
self.auto_retries += 1;
|
||||
Some(delay)
|
||||
}
|
||||
|
||||
pub fn is_failed(&self) -> bool {
|
||||
matches!(self.state, PendingState::Failed(_))
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, reason: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
self.state = PendingState::Failed(reason.into());
|
||||
cx.notify();
|
||||
@@ -139,3 +169,26 @@ impl Render for PendingPane {
|
||||
.child(body)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_a_pane_standing_in_for_a_running_one_retries_by_itself() {
|
||||
assert_eq!(
|
||||
auto_retry_delay(false, 0),
|
||||
None,
|
||||
"a new pane has nothing waiting"
|
||||
);
|
||||
let delays: Vec<u64> = (0..)
|
||||
.map_while(|made| auto_retry_delay(true, made))
|
||||
.map(|d| d.as_secs())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
delays,
|
||||
vec![2, 4, 8, 16, 16],
|
||||
"then Try Again is the user's"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user