mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 08:01:00 +00:00
fix: fix interrupted message position, handle zombie transfers on restart, fix upload status text
This commit is contained in:
+4
-1
@@ -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'
|
||||
|
||||
@@ -105,7 +105,10 @@ paused: "已暂停"
|
||||
completed: "已完成"
|
||||
failed: "失败"
|
||||
cancelled: "已取消"
|
||||
interrupted: "已中断"
|
||||
session: "会话"
|
||||
zombie: "僵尸任务"
|
||||
zombie_reason: "此传输任务因程序退出而中断,传输进程已不存在。"
|
||||
|
||||
software_builtin: "软件内置"
|
||||
retry: "重试"
|
||||
|
||||
+73
-10
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user