use std::fs;
#[cfg(target_os = "macos")]
use std::process::Command;
use std::time::Duration;
use thiserror::Error;
use tokio::sync::mpsc;
use tokio::time::interval;
use tracing::info;
use mr_common::types::IdleMonitorConfig;
#[derive(Debug, Error)]
pub enum IdleMonitorError {
#[error("Failed to read system load: {0}")]
LoadRead(String),
#[error("Failed to parse load average: {0}")]
Parse(String),
}
#[derive(Debug, Clone)]
pub struct DreamRequest {
pub force: bool,
}
pub fn get_system_load_1min() -> Result<f64, IdleMonitorError> {
#[cfg(target_os = "linux")]
{
let content = fs::read_to_string("/proc/loadavg")
.map_err(|e| IdleMonitorError::LoadRead(e.to_string()))?;
let load_str = content
.split_whitespace()
.next()
.ok_or_else(|| IdleMonitorError::Parse("No load value found".to_string()))?;
load_str
.parse::<f64>()
.map_err(|e| IdleMonitorError::Parse(e.to_string()))
}
#[cfg(target_os = "macos")]
{
let output = Command::new("sysctl")
.arg("-n")
.arg("vm.loadavg")
.output()
.map_err(|e| IdleMonitorError::LoadRead(e.to_string()))?;
let content = String::from_utf8_lossy(&output.stdout);
let load_str = content.trim().replace("{ ", "").replace(" }", "");
load_str
.split_whitespace()
.next()
.ok_or_else(|| IdleMonitorError::Parse("No load value found".to_string()))?
.parse::<f64>()
.map_err(|e| IdleMonitorError::Parse(e.to_string()))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err(IdleMonitorError::LoadRead(
"Unsupported platform".to_string(),
))
}
}
pub struct SystemIdleMonitor {
config: IdleMonitorConfig,
dream_sender: mpsc::Sender<DreamRequest>,
}
impl SystemIdleMonitor {
pub fn new(config: IdleMonitorConfig, dream_sender: mpsc::Sender<DreamRequest>) -> Self {
Self {
config,
dream_sender,
}
}
pub async fn run(self) {
if !self.config.enabled || !self.config.auto_dream_enabled {
info!("IdleMonitor disabled, exiting");
return;
}
let mut ticker = interval(Duration::from_secs(self.config.check_interval_secs));
info!(
"IdleMonitor started: interval={}s, threshold={}",
self.config.check_interval_secs, self.config.load_threshold
);
loop {
ticker.tick().await;
match get_system_load_1min() {
Ok(load) => {
if load < self.config.load_threshold {
info!(
target: "dream",
"IdleMonitor: system load {:.2} < threshold {:.2}, triggering dream",
load, self.config.load_threshold
);
let request = DreamRequest { force: false };
if let Err(e) = self.dream_sender.send(request).await {
tracing::error!("Failed to send dream request: {}", e);
}
} else {
tracing::debug!(
"IdleMonitor: system load {:.2} >= threshold {:.2}, skipping",
load,
self.config.load_threshold
);
}
}
Err(e) => {
tracing::warn!("Failed to get system load: {}", e);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_system_load_1min() {
let result = get_system_load_1min();
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
assert!(result.is_ok());
let load = result.unwrap();
assert!(load >= 0.0);
}
}
#[tokio::test]
async fn test_idle_monitor_disabled() {
let (tx, mut rx) = mpsc::channel::<DreamRequest>(10);
let config = IdleMonitorConfig {
enabled: false,
..Default::default()
};
let monitor = SystemIdleMonitor::new(config, tx);
tokio::spawn(async move {
monitor.run().await;
});
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn test_idle_monitor_auto_dream_disabled() {
let (tx, mut rx) = mpsc::channel::<DreamRequest>(10);
let config = IdleMonitorConfig {
auto_dream_enabled: false,
..Default::default()
};
let monitor = SystemIdleMonitor::new(config, tx);
tokio::spawn(async move {
monitor.run().await;
});
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(rx.try_recv().is_err());
}
}