d2b969eb05
- 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
303 lines
10 KiB
Rust
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
|
|
}
|
|
}
|