mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-13 09:49:53 -06:00
446 lines
16 KiB
Rust
446 lines
16 KiB
Rust
use anyhow::Result;
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
use std::collections::HashMap;
|
|
use std::net::IpAddr;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
|
|
use crate::config::Config;
|
|
use crate::i18n::I18n;
|
|
use crate::network::{Connection, NetworkMonitor, Process};
|
|
|
|
/// Application actions
|
|
pub enum Action {
|
|
Quit,
|
|
Refresh,
|
|
// Add more actions as needed
|
|
}
|
|
|
|
/// Application view modes
|
|
pub enum ViewMode {
|
|
Overview,
|
|
ConnectionDetails,
|
|
ProcessDetails,
|
|
Help,
|
|
}
|
|
|
|
/// Application state
|
|
pub struct App {
|
|
/// Application configuration
|
|
pub config: Config,
|
|
/// Internationalization
|
|
pub i18n: I18n,
|
|
/// Current view mode
|
|
pub mode: ViewMode,
|
|
// Whether the application should quit - field removed as it was unused, Action::Quit handles this
|
|
/// Network monitor instance
|
|
network_monitor: Option<Arc<Mutex<NetworkMonitor>>>,
|
|
/// Active connections
|
|
pub connections: Vec<Connection>,
|
|
/// Process map (pid to process)
|
|
pub processes: HashMap<u32, Process>,
|
|
/// Currently selected connection
|
|
pub selected_connection: Option<Connection>,
|
|
/// Currently selected connection index
|
|
pub selected_connection_idx: usize,
|
|
// Currently selected process index - field removed as it was unused
|
|
/// Show IP locations (requires MaxMind DB)
|
|
pub show_locations: bool,
|
|
/// Show DNS hostnames instead of IP addresses
|
|
pub show_hostnames: bool,
|
|
// Last connection sort time - field removed as it was unused
|
|
/// Connection order map (for stable ordering)
|
|
connection_order: HashMap<String, usize>,
|
|
/// Next order index for new connections
|
|
next_order_index: usize,
|
|
/// DNS cache to avoid repeated lookups
|
|
dns_cache: HashMap<IpAddr, String>,
|
|
/// Shared connection data updated by the background thread
|
|
connections_data_shared: Option<Arc<Mutex<Vec<Connection>>>>,
|
|
}
|
|
|
|
impl App {
|
|
/// Create a new application instance
|
|
pub fn new(config: Config, i18n: I18n) -> Result<Self> {
|
|
Ok(Self {
|
|
config,
|
|
i18n,
|
|
mode: ViewMode::Overview,
|
|
// should_quit: false, // Field removed
|
|
network_monitor: None,
|
|
connections: Vec::new(),
|
|
processes: HashMap::new(),
|
|
selected_connection: None,
|
|
selected_connection_idx: 0,
|
|
// selected_process_idx: 0, // Field removed
|
|
show_locations: true,
|
|
show_hostnames: false,
|
|
// last_sort_time: std::time::Instant::now(), // Field removed
|
|
connection_order: HashMap::new(),
|
|
next_order_index: 0,
|
|
dns_cache: HashMap::new(),
|
|
connections_data_shared: None,
|
|
})
|
|
}
|
|
|
|
/// Start network capture
|
|
pub fn start_capture(&mut self) -> Result<()> {
|
|
// Create network monitor
|
|
let interface = self.config.interface.clone();
|
|
let mut monitor = NetworkMonitor::new(interface)?;
|
|
|
|
// Disable process information collection by default for better performance
|
|
monitor.set_collect_process_info(false);
|
|
|
|
// Get initial connections without process info
|
|
self.connections = monitor.get_connections()?;
|
|
|
|
// Start monitoring in background thread
|
|
let monitor = Arc::new(Mutex::new(monitor));
|
|
let monitor_clone = Arc::clone(&monitor);
|
|
let connections_update = Arc::new(Mutex::new(Vec::new()));
|
|
let connections_update_clone = Arc::clone(&connections_update);
|
|
self.connections_data_shared = Some(connections_update_clone.clone()); // Store Arc for on_tick
|
|
|
|
thread::spawn(move || -> Result<()> {
|
|
loop {
|
|
let mut monitor = monitor_clone.lock().unwrap();
|
|
let new_connections = monitor.get_connections()?;
|
|
|
|
// Update shared connections
|
|
let mut connections = connections_update_clone.lock().unwrap();
|
|
*connections = new_connections;
|
|
|
|
// Sleep to avoid high CPU usage
|
|
drop(connections);
|
|
drop(monitor);
|
|
thread::sleep(std::time::Duration::from_millis(250)); // Update data more frequently
|
|
}
|
|
});
|
|
|
|
self.network_monitor = Some(monitor);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle key event
|
|
pub fn handle_key(&mut self, key: KeyEvent) -> Option<Action> {
|
|
match self.mode {
|
|
ViewMode::Overview => self.handle_overview_keys(key),
|
|
ViewMode::ConnectionDetails => self.handle_details_keys(key),
|
|
ViewMode::ProcessDetails => self.handle_process_keys(key),
|
|
ViewMode::Help => self.handle_help_keys(key),
|
|
}
|
|
}
|
|
|
|
/// Handle keys in overview mode
|
|
fn handle_overview_keys(&mut self, key: KeyEvent) -> Option<Action> {
|
|
match key.code {
|
|
KeyCode::Char('q') => Some(Action::Quit),
|
|
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
Some(Action::Quit)
|
|
}
|
|
KeyCode::Char('r') => Some(Action::Refresh),
|
|
KeyCode::Down => {
|
|
if !self.connections.is_empty() {
|
|
self.selected_connection = Some(
|
|
self.connections
|
|
[(self.selected_connection_idx + 1) % self.connections.len()]
|
|
.clone(),
|
|
);
|
|
self.selected_connection_idx =
|
|
(self.selected_connection_idx + 1) % self.connections.len();
|
|
}
|
|
None
|
|
}
|
|
KeyCode::Up => {
|
|
if !self.connections.is_empty() {
|
|
self.selected_connection = Some(
|
|
self.connections[self
|
|
.selected_connection_idx
|
|
.checked_sub(1)
|
|
.unwrap_or(self.connections.len() - 1)]
|
|
.clone(),
|
|
);
|
|
self.selected_connection_idx = self
|
|
.selected_connection_idx
|
|
.checked_sub(1)
|
|
.unwrap_or(self.connections.len() - 1);
|
|
}
|
|
None
|
|
}
|
|
KeyCode::Enter => {
|
|
if !self.connections.is_empty() {
|
|
self.mode = ViewMode::ConnectionDetails;
|
|
}
|
|
None
|
|
}
|
|
KeyCode::Char('h') => {
|
|
self.mode = ViewMode::Help;
|
|
None
|
|
}
|
|
KeyCode::Char('l') => {
|
|
self.show_locations = !self.show_locations;
|
|
None
|
|
}
|
|
KeyCode::Char('d') => {
|
|
self.show_hostnames = !self.show_hostnames;
|
|
// Clear DNS cache when toggling off to ensure fresh lookups when toggled on again
|
|
if !self.show_hostnames {
|
|
self.dns_cache.clear();
|
|
}
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Handle keys in connection details mode
|
|
fn handle_details_keys(&mut self, key: KeyEvent) -> Option<Action> {
|
|
match key.code {
|
|
KeyCode::Esc | KeyCode::Char('q') => {
|
|
self.mode = ViewMode::Overview;
|
|
None
|
|
}
|
|
KeyCode::Char('p') => {
|
|
self.mode = ViewMode::ProcessDetails;
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Handle keys in process details mode
|
|
fn handle_process_keys(&mut self, key: KeyEvent) -> Option<Action> {
|
|
match key.code {
|
|
KeyCode::Esc | KeyCode::Char('q') => {
|
|
self.mode = ViewMode::ConnectionDetails;
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Handle keys in help mode
|
|
fn handle_help_keys(&mut self, key: KeyEvent) -> Option<Action> {
|
|
match key.code {
|
|
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('h') => {
|
|
self.mode = ViewMode::Overview;
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Update application state on tick
|
|
pub fn on_tick(&mut self) -> Result<()> {
|
|
// Store currently selected connection (if any)
|
|
let selected = self.selected_connection.clone();
|
|
|
|
// Update connections from shared data updated by the background thread
|
|
if let Some(shared_data_arc) = &self.connections_data_shared {
|
|
let mut new_connections = shared_data_arc.lock().unwrap().clone();
|
|
|
|
// Extract keys for sorting
|
|
let mut keys_to_process = Vec::new();
|
|
for conn in &new_connections {
|
|
let key = self.get_connection_key(conn);
|
|
keys_to_process.push(key);
|
|
}
|
|
|
|
// Update connection order
|
|
for key in keys_to_process {
|
|
if !self.connection_order.contains_key(&key) {
|
|
self.connection_order.insert(key, self.next_order_index);
|
|
self.next_order_index += 1;
|
|
}
|
|
}
|
|
|
|
// Sort connections by their assigned order
|
|
new_connections.sort_by(|a, b| {
|
|
let key_a = self.get_connection_key(a);
|
|
let key_b = self.get_connection_key(b);
|
|
|
|
let order_a = self.connection_order.get(&key_a).unwrap_or(&usize::MAX);
|
|
let order_b = self.connection_order.get(&key_b).unwrap_or(&usize::MAX);
|
|
|
|
order_a.cmp(order_b)
|
|
});
|
|
|
|
// Update connections with the sorted list
|
|
self.connections = new_connections;
|
|
|
|
// Restore selected connection position if possible
|
|
if let Some(ref conn) = selected {
|
|
if let Some(idx) = self.find_connection_index(conn) {
|
|
self.selected_connection_idx = idx;
|
|
self.selected_connection = Some(self.connections[idx].clone());
|
|
} else if !self.connections.is_empty() {
|
|
// If previously selected connection is gone, select first one
|
|
self.selected_connection_idx = 0;
|
|
self.selected_connection = Some(self.connections[0].clone());
|
|
} else {
|
|
// If no connections left, clear selection
|
|
self.selected_connection_idx = 0;
|
|
self.selected_connection = None;
|
|
}
|
|
} else if !self.connections.is_empty() && self.selected_connection.is_none() {
|
|
// If no previous selection but we have connections, select the first one
|
|
self.selected_connection_idx = 0;
|
|
self.selected_connection = Some(self.connections[0].clone());
|
|
}
|
|
} else {
|
|
// connections_data_shared is None, likely before start_capture fully initializes it.
|
|
// self.connections will not be updated this tick.
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Refresh application data
|
|
pub fn refresh(&mut self) -> Result<()> {
|
|
// Store currently selected connection (if any)
|
|
let selected = self.selected_connection.clone();
|
|
|
|
if let Some(monitor_arc) = &self.network_monitor {
|
|
let mut monitor = monitor_arc.lock().unwrap(); // Lock the mutex
|
|
let mut new_connections = monitor.get_connections()?;
|
|
drop(monitor); // Release the mutex lock before self-mutation
|
|
|
|
// Extract keys for sorting
|
|
let mut keys_to_process = Vec::new();
|
|
for conn in &new_connections {
|
|
let key = self.get_connection_key(conn);
|
|
keys_to_process.push(key);
|
|
}
|
|
|
|
// Update connection order
|
|
for key in keys_to_process {
|
|
if !self.connection_order.contains_key(&key) {
|
|
self.connection_order.insert(key, self.next_order_index);
|
|
self.next_order_index += 1;
|
|
}
|
|
}
|
|
|
|
// Sort connections by their assigned order
|
|
new_connections.sort_by(|a, b| {
|
|
let key_a = self.get_connection_key(a);
|
|
let key_b = self.get_connection_key(b);
|
|
|
|
let order_a = self.connection_order.get(&key_a).unwrap_or(&usize::MAX);
|
|
let order_b = self.connection_order.get(&key_b).unwrap_or(&usize::MAX);
|
|
|
|
order_a.cmp(order_b)
|
|
});
|
|
|
|
// Update connections with the sorted list
|
|
self.connections = new_connections;
|
|
|
|
// Restore selected connection position if possible
|
|
if let Some(ref conn) = selected {
|
|
if let Some(idx) = self.find_connection_index(conn) {
|
|
self.selected_connection_idx = idx;
|
|
self.selected_connection = Some(self.connections[idx].clone());
|
|
} else if !self.connections.is_empty() {
|
|
// If previously selected connection is gone, select first one
|
|
self.selected_connection_idx = 0;
|
|
self.selected_connection = Some(self.connections[0].clone());
|
|
} else {
|
|
// If no connections left, clear selection
|
|
self.selected_connection_idx = 0;
|
|
self.selected_connection = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get process info for selected connection
|
|
pub fn get_process_for_selected_connection(&mut self) -> Option<Process> {
|
|
if self.connections.is_empty() || self.selected_connection_idx >= self.connections.len() {
|
|
return None;
|
|
}
|
|
|
|
// Get the selected connection
|
|
let connection = &mut self.connections[self.selected_connection_idx].clone();
|
|
|
|
// Check if we already have process info in our local cache
|
|
if let Some(pid) = connection.pid {
|
|
if let Some(process) = self.processes.get(&pid) {
|
|
return Some(process.clone());
|
|
}
|
|
}
|
|
|
|
// Otherwise, look it up on demand
|
|
if let Some(monitor_arc) = &self.network_monitor {
|
|
let monitor = monitor_arc.lock().unwrap();
|
|
|
|
// Look up the process info for this specific connection
|
|
if let Some(process) = monitor.get_platform_process_for_connection(connection) {
|
|
// Update our local cache
|
|
let pid = process.pid;
|
|
self.processes.insert(pid, process.clone());
|
|
|
|
// Update the connection in our list
|
|
if self.selected_connection_idx < self.connections.len() {
|
|
self.connections[self.selected_connection_idx].pid = Some(pid);
|
|
self.connections[self.selected_connection_idx].process_name =
|
|
Some(self.processes[&pid].name.clone());
|
|
}
|
|
|
|
return Some(process);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Generate a unique key for a connection
|
|
fn get_connection_key(&self, conn: &Connection) -> String {
|
|
format!(
|
|
"{:?}-{}-{}-{:?}",
|
|
conn.protocol, conn.local_addr, conn.remote_addr, conn.state
|
|
)
|
|
}
|
|
|
|
/// Find the index of a connection that matches the selected connection
|
|
fn find_connection_index(&self, selected: &Connection) -> Option<usize> {
|
|
let selected_key = self.get_connection_key(selected);
|
|
|
|
for (i, conn) in self.connections.iter().enumerate() {
|
|
let key = self.get_connection_key(conn);
|
|
if key == selected_key {
|
|
return Some(i);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Format a socket address with hostname if enabled (without mutating self)
|
|
pub fn format_socket_addr(&self, addr: std::net::SocketAddr) -> String {
|
|
if self.show_hostnames {
|
|
let ip = addr.ip();
|
|
// Check if it's in the cache
|
|
if let Some(hostname) = self.dns_cache.get(&ip) {
|
|
return format!("{}:{}", hostname, addr.port());
|
|
}
|
|
|
|
// Special handling without cache insertion
|
|
if ip.is_loopback() {
|
|
return format!("localhost:{}", addr.port());
|
|
}
|
|
|
|
if ip.is_unspecified() {
|
|
return format!("*:{}", addr.port());
|
|
}
|
|
|
|
// Just return the address as string if not in cache
|
|
addr.to_string()
|
|
} else {
|
|
addr.to_string()
|
|
}
|
|
}
|
|
}
|