diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index 5b4127a97f..8a3e0f10fa 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -16,7 +16,7 @@ use ratatui::widgets::TableState; use crate::acpi::{detect_cpus, read_acpi_pss, read_cpu_id, read_load, PState, read_cpu_freq_khz_sysfs, read_rapl_package_energy, rapl_power_watts}; -use crate::cpufreq::{read_governor_state, write_governor_hint}; +use crate::cpufreq::Cpufreq; use crate::cpuid::{self, CoreType, CpuId}; use crate::msr::{ read_current_perf_ctl, read_package_thermal_status, read_thermal_status, @@ -105,7 +105,7 @@ pub struct App { pub cpus: Vec, pub table_state: TableState, pub expanded_cpu: Option, - pub governor: Governor, + pub cpufreq: Cpufreq, pub throttle: ThrottleMode, pub cpu_vendor: String, pub cpu_model: String, @@ -346,7 +346,7 @@ impl App { cpus: rows, table_state, expanded_cpu: None, - governor: Governor::Ondemand, + cpufreq: Cpufreq::probe(), throttle: ThrottleMode::Auto, cpu_vendor, cpu_model, @@ -648,17 +648,8 @@ impl App { self.rapl_prev = Some(curr); } } - // Pick the current governor from the cpufreq state file. If - // the file lacks the line, keep the previously-known value — - // the user's selection must not be silently dropped on a - // transient cpufreqd write that omits it. - if let Some(rest) = read_governor_state() { - self.governor = match rest.trim() { - "performance" => Governor::Performance, - "powersave" => Governor::Powersave, - _ => Governor::Ondemand, - }; - } + // Re-read cpufreq state (catches external governor changes). + self.cpufreq.refresh(); if let Some(pkg) = read_package_thermal_status(self.cpus[0].id) { self.pkg_thermal = PackageThermal::from_msr(pkg); // PROCHOT at the package level means the entire chip is @@ -678,54 +669,23 @@ impl App { } pub fn cycle_governor(&mut self) { - let prev = self.governor; - let target = self.governor.cycle(); - self.governor = target; - if !write_governor_hint(self.governor.name()) { - self.flash_status("governor hint queued (cpufreqd not running yet)"); + if !self.cpufreq.writable { + self.flash_status("governor write requires root (sysfs permission denied)"); return; } - // Verify the kernel accepted the governor. On Linux, writing to - // /sys/.../scaling_governor may succeed from the FS perspective - // while the cpufreq driver silently rejects the governor (e.g. - // driver doesn't support "ondemand" on this CPU). Read back - // immediately to check. If rejected, revert and flash an error. - if let Some(actual) = read_governor_state() { - if actual.trim() != self.governor.name() { - self.governor = prev; - self.flash_status(format!( - "governor \"{}\" rejected by kernel (driver supports \"{}\")", - target.name(), - actual.trim() - )); - return; - } + if let Some(name) = self.cpufreq.cycle() { + self.flash_status(format!("governor → {name}")); + } else { + self.flash_status("governor cycle: no other governor available"); } - self.flash_status(format!("governor → {}", self.governor.name())); } - /// Set governor to a specific value (no cycling). Used by D-Bus - /// method calls so clients can set a known target directly. pub fn set_governor(&mut self, gov: Governor) { - let prev = self.governor; - self.governor = gov; - if !write_governor_hint(self.governor.name()) { - self.flash_status("governor hint queued (cpufreqd not running yet)"); - self.governor = prev; - return; + if self.cpufreq.set(gov.name()) { + self.flash_status(format!("governor → {}", gov.name())); + } else { + self.flash_status(format!("governor \"{}\" rejected", gov.name())); } - if let Some(actual) = read_governor_state() { - if actual.trim() != self.governor.name() { - self.governor = prev; - self.flash_status(format!( - "governor \"{}\" rejected by kernel (driver supports \"{}\")", - gov.name(), - actual.trim() - )); - return; - } - } - self.flash_status(format!("governor → {}", self.governor.name())); } /// Set the selected CPU to a specific P-state index (clamped to diff --git a/local/recipes/system/redbear-power/source/src/cpufreq.rs b/local/recipes/system/redbear-power/source/src/cpufreq.rs index bd24403dc9..929b21bab1 100644 --- a/local/recipes/system/redbear-power/source/src/cpufreq.rs +++ b/local/recipes/system/redbear-power/source/src/cpufreq.rs @@ -1,63 +1,302 @@ -//! cpufreq governor hint read/write. +//! cpufreq governor interface — platform-agnostic, self-discovering. //! -//! `cpufreqd` exposes the active governor via a flat key=value text -//! file at `/scheme/cpufreq/state`. The file may contain other keys -//! in the future; reads short-circuit on the first `governor=` line -//! and writes preserve the rest of the file. +//! ## Backend discovery //! -//! On Linux the equivalent is `/sys/devices/system/cpu/cpu0/cpufreq/ -//! scaling_governor` (a single-line text file). Writes to that file -//! require root and are propagated by the kernel ACPI cpufreq driver. +//! On startup, `Cpufreq::probe()` detects the active backend: +//! 1. `/scheme/cpufreq/state` exists → Redox (cpufreqd daemon) +//! 2. `/sys/.../scaling_governor` exists → Linux sysfs +//! 3. Neither → governor support absent (QEMU, containers, etc.) +//! +//! ## Governor discovery +//! +//! Once a backend is active, available governors are discovered: +//! - Linux: read `/sys/.../scaling_available_governors` +//! - Redox: probe by writing each candidate and reading back (the +//! cpufreqd daemon may not expose a separate "available" file) +//! - Candidates tried: performance, powersave, ondemand, conservative, +//! schedutil, userspace — in that order. +//! - The active governor is always added to the available list (even +//! if not in the candidates — some drivers have custom governors). +//! +//! ## No hardcoded assumptions +//! +//! No governor name is special-cased. The cycle order follows the +//! discovered list. If the kernel rejects a write, it is silently +//! skipped. External governor changes (thermal events, other tools) +//! are reflected on the next poll. +//! +//! ## Write verification +//! +//! Every write is immediately verified by reading back. If the kernel/ +//! daemon rejects the change, the caller is notified. This catches +//! intel_pstate dropping "ondemand", permissions issues, and locked +//! governors. use std::fs; use std::io::BufRead; -const REDOX_PATH: &str = "/scheme/cpufreq/state"; -const LINUX_PATH: &str = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"; +// ── Backend abstraction ──────────────────────────────────────────── -/// Read the live governor (e.g. "performance", "ondemand", -/// "powersave"). Tries Redox `/scheme/cpufreq/state` first, then -/// Linux `/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor`. -/// Returns `None` if neither source is reachable. -pub fn read_governor_state() -> Option { - // Try Redox scheme path first. - if let Ok(file) = fs::File::open(REDOX_PATH) { - for line in std::io::BufReader::new(file).lines().map_while(Result::ok) { - if let Some(rest) = line.strip_prefix("governor=") { - return Some(rest.trim().to_string()); - } - } - } - // Linux fallback: sysfs single-line file. - if let Ok(s) = fs::read_to_string(LINUX_PATH) { - return Some(s.trim().to_string()); - } - None +#[derive(Clone, Debug)] +enum Backend { + /// `/scheme/cpufreq/state` — key=value text file served by cpufreqd + Redox, + /// `/sys/.../scaling_governor` — single-line sysfs file + Linux, } -/// Write a new governor hint. On Redox this updates -/// `/scheme/cpufreq/state` (preserving other keys). On Linux this -/// writes to the sysfs `scaling_governor` file (kernel ACPI cpufreq -/// driver propagates the change). -pub fn write_governor_hint(governor: &str) -> bool { - // Try Redox scheme path first. - if let Ok(current) = fs::read_to_string(REDOX_PATH) { - let mut out = String::with_capacity(current.len() + 32); - let mut replaced = false; - for line in current.lines() { - if line.starts_with("governor=") { - out.push_str(&format!("governor={governor}\n")); - replaced = true; - } else { - out.push_str(line); - out.push('\n'); +impl Backend { + fn probe() -> Option { + if std::path::Path::new("/scheme/cpufreq/state").exists() { + return Some(Self::Redox); + } + if std::path::Path::new("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor").exists() { + return Some(Self::Linux); + } + None + } + + fn read_active(&self) -> Option { + match self { + Self::Redox => { + let file = fs::File::open("/scheme/cpufreq/state").ok()?; + for line in std::io::BufReader::new(file).lines().map_while(Result::ok) { + if let Some(rest) = line.strip_prefix("governor=") { + return Some(rest.trim().to_string()); + } + } + None + } + Self::Linux => { + fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") + .ok() + .map(|s| s.trim().to_string()) } } - if !replaced { - out.insert_str(0, &format!("governor={governor}\n")); - } - return fs::write(REDOX_PATH, out).is_ok(); } - // Linux fallback: write to sysfs (single line, no newline preservation). - fs::write(LINUX_PATH, governor).is_ok() -} \ No newline at end of file + + fn read_available(&self) -> Option> { + match self { + Self::Linux => { + let s = fs::read_to_string( + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors", + ) + .ok()?; + Some(s.split_whitespace().map(|s| s.to_string()).collect()) + } + // Redox cpufreqd doesn't expose an "available" list, so we + // discover by probing (see discover_governors below). + Self::Redox => None, + } + } + + fn write(&self, governor: &str) -> bool { + match self { + Self::Redox => { + let current = match fs::read_to_string("/scheme/cpufreq/state") { + Ok(c) => c, + Err(_) => return false, + }; + let mut out = String::with_capacity(current.len() + 32); + let mut replaced = false; + for line in current.lines() { + if line.starts_with("governor=") { + out.push_str(&format!("governor={governor}\n")); + replaced = true; + } else { + out.push_str(line); + out.push('\n'); + } + } + if !replaced { + out.insert_str(0, &format!("governor={governor}\n")); + } + fs::write("/scheme/cpufreq/state", out).is_ok() + } + Self::Linux => { + fs::write( + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", + governor, + ) + .is_ok() + } + } + } +} + +// ── Public API ───────────────────────────────────────────────────── + +/// Complete cpufreq state — discovered once at startup, kept current +/// by polling. +#[derive(Clone, Debug)] +pub struct Cpufreq { + backend: Option, + /// Governors known to work on this platform (may be empty before + /// first discovery). + pub available: Vec, + /// Currently active governor (may be empty before first poll). + pub active: String, + /// Whether the governor file is writable (false if RO or absent). + pub writable: bool, +} + +impl Cpufreq { + /// Probe the platform and discover available governors. + /// Safe to call multiple times — subsequent calls re-poll. + pub fn probe() -> Self { + let backend = Backend::probe(); + let mut this = Self { + backend, + available: Vec::new(), + active: String::new(), + writable: false, + }; + this.refresh(); + this + } + + /// Re-read the active governor and re-discover available governors + /// if the list is empty. Call periodically (~500ms) to catch + /// external changes. + pub fn refresh(&mut self) { + let Some(ref backend) = self.backend else { + self.available.clear(); + self.active.clear(); + self.writable = false; + return; + }; + + // Read current governor. + if let Some(active) = backend.read_active() { + self.active = active; + } + + // Discover available governors (once). + if self.available.is_empty() { + self.available = backend + .read_available() + .unwrap_or_else(|| discover_by_probing(backend)); + } + + // Always ensure the active governor is in the list (handles + // custom governors that our probe didn't try). + if !self.active.is_empty() && !self.available.contains(&self.active) { + self.available.push(self.active.clone()); + } + + // Check writability: try writing the CURRENT governor back. + // This catches permission-denied (sysfs is root-only on Linux, + // /scheme/cpufreq/state may be RO in containers). + self.writable = if let Some(ref active) = self.active.clone().into() { + backend.write(active) + } else { + false + }; + } + + /// Cycle to the next available governor. Returns the new governor + /// name on success, or None if no change was possible. + pub fn cycle(&mut self) -> Option { + let Some(ref backend) = self.backend else { return None }; + if self.available.len() < 2 { + return None; + } + let cur_pos = self.available.iter().position(|g| *g == self.active); + let start = cur_pos.unwrap_or(0); + // Try each candidate in order until one sticks. + for offset in 1..=self.available.len() { + let candidate = &self.available[(start + offset) % self.available.len()]; + if !backend.write(candidate) { + continue; + } + // Verify. + if let Some(actual) = backend.read_active() { + if actual == *candidate { + self.active = candidate.clone(); + return Some(candidate.clone()); + } + } + } + None + } + + /// Set a specific governor by name. Returns true on success. + pub fn set(&mut self, name: &str) -> bool { + let Some(ref backend) = self.backend else { return false }; + if !backend.write(name) { + return false; + } + if let Some(actual) = backend.read_active() { + if actual == name { + self.active = name.to_string(); + return true; + } + } + false + } +} + +// ── Discovery by probing ────────────────────────────────────────── + +/// Candidates to try when the platform doesn't expose an "available +/// governors" list (Redox cpufreqd, some embedded kernels). Ordered +/// by likelihood: the most common governors first. +const PROBE_CANDIDATES: &[&str] = &[ + "performance", + "powersave", + "ondemand", + "conservative", + "schedutil", + "userspace", +]; + +/// Write each candidate, read back. The ones that stick are available. +/// Restores the original governor when done. +fn discover_by_probing(backend: &Backend) -> Vec { + let original = backend.read_active().unwrap_or_default(); + let mut found: Vec = Vec::new(); + + // Always include the original — it works by definition. + if !original.is_empty() && !found.contains(&original) { + found.push(original.clone()); + } + + for candidate in PROBE_CANDIDATES { + if candidate == &original || found.iter().any(|g| g == candidate) { + continue; + } + if !backend.write(candidate) { + continue; + } + if let Some(actual) = backend.read_active() { + if actual == *candidate && !found.contains(&candidate.to_string()) { + found.push(candidate.to_string()); + } + } + } + + // Restore original. + if !original.is_empty() && backend.read_active().as_deref() != Some(&original) { + let _ = backend.write(&original); + } + + found +} + +// ── Legacy compatibility (used by redbear-power App) ────────────── + +/// Legacy: read the active governor string. Prefer `Cpufreq::probe()` +/// for new code. +pub fn read_governor_state() -> Option { + let cpufreq = Cpufreq::probe(); + if cpufreq.active.is_empty() { None } else { Some(cpufreq.active) } +} + +/// Legacy: write a governor hint. Prefer `Cpufreq::set()` for new code. +pub fn write_governor_hint(governor: &str) -> bool { + if let Some(ref backend) = Backend::probe() { + backend.write(governor) + } else { + false + } +} diff --git a/local/recipes/system/redbear-power/source/src/dbus.rs b/local/recipes/system/redbear-power/source/src/dbus.rs index dcbba1031a..54f1465d20 100644 --- a/local/recipes/system/redbear-power/source/src/dbus.rs +++ b/local/recipes/system/redbear-power/source/src/dbus.rs @@ -73,7 +73,7 @@ impl PowerSnapshot { avg_freq_khz, max_temp_c, avg_load_pct, - governor: app.governor.name().to_string(), + governor: app.cpufreq.active.clone(), throttle_mode: format!("{:?}", app.throttle), prochot_asserted, } diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index 394ad2c60e..119b89ee93 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -162,7 +162,7 @@ pub fn render_header<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { "Cores: ".set_style(theme::LABEL), format!("{} ", app.cpus.len()).into(), "Governor: ".set_style(theme::LABEL), - app.governor.name().set_style(theme::HEADER_GOVERNOR), + app.cpufreq.active.as_str().set_style(theme::HEADER_GOVERNOR), " ".into(), "Throttle: ".set_style(theme::LABEL), match app.throttle { @@ -1532,7 +1532,7 @@ pub fn render_cpu_table<'a>( pub fn render_keybar<'a>(app: &'a App) -> Paragraph<'a> { let line = format!( " g:{} ↑↓:cpu p/P:±pstate m/M:min/max t:throttle r:refresh T:tab ?:help q:quit", - app.governor.name() + app.cpufreq.active.as_str() ); Paragraph::new(line) .style(theme::VALUE_OFF)