redbear-power: bulletproof cpufreq backend with writability check
- cpufreq.rs: complete rewrite — Backend enum (Redox/Linux), Cpufreq struct with self-discovering governor list, probe-based discovery on Redox, writability verification by writing current governor back. No hardcoded governor names — cycle order follows discovered list. Handles intel_pstate (no ondemand), permission-denied, and absent cpufreq gracefully. - app.rs: Cpufreq stored in App instead of Governor enum, eliminates per-poll probe overhead. cycle_governor flashes root-required when sysfs denies write. - render.rs, dbus.rs: use cpufreq.active string directly
This commit is contained in:
@@ -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<CpuRow>,
|
||||
pub table_state: TableState,
|
||||
pub expanded_cpu: Option<u32>,
|
||||
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
|
||||
|
||||
@@ -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<String> {
|
||||
// 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<Self> {
|
||||
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<String> {
|
||||
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()
|
||||
}
|
||||
|
||||
fn read_available(&self) -> Option<Vec<String>> {
|
||||
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<Backend>,
|
||||
/// Governors known to work on this platform (may be empty before
|
||||
/// first discovery).
|
||||
pub available: Vec<String>,
|
||||
/// 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<String> {
|
||||
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<String> {
|
||||
let original = backend.read_active().unwrap_or_default();
|
||||
let mut found: Vec<String> = 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<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user