mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
power_supply&cpu: use objects
This commit is contained in:
parent
6377480312
commit
6ef4da9113
3 changed files with 473 additions and 525 deletions
567
src/cpu.rs
567
src/cpu.rs
|
@ -1,19 +1,6 @@
|
|||
use anyhow::{Context, bail};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::{fs, io, path::Path, string::ToString};
|
||||
|
||||
// // Valid EPP (Energy Performance Preference) string values.
|
||||
// const EPP_FALLBACK_VALUES: &[&str] = &[
|
||||
// "default",
|
||||
// "performance",
|
||||
// "balance-performance",
|
||||
// "balance_performance", // Alternative form with underscore.
|
||||
// "balance-power",
|
||||
// "balance_power", // Alternative form with underscore.
|
||||
// "power",
|
||||
// ];
|
||||
use std::{fs, path::Path, string::ToString};
|
||||
|
||||
fn exists(path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
|
@ -41,8 +28,24 @@ fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Get real, tunable CPUs.
|
||||
pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
|
||||
pub struct Cpu {
|
||||
pub number: u32,
|
||||
pub has_cpufreq: bool,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
pub fn new(number: u32) -> anyhow::Result<Self> {
|
||||
let mut cpu = Self {
|
||||
number,
|
||||
has_cpufreq: false,
|
||||
};
|
||||
cpu.rescan()?;
|
||||
|
||||
Ok(cpu)
|
||||
}
|
||||
|
||||
/// Get all CPUs.
|
||||
pub fn all() -> anyhow::Result<Vec<Cpu>> {
|
||||
const PATH: &str = "/sys/devices/system/cpu";
|
||||
|
||||
let mut cpus = vec![];
|
||||
|
@ -62,55 +65,40 @@ pub fn get_real_cpus() -> anyhow::Result<Vec<u32>> {
|
|||
};
|
||||
|
||||
// Has to match "cpu{N}".
|
||||
let Ok(cpu) = cpu_prefix_removed.parse::<u32>() else {
|
||||
let Ok(number) = cpu_prefix_removed.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Has to match "cpu{N}/cpufreq".
|
||||
if !entry.path().join("cpufreq").exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
cpus.push(cpu);
|
||||
cpus.push(Self::new(number)?);
|
||||
}
|
||||
|
||||
// Fall back if sysfs iteration above fails to find any cpufreq CPUs.
|
||||
if cpus.is_empty() {
|
||||
cpus = (0..num_cpus::get() as u32).collect();
|
||||
for number in 0..num_cpus::get() as u32 {
|
||||
cpus.push(Self::new(number)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cpus)
|
||||
}
|
||||
|
||||
/// Set the governor for a CPU.
|
||||
pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> {
|
||||
let governors = get_available_governors_for(cpu);
|
||||
|
||||
if !governors
|
||||
.iter()
|
||||
.any(|avail_governor| avail_governor == governor)
|
||||
{
|
||||
bail!(
|
||||
"governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}",
|
||||
governors = governors.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor"),
|
||||
governor,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"this probably means that CPU {cpu} doesn't exist or doesn't support changing governors"
|
||||
)
|
||||
})
|
||||
}
|
||||
/// Rescan CPU, tuning local copy of settings.
|
||||
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
||||
let has_cpufreq = exists(format!(
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq",
|
||||
number = self.number,
|
||||
));
|
||||
|
||||
self.has_cpufreq = has_cpufreq;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_available_governors(&self) -> Vec<String> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
/// Get available CPU governors for a CPU.
|
||||
fn get_available_governors_for(cpu: u32) -> Vec<String> {
|
||||
let Ok(content) = fs::read_to_string(format!(
|
||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors"
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors"
|
||||
)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
@ -119,23 +107,210 @@ fn get_available_governors_for(cpu: u32) -> Vec<String> {
|
|||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)]
|
||||
pub enum Turbo {
|
||||
Always,
|
||||
Never,
|
||||
}
|
||||
pub fn set_governor(&self, governor: &str) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
||||
let value_boost = match setting {
|
||||
Turbo::Always => "1", // boost = 1 means turbo is enabled.
|
||||
Turbo::Never => "0", // boost = 0 means turbo is disabled.
|
||||
let governors = self.get_available_governors();
|
||||
|
||||
if !governors
|
||||
.iter()
|
||||
.any(|avail_governor| avail_governor == governor)
|
||||
{
|
||||
bail!(
|
||||
"governor '{governor}' is not available for CPU {number}. available governors: {governors}",
|
||||
governors = governors.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"),
|
||||
governor,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"this probably means that CPU {number} doesn't exist or doesn't support changing governors"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_available_epps(&self) -> Vec<String> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
let Ok(content) = fs::read_to_string(format!(
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences"
|
||||
)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let value_boost_negated = match setting {
|
||||
Turbo::Always => "0", // no_turbo = 0 means turbo is enabled.
|
||||
Turbo::Never => "1", // no_turbo = 1 means turbo is disabled.
|
||||
content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn set_epp(&self, epp: &str) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
let epps = self.get_available_epps();
|
||||
|
||||
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
||||
bail!(
|
||||
"EPP value '{epp}' is not availabile for CPU {number}. available EPP values: {epps}",
|
||||
epps = epps.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"),
|
||||
epp,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"this probably means that CPU {number} doesn't exist or doesn't support changing EPP"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_available_epbs(&self) -> &'static [&'static str] {
|
||||
if !self.has_cpufreq {
|
||||
return &[];
|
||||
}
|
||||
|
||||
&[
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15",
|
||||
"performance",
|
||||
"balance-performance",
|
||||
"balance_performance", // Alternative form with underscore.
|
||||
"balance-power",
|
||||
"balance_power", // Alternative form with underscore.
|
||||
"power",
|
||||
]
|
||||
}
|
||||
|
||||
pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
let epbs = self.get_available_epbs();
|
||||
|
||||
if !epbs.contains(&epb) {
|
||||
bail!(
|
||||
"EPB value '{epb}' is not available for CPU {number}. available EPB values: {valid}",
|
||||
valid = epbs.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"),
|
||||
epb,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"this probably means that CPU {number} doesn't exist or doesn't support changing EPB"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_frequency_minimum(&self, frequency_mhz: u64) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
self.validate_frequency_minimum(frequency_mhz)?;
|
||||
|
||||
// We use u64 for the intermediate calculation to prevent overflow
|
||||
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||
let frequency_khz = frequency_khz.to_string();
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"),
|
||||
&frequency_khz,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("this probably means that CPU {number} doesn't exist or doesn't support changing minimum frequency")
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
let Ok(minimum_frequency_khz) = read_u64(format!(
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||
)) else {
|
||||
// Just let it pass if we can't find anything.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
|
||||
bail!(
|
||||
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {number}",
|
||||
minimum_frequency_khz / 1000,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_frequency_maximum(&self, frequency_mhz: u64) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
self.validate_frequency_maximum(frequency_mhz)?;
|
||||
|
||||
// We use u64 for the intermediate calculation to prevent overflow
|
||||
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||
let frequency_khz = frequency_khz.to_string();
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"),
|
||||
&frequency_khz,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("this probably means that CPU {number} doesn't exist or doesn't support changing maximum frequency")
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||
let Self { number, .. } = self;
|
||||
|
||||
let Ok(maximum_frequency_khz) = read_u64(format!(
|
||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||
)) else {
|
||||
// Just let it pass if we can't find anything.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
||||
bail!(
|
||||
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {number}",
|
||||
maximum_frequency_khz / 1000,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_turbo(on: bool) -> anyhow::Result<()> {
|
||||
let value_boost = match on {
|
||||
true => "1", // boost = 1 means turbo is enabled.
|
||||
false => "0", // boost = 0 means turbo is disabled.
|
||||
};
|
||||
|
||||
let value_boost_negated = match on {
|
||||
true => "0", // no_turbo = 0 means turbo is enabled.
|
||||
false => "1", // no_turbo = 1 means turbo is disabled.
|
||||
};
|
||||
|
||||
// AMD specific paths
|
||||
|
@ -161,9 +336,11 @@ pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
// Also try per-core cpufreq boost for some AMD systems.
|
||||
if get_real_cpus()?.iter().any(|cpu| {
|
||||
if Self::all()?.iter().any(|cpu| {
|
||||
let Cpu { number, .. } = cpu;
|
||||
|
||||
write(
|
||||
&format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/boost"),
|
||||
&format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"),
|
||||
value_boost,
|
||||
)
|
||||
.is_ok()
|
||||
|
@ -172,263 +349,5 @@ pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
bail!("no supported CPU boost control mechanism found");
|
||||
}
|
||||
|
||||
pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> {
|
||||
// Validate the EPP value against available options
|
||||
let epps = get_available_epps(cpu);
|
||||
|
||||
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
||||
bail!(
|
||||
"epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}",
|
||||
epps = epps.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"),
|
||||
epp,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP")
|
||||
})
|
||||
}
|
||||
|
||||
/// Get available EPP values for a CPU.
|
||||
fn get_available_epps(cpu: u32) -> Vec<String> {
|
||||
let Ok(content) = fs::read_to_string(format!(
|
||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_available_preferences"
|
||||
)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn set_epb(epb: &str, cpu: u32) -> anyhow::Result<()> {
|
||||
// Validate EPB value - should be a number 0-15 or a recognized string value.
|
||||
validate_epb_value(epb)?;
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"),
|
||||
epb,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB")
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_epb_value(epb: &str) -> anyhow::Result<()> {
|
||||
// EPB can be a number from 0-15 or a recognized string.
|
||||
|
||||
const VALID_EPB_STRINGS: &[&str] = &[
|
||||
"performance",
|
||||
"balance-performance",
|
||||
"balance_performance", // Alternative form with underscore.
|
||||
"balance-power",
|
||||
"balance_power", // Alternative form with underscore.
|
||||
"power",
|
||||
];
|
||||
|
||||
// Try parsing as a number first.
|
||||
if let Ok(value) = epb.parse::<u8>() {
|
||||
if value <= 15 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bail!("EPB numeric value must be between 0 and 15, got {value}");
|
||||
}
|
||||
|
||||
// If not a number, check if it's a recognized string value.
|
||||
if VALID_EPB_STRINGS.contains(&epb) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bail!(
|
||||
"invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}",
|
||||
valid = VALID_EPB_STRINGS.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||
validate_frequency_minimum(frequency_mhz, cpu)?;
|
||||
|
||||
// We use u64 for the intermediate calculation to prevent overflow
|
||||
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||
let frequency_khz = frequency_khz.to_string();
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"),
|
||||
&frequency_khz,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing minimum frequency")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_frequency_maximum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||
validate_max_frequency(frequency_mhz, cpu)?;
|
||||
|
||||
// We use u64 for the intermediate calculation to prevent overflow
|
||||
let frequency_khz = u64::from(frequency_mhz) * 1000;
|
||||
let frequency_khz = frequency_khz.to_string();
|
||||
|
||||
write(
|
||||
format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_max_freq"),
|
||||
&frequency_khz,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing maximum frequency")
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_frequency_minimum(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||
let Ok(minimum_frequency_khz) = read_u64(format!(
|
||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
|
||||
)) else {
|
||||
// Just let it pass if we can't find anything.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
|
||||
bail!(
|
||||
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {cpu}",
|
||||
minimum_frequency_khz / 1000,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> {
|
||||
let Ok(maximum_frequency_khz) = read_u64(format!(
|
||||
"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"
|
||||
)) else {
|
||||
// Just let it pass if we can't find anything.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
||||
bail!(
|
||||
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}",
|
||||
maximum_frequency_khz / 1000,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the platform profile.
|
||||
/// This changes the system performance, temperature, fan, and other hardware replated characteristics.
|
||||
///
|
||||
/// Also see [`The Kernel docs`] for this.
|
||||
///
|
||||
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
||||
pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> {
|
||||
let profiles = get_platform_profiles();
|
||||
|
||||
if !profiles
|
||||
.iter()
|
||||
.any(|avail_profile| avail_profile == profile)
|
||||
{
|
||||
bail!(
|
||||
"profile '{profile}' is not available for system. valid profiles: {profiles}",
|
||||
profiles = profiles.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write("/sys/firmware/acpi/platform_profile", profile)
|
||||
.context("this probably means that your system does not support changing ACPI profiles")
|
||||
}
|
||||
|
||||
/// Get the list of available platform profiles.
|
||||
pub fn get_platform_profiles() -> Vec<String> {
|
||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
||||
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Path for storing the governor override state.
|
||||
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override";
|
||||
|
||||
#[derive(Display, Debug, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum GovernorOverride {
|
||||
#[display("performance")]
|
||||
Performance,
|
||||
#[display("powersave")]
|
||||
Powersave,
|
||||
#[display("reset")]
|
||||
Reset,
|
||||
}
|
||||
|
||||
pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> {
|
||||
let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap();
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create directory '{path}'",
|
||||
path = parent.display(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
match mode {
|
||||
GovernorOverride::Reset => {
|
||||
// Remove the override file if it exists
|
||||
let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH);
|
||||
|
||||
if let Err(error) = result {
|
||||
if error.kind() != io::ErrorKind::NotFound {
|
||||
return Err(error).with_context(|| {
|
||||
format!(
|
||||
"failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"governor override has been deleted. normal profile-based settings will be used"
|
||||
);
|
||||
}
|
||||
|
||||
GovernorOverride::Performance | GovernorOverride::Powersave => {
|
||||
let governor = mode.to_string();
|
||||
|
||||
write(GOVERNOR_OVERRIDE_PATH, &governor)
|
||||
.context("failed to write governor override")?;
|
||||
|
||||
// TODO: Apply the setting too.
|
||||
|
||||
log::info!(
|
||||
"governor override set to '{governor}'. this setting will persist across reboots"
|
||||
);
|
||||
log::info!("to reset, run: superfreq set --governor-persist reset");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current governor override if set.
|
||||
pub fn get_governor_override() -> anyhow::Result<Option<String>> {
|
||||
match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) {
|
||||
Ok(governor_override) => Ok(Some(governor_override)),
|
||||
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
|
||||
Err(error) => Err(error).with_context(|| {
|
||||
format!("failed to read governor override at '{GOVERNOR_OVERRIDE_PATH}'")
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
36
src/main.rs
36
src/main.rs
|
@ -61,7 +61,7 @@ enum Command {
|
|||
|
||||
/// Set turbo boost behaviour. Has to be for all CPUs.
|
||||
#[arg(short = 't', long, conflicts_with = "for_")]
|
||||
turbo: Option<cpu::Turbo>,
|
||||
turbo: Option<bool>,
|
||||
},
|
||||
|
||||
/// Modify power supply attributes.
|
||||
|
@ -113,34 +113,42 @@ fn real_main() -> anyhow::Result<()> {
|
|||
turbo,
|
||||
} => {
|
||||
let cpus = match for_ {
|
||||
Some(cpus) => cpus,
|
||||
None => cpu::get_real_cpus()?,
|
||||
Some(numbers) => {
|
||||
let mut cpus = Vec::with_capacity(numbers.len());
|
||||
|
||||
for number in numbers {
|
||||
cpus.push(cpu::Cpu::new(number)?);
|
||||
}
|
||||
|
||||
cpus
|
||||
}
|
||||
None => cpu::Cpu::all()?,
|
||||
};
|
||||
|
||||
for cpu in cpus {
|
||||
if let Some(governor) = governor.as_ref() {
|
||||
cpu::set_governor(governor, cpu)?;
|
||||
cpu.set_governor(governor)?;
|
||||
}
|
||||
|
||||
if let Some(epp) = energy_performance_preference.as_ref() {
|
||||
cpu::set_epp(epp, cpu)?;
|
||||
cpu.set_epp(epp)?;
|
||||
}
|
||||
|
||||
if let Some(epb) = energy_performance_bias.as_ref() {
|
||||
cpu::set_epb(epb, cpu)?;
|
||||
cpu.set_epb(epb)?;
|
||||
}
|
||||
|
||||
if let Some(mhz_minimum) = frequency_mhz_minimum {
|
||||
cpu::set_frequency_minimum(mhz_minimum, cpu)?;
|
||||
cpu.set_frequency_minimum(mhz_minimum)?;
|
||||
}
|
||||
|
||||
if let Some(mhz_maximum) = frequency_mhz_maximum {
|
||||
cpu::set_frequency_maximum(mhz_maximum, cpu)?;
|
||||
cpu.set_frequency_maximum(mhz_maximum)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(turbo) = turbo {
|
||||
cpu::set_turbo(turbo)?;
|
||||
cpu::Cpu::set_turbo(turbo)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -157,13 +165,13 @@ fn real_main() -> anyhow::Result<()> {
|
|||
let power_supplies = Vec::with_capacity(names.len());
|
||||
|
||||
for name in names {
|
||||
power_supplies.push(power_supply::get_power_supply(&name)?);
|
||||
power_supplies.push(power_supply::PowerSupply::from_name(name)?);
|
||||
}
|
||||
|
||||
power_supplies
|
||||
}
|
||||
|
||||
None => power_supply::get_power_supplies()?
|
||||
None => power_supply::PowerSupply::all()?
|
||||
.into_iter()
|
||||
.filter(|power_supply| power_supply.threshold_config.is_some())
|
||||
.collect(),
|
||||
|
@ -171,16 +179,16 @@ fn real_main() -> anyhow::Result<()> {
|
|||
|
||||
for power_supply in power_supplies {
|
||||
if let Some(threshold_start) = charge_threshold_start {
|
||||
power_supply::set_charge_threshold_start(&power_supply, threshold_start)?;
|
||||
power_supply.set_charge_threshold_start(threshold_start)?;
|
||||
}
|
||||
|
||||
if let Some(threshold_end) = charge_threshold_end {
|
||||
power_supply::set_charge_threshold_end(&power_supply, threshold_end)?;
|
||||
power_supply.set_charge_threshold_end(threshold_end)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(platform_profile) = platform_profile.as_ref() {
|
||||
cpu::set_platform_profile(platform_profile)?;
|
||||
power_supply::PowerSupply::set_platform_profile(platform_profile);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::{Context, bail};
|
||||
|
||||
use std::{
|
||||
fmt, fs,
|
||||
os::macos::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
// TODO: Migrate to central utils file. Same exists in cpu.rs.
|
||||
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
fs::write(path, value).with_context(|| {
|
||||
format!(
|
||||
"failed to write '{value}' to '{path}'",
|
||||
path = path.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Represents a pattern of path suffixes used to control charge thresholds
|
||||
/// for different device vendors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
@ -50,7 +61,12 @@ pub struct PowerSupply {
|
|||
|
||||
impl fmt::Display for PowerSupply {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "power supply '{name}'", name = &self.name)?;
|
||||
write!(
|
||||
f,
|
||||
"power supply '{name}' at '{path}'",
|
||||
name = &self.name,
|
||||
path = self.path.display(),
|
||||
)?;
|
||||
|
||||
if let Some(config) = self.threshold_config.as_ref() {
|
||||
write!(
|
||||
|
@ -64,74 +80,42 @@ impl fmt::Display for PowerSupply {
|
|||
}
|
||||
}
|
||||
|
||||
impl PowerSupply {
|
||||
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
||||
self.threshold_config
|
||||
.map(|config| self.path.join(config.path_start))
|
||||
}
|
||||
|
||||
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
||||
self.threshold_config
|
||||
.map(|config| self.path.join(config.path_end))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Migrate to central utils file. Same exists in cpu.rs.
|
||||
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
fs::write(path, value).with_context(|| {
|
||||
format!(
|
||||
"failed to write '{value}' to '{path}'",
|
||||
path = path.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_battery(path: &Path) -> anyhow::Result<bool> {
|
||||
let type_path = path.join("type");
|
||||
|
||||
let type_ = fs::read_to_string(&type_path)
|
||||
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?;
|
||||
|
||||
Ok(type_ == "Battery")
|
||||
}
|
||||
|
||||
const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply";
|
||||
|
||||
/// Get power supply.
|
||||
pub fn get_power_supply(name: &str) -> anyhow::Result<PowerSupply> {
|
||||
let entry_path = Path::new(POWER_SUPPLY_PATH).join(name);
|
||||
impl PowerSupply {
|
||||
pub fn from_name(name: String) -> anyhow::Result<Self> {
|
||||
let mut power_supply = Self {
|
||||
path: Path::new(POWER_SUPPLY_PATH).join(&name),
|
||||
name,
|
||||
threshold_config: None,
|
||||
};
|
||||
|
||||
let threshold_config = is_battery(&entry_path)
|
||||
power_supply.rescan()?;
|
||||
|
||||
Ok(power_supply)
|
||||
}
|
||||
|
||||
pub fn from_path(path: PathBuf) -> anyhow::Result<Self> {
|
||||
let mut power_supply = PowerSupply {
|
||||
name: path
|
||||
.file_name()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to determine what type of power supply '{path}' is",
|
||||
path = entry_path.display(),
|
||||
)
|
||||
format!("failed to get file name of '{path}'", path = path.display(),)
|
||||
})?
|
||||
.then(|| {
|
||||
for config in POWER_SUPPLY_THRESHOLD_CONFIGS {
|
||||
if entry_path.join(config.path_start).exists()
|
||||
&& entry_path.join(config.path_end).exists()
|
||||
{
|
||||
return Some(*config);
|
||||
}
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
|
||||
path,
|
||||
|
||||
threshold_config: None,
|
||||
};
|
||||
|
||||
power_supply.rescan()?;
|
||||
|
||||
Ok(power_supply)
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.flatten();
|
||||
|
||||
Ok(PowerSupply {
|
||||
name: name.to_owned(),
|
||||
path: entry_path,
|
||||
threshold_config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all power supplies.
|
||||
pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
|
||||
pub fn all() -> anyhow::Result<Vec<PowerSupply>> {
|
||||
let mut power_supplies = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(POWER_SUPPLY_PATH)
|
||||
|
@ -146,83 +130,120 @@ pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
|
|||
}
|
||||
};
|
||||
|
||||
let entry_path = entry.path();
|
||||
|
||||
let mut power_supply_config = None;
|
||||
|
||||
if is_battery(&entry_path).with_context(|| {
|
||||
format!(
|
||||
"failed to determine what type of power supply '{path}' is",
|
||||
path = entry_path.display(),
|
||||
)
|
||||
})? {
|
||||
for config in POWER_SUPPLY_THRESHOLD_CONFIGS {
|
||||
if entry_path.join(config.path_start).exists()
|
||||
&& entry_path.join(config.path_end).exists()
|
||||
{
|
||||
power_supply_config = Some(*config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
power_supplies.push(PowerSupply {
|
||||
name: entry_path
|
||||
.file_name()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to get file name of '{path}'",
|
||||
path = entry_path.display(),
|
||||
)
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
|
||||
path: entry_path,
|
||||
|
||||
threshold_config: power_supply_config,
|
||||
});
|
||||
power_supplies.push(PowerSupply::from_path(entry.path())?);
|
||||
}
|
||||
|
||||
Ok(power_supplies)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_charge_threshold_start(
|
||||
power_supply: &PowerSupply,
|
||||
charge_threshold_start: u8,
|
||||
) -> anyhow::Result<()> {
|
||||
fn is_battery(&self) -> anyhow::Result<bool> {
|
||||
let type_path = self.path.join("type");
|
||||
|
||||
let type_ = fs::read_to_string(&type_path)
|
||||
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?;
|
||||
|
||||
Ok(type_ == "Battery")
|
||||
}
|
||||
|
||||
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
||||
let threshold_config = self
|
||||
.is_battery()
|
||||
.with_context(|| format!("failed to determine what type of power supply '{self}' is"))?
|
||||
.then(|| {
|
||||
for config in POWER_SUPPLY_THRESHOLD_CONFIGS {
|
||||
if self.path.join(config.path_start).exists()
|
||||
&& self.path.join(config.path_end).exists()
|
||||
{
|
||||
return Some(*config);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.flatten();
|
||||
|
||||
self.threshold_config = threshold_config;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
||||
self.threshold_config
|
||||
.map(|config| self.path.join(config.path_start))
|
||||
}
|
||||
|
||||
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
||||
self.threshold_config
|
||||
.map(|config| self.path.join(config.path_end))
|
||||
}
|
||||
|
||||
pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> {
|
||||
write(
|
||||
&power_supply.charge_threshold_path_start().ok_or_else(|| {
|
||||
&self.charge_threshold_path_start().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"power supply '{name}' does not support changing charge threshold levels",
|
||||
name = power_supply.name,
|
||||
name = self.name,
|
||||
)
|
||||
})?,
|
||||
&charge_threshold_start.to_string(),
|
||||
)
|
||||
.with_context(|| format!("failed to set charge threshold start for {power_supply}"))?;
|
||||
.with_context(|| format!("failed to set charge threshold start for {self}"))?;
|
||||
|
||||
log::info!("set battery threshold start for {power_supply} to {charge_threshold_start}%");
|
||||
log::info!("set battery threshold start for {self} to {charge_threshold_start}%");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_charge_threshold_end(
|
||||
power_supply: &PowerSupply,
|
||||
charge_threshold_end: u8,
|
||||
) -> anyhow::Result<()> {
|
||||
pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> {
|
||||
write(
|
||||
&power_supply.charge_threshold_path_end().ok_or_else(|| {
|
||||
&self.charge_threshold_path_end().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"power supply '{name}' does not support changing charge threshold levels",
|
||||
name = power_supply.name,
|
||||
name = self.name,
|
||||
)
|
||||
})?,
|
||||
&charge_threshold_end.to_string(),
|
||||
)
|
||||
.with_context(|| format!("failed to set charge threshold end for {power_supply}"))?;
|
||||
.with_context(|| format!("failed to set charge threshold end for {self}"))?;
|
||||
|
||||
log::info!("set battery threshold end for {power_supply} to {charge_threshold_end}%");
|
||||
log::info!("set battery threshold end for {self} to {charge_threshold_end}%");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_available_platform_profiles() -> Vec<String> {
|
||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
||||
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sets the platform profile.
|
||||
/// This changes the system performance, temperature, fan, and other hardware replated characteristics.
|
||||
///
|
||||
/// Also see [`The Kernel docs`] for this.
|
||||
///
|
||||
/// [`The Kernel docs`]: <https://docs.kernel.org/userspace-api/sysfs-platform_profile.html>
|
||||
pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> {
|
||||
let profiles = Self::get_available_platform_profiles();
|
||||
|
||||
if !profiles
|
||||
.iter()
|
||||
.any(|avail_profile| avail_profile == profile)
|
||||
{
|
||||
bail!(
|
||||
"profile '{profile}' is not available for system. valid profiles: {profiles}",
|
||||
profiles = profiles.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
write("/sys/firmware/acpi/platform_profile", profile)
|
||||
.context("this probably means that your system does not support changing ACPI profiles")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue