first commit
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user