fix: fix interrupted message position, handle zombie transfers on restart, fix upload status text

This commit is contained in:
TomZz
2026-06-16 01:06:43 +08:00
parent e4fbd65a31
commit 4c7c06f391
6 changed files with 180 additions and 24 deletions
+4 -1
View File
@@ -103,7 +103,10 @@ paused: "Paused"
completed: "Completed"
failed: "Failed"
cancelled: "Cancelled"
interrupted: "Interrupted"
session: "Session"
zombie: "Zombie"
zombie_reason: "This transfer was interrupted because the application exited. The transfer task no longer exists."
software_builtin: "Built-in"
retry: "Retry"
@@ -140,4 +143,4 @@ settings_split_pane_right: "Split Pane Right"
settings_split_pane_up: "Split Pane Up"
settings_split_pane_down: "Split Pane Down"
settings_close_pane: "Close Pane"
keybind_conflict: "\"%{key}\" is already used by \"%{action}\", please choose another key"
keybind_conflict: '"%{key}" is already used by "%{action}", please choose another key'
+3
View File
@@ -105,7 +105,10 @@ paused: "已暂停"
completed: "已完成"
failed: "失败"
cancelled: "已取消"
interrupted: "已中断"
session: "会话"
zombie: "僵尸任务"
zombie_reason: "此传输任务因程序退出而中断,传输进程已不存在。"
software_builtin: "软件内置"
retry: "重试"
+73 -10
View File
@@ -516,7 +516,10 @@ impl Ashell {
format_bytes(tot)
)
} else {
format!("{}...", t!("downloading"))
match t.info.kind {
crate::terminal::TransferType::Upload => format!("{}...", t!("uploading")),
crate::terminal::TransferType::Download => format!("{}...", t!("downloading")),
}
};
let btn_pause =
Button::new(SharedString::from(format!("pause-{}", t.info.id)))
@@ -582,6 +585,23 @@ impl Ashell {
}));
(txt, h_flex().gap_1().child(btn_resume).child(btn_cancel))
}
crate::terminal::TransferState::Interrupted(ref reason) => {
let txt = format!("{}: {}", t!("interrupted"), reason);
let btn_remove = Button::new(SharedString::from(format!(
"remove-{}",
t.info.id
)))
.ghost()
.small()
.icon(IconName::Close)
.on_click(window.listener_for(&view, {
let id = t.info.id.clone();
move |this, _, _, cx| {
this.remove_transfer(&id, cx);
}
}));
(txt, h_flex().gap_1().child(btn_remove))
}
crate::terminal::TransferState::Completed => {
let txt = t!("completed").to_string();
let mut actions = h_flex().gap_1();
@@ -603,13 +623,55 @@ impl Ashell {
});
actions = actions.child(btn_folder);
}
let btn_remove = Button::new(SharedString::from(format!(
"remove-{}",
t.info.id
)))
.ghost()
.small()
.icon(IconName::Close)
.on_click(window.listener_for(&view, {
let id = t.info.id.clone();
move |this, _, _, cx| {
this.remove_transfer(&id, cx);
}
}));
actions = actions.child(btn_remove);
(txt, actions)
}
crate::terminal::TransferState::Failed(ref err) => {
(format!("{}: {}", t!("failed"), err), h_flex().gap_1())
let txt = format!("{}: {}", t!("failed"), err);
let btn_remove = Button::new(SharedString::from(format!(
"remove-{}",
t.info.id
)))
.ghost()
.small()
.icon(IconName::Close)
.on_click(window.listener_for(&view, {
let id = t.info.id.clone();
move |this, _, _, cx| {
this.remove_transfer(&id, cx);
}
}));
(txt, h_flex().gap_1().child(btn_remove))
}
crate::terminal::TransferState::Cancelled => {
(t!("cancelled").to_string(), h_flex().gap_1())
crate::terminal::TransferState::Zombie(ref reason) => {
let txt = format!("{}: {}", t!("zombie"), reason);
let btn_remove = Button::new(SharedString::from(format!(
"remove-{}",
t.info.id
)))
.ghost()
.small()
.icon(IconName::Close)
.on_click(window.listener_for(&view, {
let id = t.info.id.clone();
move |this, _, _, cx| {
this.remove_transfer(&id, cx);
}
}));
(txt, h_flex().gap_1().child(btn_remove))
}
};
@@ -665,14 +727,14 @@ impl Ashell {
t!("session"),
t.tab_title
)),
)
.child(
div()
.text_size(px(11.))
.text_color(cx.theme().muted_foreground)
.child(status_text.clone()),
),
)
.child(
div()
.text_size(px(11.))
.text_color(cx.theme().muted_foreground)
.child(status_text),
)
.child(actions),
)
.when(
@@ -691,6 +753,7 @@ impl Ashell {
)
},
)
}));
let scroll_handle = window
+17 -1
View File
@@ -440,7 +440,15 @@ impl Ashell {
sftp_new_folder_input,
sftp_delete_scroll_handle: gpui::ScrollHandle::new(),
show_hidden_files: config.show_hidden_files(),
transfers: config.transfers(),
transfers: {
let mut transfers = config.transfers();
for t in transfers.iter_mut() {
if matches!(t.state, crate::terminal::TransferState::Running | crate::terminal::TransferState::Paused) {
t.state = crate::terminal::TransferState::Zombie(t!("zombie_reason").to_string());
}
}
transfers
},
show_transfers_dialog: false,
system_status: None,
terminal_bounds: HashMap::new(),
@@ -854,4 +862,12 @@ impl Ashell {
size(px(cell_width), px(line_height)),
))
}
pub(crate) fn remove_transfer(&mut self, transfer_id: &str, cx: &mut Context<Self>) {
self.transfers.retain(|t| t.info.id != transfer_id);
self.config.set_transfers(self.transfers.clone());
cx.notify();
}
}
+81 -11
View File
@@ -379,16 +379,26 @@ async fn run_sftp(
}
Err(err) => {
let err_msg = format!("{err:#}");
let is_cancelled = err_msg.contains("transfer cancelled");
let state = if is_cancelled {
crate::terminal::TransferState::Interrupted("User cancelled".to_string())
} else {
crate::terminal::TransferState::Failed(err_msg.clone())
};
let _ = events_clone.send(BackendEvent::SftpStatus {
tab_id: tab_id_clone.clone(),
text: t!("download_failed", err = err_msg.clone()).to_string(),
text: if is_cancelled {
"Transmission cancelled".to_string()
} else {
t!("download_failed", err = err_msg.clone()).to_string()
},
});
let _ = events_clone.send(BackendEvent::TransferProgress {
tab_id: tab_id_clone,
id: id.clone(),
transferred: 0,
total: None,
state: crate::terminal::TransferState::Failed(err_msg),
state,
});
}
}
@@ -480,16 +490,26 @@ async fn run_sftp(
}
Err(err) => {
let err_msg = format!("{err:#}");
let is_cancelled = err_msg.contains("transfer cancelled");
let state = if is_cancelled {
crate::terminal::TransferState::Interrupted("User cancelled".to_string())
} else {
crate::terminal::TransferState::Failed(err_msg.clone())
};
let _ = events_clone.send(BackendEvent::SftpStatus {
tab_id: tab_id_clone.clone(),
text: t!("upload_failed", err = err_msg.clone()).to_string(),
text: if is_cancelled {
"Transmission cancelled".to_string()
} else {
t!("upload_failed", err = err_msg.clone()).to_string()
},
});
let _ = events_clone.send(BackendEvent::TransferProgress {
tab_id: tab_id_clone,
id: id.clone(),
transferred: 0,
total: None,
state: crate::terminal::TransferState::Failed(err_msg),
state,
});
}
}
@@ -1123,6 +1143,12 @@ async fn download_path_impl(
.await
.with_context(|| format!("create {}", local_dir.display()))?;
// Check for cancellation after initial setup
let state = flag.0.load(Ordering::SeqCst);
if state == 2 {
return Err(anyhow::anyhow!("transfer cancelled"));
}
let metadata = sftp
.metadata(remote)
.await
@@ -1216,7 +1242,15 @@ async fn download_remote_directory_archive(
base_name(remote_dir),
Uuid::new_v4()
);
// Check for cancellation before creating remote archive
let state = flag.0.load(Ordering::SeqCst);
if state == 2 {
return Err(anyhow::anyhow!("transfer cancelled"));
}
create_remote_archive(handle, remote_dir, &remote_archive).await?;
let local_extract_root = local_archive
.parent()
.unwrap_or_else(|| Path::new("."))
@@ -1322,6 +1356,12 @@ async fn upload_paths_impl(
tab_id: &str,
id: &str,
) -> Result<String> {
// Check for cancellation before starting
let state = flag.0.load(Ordering::SeqCst);
if state == 2 {
return Err(anyhow::anyhow!("transfer cancelled"));
}
create_remote_dir_all(sftp, remote_dir).await?;
let mut file_count = 0usize;
let mut folder_count = 0usize;
@@ -1374,8 +1414,19 @@ async fn upload_paths_impl(
}
}
// Check for cancellation before creating directories
let state = flag.0.load(Ordering::SeqCst);
if state == 2 {
return Err(anyhow::anyhow!("transfer cancelled"));
}
// Create directories sequentially first
for dir in dirs_to_create {
// Check for cancellation between each directory creation
let state = flag.0.load(Ordering::SeqCst);
if state == 2 {
return Err(anyhow::anyhow!("transfer cancelled"));
}
create_remote_dir_all(sftp, &dir).await?;
}
@@ -1543,14 +1594,31 @@ async fn exec_remote_command(
let mut stderr = Vec::new();
let mut stdout = Vec::new();
let mut exit_status = None;
while let Some(msg) = channel.wait().await {
match msg {
russh::ChannelMsg::Data { data } => stdout.extend_from_slice(&data),
russh::ChannelMsg::ExtendedData { data, .. } => stderr.extend_from_slice(&data),
russh::ChannelMsg::ExitStatus { exit_status: code } => exit_status = Some(code),
russh::ChannelMsg::Close => break,
_ => {}
// Add timeout to prevent indefinite blocking (300 seconds = 5 minutes)
let timeout = tokio::time::Duration::from_secs(300);
let result = tokio::time::timeout(timeout, async {
loop {
// Yield to allow cancellation
tokio::task::yield_now().await;
if let Some(msg) = channel.wait().await {
match msg {
russh::ChannelMsg::Data { data } => stdout.extend_from_slice(&data),
russh::ChannelMsg::ExtendedData { data, .. } => stderr.extend_from_slice(&data),
russh::ChannelMsg::ExitStatus { exit_status: code } => exit_status = Some(code),
russh::ChannelMsg::Close => break,
_ => {}
}
} else {
break;
}
}
})
.await;
if result.is_err() {
return Err(anyhow!("remote command timeout: {command}"));
}
match exit_status.unwrap_or(0) {
@@ -1714,6 +1782,8 @@ async fn extract_archive_to(path: &Path, target_dir: &Path) -> Result<()> {
Ok(())
}
#[derive(Clone)]
struct SftpClientHandler;
+2 -1
View File
@@ -684,7 +684,8 @@ pub enum TransferState {
Paused,
Completed,
Failed(String),
Cancelled,
Interrupted(String), // 中断传输:包含原因(例如 "User cancelled", "Network timeout"
Zombie(String), // 程序重启后残留的 Running/Paused 任务
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]