mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 00:00:59 +00:00
fix: correctly handle PTY exit as graceful and add SSH keepalives for fast disconnect detection
This commit is contained in:
+27
-11
@@ -28,10 +28,9 @@ use rust_i18n::t;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::{
|
||||
sftp::SftpHandle,
|
||||
session::config::{AuthMethod, ConfigStore},
|
||||
system::{SystemSampler, SystemSnapshot},
|
||||
terminal::{self, BackendCommand, BackendEvent, TabKind, TerminalTab},
|
||||
terminal::{self, BackendEvent, TabKind, TerminalTab},
|
||||
backend::ssh,
|
||||
};
|
||||
|
||||
@@ -558,7 +557,7 @@ impl Ashell {
|
||||
if self
|
||||
.connection_progress
|
||||
.as_ref()
|
||||
.is_some_and(|progress| progress.tab_id == tab_id)
|
||||
.is_some_and(|progress| progress.tab_id == tab_id && !progress.failed)
|
||||
{
|
||||
self.connection_progress = None;
|
||||
}
|
||||
@@ -644,6 +643,7 @@ impl Ashell {
|
||||
self.remote_sample_in_flight = false;
|
||||
return changed;
|
||||
}
|
||||
let mut needs_new_progress = false;
|
||||
if let Some(progress) = self.connection_progress.as_mut() {
|
||||
if progress.tab_id == tab_id {
|
||||
progress.lines.push(reason.clone().into());
|
||||
@@ -653,16 +653,32 @@ impl Ashell {
|
||||
let _ = tab_title;
|
||||
progress.title = t!("connection_failed").into();
|
||||
progress.failed = true;
|
||||
} else if !progress.failed {
|
||||
// We were showing connecting progress, but another tab dropped!
|
||||
// Switch to failed state so the user can see it and retry.
|
||||
progress.tab_id = tab_id.clone();
|
||||
let msg = format!("{}: {}", tab_title.unwrap_or_default(), reason);
|
||||
progress.lines.push(msg.into());
|
||||
self.connection_scroll_handle.set_offset(point(px(0.), px(-99999.0)));
|
||||
progress.title = t!("connection_failed").into();
|
||||
progress.failed = true;
|
||||
} else {
|
||||
// Already showing a failure dialog, just append the new failure
|
||||
let msg = format!("{}: {}", tab_title.unwrap_or_default(), reason);
|
||||
progress.lines.push(msg.into());
|
||||
self.connection_scroll_handle.set_offset(point(px(0.), px(-99999.0)));
|
||||
}
|
||||
} else if let Some(_) = session_label {
|
||||
if !is_graceful_exit {
|
||||
self.connection_progress = Some(ConnectionProgress {
|
||||
tab_id: tab_id.clone(),
|
||||
title: t!("connection_failed").into(),
|
||||
lines: vec![reason.clone().into()],
|
||||
failed: true,
|
||||
});
|
||||
}
|
||||
needs_new_progress = true;
|
||||
}
|
||||
|
||||
if needs_new_progress && !is_graceful_exit {
|
||||
self.connection_progress = Some(ConnectionProgress {
|
||||
tab_id: tab_id.clone(),
|
||||
title: t!("connection_failed").into(),
|
||||
lines: vec![reason.clone().into()],
|
||||
failed: true,
|
||||
});
|
||||
}
|
||||
self.status = reason.into();
|
||||
}
|
||||
|
||||
+9
-3
@@ -115,7 +115,7 @@ async fn run_ssh(
|
||||
tab_id: tab_id.clone(),
|
||||
});
|
||||
|
||||
let mut exit_reason = "ssh connection lost (unknown)".to_string();
|
||||
let exit_reason;
|
||||
let mut is_graceful_close = false;
|
||||
|
||||
loop {
|
||||
@@ -146,7 +146,7 @@ async fn run_ssh(
|
||||
bytes: data.to_vec(),
|
||||
});
|
||||
}
|
||||
Some(ChannelMsg::ExitStatus { exit_status: _ }) => {
|
||||
Some(ChannelMsg::ExitStatus { exit_status: _ }) | Some(ChannelMsg::Eof) => {
|
||||
is_graceful_close = true;
|
||||
}
|
||||
Some(ChannelMsg::Close) => {
|
||||
@@ -158,7 +158,11 @@ async fn run_ssh(
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
exit_reason = "ssh connection lost (network drop)".to_string();
|
||||
if is_graceful_close {
|
||||
exit_reason = "ssh session closed".to_string();
|
||||
} else {
|
||||
exit_reason = "ssh connection lost (network drop)".to_string();
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
@@ -184,6 +188,8 @@ async fn connect_and_authenticate(
|
||||
) -> Result<russh::client::Handle<ClientHandler>> {
|
||||
let config = Arc::new(client::Config {
|
||||
inactivity_timeout: Some(std::time::Duration::from_secs(600)),
|
||||
keepalive_interval: Some(std::time::Duration::from_secs(10)),
|
||||
keepalive_max: 3,
|
||||
..Default::default()
|
||||
});
|
||||
let addr = format!("{}:{}", session.host, session.port);
|
||||
|
||||
+70
-46
@@ -16,7 +16,6 @@ use self::config::{AuthMethod, Session};
|
||||
use crate::{
|
||||
Ashell, ConnectionProgress, PaneLayout, SelectorEntry, TabGroup,
|
||||
backend::{local, ssh},
|
||||
sftp,
|
||||
terminal::{BackendCommand, RenderSnapshot, TabKind, TerminalTab},
|
||||
app::constants::{DEFAULT_COLS, DEFAULT_ROWS, SIDEBAR_WIDTH, TAB_BAR_HEIGHT, TERMINAL_PADDING_X, TERMINAL_PADDING_Y},
|
||||
};
|
||||
@@ -420,53 +419,71 @@ impl Ashell {
|
||||
let Some(progress) = self.connection_progress.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(ix) = self.tabs.iter().position(|tab| tab.id == progress.tab_id) else {
|
||||
self.connection_progress = None;
|
||||
cx.notify();
|
||||
return;
|
||||
};
|
||||
let Some(session) = self.tabs[ix].session.clone() else {
|
||||
self.connection_progress = None;
|
||||
cx.notify();
|
||||
return;
|
||||
};
|
||||
self.connection_progress = None;
|
||||
let mut groups_to_restart_sftp = std::collections::HashSet::new();
|
||||
|
||||
// Close old backend
|
||||
self.tabs[ix].backend.send(BackendCommand::Close);
|
||||
|
||||
// Spawn new backend
|
||||
let backend = ssh::spawn_ssh_terminal(
|
||||
self.runtime.handle(),
|
||||
progress.tab_id.clone(),
|
||||
session.clone(),
|
||||
DEFAULT_COLS,
|
||||
DEFAULT_ROWS,
|
||||
self.events_tx.clone(),
|
||||
);
|
||||
|
||||
// Replace tab state in-place to reuse the UI component
|
||||
self.tabs[ix] = TerminalTab::new_ssh(
|
||||
progress.tab_id.clone(),
|
||||
&session,
|
||||
backend,
|
||||
self.events_tx.clone(),
|
||||
);
|
||||
|
||||
// Restart SFTP if applicable
|
||||
if let Some(group) = self.tab_groups.iter_mut().find(|g| g.pane_root.contains(&progress.tab_id)) {
|
||||
if let Some(old_handle) = self.sftp_handles.remove(&group.id) {
|
||||
old_handle.close();
|
||||
let mut retry_tabs = Vec::new();
|
||||
for (ix, tab) in self.tabs.iter().enumerate() {
|
||||
if !tab.connected && tab.session.is_some() {
|
||||
retry_tabs.push((ix, tab.id.clone(), tab.session.clone().unwrap()));
|
||||
}
|
||||
let sftp_handle = crate::sftp::spawn_sftp(
|
||||
}
|
||||
|
||||
if retry_tabs.is_empty() {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
for (ix, tab_id, session) in retry_tabs {
|
||||
// Close old backend
|
||||
self.tabs[ix].backend.send(BackendCommand::Close);
|
||||
|
||||
// Spawn new backend
|
||||
let backend = ssh::spawn_ssh_terminal(
|
||||
self.runtime.handle(),
|
||||
group.id.clone(),
|
||||
tab_id.clone(),
|
||||
session.clone(),
|
||||
DEFAULT_COLS,
|
||||
DEFAULT_ROWS,
|
||||
self.events_tx.clone(),
|
||||
);
|
||||
self.sftp_handles.insert(group.id.clone(), sftp_handle);
|
||||
|
||||
if let Some(sftp) = group.sftp.as_mut() {
|
||||
sftp.status = rust_i18n::t!("sftp_connecting").to_string();
|
||||
|
||||
// Replace tab state in-place to reuse the UI component
|
||||
self.tabs[ix] = TerminalTab::new_ssh(
|
||||
tab_id.clone(),
|
||||
&session,
|
||||
backend,
|
||||
self.events_tx.clone(),
|
||||
);
|
||||
|
||||
// Find group to restart SFTP
|
||||
if let Some(group) = self.tab_groups.iter().find(|g| g.pane_root.contains(&tab_id)) {
|
||||
groups_to_restart_sftp.insert(group.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Restart SFTP for affected groups
|
||||
for group_id in groups_to_restart_sftp {
|
||||
if let Some(group) = self.tab_groups.iter_mut().find(|g| g.id == group_id) {
|
||||
// Use the session of any tab in that group
|
||||
let group_session = self.tabs.iter().find(|t| group.pane_root.contains(&t.id) && t.session.is_some()).and_then(|t| t.session.clone());
|
||||
|
||||
if let Some(session) = group_session {
|
||||
if let Some(old_handle) = self.sftp_handles.remove(&group.id) {
|
||||
old_handle.close();
|
||||
}
|
||||
let sftp_handle = crate::sftp::spawn_sftp(
|
||||
self.runtime.handle(),
|
||||
group.id.clone(),
|
||||
session,
|
||||
self.events_tx.clone(),
|
||||
);
|
||||
self.sftp_handles.insert(group.id.clone(), sftp_handle);
|
||||
|
||||
if let Some(sftp) = group.sftp.as_mut() {
|
||||
sftp.status = rust_i18n::t!("sftp_connecting").to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,16 +493,23 @@ impl Ashell {
|
||||
lines: vec![t!("starting_connection").into()],
|
||||
failed: false,
|
||||
});
|
||||
self.status = "ssh tab retrying".into();
|
||||
self.status = "ssh tabs retrying".into();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_connection_progress(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(progress) = self.connection_progress.clone() else {
|
||||
if self.connection_progress.is_none() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
self.connection_progress = None;
|
||||
self.close_tab(progress.tab_id, cx);
|
||||
let tabs_to_close: Vec<_> = self.tabs.iter()
|
||||
.filter(|tab| !tab.connected && tab.session.is_some())
|
||||
.map(|tab| tab.id.clone())
|
||||
.collect();
|
||||
for id in tabs_to_close {
|
||||
self.handle_tab_close(id);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -435,7 +435,6 @@ impl EventListener for TerminalListener {
|
||||
}
|
||||
}
|
||||
|
||||
use rust_i18n::t;
|
||||
fn new_term(
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
|
||||
Reference in New Issue
Block a user