网络连接:右侧栏的新工具,列出 Linux 主机的 TCP、UDP 连接和监听端口
- 卡片显示协议、状态、地址、进程和用户,可搜索、按协议和状态筛选 - 打开或切换终端时读取一次,点刷新再读;系统监控和它共用终端里执行命令的部分 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a0436caf6
commit
0d455e02f5
19 files changed
+2368
-60
No files matched your search
@@ -13,6 +13,7 @@ mod forward;
|
||||
mod host_dialog;
|
||||
mod host_tree;
|
||||
mod monitor;
|
||||
mod netstat;
|
||||
mod settings;
|
||||
mod sftp;
|
||||
mod terminal;
|
||||
|
||||
@@ -256,6 +256,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.find("tool-history").visible());
|
||||
assert!(
|
||||
window
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
//! 网络连接: the right sidebar's tool that lists the sockets of the SSH
|
||||
//! terminal's host, read over the terminal's own connection.
|
||||
|
||||
use crate::support::*;
|
||||
|
||||
/// What `ss` says about a small web server, read as root.
|
||||
const FIRST: &str = "\
|
||||
@@uid
|
||||
0
|
||||
@@users
|
||||
0 root
|
||||
33 www-data
|
||||
@@ss
|
||||
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:((\"systemd-resolve\",pid=600,fd=13)) uid:101 ino:20123 sk:1 <->
|
||||
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=891,fd=3)) ino:21233 sk:5 <->
|
||||
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:((\"nginx\",pid=913756,fd=6)) uid:33 ino:30001 sk:6 <->
|
||||
tcp ESTAB 0 0 10.0.0.5:22 203.0.113.7:51234 users:((\"sshd\",pid=1234,fd=4)) ino:123456 sk:9 <->
|
||||
tcp TIME-WAIT 0 0 10.0.0.5:80 198.51.100.2:6000 timer:(timewait,50sec,0) ino:0 sk:a
|
||||
@@counts
|
||||
1 udp UNCONN
|
||||
2 tcp LISTEN
|
||||
1 tcp ESTAB
|
||||
1 tcp TIME-WAIT
|
||||
";
|
||||
|
||||
/// The same server a moment later: the visit to port 80 has gone, another
|
||||
/// login has come.
|
||||
const SECOND: &str = "\
|
||||
@@uid
|
||||
0
|
||||
@@users
|
||||
0 root
|
||||
@@ss
|
||||
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=891,fd=3)) ino:21233 sk:5 <->
|
||||
tcp ESTAB 0 0 10.0.0.5:22 203.0.113.7:51234 users:((\"sshd\",pid=1234,fd=4)) ino:123456 sk:9 <->
|
||||
tcp ESTAB 0 0 10.0.0.5:22 203.0.113.8:40000 users:((\"sshd\",pid=1300,fd=4)) ino:123457 sk:b <->
|
||||
@@counts
|
||||
1 tcp LISTEN
|
||||
2 tcp ESTAB
|
||||
";
|
||||
|
||||
fn open_connections(
|
||||
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-connections", cx)
|
||||
});
|
||||
handle
|
||||
}
|
||||
|
||||
async fn wait_for_label(
|
||||
cx: &mut TestAppContext,
|
||||
handle: WindowHandle<Root>,
|
||||
id: &'static str,
|
||||
label: &str,
|
||||
) {
|
||||
cx.wait_for(handle.into(), Duration::from_secs(5), |window, _| {
|
||||
window
|
||||
.try_find(id)
|
||||
.is_some_and(|element| element.label() == Some(label))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_connections_list_the_hosts_sockets_with_their_processes(cx: &mut TestAppContext) {
|
||||
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST]));
|
||||
let handle = open_connections(cx, factory.clone());
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 5 条 · 3 个监听端口 · 1 条已连接",
|
||||
)
|
||||
.await;
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(
|
||||
window.find(("tool-sidebar", INITIAL_WEB_TERMINAL)).label(),
|
||||
Some("网络连接")
|
||||
);
|
||||
assert_eq!(window.find("tool-connections").checked(), Some(true));
|
||||
assert_eq!(
|
||||
window
|
||||
.find("netstat-socket:tcp 0.0.0.0:22 0.0.0.0:*")
|
||||
.label(),
|
||||
Some("TCP 监听 · 本地 0.0.0.0:22 · 远端 0.0.0.0:* · sshd (PID 891) · root")
|
||||
);
|
||||
assert_eq!(
|
||||
window
|
||||
.find("netstat-socket:tcp 0.0.0.0:80 0.0.0.0:*")
|
||||
.label(),
|
||||
Some("TCP 监听 · 本地 0.0.0.0:80 · 远端 0.0.0.0:* · nginx (PID 913756) · www-data")
|
||||
);
|
||||
// No process holds a socket in TIME_WAIT.
|
||||
assert_eq!(
|
||||
window
|
||||
.find("netstat-socket:tcp 10.0.0.5:80 198.51.100.2:6000")
|
||||
.label(),
|
||||
Some("TCP TIME_WAIT · 本地 10.0.0.5:80 · 远端 198.51.100.2:6000 · — · —")
|
||||
);
|
||||
// Root sees everything, and the list is whole: nothing to say.
|
||||
assert!(window.try_find("netstat-truncated").is_none());
|
||||
assert!(window.try_find("netstat-processes").is_none());
|
||||
assert!(window.try_find("netstat-matches").is_none());
|
||||
});
|
||||
let command = &factory.exec_commands()[0];
|
||||
assert!(command.starts_with("sh -c '") && command.contains("ss -tuanpe"));
|
||||
|
||||
// A list holds still: nothing more runs until it is asked to.
|
||||
cx.executor().advance_clock(Duration::from_secs(10));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(factory.exec_count(), 1);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_search_narrows_the_list(cx: &mut TestAppContext) {
|
||||
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST]));
|
||||
let handle = open_connections(cx, factory);
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 5 条 · 3 个监听端口 · 1 条已连接",
|
||||
)
|
||||
.await;
|
||||
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("netstat-search", cx);
|
||||
window.input("sshd", cx);
|
||||
});
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(window.find("netstat-matches").label(), Some("筛选出 2 条"));
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 0.0.0.0:22 0.0.0.0:*")
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 10.0.0.5:22 203.0.113.7:51234")
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 0.0.0.0:80 0.0.0.0:*")
|
||||
.is_none()
|
||||
);
|
||||
// The summary is still the host's.
|
||||
assert_eq!(
|
||||
window.find("netstat-summary").label(),
|
||||
Some("共 5 条 · 3 个监听端口 · 1 条已连接")
|
||||
);
|
||||
});
|
||||
|
||||
// A PID finds its process.
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.press("cmd-a", cx);
|
||||
window.input("913756", cx);
|
||||
});
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(window.find("netstat-matches").label(), Some("筛选出 1 条"));
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 0.0.0.0:80 0.0.0.0:*")
|
||||
.is_some()
|
||||
);
|
||||
});
|
||||
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.press("cmd-a", cx);
|
||||
window.input("mysql", cx);
|
||||
});
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(
|
||||
window.find("netstat-empty").label(),
|
||||
Some("没有符合条件的连接")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn refresh_reads_the_host_again(cx: &mut TestAppContext) {
|
||||
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST, SECOND]));
|
||||
let handle = open_connections(cx, factory.clone());
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 5 条 · 3 个监听端口 · 1 条已连接",
|
||||
)
|
||||
.await;
|
||||
|
||||
in_frame(cx, handle, |window, cx| window.click("netstat-refresh", cx));
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 3 条 · 1 个监听端口 · 2 条已连接",
|
||||
)
|
||||
.await;
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 10.0.0.5:22 203.0.113.8:40000")
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 0.0.0.0:80 0.0.0.0:*")
|
||||
.is_none()
|
||||
);
|
||||
});
|
||||
assert_eq!(factory.exec_count(), 2);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_connections_are_read_each_time_they_come_on_screen_and_only_then(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let factory = Arc::new(FakeTerminalFactory::answering(&[FIRST, SECOND]));
|
||||
let handle = open_connections(cx, factory.clone());
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 5 条 · 3 个监听端口 · 1 条已连接",
|
||||
)
|
||||
.await;
|
||||
|
||||
// Another tool: nothing runs.
|
||||
in_frame(cx, handle, |window, cx| window.click("tool-history", cx));
|
||||
cx.executor().advance_clock(Duration::from_secs(10));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(factory.exec_count(), 1);
|
||||
|
||||
// Back, the list it had shows at once while it is read again.
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("tool-connections", cx)
|
||||
});
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert!(
|
||||
window
|
||||
.try_find("netstat-socket:tcp 0.0.0.0:80 0.0.0.0:*")
|
||||
.is_some()
|
||||
);
|
||||
});
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 3 条 · 1 个监听端口 · 2 条已连接",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(factory.exec_count(), 2);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_connections_say_what_the_login_cannot_see_and_when_there_are_too_many(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let output = FIRST
|
||||
.replacen("@@uid\n0\n", "@@uid\n1000\n", 1)
|
||||
.replace("1 tcp TIME-WAIT", "50000 tcp TIME-WAIT");
|
||||
let handle = open_connections(cx, Arc::new(FakeTerminalFactory::answering(&[&output])));
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-summary",
|
||||
"共 50004 条 · 3 个监听端口 · 1 条已连接",
|
||||
)
|
||||
.await;
|
||||
in_frame(cx, handle, |window, _| {
|
||||
assert_eq!(
|
||||
window.find("netstat-truncated").label(),
|
||||
Some("连接太多,只列出了前 3000 条。")
|
||||
);
|
||||
assert_eq!(
|
||||
window.find("netstat-processes").label(),
|
||||
Some("当前用户不是 root,只能看到自己的进程。")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_connections_wait_for_the_terminal_to_connect(cx: &mut TestAppContext) {
|
||||
// web-01's terminal is the first started, and exits at once.
|
||||
let handle = open_connections(cx, Arc::new(FakeTerminalFactory::exit_first()));
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-message",
|
||||
"终端没有连接。连接后这里显示主机的网络连接。",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn the_connections_say_they_read_linux_hosts_only(cx: &mut TestAppContext) {
|
||||
let factory = Arc::new(FakeTerminalFactory::answering(&["@@unsupported\nDarwin\n"]));
|
||||
let handle = open_connections(cx, factory);
|
||||
wait_for_label(
|
||||
cx,
|
||||
handle,
|
||||
"netstat-message",
|
||||
"暂不支持查看 Darwin 的网络连接,目前只支持 Linux 主机。",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Reference in new issue
Block a user