问题: - SystemStatsResponse 在 bin/proxy.rs 中定义 - 库模块 proxy.rs 和 validate.rs 试图从 super 导入,但路径错误 修复: - 将 SystemStatsResponse 定义移到库模块 proxy.rs - 在 validate.rs 中添加正确的导入 - 移除 bin/proxy.rs 中的重复定义(稍后处理)
449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::fs::{self, OpenOptions};
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use super::proxy::SystemStatsResponse;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ValidationStatus {
|
|
Pass,
|
|
Warn,
|
|
Fail,
|
|
Unverified,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MetricValidation {
|
|
pub macmon_value: f64,
|
|
pub reference_value: f64,
|
|
pub diff_pct: f64,
|
|
pub status: ValidationStatus,
|
|
pub tolerance: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LayerValidation {
|
|
pub name: String,
|
|
pub requires_sudo: bool,
|
|
pub metrics: std::collections::HashMap<String, MetricValidation>,
|
|
pub summary: ValidationSummary,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationSummary {
|
|
pub total: u32,
|
|
pub passed: u32,
|
|
pub failed: u32,
|
|
pub unverified: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationResult {
|
|
pub timestamp: String,
|
|
pub layers: std::collections::HashMap<String, Option<LayerValidation>>,
|
|
pub overall_summary: ValidationSummary,
|
|
}
|
|
|
|
impl ValidationStatus {
|
|
fn from_diff(diff_pct: f64, tolerance: f64) -> Self {
|
|
if diff_pct <= tolerance {
|
|
ValidationStatus::Pass
|
|
} else if diff_pct <= tolerance * 2.0 {
|
|
ValidationStatus::Warn
|
|
} else {
|
|
ValidationStatus::Fail
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_timestamp() -> String {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
let dt = chrono::DateTime::from_timestamp(now as i64, 0).unwrap();
|
|
dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()
|
|
}
|
|
|
|
fn write_log(message: &str) {
|
|
let log_dir = std::env::var("MOMENTRY_DATA_DIR")
|
|
.unwrap_or_else(|_| ".".to_string());
|
|
let log_path = Path::new(&log_dir).join("logs").join("validation.log");
|
|
|
|
if let Some(parent) = log_path.parent() {
|
|
let _ = fs::create_dir_all(parent);
|
|
}
|
|
|
|
let timestamp = get_timestamp();
|
|
let log_line = format!("{} {}\n", timestamp, message);
|
|
|
|
if let Ok(mut file) = OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&log_path)
|
|
{
|
|
let _ = file.write_all(log_line.as_bytes());
|
|
}
|
|
}
|
|
|
|
// ==================== Layer 1: Activity Monitor Validation ====================
|
|
|
|
// CPU formula decision (2026-08-31):
|
|
// - macmon uses `cpu_scaled_ratio` (frequency-weighted: E-core work counts less due to lower freq)
|
|
// - `top` uses simple user+sys ratio (no frequency weighting)
|
|
// - These are intentionally different formulas; macmon's approach is more accurate for "actual work done"
|
|
// - Therefore, CPU validation is marked as Unverified (different formula, not comparable)
|
|
pub fn validate_cpu_usage_scaled_ratio() -> Option<MetricValidation> {
|
|
write_log("[INFO] Layer 1: CPU uses cpu_scaled_ratio (frequency-weighted) - not comparable to top");
|
|
|
|
let macmon_value = 0.0; // Will be filled by caller
|
|
|
|
write_log(&format!(
|
|
"[INFO] Layer 1 CPU: macmon cpu_scaled_ratio={:.1}% (frequency-weighted formula)",
|
|
macmon_value
|
|
));
|
|
|
|
Some(MetricValidation {
|
|
macmon_value,
|
|
reference_value: 0.0,
|
|
diff_pct: 0.0,
|
|
status: ValidationStatus::Unverified,
|
|
tolerance: 0.0,
|
|
})
|
|
}
|
|
|
|
// Memory formula decision (2026-08-31):
|
|
// - macmon uses its own memory calculation (ram_usage from crate)
|
|
// - Activity Monitor uses: wired + active + inactive + speculative - purgeable
|
|
// - These formulas are intentionally different; macmon's approach is chosen as the baseline
|
|
// - Therefore, Memory Used validation is marked as Unverified (different formula, not comparable)
|
|
pub fn validate_memory_used_macmon_formula() -> Option<MetricValidation> {
|
|
write_log("[INFO] Layer 1: Memory uses macmon formula - not comparable to Activity Monitor");
|
|
|
|
let macmon_value = 0.0; // Will be filled by caller
|
|
|
|
write_log(&format!(
|
|
"[INFO] Layer 1 Memory Used: macmon={:.1}% (macmon formula)",
|
|
macmon_value
|
|
));
|
|
|
|
Some(MetricValidation {
|
|
macmon_value,
|
|
reference_value: 0.0,
|
|
diff_pct: 0.0,
|
|
status: ValidationStatus::Unverified,
|
|
tolerance: 0.0,
|
|
})
|
|
}
|
|
|
|
pub fn validate_swap_via_sysctl() -> Option<MetricValidation> {
|
|
write_log("[INFO] Layer 1: Validating Swap via sysctl");
|
|
|
|
let output = Command::new("sysctl")
|
|
.args(["-n", "vm.swapusage"])
|
|
.output()
|
|
.ok()?;
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
// Parse: total = 9216.00M used = 8420.00M free = 796.00M (encrypted)
|
|
let parts: Vec<&str> = stdout.split_whitespace().collect();
|
|
|
|
let total_mb: f64 = parts.get(2)
|
|
.and_then(|v| v.trim_end_matches('M').parse().ok())
|
|
.unwrap_or(0.0);
|
|
let used_mb: f64 = parts.get(5)
|
|
.and_then(|v| v.trim_end_matches('M').parse().ok())
|
|
.unwrap_or(0.0);
|
|
|
|
let used_pct = if total_mb > 0.0 {
|
|
(used_mb / total_mb) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let macmon_value = 0.0; // Will be filled by caller
|
|
let tolerance = 1.0;
|
|
let diff_pct = (macmon_value - used_pct).abs();
|
|
|
|
write_log(&format!(
|
|
"[INFO] Layer 1 Swap: macmon={:.1}%, sysctl={:.1}%, diff={:.2}%",
|
|
macmon_value, used_pct, diff_pct
|
|
));
|
|
|
|
Some(MetricValidation {
|
|
macmon_value,
|
|
reference_value: used_pct,
|
|
diff_pct,
|
|
status: ValidationStatus::from_diff(diff_pct, tolerance),
|
|
tolerance,
|
|
})
|
|
}
|
|
|
|
pub fn validate_memory_total_via_sysctl() -> Option<MetricValidation> {
|
|
write_log("[INFO] Layer 1: Validating Memory Total via sysctl");
|
|
|
|
let output = Command::new("sysctl")
|
|
.args(["-n", "hw.memsize"])
|
|
.output()
|
|
.ok()?;
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let total_bytes: u64 = stdout.trim().parse().ok()?;
|
|
let total_mb = total_bytes as f64 / (1024.0 * 1024.0);
|
|
|
|
let macmon_value = 0.0; // Will be filled by caller
|
|
let tolerance = 0.0; // Exact match expected
|
|
let diff_pct = (macmon_value - total_mb).abs();
|
|
|
|
write_log(&format!(
|
|
"[INFO] Layer 1 Memory Total: macmon={:.0}MB, sysctl={:.0}MB, diff={:.0}MB",
|
|
macmon_value, total_mb, diff_pct
|
|
));
|
|
|
|
Some(MetricValidation {
|
|
macmon_value,
|
|
reference_value: total_mb,
|
|
diff_pct,
|
|
status: ValidationStatus::from_diff(diff_pct, 0.1), // Very small tolerance
|
|
tolerance: 0.1,
|
|
})
|
|
}
|
|
|
|
// ==================== Layer 2: System API Validation ====================
|
|
|
|
pub fn validate_cpu_temp_via_ioreg() -> Option<MetricValidation> {
|
|
write_log("[INFO] Layer 2: Validating CPU Temp via ioreg");
|
|
|
|
let output = Command::new("ioreg")
|
|
.args(["-r", "-n", "AppleSMC"])
|
|
.output()
|
|
.ok()?;
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
// Look for temperature values
|
|
let temp = stdout
|
|
.lines()
|
|
.find(|line| line.contains("temperature") || line.contains("TC0P") || line.contains("TC0H"))
|
|
.and_then(|line| {
|
|
// Extract numeric value
|
|
let num_str: String = line
|
|
.chars()
|
|
.filter(|c| c.is_numeric() || *c == '.' || *c == '-')
|
|
.collect();
|
|
num_str.parse::<f64>().ok()
|
|
})
|
|
.unwrap_or(0.0);
|
|
|
|
let macmon_value = 0.0; // Will be filled by caller
|
|
let tolerance = 1.0;
|
|
let diff_pct = (macmon_value - temp).abs();
|
|
|
|
write_log(&format!(
|
|
"[INFO] Layer 2 CPU Temp: macmon={:.1}°C, ioreg={:.1}°C, diff={:.1}°C",
|
|
macmon_value, temp, diff_pct
|
|
));
|
|
|
|
Some(MetricValidation {
|
|
macmon_value,
|
|
reference_value: temp,
|
|
diff_pct,
|
|
status: ValidationStatus::from_diff(diff_pct, tolerance),
|
|
tolerance,
|
|
})
|
|
}
|
|
|
|
pub fn validate_thermal_state_via_pmset() -> Option<MetricValidation> {
|
|
write_log("[INFO] Layer 2: Validating Thermal State via pmset");
|
|
|
|
let output = Command::new("pmset")
|
|
.args(["-g", "therm"])
|
|
.output()
|
|
.ok()?;
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
// Parse thermal state
|
|
let state = if stdout.contains("CPU Power Load") {
|
|
"Nominal"
|
|
} else if stdout.contains("Warning") {
|
|
"Warning"
|
|
} else {
|
|
"Nominal"
|
|
};
|
|
|
|
// For thermal state, we just log the result (it's a string comparison)
|
|
write_log(&format!("[INFO] Layer 2 Thermal State: {}", state));
|
|
|
|
None // Return None for string-based metrics
|
|
}
|
|
|
|
// ==================== Main Validation Function ====================
|
|
|
|
pub fn run_validation(layers: &[u32], macmon_stats: &SystemStatsResponse) -> ValidationResult {
|
|
write_log(&format!("[INFO] Validation started: layers={:?}", layers));
|
|
|
|
let mut result = ValidationResult {
|
|
timestamp: get_timestamp(),
|
|
layers: std::collections::HashMap::new(),
|
|
overall_summary: ValidationSummary {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
unverified: 0,
|
|
},
|
|
};
|
|
|
|
// Layer 1: Activity Monitor
|
|
if layers.contains(&1) {
|
|
let mut layer1 = LayerValidation {
|
|
name: "Activity Monitor".to_string(),
|
|
requires_sudo: false,
|
|
metrics: std::collections::HashMap::new(),
|
|
summary: ValidationSummary {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
unverified: 0,
|
|
},
|
|
};
|
|
|
|
// CPU - Unverified (different formula)
|
|
if let Some(mut val) = validate_cpu_usage_scaled_ratio() {
|
|
val.macmon_value = macmon_stats.cpu_used_percent;
|
|
layer1.metrics.insert("cpu_usage".to_string(), val);
|
|
}
|
|
|
|
// Memory Used - Unverified (different formula)
|
|
if let Some(mut val) = validate_memory_used_macmon_formula() {
|
|
val.macmon_value = macmon_stats.memory_used_percent;
|
|
layer1.metrics.insert("memory_used_percent".to_string(), val);
|
|
}
|
|
|
|
// Memory Total
|
|
if let Some(mut val) = validate_memory_total_via_sysctl() {
|
|
val.macmon_value = macmon_stats.memory_total_mb as f64;
|
|
val.diff_pct = (val.macmon_value - val.reference_value).abs();
|
|
val.status = ValidationStatus::from_diff(val.diff_pct, val.tolerance);
|
|
layer1.metrics.insert("memory_total_mb".to_string(), val);
|
|
}
|
|
|
|
// Swap
|
|
if macmon_stats.swap_total_mb > 0 {
|
|
if let Some(mut val) = validate_swap_via_sysctl() {
|
|
val.macmon_value = macmon_stats.swap_used_percent;
|
|
val.diff_pct = (val.macmon_value - val.reference_value).abs();
|
|
val.status = ValidationStatus::from_diff(val.diff_pct, val.tolerance);
|
|
layer1.metrics.insert("swap_used_percent".to_string(), val);
|
|
}
|
|
}
|
|
|
|
// Calculate summary
|
|
for (_, metric) in &layer1.metrics {
|
|
layer1.summary.total += 1;
|
|
result.overall_summary.total += 1;
|
|
match metric.status {
|
|
ValidationStatus::Pass => {
|
|
layer1.summary.passed += 1;
|
|
result.overall_summary.passed += 1;
|
|
}
|
|
ValidationStatus::Warn | ValidationStatus::Fail => {
|
|
layer1.summary.failed += 1;
|
|
result.overall_summary.failed += 1;
|
|
}
|
|
ValidationStatus::Unverified => {
|
|
layer1.summary.unverified += 1;
|
|
result.overall_summary.unverified += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
result.layers.insert("layer1".to_string(), Some(layer1));
|
|
}
|
|
|
|
// Layer 2: System API
|
|
if layers.contains(&2) {
|
|
let mut layer2 = LayerValidation {
|
|
name: "System API".to_string(),
|
|
requires_sudo: false,
|
|
metrics: std::collections::HashMap::new(),
|
|
summary: ValidationSummary {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
unverified: 0,
|
|
},
|
|
};
|
|
|
|
// CPU Temp
|
|
if let Some(temp) = macmon_stats.cpu_temp {
|
|
if let Some(mut val) = validate_cpu_temp_via_ioreg() {
|
|
val.macmon_value = temp;
|
|
val.diff_pct = (val.macmon_value - val.reference_value).abs();
|
|
val.status = ValidationStatus::from_diff(val.diff_pct, val.tolerance);
|
|
layer2.metrics.insert("cpu_temp".to_string(), val);
|
|
}
|
|
}
|
|
|
|
// Calculate summary
|
|
for (_, metric) in &layer2.metrics {
|
|
layer2.summary.total += 1;
|
|
result.overall_summary.total += 1;
|
|
match metric.status {
|
|
ValidationStatus::Pass => {
|
|
layer2.summary.passed += 1;
|
|
result.overall_summary.passed += 1;
|
|
}
|
|
ValidationStatus::Warn | ValidationStatus::Fail => {
|
|
layer2.summary.failed += 1;
|
|
result.overall_summary.failed += 1;
|
|
}
|
|
ValidationStatus::Unverified => {
|
|
layer2.summary.unverified += 1;
|
|
result.overall_summary.unverified += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
result.layers.insert("layer2".to_string(), Some(layer2));
|
|
}
|
|
|
|
// Layer 3: Requires sudo (not implemented in this phase)
|
|
if layers.contains(&3) {
|
|
result.layers.insert("layer3".to_string(), None);
|
|
write_log("[INFO] Layer 3 validation not implemented yet");
|
|
}
|
|
|
|
// Layer 4: Unverified
|
|
if layers.contains(&4) {
|
|
let layer4 = LayerValidation {
|
|
name: "Unverified".to_string(),
|
|
requires_sudo: false,
|
|
metrics: std::collections::HashMap::new(),
|
|
summary: ValidationSummary {
|
|
total: 3,
|
|
passed: 0,
|
|
failed: 0,
|
|
unverified: 3,
|
|
},
|
|
};
|
|
|
|
result.layers.insert("layer4".to_string(), Some(layer4));
|
|
result.overall_summary.total += 3;
|
|
result.overall_summary.unverified += 3;
|
|
}
|
|
|
|
write_log(&format!(
|
|
"[INFO] Validation completed: {}/{} passed, {} failed, {} unverified",
|
|
result.overall_summary.passed,
|
|
result.overall_summary.total,
|
|
result.overall_summary.failed,
|
|
result.overall_summary.unverified
|
|
));
|
|
|
|
result
|
|
} |