Skip to content

Commit f7a4d26

Browse files
committed
Додавання вкладки для керування мережевими портами
Ми додали новий функціонал, який дозволяє бачити, які програми на вашому комп'ютері використовують мережеві порти, та за потреби зупиняти ці процеси. Ось основні зміни: - Створено інтерфейс для перегляду списку відкритих портів (вкладка «Ports»). - Додано можливість завершувати роботу процесів, які займають порти, безпосередньо через інтерфейс програми. - Якщо порт використовується контейнером Docker, програма дозволяє зупинити цей контейнер, щоб звільнити мережеве з'єднання. - Додано фонові служби, які збирають інформацію про стан мережі та надсилають команди для завершення процесів (використовуючи системні інструменти `ss` та `kill`).
1 parent 70f5fd3 commit f7a4d26

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

‎src/app/popup/ports_tab.rs‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use cosmic::Element;
2+
use cosmic::widget;
3+
use cosmic_containers::PortEntry;
4+
use cosmic_containers::action_icons;
5+
use cosmic_containers::{LIST_PAGE_SIZE, clamp_page, page_range, total_pages};
6+
7+
use super::layout::SPACING_NONE;
8+
use super::layout::SPACING_XXS;
9+
use super::widgets::{
10+
action_svg_button, empty_state, item_labels_detail, kill_confirm_row, list_row, paginated_list,
11+
};
12+
use super::{App, Message};
13+
use crate::app::PendingKill;
14+
15+
pub(super) fn ports_tab<'a>(state: &'a App) -> Element<'a, Message> {
16+
if state.port_list.is_empty() && !state.loading && state.error.is_none() {
17+
return empty_state("No listening ports");
18+
}
19+
20+
let total = total_pages(state.port_list.len(), LIST_PAGE_SIZE);
21+
let page = clamp_page(state.tab_pages.ports, total);
22+
let range = page_range(page, LIST_PAGE_SIZE, state.port_list.len());
23+
24+
let mut list = widget::column![].spacing(SPACING_NONE);
25+
for entry in &state.port_list[range] {
26+
list = list.push(port_row(
27+
entry,
28+
state.container_for_port(entry.port),
29+
&state.pending_kill,
30+
));
31+
}
32+
paginated_list(list.into(), page, total)
33+
}
34+
35+
fn port_row<'a>(
36+
entry: &'a PortEntry,
37+
container: Option<&'a str>,
38+
pending_kill: &Option<PendingKill>,
39+
) -> Element<'a, Message> {
40+
if matches!(pending_kill, Some(pending) if Some(pending.pid) == entry.pid) {
41+
return kill_confirm_row(format!("Kill {}?", entry.kill_label()));
42+
}
43+
44+
list_row(
45+
item_labels_detail(
46+
entry.title(),
47+
entry.owner_line(container),
48+
Some(entry.address_line()),
49+
),
50+
port_actions(entry, container),
51+
)
52+
}
53+
54+
/// A published port is freed by stopping its container; anything else by signalling the PID.
55+
fn port_actions<'a>(entry: &PortEntry, container: Option<&str>) -> Element<'a, Message> {
56+
let mut actions = widget::row![].spacing(SPACING_XXS);
57+
if let Some(container) = container {
58+
actions = actions.push(action_svg_button(
59+
action_icons::STOP,
60+
Message::StopContainerPort(container.to_string()),
61+
false,
62+
));
63+
return actions.into();
64+
}
65+
if let Some(pid) = entry.pid {
66+
actions = actions.push(action_svg_button(
67+
action_icons::STOP,
68+
Message::RequestKill(PendingKill {
69+
pid,
70+
label: entry.kill_label(),
71+
}),
72+
false,
73+
));
74+
}
75+
actions.into()
76+
}

‎src/services/ports.rs‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! Host listening ports (`ss`) and the processes holding them (`kill`).
2+
3+
use std::time::Duration;
4+
5+
use crate::{PortEntry, parse_ss_output};
6+
7+
use super::docker::{run_host, stop_container};
8+
9+
/// A signal is asynchronous; wait this long before re-reading the socket list.
10+
const KILL_SETTLE: Duration = Duration::from_millis(400);
11+
12+
/// List listening TCP/UDP sockets (`ss -tulpnH`).
13+
///
14+
/// Sockets owned by other users (root, so every `docker-proxy`) are listed without a PID:
15+
/// `ss` only reveals process details for the current user.
16+
pub async fn list_ports() -> Result<Vec<PortEntry>, String> {
17+
let output = run_host("ss", &["-tulpnH"]).await?;
18+
if !output.status.success() {
19+
let stderr = String::from_utf8_lossy(&output.stderr);
20+
return Err(format_ss_failure(&stderr));
21+
}
22+
let stdout = String::from_utf8_lossy(&output.stdout);
23+
Ok(parse_ss_output(&stdout))
24+
}
25+
26+
/// Signal a process by PID (`kill -TERM`, or `-KILL` when `force`).
27+
pub async fn kill_process(pid: u32, force: bool) -> Result<(), String> {
28+
let signal = if force { "-KILL" } else { "-TERM" };
29+
let pid = pid.to_string();
30+
let output = run_host("kill", &[signal, &pid]).await?;
31+
if output.status.success() {
32+
return Ok(());
33+
}
34+
let stderr = String::from_utf8_lossy(&output.stderr);
35+
Err(format_kill_failure(&stderr))
36+
}
37+
38+
pub async fn kill_then_list(pid: u32, force: bool) -> Result<Vec<PortEntry>, String> {
39+
kill_process(pid, force).await?;
40+
tokio::time::sleep(KILL_SETTLE).await;
41+
list_ports().await
42+
}
43+
44+
/// Free a published port by stopping the container that owns it.
45+
pub async fn stop_container_then_list(name: String) -> Result<Vec<PortEntry>, String> {
46+
stop_container(&name).await?;
47+
list_ports().await
48+
}
49+
50+
fn format_ss_failure(stderr: &str) -> String {
51+
let stderr = stderr.trim();
52+
if stderr.is_empty() {
53+
return "ss failed. Install iproute2 to list listening ports.".to_string();
54+
}
55+
stderr.to_string()
56+
}
57+
58+
fn format_kill_failure(stderr: &str) -> String {
59+
let stderr = stderr.trim();
60+
if stderr.is_empty() {
61+
return "kill failed".to_string();
62+
}
63+
if stderr.contains("not permitted") {
64+
return format!("{stderr}\n\nHint: this process belongs to another user.");
65+
}
66+
stderr.to_string()
67+
}

0 commit comments

Comments
 (0)