Files
RedBear-OS/local/recipes/system/redbear-power/source/src/cpufreq.rs
T
vasilito d2b969eb05 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
2026-06-28 18:13:34 +03:00

303 lines
10 KiB
Rust

//! cpufreq governor interface — platform-agnostic, self-discovering.
//!
//! ## Backend discovery
//!
//! 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;
// ── Backend abstraction ────────────────────────────────────────────
#[derive(Clone, Debug)]
enum Backend {
/// `/scheme/cpufreq/state` — key=value text file served by cpufreqd
Redox,
/// `/sys/.../scaling_governor` — single-line sysfs file
Linux,
}
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())
}
}
}
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
}
}