进程管理:右侧栏的新工具,列出 Linux 主机的进程,可查看详情和结束进程

- 按内存或 CPU 排序、搜索,每 15 秒刷新;点进程打开详情,含完整命令
- 右键或详情里结束 / 强制结束进程,先确认

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-02 16:10:56 +08:00
1 parent 0d455e02f5
commit 3e8f635fa7
21 files changed
+2829 -76

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ mod host_dialog;
mod host_tree;
mod monitor;
mod netstat;
mod processes;
mod settings;
mod sftp;
mod terminal;
+1
View File
@@ -257,6 +257,7 @@ fn the_monitor_is_not_offered_on_a_host_known_not_to_run_linux(cx: &mut TestAppC
in_frame(cx, handle, |window, _| {
assert!(window.try_find("tool-monitor").is_none());
assert!(window.try_find("tool-connections").is_none());
assert!(window.try_find("tool-processes").is_none());
assert!(window.find("tool-history").visible());
assert!(
window
+444
View File
@@ -0,0 +1,444 @@
//! 进程管理: the right sidebar's tool that lists the processes of the SSH
//! terminal's host, and ends them, over the terminal's own connection.
use gpui_kit::{SharedString, Window};
use shellrs::app::EndProcess;
use crate::support::*;
/// A small host: systemd, a java service and a node app, with 2 000 000 kB
/// of memory.
const FIRST: &str = "\
@@uptime
1000.00 3000.00
@@memory
2000000
@@clock
100
@@page
4096
@@users
0 root
1000 ecs-user
@@processes
0 1 (systemd) S 0 1 1 0 -1 4194560 0 0 0 0 150 80 0 0 20 0 1 0 5 172000000 3000 0
0 2202 (java) S 1 2202 2202 0 -1 4194560 0 0 0 0 1000 0 0 0 20 0 30 0 5000 900000000 120000 0
1000 1479 (node) R 1 1479 1479 0 -1 4194560 0 0 0 0 4000 0 0 0 20 0 11 0 9000 900000000 30000 0
";
/// Two seconds later: java used a tenth of a second of CPU, node a whole
/// second.
const SECOND: &str = "\
@@uptime
1002.00 3000.00
@@memory
2000000
@@clock
100
@@page
4096
@@users
0 root
1000 ecs-user
@@processes
0 1 (systemd) S 0 1 1 0 -1 4194560 0 0 0 0 150 80 0 0 20 0 1 0 5 172000000 3000 0
0 2202 (java) S 1 2202 2202 0 -1 4194560 0 0 0 0 1010 0 0 0 20 0 30 0 5000 900000000 120000 0
1000 1479 (node) R 1 1479 1479 0 -1 4194560 0 0 0 0 4100 0 0 0 20 0 11 0 9000 900000000 30000 0
";
const JAVA: u64 = 2202;
const NODE: u64 = 1479;
const SYSTEMD: u64 = 1;
fn open_processes(
cx: &mut TestAppContext,
factory: Arc<FakeTerminalFactory>,
) -> WindowHandle<Root> {
let (handle, _) = open_workspace_with_remote_factory(cx, HostStore::seed(), factory);
in_frame(cx, handle, |window, cx| {
window.click(("terminal-tab", INITIAL_WEB_TERMINAL), cx)
});
in_frame(cx, handle, |window, cx| window.click("tool-processes", cx));
handle
}
async fn wait_for_label(
cx: &mut TestAppContext,
handle: WindowHandle<Root>,
id: impl Into<ElementId> + Clone,
label: &str,
) {
cx.wait_for(handle.into(), Duration::from_secs(5), |window, _| {
window
.try_find(id.clone())
.is_some_and(|element| element.label() == Some(label))
})
.await;
}
/// The processes from the top of the list down.
fn listed(window: &mut Window) -> Vec<u64> {
let mut cards: Vec<(f32, u64)> = [JAVA, NODE, SYSTEMD]
.into_iter()
.filter_map(|pid| {
let card = window.try_find(("process", pid))?;
Some((f32::from(card.bounds().origin.y), pid))
})
.collect();
cards.sort_by(|a, b| a.0.total_cmp(&b.0));
cards.into_iter().map(|(_, pid)| pid).collect()
}
#[gpui_kit::test]
async fn the_processes_show_by_memory_and_their_cpu_once_read_twice(cx: &mut TestAppContext) {
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST, SECOND]));
let handle = open_processes(cx, factory.clone());
wait_for_label(
cx,
handle,
"processes-summary",
"共 3 个进程 · 每 15 秒刷新",
)
.await;
in_frame(cx, handle, |window, _| {
assert_eq!(
window.find(("tool-sidebar", INITIAL_WEB_TERMINAL)).label(),
Some("进程管理")
);
assert_eq!(listed(window), [JAVA, NODE, SYSTEMD]);
assert_eq!(window.find("processes-count").label(), Some("3"));
assert_eq!(window.find("processes-sort-memory").checked(), Some(true));
});
// The second reading comes soon, and gives the CPU shares.
wait_for_label(
cx,
handle,
("process", JAVA),
"java · PID 2202 · 休眠 · root · 内存 468.75 MB · 24.0% · CPU 5.0%",
)
.await;
in_frame(cx, handle, |window, _| {
assert_eq!(
window.find(("process", NODE)).label(),
Some("node · PID 1479 · 运行中 · ecs-user · 内存 117.19 MB · 6.0% · CPU 50.0%")
);
});
assert_eq!(factory.exec_count(), 2);
// Then every 15 seconds.
cx.executor().advance_clock(Duration::from_secs(14));
cx.run_until_parked();
assert_eq!(factory.exec_count(), 2);
cx.executor().advance_clock(Duration::from_secs(2));
cx.run_until_parked();
assert_eq!(factory.exec_count(), 3);
}
#[gpui_kit::test]
async fn the_processes_sort_by_cpu_and_back(cx: &mut TestAppContext) {
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST, SECOND]));
let handle = open_processes(cx, factory);
wait_for_label(
cx,
handle,
("process", NODE),
"node · PID 1479 · 运行中 · ecs-user · 内存 117.19 MB · 6.0% · CPU 50.0%",
)
.await;
in_frame(cx, handle, |window, cx| {
window.click("processes-sort-cpu", cx)
});
in_frame(cx, handle, |window, _| {
assert_eq!(window.find("processes-sort-cpu").checked(), Some(true));
assert_eq!(window.find("processes-sort-memory").checked(), Some(false));
assert_eq!(listed(window), [NODE, JAVA, SYSTEMD]);
});
// Again: the least first.
in_frame(cx, handle, |window, cx| {
window.click("processes-sort-cpu", cx)
});
in_frame(cx, handle, |window, _| {
assert_eq!(listed(window), [SYSTEMD, JAVA, NODE]);
});
in_frame(cx, handle, |window, cx| {
window.click("processes-sort-memory", cx)
});
in_frame(cx, handle, |window, _| {
assert_eq!(listed(window), [JAVA, NODE, SYSTEMD]);
});
}
#[gpui_kit::test]
async fn the_search_finds_processes_by_name_pid_or_user(cx: &mut TestAppContext) {
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST]));
let handle = open_processes(cx, factory);
wait_for_label(
cx,
handle,
"processes-summary",
"共 3 个进程 · 每 15 秒刷新",
)
.await;
in_frame(cx, handle, |window, cx| {
window.click("processes-search", cx);
window.input("ecs", cx);
});
in_frame(cx, handle, |window, _| {
assert_eq!(listed(window), [NODE]);
assert_eq!(window.find("processes-count").label(), Some("1"));
// The summary is still the host's.
assert_eq!(
window.find("processes-summary").label(),
Some("共 3 个进程 · 每 15 秒刷新")
);
});
in_frame(cx, handle, |window, cx| {
window.press("cmd-a", cx);
window.input("2202", cx);
});
in_frame(cx, handle, |window, _| {
assert_eq!(listed(window), [JAVA]);
});
in_frame(cx, handle, |window, cx| {
window.press("cmd-a", cx);
window.input("nginx", cx);
});
in_frame(cx, handle, |window, _| {
assert_eq!(
window.find("processes-empty").label(),
Some("没有符合条件的进程")
);
});
}
#[gpui_kit::test]
async fn ending_a_process_asks_first_then_reads_the_list_again(cx: &mut TestAppContext) {
let after = SECOND
.lines()
.filter(|line| !line.contains("(java)"))
.collect::<Vec<_>>()
.join("\n");
let factory = Arc::new(FakeTerminalFactory::answering(&[
FIRST,
SECOND,
"@@status 0\n",
&after,
]));
let handle = open_processes(cx, factory.clone());
wait_for_label(
cx,
handle,
("process", JAVA),
"java · PID 2202 · 休眠 · root · 内存 468.75 MB · 24.0% · CPU 5.0%",
)
.await;
in_frame(cx, handle, |window, cx| {
window.dispatch_action(
Box::new(EndProcess {
pid: 2202,
force: false,
}),
cx,
)
});
// Nothing is sent before the answer.
assert_eq!(factory.exec_count(), 2);
in_frame(cx, handle, |window, cx| window.click("ok", cx));
cx.wait_for(handle.into(), Duration::from_secs(5), |window, _| {
window.try_find(("process", JAVA)).is_none()
})
.await;
let commands = factory.exec_commands();
assert_eq!(
commands[2],
"sh -c 'kill -TERM 2202 2>&1; echo @@status $?'"
);
assert_eq!(commands.len(), 4);
}
/// What `/proc/2202/cmdline` holds: the arguments, each ended by a NUL.
const JAVA_COMMAND: &str =
"/usr/lib/jvm/java-21-openjdk-amd64/bin/java\0-Xms128m\0-jar\0opc-server-0.0.1.jar\0";
#[gpui_kit::test]
async fn a_click_on_a_process_shows_its_details_and_command_line(cx: &mut TestAppContext) {
let factory = Arc::new(FakeTerminalFactory::answering(&[
FIRST,
SECOND,
JAVA_COMMAND,
]));
let handle = open_processes(cx, factory.clone());
wait_for_label(
cx,
handle,
("process", JAVA),
"java · PID 2202 · 休眠 · root · 内存 468.75 MB · 24.0% · CPU 5.0%",
)
.await;
in_frame(cx, handle, |window, cx| window.click(("process", JAVA), cx));
wait_for_label(
cx,
handle,
"process-command",
"/usr/lib/jvm/java-21-openjdk-amd64/bin/java -Xms128m -jar opc-server-0.0.1.jar",
)
.await;
assert_eq!(factory.exec_commands()[2], "sh -c 'cat /proc/2202/cmdline'");
in_frame(cx, handle, |window, _| {
let field = |label: &str| {
window
.find(SharedString::from(format!("process-field:{label}")))
.label()
.map(str::to_owned)
};
for (label, value) in [
("PID", "2202"),
("父进程", "systemd (1)"),
("用户", "root"),
// A session leader with threads.
("状态", "休眠 (Ssl)"),
// Up since tick 5000 of a host up 1002 s.
("运行时长", "00:15:52"),
("终端", "无"),
("优先级", "20"),
("Nice", "0"),
("CPU 使用率", "5.0%"),
("累计 CPU 时间", "00:00:10"),
("常驻内存", "468.75 MB (24.0%)"),
("虚拟内存", "858.31 MB"),
("线程", "30"),
("直接子进程", "0"),
("全部后代进程", "0"),
] {
assert_eq!(field(label).as_deref(), Some(value), "{label}");
}
});
in_frame(cx, handle, |window, cx| {
window.click("process-copy-command", cx)
});
assert_eq!(
cx.read_from_clipboard().and_then(|item| item.text()),
Some(
"/usr/lib/jvm/java-21-openjdk-amd64/bin/java -Xms128m -jar opc-server-0.0.1.jar".into()
)
);
in_frame(cx, handle, |window, cx| {
window.click("process-copy-pid", cx)
});
assert_eq!(
cx.read_from_clipboard().and_then(|item| item.text()),
Some("2202".into())
);
}
#[gpui_kit::test]
async fn the_details_end_the_process_after_asking(cx: &mut TestAppContext) {
let after = SECOND
.lines()
.filter(|line| !line.contains("(java)"))
.collect::<Vec<_>>()
.join("\n");
let factory = Arc::new(FakeTerminalFactory::answering(&[
FIRST,
SECOND,
JAVA_COMMAND,
"@@status 0\n",
&after,
]));
let handle = open_processes(cx, factory.clone());
wait_for_label(
cx,
handle,
("process", JAVA),
"java · PID 2202 · 休眠 · root · 内存 468.75 MB · 24.0% · CPU 5.0%",
)
.await;
in_frame(cx, handle, |window, cx| window.click(("process", JAVA), cx));
cx.wait_for(handle.into(), Duration::from_secs(5), |window, _| {
window.try_find("process-command").is_some()
})
.await;
// The details give way to the question: one dialog at a time.
in_frame(cx, handle, |window, cx| {
window.click("process-force-end", cx)
});
in_frame(cx, handle, |window, _| {
assert!(window.try_find("process-details").is_none());
});
assert_eq!(factory.exec_count(), 3);
in_frame(cx, handle, |window, cx| window.click("ok", cx));
cx.wait_for(handle.into(), Duration::from_secs(5), |window, _| {
window.try_find(("process", JAVA)).is_none()
})
.await;
assert_eq!(
factory.exec_commands()[3],
"sh -c 'kill -KILL 2202 2>&1; echo @@status $?'"
);
}
#[gpui_kit::test]
async fn the_processes_are_read_only_while_they_show(cx: &mut TestAppContext) {
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST, SECOND]));
let handle = open_processes(cx, factory.clone());
wait_for_label(
cx,
handle,
("process", JAVA),
"java · PID 2202 · 休眠 · root · 内存 468.75 MB · 24.0% · CPU 5.0%",
)
.await;
in_frame(cx, handle, |window, cx| window.click("tool-processes", cx));
let hidden = factory.exec_count();
cx.executor().advance_clock(Duration::from_secs(60));
cx.run_until_parked();
assert_eq!(factory.exec_count(), hidden);
// Back, the list it had shows at once while it is read again.
in_frame(cx, handle, |window, cx| window.click("tool-processes", cx));
in_frame(cx, handle, |window, _| {
assert_eq!(listed(window), [JAVA, NODE, SYSTEMD]);
});
cx.wait_for(handle.into(), Duration::from_secs(5), |_, _| {
factory.exec_count() > hidden
})
.await;
}
#[gpui_kit::test]
async fn the_processes_wait_for_the_terminal_to_connect(cx: &mut TestAppContext) {
// web-01's terminal is the first started, and exits at once.
let handle = open_processes(cx, Arc::new(FakeTerminalFactory::exit_first()));
wait_for_label(
cx,
handle,
"processes-message",
"终端没有连接。连接后这里显示主机的进程。",
)
.await;
}
#[gpui_kit::test]
async fn the_processes_say_they_read_linux_hosts_only(cx: &mut TestAppContext) {
let factory = Arc::new(FakeTerminalFactory::answering(&["@@unsupported\nDarwin\n"]));
let handle = open_processes(cx, factory);
wait_for_label(
cx,
handle,
"processes-message",
"暂不支持管理 Darwin 的进程,目前只支持 Linux 主机。",
)
.await;
}