first commit
This commit is contained in:
50
Cargo.toml
Normal file
50
Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"procmon-core",
|
||||
"procmon-tui",
|
||||
"procmon-gui",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Core system monitoring
|
||||
sysinfo = "0.32"
|
||||
procfs = "0.17"
|
||||
libc = "0.2"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
async-trait = "0.1"
|
||||
|
||||
# Error handling
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# Time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
# TUI
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
|
||||
# GUI
|
||||
eframe = "0.29"
|
||||
egui = "0.29"
|
||||
egui_plot = "0.29"
|
||||
|
||||
# Threading
|
||||
parking_lot = "0.12"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Digi J
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
188
QUICKSTART.md
Normal file
188
QUICKSTART.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Quick Start Guide
|
||||
|
||||
## Installation
|
||||
|
||||
### Install Rust (if not already installed)
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
### Clone and Build
|
||||
```bash
|
||||
cd /home/snake/RustroverProjects/untitled
|
||||
|
||||
# Build everything
|
||||
cargo build --release
|
||||
|
||||
# Or build individual components
|
||||
cargo build --release -p procmon-tui # Terminal UI
|
||||
cargo build --release -p procmon-gui # Graphical UI
|
||||
```
|
||||
|
||||
## Running the Applications
|
||||
|
||||
### Terminal UI (Recommended for servers/SSH)
|
||||
```bash
|
||||
cargo run --release -p procmon-tui
|
||||
```
|
||||
|
||||
Or run the compiled binary:
|
||||
```bash
|
||||
./target/release/procmon-tui
|
||||
```
|
||||
|
||||
### Graphical UI (Recommended for desktop)
|
||||
```bash
|
||||
cargo run --release -p procmon-gui
|
||||
```
|
||||
|
||||
Or run the compiled binary:
|
||||
```bash
|
||||
./target/release/procmon-gui
|
||||
```
|
||||
|
||||
## First Time Setup
|
||||
|
||||
For best results, ensure the following packages are installed on your Linux system:
|
||||
|
||||
### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt-get install lm-sensors build-essential
|
||||
sudo sensors-detect --auto # Detect and configure thermal sensors
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
```bash
|
||||
sudo pacman -S lm_sensors base-devel
|
||||
sudo sensors-detect --auto
|
||||
```
|
||||
|
||||
### Fedora/RHEL
|
||||
```bash
|
||||
sudo dnf install lm_sensors gcc
|
||||
sudo sensors-detect --auto
|
||||
```
|
||||
|
||||
## TUI Quick Reference
|
||||
|
||||
Once the TUI is running:
|
||||
|
||||
1. **Navigate Tabs**
|
||||
- Press `1` for Dashboard (system overview)
|
||||
- Press `2` for Processes (detailed process list)
|
||||
- Press `3` for Network (network and disk I/O)
|
||||
- Press `4` for Alerts (misbehavior alerts)
|
||||
|
||||
2. **Process List Controls**
|
||||
- `↑/↓` arrows to navigate
|
||||
- `s` to cycle through sort columns
|
||||
- `a` to toggle ascending/descending
|
||||
- `f` to filter by misbehaving processes
|
||||
|
||||
3. **Exit**
|
||||
- Press `q` or `Ctrl+C` to quit
|
||||
|
||||
## GUI Quick Reference
|
||||
|
||||
The GUI uses tabs at the top:
|
||||
|
||||
1. **Dashboard**: Overview with graphs and gauges
|
||||
2. **Processes**: Click column headers to sort
|
||||
3. **Network & I/O**: Real-time network and disk statistics
|
||||
4. **Alerts**: Color-coded alerts (Red=Critical, Yellow=Warning, Blue=Info)
|
||||
|
||||
## Understanding the Alerts
|
||||
|
||||
The application automatically detects misbehaving processes:
|
||||
|
||||
- **Critical (Red)**: Immediate attention needed
|
||||
- CPU usage > 95%
|
||||
- Memory usage > 8GB
|
||||
|
||||
- **Warning (Yellow)**: Potential issues
|
||||
- CPU > 80% for more than 60 seconds
|
||||
- Memory > 2GB for more than 30 seconds
|
||||
- High disk I/O (>100 MB/s for 60 seconds)
|
||||
|
||||
- **Info (Blue)**: Informational alerts
|
||||
- Zombie processes
|
||||
- Other noteworthy events
|
||||
|
||||
## Monitoring Specific Metrics
|
||||
|
||||
### CPU Monitoring
|
||||
- **Dashboard Tab**: Shows overall CPU usage, per-core breakdown, and temperature
|
||||
- **Temperature**: Reads from `/sys/class/thermal/` or `/sys/class/hwmon/`
|
||||
|
||||
### GPU Monitoring
|
||||
- Currently supports AMD GPUs via sysfs (`/sys/class/drm/`)
|
||||
- Shows GPU usage, VRAM, and temperature
|
||||
- For NVIDIA GPUs, you may need to install additional drivers
|
||||
|
||||
### Network Monitoring
|
||||
- **Network Tab**: Shows all active network interfaces
|
||||
- Statistics are cumulative since boot
|
||||
- Includes packet counts and error rates
|
||||
|
||||
### Disk I/O
|
||||
- **Network Tab**: Shows per-device disk statistics
|
||||
- Read/write operations and bytes transferred
|
||||
- Filters out loop and ram devices for clarity
|
||||
|
||||
### Process Details
|
||||
- **Processes Tab**: Complete process information
|
||||
- Click or select a process for details
|
||||
- Sortable by CPU, Memory, Disk I/O, Name, or User
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Update Interval**: Default is 1 second. This balances responsiveness with CPU usage.
|
||||
|
||||
2. **Running as Root**: Some metrics require root access:
|
||||
```bash
|
||||
sudo ./target/release/procmon-tui
|
||||
```
|
||||
|
||||
3. **Filtering**: Use the filter feature (`f` in TUI) to focus on problematic processes
|
||||
|
||||
4. **Sorting**: Sort by CPU or Memory to quickly identify resource hogs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No GPU Detected
|
||||
- Check if `/sys/class/drm/` exists: `ls /sys/class/drm/`
|
||||
- Ensure GPU drivers are properly installed
|
||||
- NVIDIA users may need additional work (see README)
|
||||
|
||||
### No Temperature Readings
|
||||
- Run `sensors` command to check if thermal sensors are detected
|
||||
- Run `sudo sensors-detect` to configure
|
||||
- Check permissions on `/sys/class/thermal/`
|
||||
|
||||
### Permission Denied Errors
|
||||
- Some metrics require root access
|
||||
- Try running with `sudo`
|
||||
- Check that you can read `/proc/` entries
|
||||
|
||||
### High CPU Usage from Monitor
|
||||
- This is unusual; the monitor should use <1% CPU
|
||||
- Try increasing the update interval in the code
|
||||
- Check for runaway processes being monitored
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Explore the codebase in `procmon-core/` to customize detection rules
|
||||
- Modify alert thresholds in `procmon-core/src/detector.rs`
|
||||
- Add custom monitoring logic to suit your needs
|
||||
- Contribute improvements back to the project
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
1. Check the main README.md for detailed information
|
||||
2. Review system logs for errors
|
||||
3. Ensure all dependencies are installed
|
||||
4. Verify Rust toolchain is up to date: `rustup update`
|
||||
|
||||
Enjoy monitoring your system!
|
||||
248
README.md
Normal file
248
README.md
Normal file
@@ -0,0 +1,248 @@
|
||||
Note: The TUI is more complete and functional than the GUI
|
||||
|
||||
# Process Monitor
|
||||
|
||||
A comprehensive system and process monitoring application written in Rust with both Terminal UI (TUI) and Graphical UI (GUI) interfaces.
|
||||
|
||||
## Features
|
||||
|
||||
### System Monitoring
|
||||
- **CPU Monitoring**
|
||||
- Overall CPU usage percentage
|
||||
- Per-core CPU usage with visualization
|
||||
- CPU temperature tracking
|
||||
- CPU frequency monitoring
|
||||
|
||||
- **Memory Monitoring**
|
||||
- Total, used, and available memory
|
||||
- Swap memory usage
|
||||
- Per-process memory consumption
|
||||
|
||||
- **GPU Monitoring**
|
||||
- GPU usage percentage
|
||||
- VRAM usage
|
||||
- GPU temperature
|
||||
- Support for AMD GPUs (via sysfs)
|
||||
|
||||
- **Network Monitoring**
|
||||
- Per-interface network statistics
|
||||
- Bytes sent/received
|
||||
- Packets sent/received
|
||||
- Error tracking
|
||||
|
||||
- **Disk I/O Monitoring**
|
||||
- Read/write operations per device
|
||||
- Bytes read/written
|
||||
- Per-process disk I/O tracking
|
||||
|
||||
- **USB Monitoring**
|
||||
- Connected USB device detection
|
||||
- Device identification (vendor/product IDs)
|
||||
|
||||
### Process Monitoring
|
||||
- Real-time process listing
|
||||
- Process owner (user) tracking
|
||||
- Per-process statistics:
|
||||
- CPU usage
|
||||
- Memory usage (RSS and virtual)
|
||||
- Disk I/O (read/write bytes)
|
||||
- Thread count
|
||||
- Process status
|
||||
- Runtime duration
|
||||
|
||||
### Misbehavior Detection
|
||||
Automatic detection of misbehaving applications based on configurable rules:
|
||||
|
||||
- **High CPU Usage**: Alerts when processes exceed CPU thresholds
|
||||
- **Memory Leaks**: Detects processes with excessive memory consumption
|
||||
- **Excessive Disk I/O**: Identifies processes with high disk activity
|
||||
- **Zombie Processes**: Flags processes in zombie state
|
||||
- **Network I/O**: Monitors excessive network usage
|
||||
|
||||
Default alert levels:
|
||||
- **Critical**: Immediate attention required (>95% CPU, >8GB RAM)
|
||||
- **Warning**: Potential issues (>80% CPU for 60s, >2GB RAM for 30s)
|
||||
- **Info**: Informational alerts
|
||||
|
||||
## Architecture
|
||||
|
||||
The project is organized as a Cargo workspace with three crates:
|
||||
|
||||
```
|
||||
procmon/
|
||||
├── procmon-core/ # Core monitoring library
|
||||
│ ├── monitor.rs # System monitoring implementation
|
||||
│ ├── metrics.rs # Metric data structures
|
||||
│ ├── process.rs # Process information types
|
||||
│ └── detector.rs # Misbehavior detection logic
|
||||
├── procmon-tui/ # Terminal UI application
|
||||
│ ├── main.rs # TUI entry point
|
||||
│ ├── app.rs # Application state
|
||||
│ └── ui.rs # Rendering logic
|
||||
└── procmon-gui/ # Graphical UI application
|
||||
└── main.rs # GUI application with egui
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
- Rust 1.70 or later
|
||||
- Linux (designed for Linux systems)
|
||||
|
||||
### Build Commands
|
||||
|
||||
Build all components:
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Build individual components:
|
||||
```bash
|
||||
cargo build --release -p procmon-tui
|
||||
cargo build --release -p procmon-gui
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
### Terminal UI (TUI)
|
||||
```bash
|
||||
cargo run --release -p procmon-tui
|
||||
```
|
||||
|
||||
### Graphical UI (GUI)
|
||||
```bash
|
||||
cargo run --release -p procmon-gui
|
||||
```
|
||||
|
||||
## TUI Controls
|
||||
|
||||
- **q** or **Ctrl+C**: Quit application
|
||||
- **Tab**: Next tab
|
||||
- **Shift+Tab**: Previous tab
|
||||
- **1-4**: Jump to specific tab (Dashboard, Processes, Network, Alerts)
|
||||
- **↑/↓**: Navigate process list
|
||||
- **s**: Change sort column
|
||||
- **a**: Toggle sort order (ascending/descending)
|
||||
- **f**: Toggle filter for misbehaving processes
|
||||
|
||||
## TUI Tabs
|
||||
|
||||
1. **Dashboard**: System overview with CPU, memory, temperature, and top processes
|
||||
2. **Processes**: Detailed process list with sorting and filtering
|
||||
3. **Network**: Network interfaces and disk I/O statistics
|
||||
4. **Alerts**: Real-time misbehavior alerts
|
||||
|
||||
## GUI Features
|
||||
|
||||
The GUI provides an alternative interface with the same monitoring capabilities:
|
||||
|
||||
- **Dashboard Tab**: Visual system overview with graphs and gauges
|
||||
- **Processes Tab**: Sortable process table
|
||||
- **Network & I/O Tab**: Network interfaces and disk statistics
|
||||
- **Alerts Tab**: Color-coded alert list
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Monitoring
|
||||
- `sysinfo`: Cross-platform system information
|
||||
- `procfs`: Linux /proc filesystem parsing
|
||||
- `nix`: Unix system APIs
|
||||
|
||||
### TUI
|
||||
- `ratatui`: Terminal UI framework
|
||||
- `crossterm`: Terminal manipulation
|
||||
|
||||
### GUI
|
||||
- `egui`: Immediate mode GUI framework
|
||||
- `eframe`: egui application framework
|
||||
|
||||
### Common
|
||||
- `tokio`: Async runtime
|
||||
- `serde`: Serialization
|
||||
- `chrono`: Date/time handling
|
||||
- `tracing`: Logging
|
||||
|
||||
## Platform Support
|
||||
|
||||
Currently optimized for **Linux**. The application reads from:
|
||||
- `/proc/` filesystem for process information
|
||||
- `/sys/class/thermal/` for CPU temperature
|
||||
- `/sys/class/drm/` for GPU information
|
||||
- `/sys/bus/usb/devices/` for USB devices
|
||||
- `/proc/diskstats` for disk I/O
|
||||
- `/etc/passwd` for user information
|
||||
|
||||
## Customizing Misbehavior Rules
|
||||
|
||||
The misbehavior detector can be customized by modifying `procmon-core/src/detector.rs`. Default rules include:
|
||||
|
||||
```rust
|
||||
MisbehaviorRule {
|
||||
name: "High CPU Usage",
|
||||
description: "Process using more than 80% CPU for extended period",
|
||||
condition: MisbehaviorCondition::CpuUsageAbove {
|
||||
threshold: 80.0,
|
||||
duration_secs: 60,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- Updates every 1 second by default
|
||||
- Minimal CPU overhead (typically <1% on modern systems)
|
||||
- Efficient memory usage with bounded alert history (last 100 alerts)
|
||||
|
||||
## Permissions
|
||||
|
||||
Some features may require elevated permissions:
|
||||
- Reading certain `/proc` entries may require root access
|
||||
- Hardware sensor data may need appropriate permissions
|
||||
|
||||
Run with sudo if you encounter permission errors:
|
||||
```bash
|
||||
sudo cargo run --release -p procmon-tui
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Areas for improvement:
|
||||
- NVIDIA GPU support (via NVML)
|
||||
- macOS and Windows support
|
||||
- Per-process network I/O tracking
|
||||
- Historical data graphing
|
||||
- Configuration file support
|
||||
- Export monitoring data to formats (CSV, JSON)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No GPU detected
|
||||
- Ensure GPU drivers are installed
|
||||
- Check `/sys/class/drm/` exists and is accessible
|
||||
- AMD GPUs are currently better supported than NVIDIA
|
||||
|
||||
### No temperature readings
|
||||
- Install `lm-sensors` package
|
||||
- Run `sensors-detect` to configure thermal sensors
|
||||
- Check `/sys/class/thermal/` permissions
|
||||
|
||||
### Inaccurate network stats
|
||||
- Network statistics are cumulative since boot
|
||||
- Ensure proper permissions to read network interface data
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### CPU Usage Calculation
|
||||
CPU usage is calculated by the `sysinfo` crate using process CPU time divided by elapsed time.
|
||||
|
||||
### Memory Values
|
||||
- RSS (Resident Set Size): Physical memory used
|
||||
- Virtual Memory: Total virtual address space
|
||||
|
||||
### Disk I/O
|
||||
Disk I/O statistics are read from `/proc/diskstats` and represent cumulative values since boot.
|
||||
52
build.sh
Normal file
52
build.sh
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "================================"
|
||||
echo " Process Monitor Build Script "
|
||||
echo "================================"
|
||||
echo
|
||||
|
||||
# Check if cargo is installed
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "Error: Cargo is not installed."
|
||||
echo "Please install Rust from https://rustup.rs/"
|
||||
echo
|
||||
echo "Run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Rust version:"
|
||||
rustc --version
|
||||
cargo --version
|
||||
echo
|
||||
|
||||
# Build all workspace members
|
||||
echo "Building all workspace members..."
|
||||
echo
|
||||
|
||||
echo "[1/3] Building procmon-core..."
|
||||
cargo build --release -p procmon-core
|
||||
|
||||
echo
|
||||
echo "[2/3] Building procmon-tui..."
|
||||
cargo build --release -p procmon-tui
|
||||
|
||||
echo
|
||||
echo "[3/3] Building procmon-gui..."
|
||||
cargo build --release -p procmon-gui
|
||||
|
||||
echo
|
||||
echo "================================"
|
||||
echo " Build Completed Successfully! "
|
||||
echo "================================"
|
||||
echo
|
||||
echo "Binaries are located in ./target/release/"
|
||||
echo
|
||||
echo "To run the Terminal UI:"
|
||||
echo " ./target/release/procmon-tui"
|
||||
echo
|
||||
echo "To run the Graphical UI:"
|
||||
echo " ./target/release/procmon-gui"
|
||||
echo
|
||||
echo "For more information, see README.md and QUICKSTART.md"
|
||||
1
package.json
Normal file
1
package.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
23
procmon-core/Cargo.toml
Normal file
23
procmon-core/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "procmon-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
sysinfo.workspace = true
|
||||
procfs.workspace = true
|
||||
libc.workspace = true
|
||||
tokio.workspace = true
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
chrono.workspace = true
|
||||
tracing.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
# Additional dependencies for system monitoring
|
||||
nix = { version = "0.29", features = ["process", "user"] }
|
||||
299
procmon-core/src/detector.rs
Normal file
299
procmon-core/src/detector.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use crate::process::ProcessSnapshot;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MisbehaviorRule {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub condition: MisbehaviorCondition,
|
||||
pub severity: Severity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MisbehaviorCondition {
|
||||
CpuUsageAbove { threshold: f32, duration_secs: u64 },
|
||||
MemoryUsageAbove { threshold_bytes: u64, duration_secs: u64 },
|
||||
MemoryPercentAbove { threshold_percent: f32, duration_secs: u64 },
|
||||
DiskIoAbove { threshold_bytes_per_sec: u64, duration_secs: u64 },
|
||||
NetworkIoAbove { threshold_bytes_per_sec: u64, duration_secs: u64 },
|
||||
TooManyThreads { threshold: u32 },
|
||||
ZombieProcess,
|
||||
HighDiskWrites { threshold_bytes_per_sec: u64, duration_secs: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MisbehaviorAlert {
|
||||
pub pid: u32,
|
||||
pub process_name: String,
|
||||
pub rule_name: String,
|
||||
pub description: String,
|
||||
pub severity: Severity,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub details: String,
|
||||
}
|
||||
|
||||
pub struct MisbehaviorDetector {
|
||||
rules: Vec<MisbehaviorRule>,
|
||||
violation_history: HashMap<u32, Vec<ViolationRecord>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ViolationRecord {
|
||||
rule_name: String,
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl MisbehaviorDetector {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rules: Self::default_rules(),
|
||||
violation_history: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_rules(rules: Vec<MisbehaviorRule>) -> Self {
|
||||
Self {
|
||||
rules,
|
||||
violation_history: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_rules() -> Vec<MisbehaviorRule> {
|
||||
vec![
|
||||
MisbehaviorRule {
|
||||
name: "High CPU Usage".to_string(),
|
||||
description: "Process using more than 80% CPU for extended period".to_string(),
|
||||
condition: MisbehaviorCondition::CpuUsageAbove {
|
||||
threshold: 80.0,
|
||||
duration_secs: 60,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "Extreme CPU Usage".to_string(),
|
||||
description: "Process using more than 95% CPU".to_string(),
|
||||
condition: MisbehaviorCondition::CpuUsageAbove {
|
||||
threshold: 95.0,
|
||||
duration_secs: 10,
|
||||
},
|
||||
severity: Severity::Critical,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "High Memory Usage".to_string(),
|
||||
description: "Process using more than 2GB of RAM".to_string(),
|
||||
condition: MisbehaviorCondition::MemoryUsageAbove {
|
||||
threshold_bytes: 2 * 1024 * 1024 * 1024,
|
||||
duration_secs: 30,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "Memory Leak Suspected".to_string(),
|
||||
description: "Process using more than 8GB of RAM".to_string(),
|
||||
condition: MisbehaviorCondition::MemoryUsageAbove {
|
||||
threshold_bytes: 8 * 1024 * 1024 * 1024,
|
||||
duration_secs: 10,
|
||||
},
|
||||
severity: Severity::Critical,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "Zombie Process".to_string(),
|
||||
description: "Process is in zombie state".to_string(),
|
||||
condition: MisbehaviorCondition::ZombieProcess,
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "High Disk I/O".to_string(),
|
||||
description: "Process performing excessive disk operations".to_string(),
|
||||
condition: MisbehaviorCondition::DiskIoAbove {
|
||||
threshold_bytes_per_sec: 100 * 1024 * 1024, // 100 MB/s
|
||||
duration_secs: 60,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn add_rule(&mut self, rule: MisbehaviorRule) {
|
||||
self.rules.push(rule);
|
||||
}
|
||||
|
||||
pub fn check_process(&mut self, snapshot: &ProcessSnapshot) -> Vec<MisbehaviorAlert> {
|
||||
let mut alerts = Vec::new();
|
||||
let rules = self.rules.clone();
|
||||
|
||||
for rule in &rules {
|
||||
if self.check_rule(snapshot, rule) {
|
||||
let alert = MisbehaviorAlert {
|
||||
pid: snapshot.info.pid,
|
||||
process_name: snapshot.info.name.clone(),
|
||||
rule_name: rule.name.clone(),
|
||||
description: rule.description.clone(),
|
||||
severity: rule.severity,
|
||||
timestamp: chrono::Utc::now(),
|
||||
details: self.get_violation_details(snapshot, &rule.condition),
|
||||
};
|
||||
|
||||
alerts.push(alert);
|
||||
}
|
||||
}
|
||||
|
||||
alerts
|
||||
}
|
||||
|
||||
fn check_rule(&mut self, snapshot: &ProcessSnapshot, rule: &MisbehaviorRule) -> bool {
|
||||
match &rule.condition {
|
||||
MisbehaviorCondition::CpuUsageAbove { threshold, duration_secs } => {
|
||||
if snapshot.stats.cpu_usage > *threshold {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::MemoryUsageAbove { threshold_bytes, duration_secs } => {
|
||||
if snapshot.stats.memory_usage > *threshold_bytes {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::MemoryPercentAbove { threshold_percent, duration_secs } => {
|
||||
if snapshot.stats.memory_percent > *threshold_percent {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::DiskIoAbove { threshold_bytes_per_sec, duration_secs } => {
|
||||
let total_io = snapshot.stats.disk_read_bytes + snapshot.stats.disk_write_bytes;
|
||||
let io_per_sec = total_io / snapshot.stats.run_time.as_secs().max(1);
|
||||
|
||||
if io_per_sec > *threshold_bytes_per_sec {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::NetworkIoAbove { threshold_bytes_per_sec, duration_secs } => {
|
||||
let total_net = snapshot.stats.network_rx_bytes + snapshot.stats.network_tx_bytes;
|
||||
let net_per_sec = total_net / snapshot.stats.run_time.as_secs().max(1);
|
||||
|
||||
if net_per_sec > *threshold_bytes_per_sec {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::TooManyThreads { threshold } => {
|
||||
snapshot.stats.num_threads > *threshold
|
||||
}
|
||||
MisbehaviorCondition::ZombieProcess => {
|
||||
matches!(snapshot.info.status, crate::process::ProcessStatus::Zombie)
|
||||
}
|
||||
MisbehaviorCondition::HighDiskWrites { threshold_bytes_per_sec, duration_secs } => {
|
||||
let write_per_sec = snapshot.stats.disk_write_bytes / snapshot.stats.run_time.as_secs().max(1);
|
||||
|
||||
if write_per_sec > *threshold_bytes_per_sec {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_violation(&mut self, pid: u32, rule_name: &str, duration_secs: u64) -> bool {
|
||||
let now = chrono::Utc::now();
|
||||
let history = self.violation_history.entry(pid).or_insert_with(Vec::new);
|
||||
|
||||
// Add new violation
|
||||
history.push(ViolationRecord {
|
||||
rule_name: rule_name.to_string(),
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
// Clean up old violations
|
||||
let cutoff = now - chrono::Duration::seconds(duration_secs as i64);
|
||||
history.retain(|v| v.timestamp > cutoff && v.rule_name == rule_name);
|
||||
|
||||
// Check if violation has persisted for the required duration
|
||||
if let Some(first) = history.first() {
|
||||
let violation_duration = (now - first.timestamp).num_seconds() as u64;
|
||||
violation_duration >= duration_secs
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn get_violation_details(&self, snapshot: &ProcessSnapshot, condition: &MisbehaviorCondition) -> String {
|
||||
match condition {
|
||||
MisbehaviorCondition::CpuUsageAbove { threshold, .. } => {
|
||||
format!("CPU usage: {:.1}% (threshold: {:.1}%)", snapshot.stats.cpu_usage, threshold)
|
||||
}
|
||||
MisbehaviorCondition::MemoryUsageAbove { threshold_bytes, .. } => {
|
||||
format!(
|
||||
"Memory usage: {:.2} GB (threshold: {:.2} GB)",
|
||||
snapshot.stats.memory_usage as f64 / (1024.0 * 1024.0 * 1024.0),
|
||||
*threshold_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
MisbehaviorCondition::MemoryPercentAbove { threshold_percent, .. } => {
|
||||
format!("Memory usage: {:.1}% (threshold: {:.1}%)", snapshot.stats.memory_percent, threshold_percent)
|
||||
}
|
||||
MisbehaviorCondition::DiskIoAbove { threshold_bytes_per_sec, .. } => {
|
||||
let total_io = snapshot.stats.disk_read_bytes + snapshot.stats.disk_write_bytes;
|
||||
let io_per_sec = total_io / snapshot.stats.run_time.as_secs().max(1);
|
||||
format!(
|
||||
"Disk I/O: {:.2} MB/s (threshold: {:.2} MB/s)",
|
||||
io_per_sec as f64 / (1024.0 * 1024.0),
|
||||
*threshold_bytes_per_sec as f64 / (1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
MisbehaviorCondition::NetworkIoAbove { threshold_bytes_per_sec, .. } => {
|
||||
let total_net = snapshot.stats.network_rx_bytes + snapshot.stats.network_tx_bytes;
|
||||
let net_per_sec = total_net / snapshot.stats.run_time.as_secs().max(1);
|
||||
format!(
|
||||
"Network I/O: {:.2} MB/s (threshold: {:.2} MB/s)",
|
||||
net_per_sec as f64 / (1024.0 * 1024.0),
|
||||
*threshold_bytes_per_sec as f64 / (1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
MisbehaviorCondition::TooManyThreads { threshold } => {
|
||||
format!("Threads: {} (threshold: {})", snapshot.stats.num_threads, threshold)
|
||||
}
|
||||
MisbehaviorCondition::ZombieProcess => {
|
||||
"Process is in zombie state".to_string()
|
||||
}
|
||||
MisbehaviorCondition::HighDiskWrites { threshold_bytes_per_sec, .. } => {
|
||||
let write_per_sec = snapshot.stats.disk_write_bytes / snapshot.stats.run_time.as_secs().max(1);
|
||||
format!(
|
||||
"Disk writes: {:.2} MB/s (threshold: {:.2} MB/s)",
|
||||
write_per_sec as f64 / (1024.0 * 1024.0),
|
||||
*threshold_bytes_per_sec as f64 / (1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup_dead_processes(&mut self, active_pids: &[u32]) {
|
||||
self.violation_history.retain(|pid, _| active_pids.contains(pid));
|
||||
}
|
||||
|
||||
pub fn get_rules(&self) -> &[MisbehaviorRule] {
|
||||
&self.rules
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MisbehaviorDetector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
299
procmon-core/src/detectors.rs
Normal file
299
procmon-core/src/detectors.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use crate::process::ProcessSnapshot;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MisbehaviorRule {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub condition: MisbehaviorCondition,
|
||||
pub severity: Severity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MisbehaviorCondition {
|
||||
CpuUsageAbove { threshold: f32, duration_secs: u64 },
|
||||
MemoryUsageAbove { threshold_bytes: u64, duration_secs: u64 },
|
||||
MemoryPercentAbove { threshold_percent: f32, duration_secs: u64 },
|
||||
DiskIoAbove { threshold_bytes_per_sec: u64, duration_secs: u64 },
|
||||
NetworkIoAbove { threshold_bytes_per_sec: u64, duration_secs: u64 },
|
||||
TooManyThreads { threshold: u32 },
|
||||
ZombieProcess,
|
||||
HighDiskWrites { threshold_bytes_per_sec: u64, duration_secs: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MisbehaviorAlert {
|
||||
pub pid: u32,
|
||||
pub process_name: String,
|
||||
pub rule_name: String,
|
||||
pub description: String,
|
||||
pub severity: Severity,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub details: String,
|
||||
}
|
||||
|
||||
pub struct MisbehaviorDetector {
|
||||
rules: Vec<MisbehaviorRule>,
|
||||
violation_history: HashMap<u32, Vec<ViolationRecord>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ViolationRecord {
|
||||
rule_name: String,
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl MisbehaviorDetector {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rules: Self::default_rules(),
|
||||
violation_history: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_rules(rules: Vec<MisbehaviorRule>) -> Self {
|
||||
Self {
|
||||
rules,
|
||||
violation_history: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_rules() -> Vec<MisbehaviorRule> {
|
||||
vec![
|
||||
MisbehaviorRule {
|
||||
name: "High CPU Usage".to_string(),
|
||||
description: "Process using more than 80% CPU for extended period".to_string(),
|
||||
condition: MisbehaviorCondition::CpuUsageAbove {
|
||||
threshold: 80.0,
|
||||
duration_secs: 60,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "Extreme CPU Usage".to_string(),
|
||||
description: "Process using more than 95% CPU".to_string(),
|
||||
condition: MisbehaviorCondition::CpuUsageAbove {
|
||||
threshold: 95.0,
|
||||
duration_secs: 10,
|
||||
},
|
||||
severity: Severity::Critical,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "High Memory Usage".to_string(),
|
||||
description: "Process using more than 2GB of RAM".to_string(),
|
||||
condition: MisbehaviorCondition::MemoryUsageAbove {
|
||||
threshold_bytes: 2 * 1024 * 1024 * 1024,
|
||||
duration_secs: 30,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "Memory Leak Suspected".to_string(),
|
||||
description: "Process using more than 8GB of RAM".to_string(),
|
||||
condition: MisbehaviorCondition::MemoryUsageAbove {
|
||||
threshold_bytes: 8 * 1024 * 1024 * 1024,
|
||||
duration_secs: 10,
|
||||
},
|
||||
severity: Severity::Critical,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "Zombie Process".to_string(),
|
||||
description: "Process is in zombie state".to_string(),
|
||||
condition: MisbehaviorCondition::ZombieProcess,
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
MisbehaviorRule {
|
||||
name: "High Disk I/O".to_string(),
|
||||
description: "Process performing excessive disk operations".to_string(),
|
||||
condition: MisbehaviorCondition::DiskIoAbove {
|
||||
threshold_bytes_per_sec: 100 * 1024 * 1024, // 100 MB/s
|
||||
duration_secs: 60,
|
||||
},
|
||||
severity: Severity::Warning,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn add_rule(&mut self, rule: MisbehaviorRule) {
|
||||
self.rules.push(rule);
|
||||
}
|
||||
|
||||
pub fn check_process(&mut self, snapshot: &ProcessSnapshot) -> Vec<MisbehaviorAlert> {
|
||||
let mut alerts = Vec::new();
|
||||
let rules = self.rules.clone();
|
||||
|
||||
for rule in &rules {
|
||||
if self.check_rule(snapshot, rule) {
|
||||
let alert = MisbehaviorAlert {
|
||||
pid: snapshot.info.pid,
|
||||
process_name: snapshot.info.name.clone(),
|
||||
rule_name: rule.name.clone(),
|
||||
description: rule.description.clone(),
|
||||
severity: rule.severity,
|
||||
timestamp: chrono::Utc::now(),
|
||||
details: self.get_violation_details(snapshot, &rule.condition),
|
||||
};
|
||||
|
||||
alerts.push(alert);
|
||||
}
|
||||
}
|
||||
|
||||
alerts
|
||||
}
|
||||
|
||||
fn check_rule(&mut self, snapshot: &ProcessSnapshot, rule: &MisbehaviorRule) -> bool {
|
||||
match &rule.condition {
|
||||
MisbehaviorCondition::CpuUsageAbove { threshold, duration_secs } => {
|
||||
if snapshot.stats.cpu_usage > *threshold {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::MemoryUsageAbove { threshold_bytes, duration_secs } => {
|
||||
if snapshot.stats.memory_usage > *threshold_bytes {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::MemoryPercentAbove { threshold_percent, duration_secs } => {
|
||||
if snapshot.stats.memory_percent > *threshold_percent {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::DiskIoAbove { threshold_bytes_per_sec, duration_secs } => {
|
||||
let total_io = snapshot.stats.disk_read_bytes + snapshot.stats.disk_write_bytes;
|
||||
let io_per_sec = total_io / snapshot.stats.run_time.as_secs().max(1);
|
||||
|
||||
if io_per_sec > *threshold_bytes_per_sec {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::NetworkIoAbove { threshold_bytes_per_sec, duration_secs } => {
|
||||
let total_net = snapshot.stats.network_rx_bytes + snapshot.stats.network_tx_bytes;
|
||||
let net_per_sec = total_net / snapshot.stats.run_time.as_secs().max(1);
|
||||
|
||||
if net_per_sec > *threshold_bytes_per_sec {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MisbehaviorCondition::TooManyThreads { threshold } => {
|
||||
snapshot.stats.num_threads > *threshold
|
||||
}
|
||||
MisbehaviorCondition::ZombieProcess => {
|
||||
matches!(snapshot.info.status, crate::process::ProcessStatus::Zombie)
|
||||
}
|
||||
MisbehaviorCondition::HighDiskWrites { threshold_bytes_per_sec, duration_secs } => {
|
||||
let write_per_sec = snapshot.stats.disk_write_bytes / snapshot.stats.run_time.as_secs().max(1);
|
||||
|
||||
if write_per_sec > *threshold_bytes_per_sec {
|
||||
self.record_violation(snapshot.info.pid, &rule.name, *duration_secs)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_violation(&mut self, pid: u32, rule_name: &str, duration_secs: u64) -> bool {
|
||||
let now = chrono::Utc::now();
|
||||
let history = self.violation_history.entry(pid).or_insert_with(Vec::new);
|
||||
|
||||
// Add new violation
|
||||
history.push(ViolationRecord {
|
||||
rule_name: rule_name.to_string(),
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
// Clean up old violations
|
||||
let cutoff = now - chrono::Duration::seconds(duration_secs as i64);
|
||||
history.retain(|v| v.timestamp > cutoff && v.rule_name == rule_name);
|
||||
|
||||
// Check if violation has persisted for the required duration
|
||||
if let Some(first) = history.first() {
|
||||
let violation_duration = (now - first.timestamp).num_seconds() as u64;
|
||||
violation_duration >= duration_secs
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn get_violation_details(&self, snapshot: &ProcessSnapshot, condition: &MisbehaviorCondition) -> String {
|
||||
match condition {
|
||||
MisbehaviorCondition::CpuUsageAbove { threshold, .. } => {
|
||||
format!("CPU usage: {:.1}% (threshold: {:.1}%)", snapshot.stats.cpu_usage, threshold)
|
||||
}
|
||||
MisbehaviorCondition::MemoryUsageAbove { threshold_bytes, .. } => {
|
||||
format!(
|
||||
"Memory usage: {:.2} GB (threshold: {:.2} GB)",
|
||||
snapshot.stats.memory_usage as f64 / (1024.0 * 1024.0 * 1024.0),
|
||||
*threshold_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
MisbehaviorCondition::MemoryPercentAbove { threshold_percent, .. } => {
|
||||
format!("Memory usage: {:.1}% (threshold: {:.1}%)", snapshot.stats.memory_percent, threshold_percent)
|
||||
}
|
||||
MisbehaviorCondition::DiskIoAbove { threshold_bytes_per_sec, .. } => {
|
||||
let total_io = snapshot.stats.disk_read_bytes + snapshot.stats.disk_write_bytes;
|
||||
let io_per_sec = total_io / snapshot.stats.run_time.as_secs().max(1);
|
||||
format!(
|
||||
"Disk I/O: {:.2} MB/s (threshold: {:.2} MB/s)",
|
||||
io_per_sec as f64 / (1024.0 * 1024.0),
|
||||
*threshold_bytes_per_sec as f64 / (1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
MisbehaviorCondition::NetworkIoAbove { threshold_bytes_per_sec, .. } => {
|
||||
let total_net = snapshot.stats.network_rx_bytes + snapshot.stats.network_tx_bytes;
|
||||
let net_per_sec = total_net / snapshot.stats.run_time.as_secs().max(1);
|
||||
format!(
|
||||
"Network I/O: {:.2} MB/s (threshold: {:.2} MB/s)",
|
||||
net_per_sec as f64 / (1024.0 * 1024.0),
|
||||
*threshold_bytes_per_sec as f64 / (1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
MisbehaviorCondition::TooManyThreads { threshold } => {
|
||||
format!("Threads: {} (threshold: {})", snapshot.stats.num_threads, threshold)
|
||||
}
|
||||
MisbehaviorCondition::ZombieProcess => {
|
||||
"Process is in zombie state".to_string()
|
||||
}
|
||||
MisbehaviorCondition::HighDiskWrites { threshold_bytes_per_sec, .. } => {
|
||||
let write_per_sec = snapshot.stats.disk_write_bytes / snapshot.stats.run_time.as_secs().max(1);
|
||||
format!(
|
||||
"Disk writes: {:.2} MB/s (threshold: {:.2} MB/s)",
|
||||
write_per_sec as f64 / (1024.0 * 1024.0),
|
||||
*threshold_bytes_per_sec as f64 / (1024.0 * 1024.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup_dead_processes(&mut self, active_pids: &[u32]) {
|
||||
self.violation_history.retain(|pid, _| active_pids.contains(pid));
|
||||
}
|
||||
|
||||
pub fn get_rules(&self) -> &[MisbehaviorRule] {
|
||||
&self.rules
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MisbehaviorDetector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
16
procmon-core/src/lib.rs
Normal file
16
procmon-core/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod monitor;
|
||||
pub mod process;
|
||||
pub mod metrics;
|
||||
pub mod detector;
|
||||
pub mod partition;
|
||||
pub mod service;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use monitor::SystemMonitor;
|
||||
pub use process::{ProcessInfo, ProcessStats};
|
||||
pub use metrics::*;
|
||||
pub use detector::{MisbehaviorDetector, MisbehaviorRule, MisbehaviorAlert};
|
||||
pub use partition::{PartitionManager, Disk, Partition};
|
||||
pub use service::{ServiceManager, SystemService, ServiceState};
|
||||
105
procmon-core/src/metrics.rs
Normal file
105
procmon-core/src/metrics.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CpuMetrics {
|
||||
pub total_usage: f32,
|
||||
pub per_core_usage: Vec<f32>,
|
||||
pub temperature: Option<f32>,
|
||||
pub frequency: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GpuMetrics {
|
||||
pub name: String,
|
||||
pub usage: f32,
|
||||
pub memory_used: u64,
|
||||
pub memory_total: u64,
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkMetrics {
|
||||
pub interface_name: String,
|
||||
pub bytes_sent: u64,
|
||||
pub bytes_received: u64,
|
||||
pub packets_sent: u64,
|
||||
pub packets_received: u64,
|
||||
pub errors_in: u64,
|
||||
pub errors_out: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiskIoMetrics {
|
||||
pub device_name: String,
|
||||
pub read_bytes: u64,
|
||||
pub write_bytes: u64,
|
||||
pub read_ops: u64,
|
||||
pub write_ops: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UsbIoMetrics {
|
||||
pub device_id: String,
|
||||
pub device_name: String,
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub bytes_transferred: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemoryMetrics {
|
||||
pub total: u64,
|
||||
pub used: u64,
|
||||
pub available: u64,
|
||||
pub swap_total: u64,
|
||||
pub swap_used: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemMetrics {
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub cpu: CpuMetrics,
|
||||
pub memory: MemoryMetrics,
|
||||
pub gpus: Vec<GpuMetrics>,
|
||||
pub network: HashMap<String, NetworkMetrics>,
|
||||
pub disk_io: HashMap<String, DiskIoMetrics>,
|
||||
pub usb_io: Vec<UsbIoMetrics>,
|
||||
}
|
||||
|
||||
impl Default for CpuMetrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
total_usage: 0.0,
|
||||
per_core_usage: Vec::new(),
|
||||
temperature: None,
|
||||
frequency: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryMetrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
total: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
swap_total: 0,
|
||||
swap_used: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SystemMetrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timestamp: chrono::Utc::now(),
|
||||
cpu: CpuMetrics::default(),
|
||||
memory: MemoryMetrics::default(),
|
||||
gpus: Vec::new(),
|
||||
network: HashMap::new(),
|
||||
disk_io: HashMap::new(),
|
||||
usb_io: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
428
procmon-core/src/monitor.rs
Normal file
428
procmon-core/src/monitor.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
use crate::metrics::*;
|
||||
use crate::process::{ProcessInfo, ProcessStats, ProcessSnapshot, ProcessStatus};
|
||||
use anyhow::Result;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use sysinfo::{System, Process, Pid, Networks, Disks};
|
||||
|
||||
pub struct SystemMonitor {
|
||||
system: Arc<RwLock<System>>,
|
||||
networks: Arc<RwLock<Networks>>,
|
||||
disks: Arc<RwLock<Disks>>,
|
||||
previous_disk_stats: Arc<RwLock<HashMap<String, (u64, u64)>>>,
|
||||
previous_net_stats: Arc<RwLock<HashMap<String, (u64, u64)>>>,
|
||||
}
|
||||
|
||||
impl SystemMonitor {
|
||||
pub fn new() -> Self {
|
||||
// Start with empty system, we'll populate it on first refresh
|
||||
let system = System::new();
|
||||
|
||||
Self {
|
||||
system: Arc::new(RwLock::new(system)),
|
||||
networks: Arc::new(RwLock::new(Networks::new_with_refreshed_list())),
|
||||
disks: Arc::new(RwLock::new(Disks::new_with_refreshed_list())),
|
||||
previous_disk_stats: Arc::new(RwLock::new(HashMap::new())),
|
||||
previous_net_stats: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(&self) {
|
||||
let mut system = self.system.write();
|
||||
// IMPORTANT: We need to completely rebuild the process list to avoid stale PIDs
|
||||
// sysinfo has a known issue where it doesn't properly remove terminated processes
|
||||
// So we clear the process list and rebuild it from scratch
|
||||
use sysinfo::{ProcessRefreshKind, RefreshKind, MemoryRefreshKind, CpuRefreshKind};
|
||||
|
||||
// Create a completely fresh system to avoid accumulated stale processes
|
||||
*system = System::new_with_specifics(RefreshKind::new()
|
||||
.with_processes(ProcessRefreshKind::everything())
|
||||
.with_memory(MemoryRefreshKind::everything())
|
||||
.with_cpu(CpuRefreshKind::everything()));
|
||||
|
||||
let mut networks = self.networks.write();
|
||||
networks.refresh();
|
||||
|
||||
let mut disks = self.disks.write();
|
||||
disks.refresh();
|
||||
}
|
||||
|
||||
pub fn get_system_metrics(&self) -> Result<SystemMetrics> {
|
||||
let system = self.system.read();
|
||||
let networks = self.networks.read();
|
||||
|
||||
let cpu = self.get_cpu_metrics(&system)?;
|
||||
let memory = self.get_memory_metrics(&system)?;
|
||||
let gpus = self.get_gpu_metrics()?;
|
||||
let network = self.get_network_metrics(&networks)?;
|
||||
let disk_io = self.get_disk_io_metrics()?;
|
||||
let usb_io = self.get_usb_io_metrics()?;
|
||||
|
||||
Ok(SystemMetrics {
|
||||
timestamp: chrono::Utc::now(),
|
||||
cpu,
|
||||
memory,
|
||||
gpus,
|
||||
network,
|
||||
disk_io,
|
||||
usb_io,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_cpu_metrics(&self, system: &System) -> Result<CpuMetrics> {
|
||||
let cpus = system.cpus();
|
||||
let total_usage = system.global_cpu_usage();
|
||||
let per_core_usage: Vec<f32> = cpus.iter().map(|cpu| cpu.cpu_usage()).collect();
|
||||
|
||||
let temperature = self.read_cpu_temperature();
|
||||
let frequency = cpus.first().map(|cpu| cpu.frequency());
|
||||
|
||||
Ok(CpuMetrics {
|
||||
total_usage,
|
||||
per_core_usage,
|
||||
temperature,
|
||||
frequency,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_memory_metrics(&self, system: &System) -> Result<MemoryMetrics> {
|
||||
Ok(MemoryMetrics {
|
||||
total: system.total_memory(),
|
||||
used: system.used_memory(),
|
||||
available: system.available_memory(),
|
||||
swap_total: system.total_swap(),
|
||||
swap_used: system.used_swap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_gpu_metrics(&self) -> Result<Vec<GpuMetrics>> {
|
||||
// GPU monitoring is complex and platform-specific
|
||||
// On Linux, we can read from /sys/class/drm or use nvml for NVIDIA
|
||||
let mut gpus = Vec::new();
|
||||
|
||||
// Try to detect AMD GPUs via sysfs
|
||||
if let Ok(entries) = fs::read_dir("/sys/class/drm") {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
|
||||
if name_str.starts_with("card") && !name_str.contains('-') {
|
||||
if let Some(gpu) = self.read_amd_gpu_info(&path) {
|
||||
gpus.push(gpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(gpus)
|
||||
}
|
||||
|
||||
fn read_amd_gpu_info(&self, card_path: &Path) -> Option<GpuMetrics> {
|
||||
let device_path = card_path.join("device");
|
||||
|
||||
let name = fs::read_to_string(device_path.join("product_name"))
|
||||
.or_else(|_| fs::read_to_string(device_path.join("model")))
|
||||
.unwrap_or_else(|_| "Unknown GPU".to_string())
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
// Try to read GPU usage
|
||||
let usage = fs::read_to_string(device_path.join("gpu_busy_percent"))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<f32>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Try to read VRAM usage
|
||||
let memory_used = fs::read_to_string(device_path.join("mem_info_vram_used"))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let memory_total = fs::read_to_string(device_path.join("mem_info_vram_total"))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Try to read temperature
|
||||
let temperature = fs::read_to_string(device_path.join("hwmon/hwmon0/temp1_input"))
|
||||
.or_else(|_| fs::read_to_string(device_path.join("hwmon/hwmon1/temp1_input")))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<f32>().ok())
|
||||
.map(|t| t / 1000.0); // Convert from millidegrees
|
||||
|
||||
Some(GpuMetrics {
|
||||
name,
|
||||
usage,
|
||||
memory_used,
|
||||
memory_total,
|
||||
temperature,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_network_metrics(&self, networks: &Networks) -> Result<HashMap<String, NetworkMetrics>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for (interface_name, data) in networks.iter() {
|
||||
let metrics = NetworkMetrics {
|
||||
interface_name: interface_name.to_string(),
|
||||
bytes_sent: data.total_transmitted(),
|
||||
bytes_received: data.total_received(),
|
||||
packets_sent: data.total_packets_transmitted(),
|
||||
packets_received: data.total_packets_received(),
|
||||
errors_in: data.total_errors_on_received(),
|
||||
errors_out: data.total_errors_on_transmitted(),
|
||||
};
|
||||
result.insert(interface_name.to_string(), metrics);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_disk_io_metrics(&self) -> Result<HashMap<String, DiskIoMetrics>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
// Read disk I/O stats from /proc/diskstats on Linux
|
||||
if let Ok(content) = fs::read_to_string("/proc/diskstats") {
|
||||
for line in content.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 14 {
|
||||
let device_name = parts[2].to_string();
|
||||
|
||||
// Skip loop and ram devices
|
||||
if device_name.starts_with("loop") || device_name.starts_with("ram") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let read_ops = parts[3].parse::<u64>().unwrap_or(0);
|
||||
let read_sectors = parts[5].parse::<u64>().unwrap_or(0);
|
||||
let write_ops = parts[7].parse::<u64>().unwrap_or(0);
|
||||
let write_sectors = parts[9].parse::<u64>().unwrap_or(0);
|
||||
|
||||
let metrics = DiskIoMetrics {
|
||||
device_name: device_name.clone(),
|
||||
read_bytes: read_sectors * 512, // sectors are 512 bytes
|
||||
write_bytes: write_sectors * 512,
|
||||
read_ops,
|
||||
write_ops,
|
||||
};
|
||||
|
||||
result.insert(device_name, metrics);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_usb_io_metrics(&self) -> Result<Vec<UsbIoMetrics>> {
|
||||
let mut usb_devices = Vec::new();
|
||||
|
||||
// Read USB device information from /sys/bus/usb/devices
|
||||
if let Ok(entries) = fs::read_dir("/sys/bus/usb/devices") {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Read vendor and product IDs
|
||||
let vendor_id = fs::read_to_string(path.join("idVendor"))
|
||||
.ok()
|
||||
.and_then(|s| u16::from_str_radix(s.trim(), 16).ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let product_id = fs::read_to_string(path.join("idProduct"))
|
||||
.ok()
|
||||
.and_then(|s| u16::from_str_radix(s.trim(), 16).ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
if vendor_id == 0 && product_id == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let device_name = fs::read_to_string(path.join("product"))
|
||||
.unwrap_or_else(|_| "Unknown USB Device".to_string())
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let device_id = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
usb_devices.push(UsbIoMetrics {
|
||||
device_id,
|
||||
device_name,
|
||||
vendor_id,
|
||||
product_id,
|
||||
bytes_transferred: 0, // Would need more complex tracking
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(usb_devices)
|
||||
}
|
||||
|
||||
fn read_cpu_temperature(&self) -> Option<f32> {
|
||||
// Try to read from common thermal zones
|
||||
for i in 0..10 {
|
||||
let temp_path = format!("/sys/class/thermal/thermal_zone{}/temp", i);
|
||||
if let Ok(temp_str) = fs::read_to_string(&temp_path) {
|
||||
if let Ok(temp) = temp_str.trim().parse::<f32>() {
|
||||
return Some(temp / 1000.0); // Convert from millidegrees
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try hwmon
|
||||
if let Ok(entries) = fs::read_dir("/sys/class/hwmon") {
|
||||
for entry in entries.flatten() {
|
||||
let temp_path = entry.path().join("temp1_input");
|
||||
if let Ok(temp_str) = fs::read_to_string(&temp_path) {
|
||||
if let Ok(temp) = temp_str.trim().parse::<f32>() {
|
||||
return Some(temp / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_all_processes(&self) -> Result<Vec<ProcessSnapshot>> {
|
||||
let system = self.system.read();
|
||||
let mut processes = Vec::new();
|
||||
|
||||
let total_from_sysinfo = system.processes().len();
|
||||
let mut skipped_count = 0;
|
||||
|
||||
// Build a set of actual process PIDs (not threads) by reading /proc directory
|
||||
// This is the most reliable way to distinguish processes from threads
|
||||
let mut real_pids = std::collections::HashSet::new();
|
||||
if let Ok(entries) = fs::read_dir("/proc") {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if let Ok(pid) = file_name.parse::<u32>() {
|
||||
real_pids.insert(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (pid, process) in system.processes() {
|
||||
let pid_u32 = pid.as_u32();
|
||||
|
||||
// Only include PIDs that are actual processes (in /proc directory listing)
|
||||
// This filters out threads which have /proc/{tid} entries but aren't in directory listing
|
||||
if !real_pids.contains(&pid_u32) {
|
||||
skipped_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(snapshot) = self.process_to_snapshot(*pid, process) {
|
||||
processes.push(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
eprintln!("get_all_processes: sysinfo reported {}, skipped {}, returning {}",
|
||||
total_from_sysinfo, skipped_count, processes.len());
|
||||
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
pub fn get_process(&self, pid: u32) -> Result<Option<ProcessSnapshot>> {
|
||||
let system = self.system.read();
|
||||
let pid = Pid::from_u32(pid);
|
||||
|
||||
Ok(system.process(pid).and_then(|p| self.process_to_snapshot(pid, p)))
|
||||
}
|
||||
|
||||
fn process_to_snapshot(&self, pid: Pid, process: &Process) -> Option<ProcessSnapshot> {
|
||||
let user = self.get_process_user(pid.as_u32());
|
||||
|
||||
let info = ProcessInfo {
|
||||
pid: pid.as_u32(),
|
||||
name: process.name().to_string_lossy().to_string(),
|
||||
user: user.0,
|
||||
uid: user.1,
|
||||
exe_path: process.exe().map(|p| p.to_path_buf()),
|
||||
command_line: process.cmd().iter().map(|s| s.to_string_lossy().to_string()).collect(),
|
||||
status: self.convert_process_status(process.status()),
|
||||
parent_pid: process.parent().map(|p| p.as_u32()),
|
||||
};
|
||||
|
||||
let stats = ProcessStats {
|
||||
pid: pid.as_u32(),
|
||||
cpu_usage: process.cpu_usage(),
|
||||
memory_usage: process.memory(),
|
||||
memory_percent: 0.0, // Calculate if needed
|
||||
virtual_memory: process.virtual_memory(),
|
||||
disk_read_bytes: process.disk_usage().read_bytes,
|
||||
disk_write_bytes: process.disk_usage().written_bytes,
|
||||
network_rx_bytes: 0, // Would need per-process network tracking
|
||||
network_tx_bytes: 0,
|
||||
num_threads: 0, // Not available in sysinfo
|
||||
start_time: chrono::Utc::now(), // Would need to calculate from process start time
|
||||
run_time: std::time::Duration::from_secs(process.run_time()),
|
||||
};
|
||||
|
||||
Some(ProcessSnapshot {
|
||||
info,
|
||||
stats,
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_process_user(&self, pid: u32) -> (String, u32) {
|
||||
// Try to read user from /proc
|
||||
let status_path = format!("/proc/{}/status", pid);
|
||||
if let Ok(content) = fs::read_to_string(&status_path) {
|
||||
for line in content.lines() {
|
||||
if line.starts_with("Uid:") {
|
||||
if let Some(uid_str) = line.split_whitespace().nth(1) {
|
||||
if let Ok(uid) = uid_str.parse::<u32>() {
|
||||
let username = self.uid_to_username(uid);
|
||||
return (username, uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
("unknown".to_string(), 0)
|
||||
}
|
||||
|
||||
fn uid_to_username(&self, uid: u32) -> String {
|
||||
// Try to read from /etc/passwd
|
||||
if let Ok(content) = fs::read_to_string("/etc/passwd") {
|
||||
for line in content.lines() {
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
if parts.len() >= 3 {
|
||||
if let Ok(line_uid) = parts[2].parse::<u32>() {
|
||||
if line_uid == uid {
|
||||
return parts[0].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format!("uid:{}", uid)
|
||||
}
|
||||
|
||||
fn convert_process_status(&self, status: sysinfo::ProcessStatus) -> ProcessStatus {
|
||||
match status {
|
||||
sysinfo::ProcessStatus::Run => ProcessStatus::Running,
|
||||
sysinfo::ProcessStatus::Sleep => ProcessStatus::Sleeping,
|
||||
sysinfo::ProcessStatus::Stop => ProcessStatus::Stopped,
|
||||
sysinfo::ProcessStatus::Zombie => ProcessStatus::Zombie,
|
||||
sysinfo::ProcessStatus::Dead => ProcessStatus::Dead,
|
||||
_ => ProcessStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SystemMonitor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
437
procmon-core/src/partition.rs
Normal file
437
procmon-core/src/partition.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Partition {
|
||||
pub device: String,
|
||||
pub partition_number: Option<u32>,
|
||||
pub filesystem: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub size_bytes: u64,
|
||||
pub used_bytes: u64,
|
||||
pub mount_point: Option<String>,
|
||||
pub partition_type: Option<String>,
|
||||
pub flags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Disk {
|
||||
pub device: String,
|
||||
pub model: String,
|
||||
pub size_bytes: u64,
|
||||
pub logical_sector_size: u32,
|
||||
pub physical_sector_size: u32,
|
||||
pub partitions: Vec<Partition>,
|
||||
}
|
||||
|
||||
pub struct PartitionManager {
|
||||
}
|
||||
|
||||
impl PartitionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
/// List all block devices and their partitions
|
||||
pub fn list_disks(&self) -> Result<Vec<Disk>> {
|
||||
let mut disks = Vec::new();
|
||||
|
||||
// Use lsblk to get block device information
|
||||
let output = Command::new("lsblk")
|
||||
.args(&["-J", "-b", "-o", "NAME,TYPE,SIZE,FSTYPE,LABEL,MOUNTPOINT,MODEL"])
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let json_str = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(lsblk_data) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
if let Some(blockdevices) = lsblk_data["blockdevices"].as_array() {
|
||||
for device in blockdevices {
|
||||
if device["type"].as_str() == Some("disk") {
|
||||
disks.push(self.parse_disk(device)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(disks)
|
||||
}
|
||||
|
||||
fn parse_disk(&self, device: &serde_json::Value) -> Result<Disk> {
|
||||
let device_name = device["name"].as_str().unwrap_or("unknown").to_string();
|
||||
let model = device["model"].as_str().unwrap_or("Unknown").trim().to_string();
|
||||
let size_bytes = device["size"].as_str()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Get sector sizes from sysfs
|
||||
let (logical_sector_size, physical_sector_size) = self.get_sector_sizes(&device_name);
|
||||
|
||||
// Parse partitions
|
||||
let mut partitions = Vec::new();
|
||||
if let Some(children) = device["children"].as_array() {
|
||||
for child in children {
|
||||
if let Some(part) = self.parse_partition(child, &device_name) {
|
||||
partitions.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Disk {
|
||||
device: format!("/dev/{}", device_name),
|
||||
model,
|
||||
size_bytes,
|
||||
logical_sector_size,
|
||||
physical_sector_size,
|
||||
partitions,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_partition(&self, part: &serde_json::Value, parent_device: &str) -> Option<Partition> {
|
||||
let name = part["name"].as_str()?;
|
||||
let size_bytes = part["size"].as_str()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Extract partition number
|
||||
let partition_number = name.trim_start_matches(parent_device)
|
||||
.trim_start_matches('p')
|
||||
.parse::<u32>().ok();
|
||||
|
||||
// Get filesystem info
|
||||
let filesystem = part["fstype"].as_str().map(|s| s.to_string());
|
||||
let label = part["label"].as_str().map(|s| s.to_string());
|
||||
let mount_point = part["mountpoint"].as_str().map(|s| s.to_string());
|
||||
|
||||
// Get partition type and flags from parted
|
||||
let (partition_type, flags) = self.get_partition_info(&format!("/dev/{}", name));
|
||||
|
||||
// Get used space if mounted
|
||||
let used_bytes = if let Some(ref mp) = mount_point {
|
||||
self.get_used_space(mp).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Some(Partition {
|
||||
device: format!("/dev/{}", name),
|
||||
partition_number,
|
||||
filesystem,
|
||||
label,
|
||||
size_bytes,
|
||||
used_bytes,
|
||||
mount_point,
|
||||
partition_type,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_sector_sizes(&self, device: &str) -> (u32, u32) {
|
||||
let logical = fs::read_to_string(format!("/sys/block/{}/queue/logical_block_size", device))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.unwrap_or(512);
|
||||
|
||||
let physical = fs::read_to_string(format!("/sys/block/{}/queue/physical_block_size", device))
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.unwrap_or(512);
|
||||
|
||||
(logical, physical)
|
||||
}
|
||||
|
||||
fn get_partition_info(&self, device: &str) -> (Option<String>, Vec<String>) {
|
||||
// Use parted to get partition type and flags
|
||||
let output = Command::new("parted")
|
||||
.args(&[device, "print"])
|
||||
.output();
|
||||
|
||||
if let Ok(output) = output {
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// Parse parted output for partition type and flags
|
||||
// This is a simplified version - full parsing would be more complex
|
||||
for line in stdout.lines() {
|
||||
if line.contains("Partition Table:") {
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
if parts.len() > 1 {
|
||||
return (Some(parts[1].trim().to_string()), Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(None, Vec::new())
|
||||
}
|
||||
|
||||
fn get_used_space(&self, mount_point: &str) -> Option<u64> {
|
||||
let output = Command::new("df")
|
||||
.args(&["-B1", mount_point])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
if lines.len() > 1 {
|
||||
let fields: Vec<&str> = lines[1].split_whitespace().collect();
|
||||
if fields.len() > 2 {
|
||||
return fields[2].parse::<u64>().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Create a new partition table (WARNING: destroys all data)
|
||||
pub fn create_partition_table(&self, device: &str, table_type: &str) -> Result<()> {
|
||||
// table_type can be: gpt, msdos, etc.
|
||||
let output = Command::new("parted")
|
||||
.args(&["-s", device, "mklabel", table_type])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to create partition table: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new partition
|
||||
pub fn create_partition(
|
||||
&self,
|
||||
device: &str,
|
||||
start: &str,
|
||||
end: &str,
|
||||
fs_type: &str,
|
||||
) -> Result<()> {
|
||||
let output = Command::new("parted")
|
||||
.args(&["-s", device, "mkpart", "primary", fs_type, start, end])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to create partition: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a partition
|
||||
pub fn delete_partition(&self, device: &str, partition_number: u32) -> Result<()> {
|
||||
let output = Command::new("parted")
|
||||
.args(&["-s", device, "rm", &partition_number.to_string()])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to delete partition: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resize a partition
|
||||
pub fn resize_partition(
|
||||
&self,
|
||||
device: &str,
|
||||
partition_number: u32,
|
||||
end: &str,
|
||||
) -> Result<()> {
|
||||
let output = Command::new("parted")
|
||||
.args(&["-s", device, "resizepart", &partition_number.to_string(), end])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to resize partition: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Format a partition with specified filesystem
|
||||
pub fn format_partition(&self, device: &str, filesystem: &str, label: Option<&str>) -> Result<()> {
|
||||
let mut args = vec![device];
|
||||
|
||||
match filesystem {
|
||||
"ext2" | "ext3" | "ext4" => {
|
||||
let mut cmd = Command::new(&format!("mkfs.{}", filesystem));
|
||||
if let Some(lbl) = label {
|
||||
cmd.args(&["-L", lbl]);
|
||||
}
|
||||
cmd.arg(device);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to format: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
"xfs" => {
|
||||
let mut cmd = Command::new("mkfs.xfs");
|
||||
cmd.args(&["-f"]);
|
||||
if let Some(lbl) = label {
|
||||
cmd.args(&["-L", lbl]);
|
||||
}
|
||||
cmd.arg(device);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to format: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
"btrfs" => {
|
||||
let mut cmd = Command::new("mkfs.btrfs");
|
||||
cmd.args(&["-f"]);
|
||||
if let Some(lbl) = label {
|
||||
cmd.args(&["-L", lbl]);
|
||||
}
|
||||
cmd.arg(device);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to format: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
"f2fs" => {
|
||||
let mut cmd = Command::new("mkfs.f2fs");
|
||||
if let Some(lbl) = label {
|
||||
cmd.args(&["-l", lbl]);
|
||||
}
|
||||
cmd.arg(device);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to format: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
"ntfs" => {
|
||||
let mut cmd = Command::new("mkfs.ntfs");
|
||||
cmd.args(&["-f"]);
|
||||
if let Some(lbl) = label {
|
||||
cmd.args(&["-L", lbl]);
|
||||
}
|
||||
cmd.arg(device);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to format: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
"fat32" | "vfat" => {
|
||||
let mut cmd = Command::new("mkfs.vfat");
|
||||
cmd.args(&["-F", "32"]);
|
||||
if let Some(lbl) = label {
|
||||
cmd.args(&["-n", lbl]);
|
||||
}
|
||||
cmd.arg(device);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to format: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Unsupported filesystem type: {}", filesystem),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resize filesystem (must be done after partition resize)
|
||||
pub fn resize_filesystem(&self, device: &str, filesystem: &str) -> Result<()> {
|
||||
match filesystem {
|
||||
"ext2" | "ext3" | "ext4" => {
|
||||
let output = Command::new("resize2fs")
|
||||
.arg(device)
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to resize filesystem: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
"xfs" => {
|
||||
// XFS requires the filesystem to be mounted
|
||||
anyhow::bail!("XFS filesystem must be mounted to resize. Use 'xfs_growfs' on the mount point.");
|
||||
}
|
||||
"btrfs" => {
|
||||
let output = Command::new("btrfs")
|
||||
.args(&["filesystem", "resize", "max", device])
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to resize filesystem: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Filesystem resize not supported for: {}", filesystem),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set partition flags
|
||||
pub fn set_partition_flag(&self, device: &str, partition_number: u32, flag: &str, state: bool) -> Result<()> {
|
||||
let state_str = if state { "on" } else { "off" };
|
||||
let output = Command::new("parted")
|
||||
.args(&["-s", device, "set", &partition_number.to_string(), flag, state_str])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to set flag: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check filesystem for errors
|
||||
pub fn check_filesystem(&self, device: &str, filesystem: &str, repair: bool) -> Result<String> {
|
||||
let output = match filesystem {
|
||||
"ext2" | "ext3" | "ext4" => {
|
||||
let mut cmd = Command::new("e2fsck");
|
||||
if repair {
|
||||
cmd.args(&["-p"]); // Automatic repair
|
||||
} else {
|
||||
cmd.args(&["-n"]); // No changes, just check
|
||||
}
|
||||
cmd.arg(device).output()?
|
||||
}
|
||||
"xfs" => {
|
||||
Command::new("xfs_repair")
|
||||
.args(&[if repair { "-n" } else { "-n" }, device])
|
||||
.output()?
|
||||
}
|
||||
"btrfs" => {
|
||||
Command::new("btrfs")
|
||||
.args(&["check", if repair { "--repair" } else { "" }, device])
|
||||
.output()?
|
||||
}
|
||||
_ => anyhow::bail!("Filesystem check not supported for: {}", filesystem),
|
||||
};
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
/// Get supported filesystems on this system
|
||||
pub fn get_supported_filesystems(&self) -> Vec<String> {
|
||||
let mut filesystems = vec![
|
||||
"ext2", "ext3", "ext4", "xfs", "btrfs", "f2fs",
|
||||
"ntfs", "vfat", "fat32", "exfat", "swap"
|
||||
];
|
||||
|
||||
// Check which mkfs utilities are available
|
||||
let mut available = Vec::new();
|
||||
for fs in filesystems {
|
||||
let binary = match fs {
|
||||
"fat32" | "vfat" => "mkfs.vfat",
|
||||
_ => &format!("mkfs.{}", fs),
|
||||
};
|
||||
|
||||
if Command::new("which").arg(binary).output().ok()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
available.push(fs.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
available
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PartitionManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
86
procmon-core/src/process.rs
Normal file
86
procmon-core/src/process.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessInfo {
|
||||
pub pid: u32,
|
||||
pub name: String,
|
||||
pub user: String,
|
||||
pub uid: u32,
|
||||
pub exe_path: Option<PathBuf>,
|
||||
pub command_line: Vec<String>,
|
||||
pub status: ProcessStatus,
|
||||
pub parent_pid: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ProcessStatus {
|
||||
Running,
|
||||
Sleeping,
|
||||
Stopped,
|
||||
Zombie,
|
||||
Dead,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessStats {
|
||||
pub pid: u32,
|
||||
pub cpu_usage: f32,
|
||||
pub memory_usage: u64,
|
||||
pub memory_percent: f32,
|
||||
pub virtual_memory: u64,
|
||||
pub disk_read_bytes: u64,
|
||||
pub disk_write_bytes: u64,
|
||||
pub network_rx_bytes: u64,
|
||||
pub network_tx_bytes: u64,
|
||||
pub num_threads: u32,
|
||||
pub start_time: chrono::DateTime<chrono::Utc>,
|
||||
pub run_time: std::time::Duration,
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
pub fn new(
|
||||
pid: u32,
|
||||
name: String,
|
||||
user: String,
|
||||
uid: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
pid,
|
||||
name,
|
||||
user,
|
||||
uid,
|
||||
exe_path: None,
|
||||
command_line: Vec::new(),
|
||||
status: ProcessStatus::Unknown,
|
||||
parent_pid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProcessStats {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pid: 0,
|
||||
cpu_usage: 0.0,
|
||||
memory_usage: 0,
|
||||
memory_percent: 0.0,
|
||||
virtual_memory: 0,
|
||||
disk_read_bytes: 0,
|
||||
disk_write_bytes: 0,
|
||||
network_rx_bytes: 0,
|
||||
network_tx_bytes: 0,
|
||||
num_threads: 0,
|
||||
start_time: chrono::Utc::now(),
|
||||
run_time: std::time::Duration::from_secs(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessSnapshot {
|
||||
pub info: ProcessInfo,
|
||||
pub stats: ProcessStats,
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
227
procmon-core/src/service.rs
Normal file
227
procmon-core/src/service.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemService {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub state: ServiceState,
|
||||
pub enabled: bool,
|
||||
pub active_state: String,
|
||||
pub sub_state: String,
|
||||
pub memory_usage: Option<u64>,
|
||||
pub cpu_usage: Option<f32>,
|
||||
pub main_pid: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServiceState {
|
||||
Running,
|
||||
Stopped,
|
||||
Failed,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&str> for ServiceState {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
"active" | "running" => ServiceState::Running,
|
||||
"inactive" | "dead" => ServiceState::Stopped,
|
||||
"failed" => ServiceState::Failed,
|
||||
_ => ServiceState::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServiceManager {
|
||||
// No state needed, operates on systemctl
|
||||
}
|
||||
|
||||
impl ServiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
/// List all systemd services
|
||||
pub fn list_services(&self) -> Result<Vec<SystemService>> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["list-units", "--type=service", "--all", "--no-pager", "--plain"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to list services: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut services = Vec::new();
|
||||
|
||||
for line in stdout.lines().skip(1) { // Skip header
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format: UNIT LOAD ACTIVE SUB DESCRIPTION
|
||||
let unit_name = parts[0];
|
||||
if !unit_name.ends_with(".service") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = unit_name.trim_end_matches(".service").to_string();
|
||||
let active_state = parts[2].to_string();
|
||||
let sub_state = parts[3].to_string();
|
||||
let description = if parts.len() > 4 {
|
||||
parts[4..].join(" ")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let state = ServiceState::from(active_state.as_str());
|
||||
|
||||
// Check if service is enabled
|
||||
let enabled = self.is_service_enabled(&name).unwrap_or(false);
|
||||
|
||||
// Get detailed info including PID and resource usage
|
||||
let (main_pid, memory_usage, cpu_usage) = self.get_service_details(&name).unwrap_or((None, None, None));
|
||||
|
||||
services.push(SystemService {
|
||||
name,
|
||||
description,
|
||||
state,
|
||||
enabled,
|
||||
active_state,
|
||||
sub_state,
|
||||
memory_usage,
|
||||
cpu_usage,
|
||||
main_pid,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
/// Get detailed information about a service
|
||||
fn get_service_details(&self, service_name: &str) -> Result<(Option<u32>, Option<u64>, Option<f32>)> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["show", &format!("{}.service", service_name), "--no-pager"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok((None, None, None));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut main_pid = None;
|
||||
let mut memory_usage = None;
|
||||
|
||||
for line in stdout.lines() {
|
||||
if let Some(value) = line.strip_prefix("MainPID=") {
|
||||
if let Ok(pid) = value.parse::<u32>() {
|
||||
if pid > 0 {
|
||||
main_pid = Some(pid);
|
||||
}
|
||||
}
|
||||
} else if let Some(value) = line.strip_prefix("MemoryCurrent=") {
|
||||
if let Ok(mem) = value.parse::<u64>() {
|
||||
if mem > 0 {
|
||||
memory_usage = Some(mem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU usage would require tracking over time, skip for now
|
||||
Ok((main_pid, memory_usage, None))
|
||||
}
|
||||
|
||||
/// Check if a service is enabled
|
||||
fn is_service_enabled(&self, service_name: &str) -> Result<bool> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["is-enabled", &format!("{}.service", service_name)])
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
Ok(stdout.trim() == "enabled")
|
||||
}
|
||||
|
||||
/// Start a service
|
||||
pub fn start_service(&self, service_name: &str) -> Result<()> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["start", &format!("{}.service", service_name)])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to start service: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop a service
|
||||
pub fn stop_service(&self, service_name: &str) -> Result<()> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["stop", &format!("{}.service", service_name)])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to stop service: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restart a service
|
||||
pub fn restart_service(&self, service_name: &str) -> Result<()> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["restart", &format!("{}.service", service_name)])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to restart service: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable a service
|
||||
pub fn enable_service(&self, service_name: &str) -> Result<()> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["enable", &format!("{}.service", service_name)])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to enable service: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable a service
|
||||
pub fn disable_service(&self, service_name: &str) -> Result<()> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["disable", &format!("{}.service", service_name)])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Failed to disable service: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get service status details
|
||||
pub fn get_service_status(&self, service_name: &str) -> Result<String> {
|
||||
let output = Command::new("systemctl")
|
||||
.args(&["status", &format!("{}.service", service_name), "--no-pager"])
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServiceManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
95
procmon-core/src/tests.rs
Normal file
95
procmon-core/src/tests.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_pid_accuracy() {
|
||||
// Get PIDs from our monitoring code using get_all_processes() which has the /proc filter
|
||||
let monitor = crate::monitor::SystemMonitor::new();
|
||||
// Refresh multiple times to ensure clean data
|
||||
monitor.refresh();
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
monitor.refresh();
|
||||
|
||||
// This should now return only valid PIDs due to our /proc filter
|
||||
let processes = monitor.get_all_processes().unwrap();
|
||||
|
||||
println!("Total processes returned: {}", processes.len());
|
||||
|
||||
// Check for duplicates
|
||||
let our_pids: HashSet<u32> = processes.iter().map(|p| p.info.pid).collect();
|
||||
println!("Unique PIDs: {}, Total processes: {}", our_pids.len(), processes.len());
|
||||
if our_pids.len() != processes.len() {
|
||||
println!("WARNING: Duplicate PIDs detected!");
|
||||
}
|
||||
|
||||
// Get PIDs from /proc directly
|
||||
let mut proc_pids = HashSet::new();
|
||||
if let Ok(entries) = fs::read_dir("/proc") {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(file_name) = entry.file_name().into_string() {
|
||||
if let Ok(pid) = file_name.parse::<u32>() {
|
||||
proc_pids.insert(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Our filtered PIDs: {}, /proc PIDs: {}", our_pids.len(), proc_pids.len());
|
||||
|
||||
// Find some examples of PIDs we have that /proc doesn't
|
||||
let mut example_count = 0;
|
||||
for pid in &our_pids {
|
||||
if !proc_pids.contains(pid) && example_count < 5 {
|
||||
eprintln!("Example missing PID: {} (checking if /proc/{}/stat exists...)", pid, pid);
|
||||
let stat_path = format!("/proc/{}/stat", pid);
|
||||
let exists = std::path::Path::new(&stat_path).exists();
|
||||
let can_read = fs::read_to_string(&stat_path).is_ok();
|
||||
eprintln!(" - Path exists: {}, Can read: {}", exists, can_read);
|
||||
example_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that ALL of our PIDs exist in /proc (since we filter them)
|
||||
let mut matched = 0;
|
||||
let mut total = 0;
|
||||
for pid in &our_pids {
|
||||
total += 1;
|
||||
if proc_pids.contains(pid) {
|
||||
matched += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Should be 100% or very close (allowing for tiny race conditions)
|
||||
let match_rate = (matched as f64 / total as f64) * 100.0;
|
||||
assert!(match_rate > 99.0,
|
||||
"Only {:.1}% of filtered PIDs matched /proc. Expected >99%. Matched: {}/{}",
|
||||
match_rate, matched, total);
|
||||
|
||||
println!("PID accuracy test PASSED: {}/{} ({:.1}%) PIDs verified", matched, total, match_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_specific_process_pid() {
|
||||
let monitor = crate::monitor::SystemMonitor::new();
|
||||
monitor.refresh();
|
||||
let processes = monitor.get_all_processes().unwrap();
|
||||
|
||||
// Find init process (PID 1) - should always exist
|
||||
let init = processes.iter().find(|p| p.info.pid == 1);
|
||||
assert!(init.is_some(), "Init process (PID 1) not found");
|
||||
|
||||
// Verify our PID matches what's in /proc
|
||||
for process in processes.iter().take(10) {
|
||||
let pid = process.info.pid;
|
||||
let proc_path = format!("/proc/{}/cmdline", pid);
|
||||
|
||||
// If /proc/<pid> exists, verify it
|
||||
if std::path::Path::new(&proc_path).exists() {
|
||||
println!("Verified PID {} exists: {}", pid, process.info.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
procmon-gui/Cargo.toml
Normal file
23
procmon-gui/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "procmon-gui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "procmon-gui"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
procmon-core = { path = "../procmon-core" }
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
eframe.workspace = true
|
||||
egui.workspace = true
|
||||
egui_plot.workspace = true
|
||||
chrono.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde.workspace = true
|
||||
parking_lot.workspace = true
|
||||
1009
procmon-gui/src/main.rs
Normal file
1009
procmon-gui/src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
21
procmon-tui/Cargo.toml
Normal file
21
procmon-tui/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "procmon-tui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "procmon-tui"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
procmon-core = { path = "../procmon-core" }
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
chrono.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde.workspace = true
|
||||
764
procmon-tui/src/app.rs
Normal file
764
procmon-tui/src/app.rs
Normal file
@@ -0,0 +1,764 @@
|
||||
use anyhow::Result;
|
||||
use procmon_core::{
|
||||
MisbehaviorDetector, SystemMetrics, SystemMonitor,
|
||||
process::ProcessSnapshot,
|
||||
ServiceManager, SystemService,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Tab {
|
||||
Dashboard,
|
||||
Processes,
|
||||
Services,
|
||||
Storage,
|
||||
Network,
|
||||
Partitions,
|
||||
Alerts,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SortColumn {
|
||||
Name,
|
||||
Cpu,
|
||||
Memory,
|
||||
DiskIo,
|
||||
User,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub monitor: SystemMonitor,
|
||||
pub detector: MisbehaviorDetector,
|
||||
pub partition_manager: procmon_core::PartitionManager,
|
||||
pub service_manager: ServiceManager,
|
||||
pub system_metrics: SystemMetrics,
|
||||
pub processes: Vec<ProcessSnapshot>,
|
||||
pub filtered_processes: Vec<ProcessSnapshot>,
|
||||
pub services: Vec<SystemService>,
|
||||
pub filtered_services: Vec<SystemService>,
|
||||
pub disks: Vec<procmon_core::Disk>,
|
||||
pub alerts: Vec<procmon_core::MisbehaviorAlert>,
|
||||
pub current_tab: Tab,
|
||||
pub selected_process: usize,
|
||||
pub selected_service: usize,
|
||||
pub selected_disk: usize,
|
||||
pub selected_partition: usize,
|
||||
pub sort_column: SortColumn,
|
||||
pub sort_ascending: bool,
|
||||
pub show_only_misbehaving: bool,
|
||||
pub show_context_menu: bool,
|
||||
pub show_service_menu: bool,
|
||||
pub show_partition_menu: bool,
|
||||
pub context_menu_pid: Option<u32>,
|
||||
pub context_menu_service: Option<String>,
|
||||
pub status_message: Option<String>,
|
||||
pub search_query: String,
|
||||
pub search_mode: bool,
|
||||
pub scroll_offset: usize,
|
||||
pub process_list_area: Option<(u16, u16, u16, u16)>, // (x, y, width, height) for process table
|
||||
last_update: Instant,
|
||||
update_interval: Duration,
|
||||
last_click_time: Option<Instant>,
|
||||
last_click_row: Option<usize>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let monitor = SystemMonitor::new();
|
||||
let detector = MisbehaviorDetector::new();
|
||||
let partition_manager = procmon_core::PartitionManager::new();
|
||||
let service_manager = ServiceManager::new();
|
||||
|
||||
monitor.refresh();
|
||||
let system_metrics = monitor.get_system_metrics()?;
|
||||
let processes = monitor.get_all_processes()?;
|
||||
let disks = partition_manager.list_disks().unwrap_or_default();
|
||||
let services = service_manager.list_services().unwrap_or_default();
|
||||
|
||||
let filtered_processes = processes.clone();
|
||||
let filtered_services = services.clone();
|
||||
|
||||
Ok(Self {
|
||||
monitor,
|
||||
detector,
|
||||
partition_manager,
|
||||
service_manager,
|
||||
system_metrics,
|
||||
processes,
|
||||
filtered_processes,
|
||||
services,
|
||||
filtered_services,
|
||||
disks,
|
||||
alerts: Vec::new(),
|
||||
current_tab: Tab::Dashboard,
|
||||
selected_process: 0,
|
||||
selected_service: 0,
|
||||
selected_disk: 0,
|
||||
selected_partition: 0,
|
||||
sort_column: SortColumn::Cpu,
|
||||
sort_ascending: false,
|
||||
show_only_misbehaving: false,
|
||||
show_context_menu: false,
|
||||
show_service_menu: false,
|
||||
show_partition_menu: false,
|
||||
context_menu_pid: None,
|
||||
context_menu_service: None,
|
||||
status_message: None,
|
||||
search_query: String::new(),
|
||||
search_mode: false,
|
||||
scroll_offset: 0,
|
||||
process_list_area: None,
|
||||
last_update: Instant::now(),
|
||||
update_interval: Duration::from_millis(1000),
|
||||
last_click_time: None,
|
||||
last_click_row: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle_mouse_click(&mut self, x: u16, y: u16) {
|
||||
// Check if click is within process list area
|
||||
if let Some((area_x, area_y, area_width, area_height)) = self.process_list_area {
|
||||
if x >= area_x && x < area_x + area_width && y >= area_y && y < area_y + area_height {
|
||||
// Calculate which row was clicked (accounting for borders and header)
|
||||
// area_y is the top of the block, +1 for border, +2 for header with spacing
|
||||
let header_offset = 3; // border + header + spacing
|
||||
if y >= area_y + header_offset {
|
||||
let clicked_row = (y - area_y - header_offset) as usize;
|
||||
let actual_index = clicked_row + self.scroll_offset;
|
||||
|
||||
if actual_index < self.filtered_processes.len() {
|
||||
// Check for double-click (within 500ms)
|
||||
let now = Instant::now();
|
||||
let is_double_click = if let (Some(last_time), Some(last_row)) = (self.last_click_time, self.last_click_row) {
|
||||
now.duration_since(last_time) < Duration::from_millis(500) && last_row == actual_index
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.selected_process = actual_index;
|
||||
|
||||
if is_double_click {
|
||||
// Double-click opens context menu
|
||||
self.toggle_context_menu();
|
||||
self.last_click_time = None;
|
||||
self.last_click_row = None;
|
||||
} else {
|
||||
// Single click just selects
|
||||
self.last_click_time = Some(now);
|
||||
self.last_click_row = Some(actual_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_process_list_area(&mut self, x: u16, y: u16, width: u16, height: u16) {
|
||||
self.process_list_area = Some((x, y, width, height));
|
||||
}
|
||||
|
||||
pub fn toggle_search_mode(&mut self) {
|
||||
self.search_mode = !self.search_mode;
|
||||
if !self.search_mode {
|
||||
self.search_query.clear();
|
||||
self.filter_processes();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_search_char(&mut self, c: char) {
|
||||
self.search_query.push(c);
|
||||
self.filter_processes();
|
||||
self.selected_process = 0;
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
pub fn remove_search_char(&mut self) {
|
||||
self.search_query.pop();
|
||||
self.filter_processes();
|
||||
self.selected_process = 0;
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn filter_processes(&mut self) {
|
||||
if self.search_query.is_empty() {
|
||||
self.filtered_processes = self.processes.clone();
|
||||
} else {
|
||||
let query_lower = self.search_query.to_lowercase();
|
||||
self.filtered_processes = self.processes
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.info.name.to_lowercase().contains(&query_lower)
|
||||
|| p.info.pid.to_string().contains(&query_lower)
|
||||
|| p.info.user.to_lowercase().contains(&query_lower)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_up(&mut self, amount: usize) {
|
||||
if self.scroll_offset >= amount {
|
||||
self.scroll_offset -= amount;
|
||||
} else {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_down(&mut self, amount: usize, max_visible: usize) {
|
||||
let max_scroll = self.filtered_processes.len().saturating_sub(max_visible);
|
||||
self.scroll_offset = (self.scroll_offset + amount).min(max_scroll);
|
||||
}
|
||||
|
||||
pub fn get_filtered_processes(&self) -> &[ProcessSnapshot] {
|
||||
&self.filtered_processes
|
||||
}
|
||||
|
||||
pub fn next_disk(&mut self) {
|
||||
if !self.disks.is_empty() {
|
||||
self.selected_disk = (self.selected_disk + 1) % self.disks.len();
|
||||
self.selected_partition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_disk(&mut self) {
|
||||
if !self.disks.is_empty() {
|
||||
if self.selected_disk == 0 {
|
||||
self.selected_disk = self.disks.len() - 1;
|
||||
} else {
|
||||
self.selected_disk -= 1;
|
||||
}
|
||||
self.selected_partition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_partition(&mut self) {
|
||||
if self.selected_disk < self.disks.len() {
|
||||
let partitions = &self.disks[self.selected_disk].partitions;
|
||||
if !partitions.is_empty() {
|
||||
self.selected_partition = (self.selected_partition + 1) % partitions.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_partition(&mut self) {
|
||||
if self.selected_disk < self.disks.len() {
|
||||
let partitions = &self.disks[self.selected_disk].partitions;
|
||||
if !partitions.is_empty() {
|
||||
if self.selected_partition == 0 {
|
||||
self.selected_partition = partitions.len() - 1;
|
||||
} else {
|
||||
self.selected_partition -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_partition_menu(&mut self) {
|
||||
self.show_partition_menu = !self.show_partition_menu;
|
||||
}
|
||||
|
||||
pub fn refresh_disks(&mut self) {
|
||||
if let Ok(disks) = self.partition_manager.list_disks() {
|
||||
self.disks = disks;
|
||||
self.status_message = Some("Disk list refreshed".to_string());
|
||||
} else {
|
||||
self.status_message = Some("Failed to refresh disks".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_selected_partition(&mut self, filesystem: &str) -> Result<()> {
|
||||
if self.selected_disk >= self.disks.len() {
|
||||
self.status_message = Some("No disk selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let disk = &self.disks[self.selected_disk];
|
||||
if self.selected_partition >= disk.partitions.len() {
|
||||
self.status_message = Some("No partition selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let partition = &disk.partitions[self.selected_partition];
|
||||
let device = &partition.device;
|
||||
|
||||
match self.partition_manager.format_partition(device, filesystem, None) {
|
||||
Ok(_) => {
|
||||
self.status_message = Some(format!("Formatted {} as {}", device, filesystem));
|
||||
self.refresh_disks();
|
||||
}
|
||||
Err(e) => {
|
||||
self.status_message = Some(format!("Format failed: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_selected_partition(&mut self) -> Result<()> {
|
||||
if self.selected_disk >= self.disks.len() {
|
||||
self.status_message = Some("No disk selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let disk = &self.disks[self.selected_disk];
|
||||
if self.selected_partition >= disk.partitions.len() {
|
||||
self.status_message = Some("No partition selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let partition = &disk.partitions[self.selected_partition];
|
||||
if let Some(part_num) = partition.partition_number {
|
||||
match self.partition_manager.delete_partition(&disk.device, part_num) {
|
||||
Ok(_) => {
|
||||
self.status_message = Some(format!("Deleted partition {}", partition.device));
|
||||
self.refresh_disks();
|
||||
}
|
||||
Err(e) => {
|
||||
self.status_message = Some(format!("Delete failed: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.status_message = Some("Cannot determine partition number".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_selected_partition(&mut self) -> Result<()> {
|
||||
if self.selected_disk >= self.disks.len() {
|
||||
self.status_message = Some("No disk selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let disk = &self.disks[self.selected_disk];
|
||||
if self.selected_partition >= disk.partitions.len() {
|
||||
self.status_message = Some("No partition selected".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let partition = &disk.partitions[self.selected_partition];
|
||||
if let Some(ref fs) = partition.filesystem {
|
||||
match self.partition_manager.check_filesystem(&partition.device, fs, false) {
|
||||
Ok(result) => {
|
||||
self.status_message = Some(format!("Check complete. See logs for details."));
|
||||
}
|
||||
Err(e) => {
|
||||
self.status_message = Some(format!("Check failed: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.status_message = Some("No filesystem detected".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update(&mut self) -> Result<()> {
|
||||
if self.last_update.elapsed() >= self.update_interval {
|
||||
self.monitor.refresh();
|
||||
self.system_metrics = self.monitor.get_system_metrics()?;
|
||||
self.processes = self.monitor.get_all_processes()?;
|
||||
|
||||
// Update services list
|
||||
if let Ok(services) = self.service_manager.list_services() {
|
||||
self.services = services;
|
||||
self.filtered_services = self.services.clone();
|
||||
}
|
||||
|
||||
// Check for misbehaving processes
|
||||
let mut new_alerts = Vec::new();
|
||||
for process in &self.processes {
|
||||
let process_alerts = self.detector.check_process(process);
|
||||
new_alerts.extend(process_alerts);
|
||||
}
|
||||
|
||||
// Keep only recent alerts (last 100)
|
||||
self.alerts.extend(new_alerts);
|
||||
if self.alerts.len() > 100 {
|
||||
self.alerts.drain(0..self.alerts.len() - 100);
|
||||
}
|
||||
|
||||
// Cleanup detector state for dead processes
|
||||
let active_pids: Vec<u32> = self.processes.iter().map(|p| p.info.pid).collect();
|
||||
self.detector.cleanup_dead_processes(&active_pids);
|
||||
|
||||
// Sort processes and apply filter
|
||||
self.sort_processes();
|
||||
self.filter_processes();
|
||||
|
||||
self.last_update = Instant::now();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sort_processes(&mut self) {
|
||||
let ascending = self.sort_ascending;
|
||||
match self.sort_column {
|
||||
SortColumn::Name => {
|
||||
self.processes.sort_by(|a, b| {
|
||||
if ascending {
|
||||
a.info.name.cmp(&b.info.name)
|
||||
} else {
|
||||
b.info.name.cmp(&a.info.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
SortColumn::Cpu => {
|
||||
self.processes.sort_by(|a, b| {
|
||||
if ascending {
|
||||
a.stats.cpu_usage.partial_cmp(&b.stats.cpu_usage).unwrap()
|
||||
} else {
|
||||
b.stats.cpu_usage.partial_cmp(&a.stats.cpu_usage).unwrap()
|
||||
}
|
||||
});
|
||||
}
|
||||
SortColumn::Memory => {
|
||||
self.processes.sort_by(|a, b| {
|
||||
if ascending {
|
||||
a.stats.memory_usage.cmp(&b.stats.memory_usage)
|
||||
} else {
|
||||
b.stats.memory_usage.cmp(&a.stats.memory_usage)
|
||||
}
|
||||
});
|
||||
}
|
||||
SortColumn::DiskIo => {
|
||||
self.processes.sort_by(|a, b| {
|
||||
let a_io = a.stats.disk_read_bytes + a.stats.disk_write_bytes;
|
||||
let b_io = b.stats.disk_read_bytes + b.stats.disk_write_bytes;
|
||||
if ascending {
|
||||
a_io.cmp(&b_io)
|
||||
} else {
|
||||
b_io.cmp(&a_io)
|
||||
}
|
||||
});
|
||||
}
|
||||
SortColumn::User => {
|
||||
self.processes.sort_by(|a, b| {
|
||||
if ascending {
|
||||
a.info.user.cmp(&b.info.user)
|
||||
} else {
|
||||
b.info.user.cmp(&a.info.user)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_process(&mut self) {
|
||||
if !self.filtered_processes.is_empty() {
|
||||
self.selected_process = (self.selected_process + 1) % self.filtered_processes.len();
|
||||
self.ensure_selected_visible();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_process(&mut self) {
|
||||
if !self.filtered_processes.is_empty() {
|
||||
if self.selected_process == 0 {
|
||||
self.selected_process = self.filtered_processes.len() - 1;
|
||||
} else {
|
||||
self.selected_process -= 1;
|
||||
}
|
||||
self.ensure_selected_visible();
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_selected_visible(&mut self) {
|
||||
// Assume visible area is around 20 rows (will be adjusted dynamically in UI)
|
||||
let visible_rows = 20;
|
||||
|
||||
// If selected is below visible area, scroll down
|
||||
if self.selected_process >= self.scroll_offset + visible_rows {
|
||||
self.scroll_offset = self.selected_process.saturating_sub(visible_rows - 1);
|
||||
}
|
||||
|
||||
// If selected is above visible area, scroll up
|
||||
if self.selected_process < self.scroll_offset {
|
||||
self.scroll_offset = self.selected_process;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_visible_rows(&mut self, rows: usize) {
|
||||
// This will be called from UI to set the actual visible area
|
||||
// For now we use a default in ensure_selected_visible
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.current_tab = match self.current_tab {
|
||||
Tab::Dashboard => Tab::Processes,
|
||||
Tab::Processes => Tab::Services,
|
||||
Tab::Services => Tab::Storage,
|
||||
Tab::Storage => Tab::Network,
|
||||
Tab::Network => Tab::Partitions,
|
||||
Tab::Partitions => Tab::Alerts,
|
||||
Tab::Alerts => Tab::Dashboard,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
self.current_tab = match self.current_tab {
|
||||
Tab::Dashboard => Tab::Alerts,
|
||||
Tab::Processes => Tab::Dashboard,
|
||||
Tab::Services => Tab::Processes,
|
||||
Tab::Storage => Tab::Services,
|
||||
Tab::Network => Tab::Storage,
|
||||
Tab::Partitions => Tab::Network,
|
||||
Tab::Alerts => Tab::Partitions,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_tab(&mut self, index: usize) {
|
||||
self.current_tab = match index {
|
||||
0 => Tab::Dashboard,
|
||||
1 => Tab::Processes,
|
||||
2 => Tab::Services,
|
||||
3 => Tab::Storage,
|
||||
4 => Tab::Network,
|
||||
5 => Tab::Partitions,
|
||||
6 => Tab::Alerts,
|
||||
_ => self.current_tab,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_sort_ascending(&mut self) {
|
||||
self.sort_ascending = !self.sort_ascending;
|
||||
self.sort_processes();
|
||||
}
|
||||
|
||||
pub fn next_sort_column(&mut self) {
|
||||
self.sort_column = match self.sort_column {
|
||||
SortColumn::Name => SortColumn::Cpu,
|
||||
SortColumn::Cpu => SortColumn::Memory,
|
||||
SortColumn::Memory => SortColumn::DiskIo,
|
||||
SortColumn::DiskIo => SortColumn::User,
|
||||
SortColumn::User => SortColumn::Name,
|
||||
};
|
||||
self.sort_processes();
|
||||
}
|
||||
|
||||
pub fn toggle_filter(&mut self) {
|
||||
self.show_only_misbehaving = !self.show_only_misbehaving;
|
||||
}
|
||||
|
||||
pub fn get_tab_index(&self) -> usize {
|
||||
match self.current_tab {
|
||||
Tab::Dashboard => 0,
|
||||
Tab::Processes => 1,
|
||||
Tab::Services => 2,
|
||||
Tab::Storage => 3,
|
||||
Tab::Network => 4,
|
||||
Tab::Partitions => 5,
|
||||
Tab::Alerts => 6,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_context_menu(&mut self) {
|
||||
if !self.filtered_processes.is_empty() && self.selected_process < self.filtered_processes.len() {
|
||||
self.show_context_menu = !self.show_context_menu;
|
||||
if self.show_context_menu {
|
||||
self.context_menu_pid = Some(self.filtered_processes[self.selected_process].info.pid);
|
||||
} else {
|
||||
self.context_menu_pid = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kill_process(&mut self) -> Result<()> {
|
||||
if let Some(pid) = self.context_menu_pid {
|
||||
use std::process::Command;
|
||||
Command::new("kill")
|
||||
.arg(pid.to_string())
|
||||
.output()?;
|
||||
self.show_context_menu = false;
|
||||
self.context_menu_pid = None;
|
||||
|
||||
// Immediately refresh the process list
|
||||
self.monitor.refresh();
|
||||
self.processes = self.monitor.get_all_processes()?;
|
||||
self.sort_processes();
|
||||
self.filter_processes();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kill_process_tree(&mut self) -> Result<()> {
|
||||
if let Some(pid) = self.context_menu_pid {
|
||||
use std::process::Command;
|
||||
// Kill process and all children
|
||||
Command::new("kill")
|
||||
.arg("-TERM")
|
||||
.arg("--")
|
||||
.arg(format!("-{}", pid))
|
||||
.output()?;
|
||||
self.show_context_menu = false;
|
||||
self.context_menu_pid = None;
|
||||
|
||||
// Immediately refresh the process list
|
||||
self.monitor.refresh();
|
||||
self.processes = self.monitor.get_all_processes()?;
|
||||
self.sort_processes();
|
||||
self.filter_processes();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open_process_folder(&mut self) -> Result<()> {
|
||||
if let Some(pid) = self.context_menu_pid {
|
||||
if let Some(process) = self.processes.iter().find(|p| p.info.pid == pid) {
|
||||
if let Some(exe_path) = &process.info.exe_path {
|
||||
if let Some(parent) = exe_path.parent() {
|
||||
use std::process::Command;
|
||||
Command::new("xdg-open")
|
||||
.arg(parent)
|
||||
.spawn()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.show_context_menu = false;
|
||||
self.context_menu_pid = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restart_process(&mut self) -> Result<()> {
|
||||
if let Some(pid) = self.context_menu_pid {
|
||||
if let Some(process) = self.processes.iter().find(|p| p.info.pid == pid) {
|
||||
// Get the command line and executable path
|
||||
let exe_path = process.info.exe_path.clone();
|
||||
let cmd_line = process.info.command_line.clone();
|
||||
|
||||
// Kill the process first
|
||||
use std::process::Command;
|
||||
Command::new("kill")
|
||||
.arg(pid.to_string())
|
||||
.output()?;
|
||||
|
||||
// Wait a bit for the process to terminate
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
// Restart the process with the same command line
|
||||
if let Some(exe) = exe_path {
|
||||
let mut command = Command::new(exe);
|
||||
if cmd_line.len() > 1 {
|
||||
// Skip the first argument (the executable itself)
|
||||
command.args(&cmd_line[1..]);
|
||||
}
|
||||
command.spawn()?;
|
||||
}
|
||||
}
|
||||
self.show_context_menu = false;
|
||||
self.context_menu_pid = None;
|
||||
|
||||
// Immediately refresh the process list
|
||||
self.monitor.refresh();
|
||||
self.processes = self.monitor.get_all_processes()?;
|
||||
self.sort_processes();
|
||||
self.filter_processes();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Service navigation methods
|
||||
pub fn next_service(&mut self) {
|
||||
if !self.filtered_services.is_empty() {
|
||||
self.selected_service = (self.selected_service + 1) % self.filtered_services.len();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_service(&mut self) {
|
||||
if !self.filtered_services.is_empty() {
|
||||
if self.selected_service == 0 {
|
||||
self.selected_service = self.filtered_services.len() - 1;
|
||||
} else {
|
||||
self.selected_service -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_service_menu(&mut self) {
|
||||
if !self.filtered_services.is_empty() && self.selected_service < self.filtered_services.len() {
|
||||
self.show_service_menu = !self.show_service_menu;
|
||||
if self.show_service_menu {
|
||||
self.context_menu_service = Some(self.filtered_services[self.selected_service].name.clone());
|
||||
} else {
|
||||
self.context_menu_service = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service management methods
|
||||
pub fn start_service(&mut self) -> Result<()> {
|
||||
if let Some(ref service_name) = self.context_menu_service {
|
||||
self.service_manager.start_service(service_name)?;
|
||||
self.show_service_menu = false;
|
||||
self.context_menu_service = None;
|
||||
|
||||
// Refresh service list
|
||||
if let Ok(services) = self.service_manager.list_services() {
|
||||
self.services = services;
|
||||
self.filtered_services = self.services.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_service(&mut self) -> Result<()> {
|
||||
if let Some(ref service_name) = self.context_menu_service {
|
||||
self.service_manager.stop_service(service_name)?;
|
||||
self.show_service_menu = false;
|
||||
self.context_menu_service = None;
|
||||
|
||||
// Refresh service list
|
||||
if let Ok(services) = self.service_manager.list_services() {
|
||||
self.services = services;
|
||||
self.filtered_services = self.services.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restart_service(&mut self) -> Result<()> {
|
||||
if let Some(ref service_name) = self.context_menu_service {
|
||||
self.service_manager.restart_service(service_name)?;
|
||||
self.show_service_menu = false;
|
||||
self.context_menu_service = None;
|
||||
|
||||
// Refresh service list
|
||||
if let Ok(services) = self.service_manager.list_services() {
|
||||
self.services = services;
|
||||
self.filtered_services = self.services.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_service(&mut self) -> Result<()> {
|
||||
if let Some(ref service_name) = self.context_menu_service {
|
||||
self.service_manager.enable_service(service_name)?;
|
||||
self.show_service_menu = false;
|
||||
self.context_menu_service = None;
|
||||
|
||||
// Refresh service list
|
||||
if let Ok(services) = self.service_manager.list_services() {
|
||||
self.services = services;
|
||||
self.filtered_services = self.services.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_service(&mut self) -> Result<()> {
|
||||
if let Some(ref service_name) = self.context_menu_service {
|
||||
self.service_manager.disable_service(service_name)?;
|
||||
self.show_service_menu = false;
|
||||
self.context_menu_service = None;
|
||||
|
||||
// Refresh service list
|
||||
if let Ok(services) = self.service_manager.list_services() {
|
||||
self.services = services;
|
||||
self.filtered_services = self.services.clone();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
212
procmon-tui/src/main.rs
Normal file
212
procmon-tui/src/main.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
mod app;
|
||||
mod ui;
|
||||
|
||||
use anyhow::Result;
|
||||
use app::App;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
Terminal,
|
||||
};
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Setup logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Create app
|
||||
let mut app = App::new().await?;
|
||||
|
||||
// Run app
|
||||
let res = run_app(&mut terminal, &mut app).await;
|
||||
|
||||
// Restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
eprintln!("Error: {:?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_app<B: ratatui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
app: &mut App,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, app))?;
|
||||
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
// Handle search mode separately
|
||||
if app.search_mode {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.add_search_char(c),
|
||||
KeyCode::Backspace => app.remove_search_char(),
|
||||
KeyCode::Esc => app.toggle_search_mode(),
|
||||
KeyCode::Enter => app.toggle_search_mode(),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Char('/') => app.toggle_search_mode(),
|
||||
KeyCode::Up => {
|
||||
if app.current_tab == app::Tab::Partitions {
|
||||
app.previous_partition();
|
||||
} else if app.current_tab == app::Tab::Services {
|
||||
app.previous_service();
|
||||
} else {
|
||||
app.previous_process();
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if app.current_tab == app::Tab::Partitions {
|
||||
app.next_partition();
|
||||
} else if app.current_tab == app::Tab::Services {
|
||||
app.next_service();
|
||||
} else {
|
||||
app.next_process();
|
||||
}
|
||||
}
|
||||
KeyCode::PageUp => app.scroll_up(10),
|
||||
KeyCode::PageDown => app.scroll_down(10, 20),
|
||||
KeyCode::Left if app.current_tab == app::Tab::Partitions => {
|
||||
app.previous_disk();
|
||||
}
|
||||
KeyCode::Right if app.current_tab == app::Tab::Partitions => {
|
||||
app.next_disk();
|
||||
}
|
||||
KeyCode::Tab => app.next_tab(),
|
||||
KeyCode::BackTab => app.previous_tab(),
|
||||
KeyCode::Char('1') => app.set_tab(0),
|
||||
KeyCode::Char('2') => app.set_tab(1),
|
||||
KeyCode::Char('3') => app.set_tab(2),
|
||||
KeyCode::Char('4') => app.set_tab(3),
|
||||
KeyCode::Char('5') => app.set_tab(4),
|
||||
KeyCode::Char('6') => app.set_tab(5),
|
||||
KeyCode::Char('7') => app.set_tab(6),
|
||||
KeyCode::Char('a') => app.toggle_sort_ascending(),
|
||||
KeyCode::Char('s') => app.next_sort_column(),
|
||||
KeyCode::Char('f') => app.toggle_filter(),
|
||||
KeyCode::Char('m') | KeyCode::Enter => {
|
||||
if app.current_tab == app::Tab::Partitions {
|
||||
app.toggle_partition_menu();
|
||||
} else if app.current_tab == app::Tab::Services {
|
||||
app.toggle_service_menu();
|
||||
} else {
|
||||
app.toggle_context_menu();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') if app.current_tab == app::Tab::Partitions => {
|
||||
app.refresh_disks();
|
||||
}
|
||||
KeyCode::Char('d') if app.show_partition_menu => {
|
||||
let _ = app.delete_selected_partition();
|
||||
app.show_partition_menu = false;
|
||||
}
|
||||
KeyCode::Char('c') if app.show_partition_menu => {
|
||||
let _ = app.check_selected_partition();
|
||||
app.show_partition_menu = false;
|
||||
}
|
||||
KeyCode::Char('e') if app.show_partition_menu => {
|
||||
let _ = app.format_selected_partition("ext4");
|
||||
app.show_partition_menu = false;
|
||||
}
|
||||
KeyCode::Char('x') if app.show_partition_menu => {
|
||||
let _ = app.format_selected_partition("xfs");
|
||||
app.show_partition_menu = false;
|
||||
}
|
||||
KeyCode::Char('b') if app.show_partition_menu => {
|
||||
let _ = app.format_selected_partition("btrfs");
|
||||
app.show_partition_menu = false;
|
||||
}
|
||||
KeyCode::Char('n') if app.show_partition_menu => {
|
||||
let _ = app.format_selected_partition("ntfs");
|
||||
app.show_partition_menu = false;
|
||||
}
|
||||
KeyCode::Char('k') if app.show_context_menu => {
|
||||
let _ = app.kill_process();
|
||||
}
|
||||
KeyCode::Char('t') if app.show_context_menu => {
|
||||
let _ = app.kill_process_tree();
|
||||
}
|
||||
KeyCode::Char('o') if app.show_context_menu => {
|
||||
let _ = app.open_process_folder();
|
||||
}
|
||||
KeyCode::Char('r') if app.show_context_menu => {
|
||||
let _ = app.restart_process();
|
||||
}
|
||||
// Service menu actions
|
||||
KeyCode::Char('s') if app.show_service_menu => {
|
||||
let _ = app.start_service();
|
||||
}
|
||||
KeyCode::Char('p') if app.show_service_menu => {
|
||||
let _ = app.stop_service();
|
||||
}
|
||||
KeyCode::Char('r') if app.show_service_menu => {
|
||||
let _ = app.restart_service();
|
||||
}
|
||||
KeyCode::Char('e') if app.show_service_menu => {
|
||||
let _ = app.enable_service();
|
||||
}
|
||||
KeyCode::Char('d') if app.show_service_menu => {
|
||||
let _ = app.disable_service();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
if app.show_context_menu {
|
||||
app.show_context_menu = false;
|
||||
app.context_menu_pid = None;
|
||||
} else if app.show_service_menu {
|
||||
app.show_service_menu = false;
|
||||
app.context_menu_service = None;
|
||||
} else if app.search_mode {
|
||||
app.toggle_search_mode();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
match mouse.kind {
|
||||
MouseEventKind::ScrollDown => app.scroll_down(3, 20),
|
||||
MouseEventKind::ScrollUp => app.scroll_up(3),
|
||||
MouseEventKind::Down(_button) => {
|
||||
// Handle mouse click
|
||||
app.handle_mouse_click(mouse.column, mouse.row);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
app.update().await?;
|
||||
}
|
||||
}
|
||||
769
procmon-tui/src/ui.rs
Normal file
769
procmon-tui/src/ui.rs
Normal file
@@ -0,0 +1,769 @@
|
||||
use crate::app::{App, SortColumn, Tab};
|
||||
use procmon_core::detector::Severity;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{
|
||||
Bar, BarChart, BarGroup, Block, Borders, Cell, Gauge, List, ListItem, Paragraph, Row,
|
||||
Table, Tabs,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
draw_tabs(f, app, chunks[0]);
|
||||
draw_main_content(f, app, chunks[1]);
|
||||
draw_footer(f, app, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_tabs(f: &mut Frame, app: &App, area: Rect) {
|
||||
let titles = vec![
|
||||
"Dashboard (1)",
|
||||
"Processes (2)",
|
||||
"Services (3)",
|
||||
"Storage (4)",
|
||||
"Network (5)",
|
||||
"Partitions (6)",
|
||||
"Alerts (7)"
|
||||
];
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Process Monitor with Partition Manager"))
|
||||
.select(app.get_tab_index())
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
f.render_widget(tabs, area);
|
||||
}
|
||||
|
||||
fn draw_main_content(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
match app.current_tab {
|
||||
Tab::Dashboard => draw_dashboard(f, app, area),
|
||||
Tab::Processes => draw_processes(f, app, area),
|
||||
Tab::Services => draw_services(f, app, area),
|
||||
Tab::Storage => draw_storage(f, app, area),
|
||||
Tab::Network => draw_network(f, app, area),
|
||||
Tab::Partitions => draw_partitions(f, app, area),
|
||||
Tab::Alerts => draw_alerts(f, app, area),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_dashboard(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(7),
|
||||
Constraint::Length(10),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
draw_system_overview(f, app, chunks[0]);
|
||||
draw_cpu_cores(f, app, chunks[1]);
|
||||
draw_top_processes(f, app, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_system_overview(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
// CPU Usage
|
||||
let cpu_gauge = Gauge::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("CPU Usage"))
|
||||
.gauge_style(Style::default().fg(get_usage_color(app.system_metrics.cpu.total_usage)))
|
||||
.percent(app.system_metrics.cpu.total_usage as u16)
|
||||
.label(format!("{:.1}%", app.system_metrics.cpu.total_usage));
|
||||
f.render_widget(cpu_gauge, chunks[0]);
|
||||
|
||||
// Memory Usage
|
||||
let mem_percent = (app.system_metrics.memory.used as f64 / app.system_metrics.memory.total as f64 * 100.0) as u16;
|
||||
let mem_gauge = Gauge::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Memory"))
|
||||
.gauge_style(Style::default().fg(get_usage_color(mem_percent as f32)))
|
||||
.percent(mem_percent)
|
||||
.label(format!(
|
||||
"{:.1} / {:.1} GB",
|
||||
app.system_metrics.memory.used as f64 / (1024.0 * 1024.0 * 1024.0),
|
||||
app.system_metrics.memory.total as f64 / (1024.0 * 1024.0 * 1024.0)
|
||||
));
|
||||
f.render_widget(mem_gauge, chunks[1]);
|
||||
|
||||
// CPU Temperature
|
||||
let temp_text = if let Some(temp) = app.system_metrics.cpu.temperature {
|
||||
format!("{:.1}°C", temp)
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
};
|
||||
let temp_color = app.system_metrics.cpu.temperature
|
||||
.map(|t| {
|
||||
if t > 80.0 {
|
||||
Color::Red
|
||||
} else if t > 60.0 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Green
|
||||
}
|
||||
})
|
||||
.unwrap_or(Color::Gray);
|
||||
let temp_para = Paragraph::new(temp_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("CPU Temp"))
|
||||
.style(Style::default().fg(temp_color))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(temp_para, chunks[2]);
|
||||
|
||||
// GPU Info
|
||||
let gpu_text = if let Some(gpu) = app.system_metrics.gpus.first() {
|
||||
format!("{}\n{:.1}%", gpu.name, gpu.usage)
|
||||
} else {
|
||||
"No GPU\nDetected".to_string()
|
||||
};
|
||||
let gpu_para = Paragraph::new(gpu_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("GPU"))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(gpu_para, chunks[3]);
|
||||
}
|
||||
|
||||
fn draw_cpu_cores(f: &mut Frame, app: &App, area: Rect) {
|
||||
let core_data: Vec<(&str, u64)> = app.system_metrics.cpu.per_core_usage
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, usage)| {
|
||||
(Box::leak(format!("{}", i).into_boxed_str()) as &str, *usage as u64)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let bars: Vec<Bar> = core_data
|
||||
.iter()
|
||||
.map(|(label, value)| {
|
||||
Bar::default()
|
||||
.value(*value)
|
||||
.label(Line::from(*label))
|
||||
.style(Style::default().fg(get_usage_color(*value as f32)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let chart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("CPU Cores"))
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.bar_width(3)
|
||||
.bar_gap(1);
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn draw_top_processes(f: &mut Frame, app: &App, area: Rect) {
|
||||
let mut processes = app.processes.clone();
|
||||
processes.sort_by(|a, b| b.stats.cpu_usage.partial_cmp(&a.stats.cpu_usage).unwrap());
|
||||
processes.truncate(10);
|
||||
|
||||
let rows: Vec<Row> = processes
|
||||
.iter()
|
||||
.map(|p| {
|
||||
Row::new(vec![
|
||||
Cell::from(p.info.pid.to_string()),
|
||||
Cell::from(p.info.name.clone()),
|
||||
Cell::from(p.info.user.clone()),
|
||||
Cell::from(format!("{:.1}%", p.stats.cpu_usage)),
|
||||
Cell::from(format!("{:.1} MB", p.stats.memory_usage as f64 / (1024.0 * 1024.0))),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(8),
|
||||
Constraint::Min(20),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(12),
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["PID", "Name", "User", "CPU", "Memory"])
|
||||
.style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL).title("Top Processes by CPU"));
|
||||
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
|
||||
fn draw_processes(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
use ratatui::widgets::TableState;
|
||||
|
||||
// Split area for search bar if needed
|
||||
let (main_area, search_area) = if app.search_mode {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(3)])
|
||||
.split(area);
|
||||
(chunks[0], Some(chunks[1]))
|
||||
} else {
|
||||
(area, None)
|
||||
};
|
||||
|
||||
// Store the area for mouse click handling
|
||||
app.set_process_list_area(main_area.x, main_area.y, main_area.width, main_area.height);
|
||||
|
||||
let sort_indicator = if app.sort_ascending { "↑" } else { "↓" };
|
||||
let sort_column_name = match app.sort_column {
|
||||
SortColumn::Name => "Name",
|
||||
SortColumn::Cpu => "CPU",
|
||||
SortColumn::Memory => "Memory",
|
||||
SortColumn::DiskIo => "Disk I/O",
|
||||
SortColumn::User => "User",
|
||||
};
|
||||
|
||||
let filtered_procs = app.get_filtered_processes();
|
||||
|
||||
let rows: Vec<Row> = filtered_procs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
Row::new(vec![
|
||||
Cell::from(p.info.pid.to_string()),
|
||||
Cell::from(p.info.name.clone()),
|
||||
Cell::from(p.info.user.clone()),
|
||||
Cell::from(format!("{:.1}%", p.stats.cpu_usage)),
|
||||
Cell::from(format!("{:.1}", p.stats.memory_usage as f64 / (1024.0 * 1024.0))),
|
||||
Cell::from(format!("{:.1}", (p.stats.disk_read_bytes + p.stats.disk_write_bytes) as f64 / (1024.0 * 1024.0))),
|
||||
Cell::from(format!("{:?}", p.info.status)),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let title = if app.search_mode {
|
||||
format!("Processes ({}) - Search Mode Active", filtered_procs.len())
|
||||
} else {
|
||||
format!("Processes ({}) - Sort: {} {} - ↑↓: Select, Enter: Menu, /: Search",
|
||||
filtered_procs.len(), sort_column_name, sort_indicator)
|
||||
};
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(8),
|
||||
Constraint::Min(20),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(10),
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["PID", "Name", "User", "CPU %", "Mem (MB)", "Disk (MB)", "Status"])
|
||||
.style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
)
|
||||
.row_highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::Blue)
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// Create table state and set selected
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(app.selected_process));
|
||||
|
||||
f.render_stateful_widget(table, main_area, &mut table_state);
|
||||
|
||||
// Draw search bar if in search mode
|
||||
if let Some(search_area) = search_area {
|
||||
let search_text = format!("Search: {}", app.search_query);
|
||||
let search_bar = Paragraph::new(search_text)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.block(Block::default().borders(Borders::ALL).title("Search (ESC to exit)"));
|
||||
f.render_widget(search_bar, search_area);
|
||||
}
|
||||
|
||||
// Draw context menu if active
|
||||
if app.show_context_menu {
|
||||
draw_context_menu(f, app);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_context_menu(f: &mut Frame, app: &App) {
|
||||
// Create a centered popup
|
||||
let area = f.area();
|
||||
let popup_width = 40;
|
||||
let popup_height = 8;
|
||||
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
|
||||
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
|
||||
|
||||
let popup_area = Rect {
|
||||
x: popup_x,
|
||||
y: popup_y,
|
||||
width: popup_width,
|
||||
height: popup_height,
|
||||
};
|
||||
|
||||
// Get selected process info from filtered processes
|
||||
let filtered_procs = app.get_filtered_processes();
|
||||
let process_info = if !filtered_procs.is_empty() && app.selected_process < filtered_procs.len() {
|
||||
let p = &filtered_procs[app.selected_process];
|
||||
format!("{} (PID: {})", p.info.name, p.info.pid)
|
||||
} else {
|
||||
"No process selected".to_string()
|
||||
};
|
||||
|
||||
let menu_items = vec![
|
||||
Line::from(Span::styled(process_info, Style::default().add_modifier(Modifier::BOLD))),
|
||||
Line::from(""),
|
||||
Line::from(Span::raw("k - Kill process")),
|
||||
Line::from(Span::raw("t - Kill process tree")),
|
||||
Line::from(Span::raw("o - Open process folder")),
|
||||
Line::from(Span::raw("r - Restart process")),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled("ESC - Close menu", Style::default().fg(Color::Gray))),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(menu_items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title("Process Actions")
|
||||
.style(Style::default().bg(Color::Black))
|
||||
)
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
f.render_widget(paragraph, popup_area);
|
||||
}
|
||||
|
||||
fn draw_services(f: &mut Frame, app: &App, area: Rect) {
|
||||
use ratatui::widgets::TableState;
|
||||
use procmon_core::ServiceState;
|
||||
|
||||
let services = &app.filtered_services;
|
||||
|
||||
let rows: Vec<Row> = services
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let state_style = match s.state {
|
||||
ServiceState::Running => Style::default().fg(Color::Green),
|
||||
ServiceState::Stopped => Style::default().fg(Color::Gray),
|
||||
ServiceState::Failed => Style::default().fg(Color::Red),
|
||||
ServiceState::Unknown => Style::default().fg(Color::Yellow),
|
||||
};
|
||||
|
||||
let state_str = format!("{:?}", s.state);
|
||||
let enabled_str = if s.enabled { "enabled" } else { "disabled" };
|
||||
|
||||
let mem_str = if let Some(mem) = s.memory_usage {
|
||||
format!("{:.1} MB", mem as f64 / (1024.0 * 1024.0))
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
let pid_str = if let Some(pid) = s.main_pid {
|
||||
pid.to_string()
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(s.name.clone()),
|
||||
Cell::from(state_str).style(state_style),
|
||||
Cell::from(s.sub_state.clone()),
|
||||
Cell::from(enabled_str),
|
||||
Cell::from(pid_str),
|
||||
Cell::from(mem_str),
|
||||
Cell::from(s.description.clone()),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let title = format!("Services ({}) - ↑↓: Select, Enter: Menu", services.len());
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(25),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(8),
|
||||
Constraint::Length(12),
|
||||
Constraint::Min(30),
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["Name", "State", "Sub State", "Enabled", "PID", "Memory", "Description"])
|
||||
.style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
)
|
||||
.row_highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::Blue)
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// Create table state and set selected
|
||||
let mut table_state = TableState::default();
|
||||
table_state.select(Some(app.selected_service));
|
||||
|
||||
f.render_stateful_widget(table, area, &mut table_state);
|
||||
|
||||
// Draw service menu if active
|
||||
if app.show_service_menu {
|
||||
draw_service_menu(f, app);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_service_menu(f: &mut Frame, app: &App) {
|
||||
// Create a centered popup
|
||||
let area = f.area();
|
||||
let popup_width = 40;
|
||||
let popup_height = 10;
|
||||
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
|
||||
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
|
||||
|
||||
let popup_area = Rect {
|
||||
x: popup_x,
|
||||
y: popup_y,
|
||||
width: popup_width,
|
||||
height: popup_height,
|
||||
};
|
||||
|
||||
// Get selected service info
|
||||
let service_info = if !app.filtered_services.is_empty() && app.selected_service < app.filtered_services.len() {
|
||||
let s = &app.filtered_services[app.selected_service];
|
||||
format!("{} ({:?})", s.name, s.state)
|
||||
} else {
|
||||
"No service selected".to_string()
|
||||
};
|
||||
|
||||
let menu_items = vec![
|
||||
Line::from(Span::styled(service_info, Style::default().add_modifier(Modifier::BOLD))),
|
||||
Line::from(""),
|
||||
Line::from(Span::raw("s - Start service")),
|
||||
Line::from(Span::raw("p - Stop service")),
|
||||
Line::from(Span::raw("r - Restart service")),
|
||||
Line::from(Span::raw("e - Enable service")),
|
||||
Line::from(Span::raw("d - Disable service")),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled("ESC - Close menu", Style::default().fg(Color::Gray))),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(menu_items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title("Service Actions")
|
||||
.style(Style::default().bg(Color::Black))
|
||||
)
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
f.render_widget(paragraph, popup_area);
|
||||
}
|
||||
|
||||
fn draw_storage(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
|
||||
.split(area);
|
||||
|
||||
// Disk I/O summary
|
||||
let disk_items: Vec<ListItem> = app
|
||||
.system_metrics
|
||||
.disk_io
|
||||
.iter()
|
||||
.map(|(name, metrics)| {
|
||||
let content = format!(
|
||||
"{}: Read: {:.2} MB ({} ops) Write: {:.2} MB ({} ops)",
|
||||
name,
|
||||
metrics.read_bytes as f64 / (1024.0 * 1024.0),
|
||||
metrics.read_ops,
|
||||
metrics.write_bytes as f64 / (1024.0 * 1024.0),
|
||||
metrics.write_ops
|
||||
);
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let disk_list = List::new(disk_items)
|
||||
.block(Block::default().borders(Borders::ALL).title("Disk I/O"));
|
||||
f.render_widget(disk_list, chunks[0]);
|
||||
|
||||
// Top processes by disk I/O
|
||||
let mut processes = app.processes.clone();
|
||||
processes.sort_by(|a, b| {
|
||||
let a_io = a.stats.disk_read_bytes + a.stats.disk_write_bytes;
|
||||
let b_io = b.stats.disk_read_bytes + b.stats.disk_write_bytes;
|
||||
b_io.cmp(&a_io)
|
||||
});
|
||||
processes.truncate(20);
|
||||
|
||||
let rows: Vec<Row> = processes
|
||||
.iter()
|
||||
.map(|p| {
|
||||
Row::new(vec![
|
||||
Cell::from(p.info.pid.to_string()),
|
||||
Cell::from(p.info.name.clone()),
|
||||
Cell::from(format!("{:.2}", p.stats.disk_read_bytes as f64 / (1024.0 * 1024.0))),
|
||||
Cell::from(format!("{:.2}", p.stats.disk_write_bytes as f64 / (1024.0 * 1024.0))),
|
||||
Cell::from(format!("{:.2}", (p.stats.disk_read_bytes + p.stats.disk_write_bytes) as f64 / (1024.0 * 1024.0))),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(8),
|
||||
Constraint::Min(20),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["PID", "Name", "Read (MB)", "Write (MB)", "Total (MB)"])
|
||||
.style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().borders(Borders::ALL).title("Processes by Disk I/O"));
|
||||
|
||||
f.render_widget(table, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_network(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area);
|
||||
|
||||
// Network interfaces
|
||||
let net_items: Vec<ListItem> = app
|
||||
.system_metrics
|
||||
.network
|
||||
.iter()
|
||||
.map(|(name, metrics)| {
|
||||
let content = format!(
|
||||
"{}: ↓ {:.2} MB ↑ {:.2} MB (Packets: ↓ {} ↑ {})",
|
||||
name,
|
||||
metrics.bytes_received as f64 / (1024.0 * 1024.0),
|
||||
metrics.bytes_sent as f64 / (1024.0 * 1024.0),
|
||||
metrics.packets_received,
|
||||
metrics.packets_sent
|
||||
);
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let net_list = List::new(net_items)
|
||||
.block(Block::default().borders(Borders::ALL).title("Network Interfaces"));
|
||||
f.render_widget(net_list, chunks[0]);
|
||||
|
||||
// Top processes by network (placeholder - we don't have per-process network stats yet)
|
||||
let text = Paragraph::new("Per-process network statistics not yet available.\nThis will show processes sorted by network usage.")
|
||||
.block(Block::default().borders(Borders::ALL).title("Processes by Network Usage"))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(text, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_alerts(f: &mut Frame, app: &App, area: Rect) {
|
||||
let alert_items: Vec<ListItem> = app
|
||||
.alerts
|
||||
.iter()
|
||||
.rev()
|
||||
.take(50)
|
||||
.map(|alert| {
|
||||
let severity_color = match alert.severity {
|
||||
Severity::Critical => Color::Red,
|
||||
Severity::Warning => Color::Yellow,
|
||||
Severity::Info => Color::Blue,
|
||||
};
|
||||
|
||||
let content = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
format!("[{:?}] ", alert.severity),
|
||||
Style::default().fg(severity_color).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
"{} - {} (PID: {})",
|
||||
alert.timestamp.format("%H:%M:%S"),
|
||||
alert.process_name,
|
||||
alert.pid
|
||||
)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::raw(&alert.rule_name),
|
||||
Span::raw(": "),
|
||||
Span::raw(&alert.details),
|
||||
]),
|
||||
];
|
||||
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let alert_list = List::new(alert_items).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(format!("Alerts ({} total)", app.alerts.len())),
|
||||
);
|
||||
|
||||
f.render_widget(alert_list, area);
|
||||
}
|
||||
|
||||
fn draw_partitions(f: &mut Frame, app: &App, area: Rect) {
|
||||
if app.disks.is_empty() {
|
||||
let text = Paragraph::new("No disks found or permission denied.\nRun with sudo for full partition management capabilities.")
|
||||
.block(Block::default().borders(Borders::ALL).title("Partition Manager"))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(text, area);
|
||||
return;
|
||||
}
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
|
||||
.split(area);
|
||||
|
||||
// Disk list
|
||||
let disk_items: Vec<ListItem> = app
|
||||
.disks
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
let size_gb = disk.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
let content = format!(
|
||||
"{} - {} ({:.2} GB) - {} partitions",
|
||||
disk.device,
|
||||
disk.model,
|
||||
size_gb,
|
||||
disk.partitions.len()
|
||||
);
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let disk_list = List::new(disk_items)
|
||||
.block(Block::default().borders(Borders::ALL).title("Disks (Select with ↑↓)"));
|
||||
f.render_widget(disk_list, chunks[0]);
|
||||
|
||||
// Partition table for selected disk
|
||||
if app.selected_disk < app.disks.len() {
|
||||
let disk = &app.disks[app.selected_disk];
|
||||
|
||||
if disk.partitions.is_empty() {
|
||||
let text = Paragraph::new(format!("No partitions on {}\n\nUse gparted or parted to create partitions.", disk.device))
|
||||
.block(Block::default().borders(Borders::ALL).title(format!("Partitions on {}", disk.device)))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(text, chunks[1]);
|
||||
} else {
|
||||
let rows: Vec<Row> = disk
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let size_gb = p.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
let used_gb = p.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
let used_percent = if p.size_bytes > 0 {
|
||||
(p.used_bytes as f64 / p.size_bytes as f64 * 100.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(p.device.clone()),
|
||||
Cell::from(p.filesystem.clone().unwrap_or_else(|| "unknown".to_string())),
|
||||
Cell::from(p.label.clone().unwrap_or_else(|| "-".to_string())),
|
||||
Cell::from(format!("{:.2}", size_gb)),
|
||||
Cell::from(format!("{:.2} ({:.1}%)", used_gb, used_percent)),
|
||||
Cell::from(p.mount_point.clone().unwrap_or_else(|| "-".to_string())),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(18),
|
||||
Constraint::Min(20),
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["Device", "Filesystem", "Label", "Size (GB)", "Used (GB)", "Mount Point"])
|
||||
.style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(format!("Partitions on {} - {} ({:.2} GB)",
|
||||
disk.device,
|
||||
disk.model,
|
||||
disk.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
|
||||
))
|
||||
);
|
||||
|
||||
f.render_widget(table, chunks[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_footer(f: &mut Frame, app: &App, area: Rect) {
|
||||
let text = if app.search_mode {
|
||||
"Search Mode: Type to search, Backspace to delete, Enter/ESC to exit"
|
||||
} else {
|
||||
"q: Quit | Tab: Next Tab | 1-7: Switch Tabs | ↑↓: Navigate | /: Search | s: Sort | a: Order | m: Menu | PgUp/PgDn: Scroll | Mouse Wheel: Scroll"
|
||||
};
|
||||
let footer = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(footer, area);
|
||||
}
|
||||
|
||||
fn get_usage_color(usage: f32) -> Color {
|
||||
if usage > 80.0 {
|
||||
Color::Red
|
||||
} else if usage > 60.0 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Green
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user