ram-sentinel 0.2.0

A surgical OOM prevention daemon for Linux desktops. Configurably monitors RAM, swap, and/or PSI (Pressure Stall Information) to selectively kill low-priority processes (e.g., browser tabs) before the system freezes.
use byte_unit::Byte;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::fmt;

// --- Enums ---

#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize, ValueEnum)]
#[repr(u8)]
pub enum LogLevel {
    Error = 1,
    Warn = 2,
    Info = 3,
    Debug = 4,
}

impl LogLevel {
    pub fn from_u8(v: u8) -> Self {
        match v {
            1 => LogLevel::Error,
            2 => LogLevel::Warn,
            3 => LogLevel::Info,
            4 => LogLevel::Debug,
            _ => LogLevel::Info, // Safe fallback
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            LogLevel::Error => "ERROR",
            LogLevel::Warn => "WARN",
            LogLevel::Info => "INFO",
            LogLevel::Debug => "DEBUG",
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, ValueEnum)]
#[repr(u8)]
pub enum LogMode {
    Compact = 0,
    Json = 1,
}

impl LogMode {
    pub fn from_u8(v: u8) -> Self {
        match v {
            1 => LogMode::Json,
            _ => LogMode::Compact,
        }
    }
}

// --- Utility Functions ---

fn format_bytes(bytes: u64) -> String {
    format!(
        "{:.2}",
        Byte::from_u64(bytes).get_appropriate_unit(byte_unit::UnitType::Decimal)
    )
}

// --- Event Definition ---

#[derive(Serialize, Clone)]
#[serde(tag = "message", rename_all = "snake_case")]
pub enum SentinelEvent<'a> {
    // Generic Message Wrapper
    Message {
        #[serde(skip)] // We don't need "level" twice in JSON (it's in the root)
        level: LogLevel,
        text: &'a str,
    },

    Startup {
        interval_ms: u64,
    },
    Monitor {
        memory_available_bytes: Option<u64>,
        memory_available_percent: Option<f64>,
        swap_free_bytes: Option<u64>,
        swap_free_percent: Option<f64>,
        zram_free_bytes: Option<u64>,
        zram_free_percent: Option<f64>,
        psi_pressure: Option<f64>,
    },
    LowMemoryWarn {
        available_bytes: u64,
        available_percent: f64,
        threshold_type: &'a str,
        threshold_value: f64,
    },
    LowSwapWarn {
        free_bytes: u64,
        free_percent: f64,
        threshold_type: &'a str,
        threshold_value: f64,
    },
    LowZramWarn {
        free_bytes: u64,
        free_percent: f64,
        threshold_type: &'a str,
        threshold_value: f64,
    },
    PsiPressureWarn {
        pressure_curr: f64,
        threshold: f64,
    },
    KillTriggered {
        trigger: &'a str,
        observed_value: f64,
        threshold_value: f64,
        threshold_type: &'a str,
        amount_needed: Option<u64>,
    },
    KillCandidateSelected {
        pid: u32,
        process_name: &'a str,
        score: u64,
        rss: u64,
        match_index: usize,
    },
    KillExecuted {
        pid: u32,
        process_name: &'a str,
        strategy: &'a str,
        rss_freed: u64,
    },
    KillSequenceFinished {
        reason: &'a str,
    },
    KillSequenceAborted {
        reason: &'a str,
    },
    KillCandidateIgnored {
        pid: u32,
        reason: &'a str,
    },
}

