first commit

This commit is contained in:
DigiJ
2026-03-13 13:59:56 -07:00
commit 2a4867296e
22 changed files with 5373 additions and 0 deletions

23
procmon-core/Cargo.toml Normal file
View 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"] }

View 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()
}
}

View 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
View 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
View 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
View 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()
}
}

View 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()
}
}

View 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
View 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
View 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);
}
}
}
}