进程管理:右侧栏的新工具,列出 Linux 主机的进程,可查看详情和结束进程
- 按内存或 CPU 排序、搜索,每 15 秒刷新;点进程打开详情,含完整命令 - 右键或详情里结束 / 强制结束进程,先确认 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d455e02f5
commit
3e8f635fa7
21 files changed
+2829
-76
No files matched your search
@@ -14,6 +14,7 @@ mod host_dialog;
|
||||
mod host_tree;
|
||||
mod monitor;
|
||||
mod netstat;
|
||||
mod processes;
|
||||
mod settings;
|
||||
mod sftp;
|
||||
mod terminal;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user