// --- Display Implementation (for Compact Mode) ---
impl fmt::Display for SentinelEvent<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SentinelEvent::Message { text, .. } => write!(f, "{}", text),

            SentinelEvent::Startup { interval_ms } => {
                write!(f, "ram-sentinel started. Interval: {}ms", interval_ms)
            }
            SentinelEvent::Monitor {
                memory_available_bytes,
                memory_available_percent: _,
                swap_free_bytes,
                swap_free_percent: _,
                zram_free_bytes,
                zram_free_percent: _,
                psi_pressure,
            } => {
                let avail_str = match memory_available_bytes {
                    Some(b) => format_bytes(*b),
                    None => "N/A".to_string(),
                };

                let swap_str = match swap_free_bytes {
                    Some(b) => format_bytes(*b),
                    None => "N/A".to_string(),
                };

                let zram_str = match zram_free_bytes {
                    Some(b) => format_bytes(*b),
                    None => "N/A".to_string(),
                };

                let psi_str = match psi_pressure {
                    Some(p) => format!("{:.2}", p),
                    None => "N/A".to_string(),
                };

                write!(
                    f,
                    "Memory: {} available, Swap: {} available, ZRAM: {} available, PSI: {}",
                    avail_str, swap_str, zram_str, psi_str
                )
            }
            SentinelEvent::LowMemoryWarn {
                available_bytes,
                available_percent,
                threshold_type,
                threshold_value,
            } => {
                let avail_str = format_bytes(*available_bytes);
                if *threshold_type == "bytes" {
                    let thresh_str = format_bytes(*threshold_value as u64);
                    write!(
                        f,
                        "Low RAM: {} available (Limit: {})",
                        avail_str, thresh_str
                    )
                } else {
                    write!(
                        f,
                        "Low RAM: {} ({:.2}%) available (Limit: {:.2}%)",
                        avail_str, available_percent, threshold_value
                    )
                }
            }
            SentinelEvent::LowSwapWarn {
                free_bytes,
                free_percent,
                threshold_type,
                threshold_value,
            } => {
                let free_str = format_bytes(*free_bytes);
                if *threshold_type == "bytes" {
                    let thresh_str = format_bytes(*threshold_value as u64);
                    write!(
                        f,
                        "Low Swap: {} available (Limit: {})",
                        free_str, thresh_str
                    )
                } else {
                    write!(
                        f,
                        "Low Swap: {} ({:.2}%) available (Limit: {:.2}%)",
                        free_str, free_percent, threshold_value
                    )
                }
            }
            SentinelEvent::LowZramWarn {
                free_bytes,
                free_percent,
                threshold_type,
                threshold_value,
            } => {
                let free_str = format_bytes(*free_bytes);
                if *threshold_type == "bytes" {
                    let thresh_str = format_bytes(*threshold_value as u64);
                    write!(
                        f,
                        "Low ZRAM: {} available (Limit: {})",
                        free_str, thresh_str
                    )
                } else {
                    write!(
                        f,
                        "Low ZRAM: {} ({:.2}%) available (Limit: {:.2}%)",
                        free_str, free_percent, threshold_value
                    )
                }
            }
            SentinelEvent::PsiPressureWarn {
                pressure_curr,
                threshold,
            } => {
                write!(
                    f,
                    "Memory Pressure: {:.2}% (Limit: {:.2}%)",
                    pressure_curr, threshold
                )
            }
            SentinelEvent::KillTriggered {
                trigger,
                observed_value,
                threshold_value,
                threshold_type,
                ..
            } => {
                let observed_str = if *threshold_type == "bytes" {
                    format_bytes(*observed_value as u64)
                } else {
                    format!("{:.2}%", observed_value)
                };
                let limit_str = if *threshold_type == "bytes" {
                    format_bytes(*threshold_value as u64)
                } else {
                    format!("{:.2}%", threshold_value)
                };
                write!(
                    f,
                    "Kill Triggered: {} - Observed {} < Limit {}",
                    trigger, observed_str, limit_str
                )
            }
            SentinelEvent::KillCandidateSelected {
                process_name,
                pid,
                score,
                rss,
                ..
            } => {
                let rss_str = format_bytes(*rss);
                write!(
                    f,
                    "Selected Target: {} (PID {}). Score: {}, RSS: {}",
                    process_name, pid, score, rss_str
                )
            }
            SentinelEvent::KillExecuted {
                process_name,
                pid,
                strategy,
                rss_freed,
            } => {
                let rss_str = format_bytes(*rss_freed);
                write!(
                    f,
                    "{} {} (PID {}). Freed: {}",
                    strategy, process_name, pid, rss_str
                )
            }
            SentinelEvent::KillSequenceFinished { reason } => {
                write!(f, "Kill Sequence Finished: {}", reason)
            }
            SentinelEvent::KillSequenceAborted { reason } => {
                write!(f, "Kill Sequence Aborted: {}", reason)
            }
            SentinelEvent::KillCandidateIgnored { pid, reason } => {
                write!(f, "Ignored Candidate PID {}: {}", pid, reason)
            }
        }
    }
}

impl SentinelEvent<'_> {
    /// Determines the log severity of the current event.
    pub fn severity(&self) -> LogLevel {
        match self {
            // For the generic message, we simply return the contained level
            SentinelEvent::Message { level, .. } => *level,
            SentinelEvent::Monitor { .. } => LogLevel::Debug,

            SentinelEvent::Startup { .. }
            | SentinelEvent::KillCandidateSelected { .. }
            | SentinelEvent::KillExecuted { .. }
            | SentinelEvent::KillSequenceFinished { .. }
            | SentinelEvent::KillSequenceAborted { .. }
            | SentinelEvent::KillCandidateIgnored { .. } => LogLevel::Info,

            SentinelEvent::LowMemoryWarn { .. }
            | SentinelEvent::LowSwapWarn { .. }
            | SentinelEvent::LowZramWarn { .. }
            | SentinelEvent::PsiPressureWarn { .. } => LogLevel::Warn,

            SentinelEvent::KillTriggered { .. } => LogLevel::Error,
        }
    }
}