256 lines
9.1 KiB
Rust
256 lines
9.1 KiB
Rust
use crate::cli::PowerCommands;
|
|
use log::{error, info};
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use zbus::Connection;
|
|
use zbus::proxy;
|
|
|
|
#[proxy(
|
|
interface = "org.lapctl1",
|
|
default_service = "org.lapctl",
|
|
default_path = "/org/lapctl"
|
|
)]
|
|
trait Lapctl {
|
|
async fn set_power_profile(&self, profile: String) -> zbus::Result<()>;
|
|
async fn set_tdp_limit(&self, watts: u32) -> zbus::Result<()>;
|
|
}
|
|
|
|
fn try_call_daemon(command: &PowerCommands) -> bool {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
rt.block_on(async {
|
|
let connection = match Connection::system().await {
|
|
Ok(conn) => conn,
|
|
Err(_) => return false,
|
|
};
|
|
let proxy = match LapctlProxy::new(&connection).await {
|
|
Ok(p) => p,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
match command {
|
|
PowerCommands::Performance => proxy
|
|
.set_power_profile("performance".to_string())
|
|
.await
|
|
.is_ok(),
|
|
PowerCommands::Balanced => proxy
|
|
.set_power_profile("balanced".to_string())
|
|
.await
|
|
.is_ok(),
|
|
PowerCommands::BatterySave => proxy
|
|
.set_power_profile("battery-save".to_string())
|
|
.await
|
|
.is_ok(),
|
|
PowerCommands::LimitTdp { watts } => proxy.set_tdp_limit(*watts).await.is_ok(),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn set_cpu_governor(governor: &str) {
|
|
let cpu_dir = Path::new("/sys/devices/system/cpu");
|
|
if !cpu_dir.exists() {
|
|
return;
|
|
}
|
|
|
|
let mut success_count = 0;
|
|
if let Ok(entries) = fs::read_dir(cpu_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let name = path.file_name().unwrap_or_default().to_string_lossy();
|
|
if name.starts_with("cpu") && name.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) {
|
|
let scaling_gov_path = path.join("cpufreq/scaling_governor");
|
|
if scaling_gov_path.exists() {
|
|
match fs::write(&scaling_gov_path, governor) {
|
|
Ok(_) => success_count += 1,
|
|
Err(e) => log::debug!("Failed to set CPU governor for {}: {}", name, e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if success_count > 0 {
|
|
info!(
|
|
"Set CPU scaling governor to '{}' on {} cores",
|
|
governor, success_count
|
|
);
|
|
} else {
|
|
log::debug!("Could not set CPU scaling governor");
|
|
}
|
|
}
|
|
|
|
fn set_energy_performance_preference(epp: &str) {
|
|
let cpu_dir = Path::new("/sys/devices/system/cpu");
|
|
if !cpu_dir.exists() {
|
|
return;
|
|
}
|
|
|
|
let mut success_count = 0;
|
|
if let Ok(entries) = fs::read_dir(cpu_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let name = path.file_name().unwrap_or_default().to_string_lossy();
|
|
if name.starts_with("cpu") && name.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) {
|
|
let epp_path = path.join("cpufreq/energy_performance_preference");
|
|
if epp_path.exists() {
|
|
match fs::write(&epp_path, epp) {
|
|
Ok(_) => success_count += 1,
|
|
Err(e) => log::debug!("Failed to set EPP for {}: {}", name, e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if success_count > 0 {
|
|
info!(
|
|
"Set Energy Performance Preference to '{}' on {} cores",
|
|
epp, success_count
|
|
);
|
|
}
|
|
}
|
|
fn set_intel_rapl_limit(watts: u32) -> bool {
|
|
let mut success = false;
|
|
let microwatts = watts * 1_000_000;
|
|
|
|
// Intel RAPL
|
|
let rapl_dir = Path::new("/sys/class/powercap/intel-rapl");
|
|
if rapl_dir.exists()
|
|
&& let Ok(entries) = fs::read_dir(rapl_dir)
|
|
{
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let name = path.file_name().unwrap_or_default().to_string_lossy();
|
|
|
|
// Set limits for package-0 (entire CPU package)
|
|
if name.starts_with("intel-rapl:") {
|
|
let constraint_0 = path.join("constraint_0_power_limit_uw"); // PL1
|
|
let constraint_1 = path.join("constraint_1_power_limit_uw"); // PL2
|
|
|
|
if constraint_0.exists() {
|
|
match fs::write(&constraint_0, microwatts.to_string()) {
|
|
Ok(_) => {
|
|
log::debug!("Set RAPL constraint 0 to {}uW", microwatts);
|
|
success = true;
|
|
}
|
|
Err(e) => log::debug!("Failed to write RAPL 0: {}", e),
|
|
}
|
|
}
|
|
if constraint_1.exists() {
|
|
// Usually PL2 (Boost limit) is set ~20% higher than PL1 (Sustained limit)
|
|
// But for a strict override we clamp BOTH to the requested static wattage limit.
|
|
match fs::write(&constraint_1, microwatts.to_string()) {
|
|
Ok(_) => {
|
|
log::debug!("Set RAPL constraint 1 to {}uW", microwatts);
|
|
success = true;
|
|
}
|
|
Err(e) => log::debug!("Failed to write RAPL 1: {}", e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
success
|
|
}
|
|
|
|
fn set_amd_hwmon_limit(watts: u32) -> bool {
|
|
let mut success = false;
|
|
let microwatts = watts * 1_000_000;
|
|
|
|
// AMD hwmon (Typically exposed via hwmon platform drivers like k10temp or amd_pmc)
|
|
let hwmon_dir = Path::new("/sys/class/hwmon");
|
|
if hwmon_dir.exists()
|
|
&& let Ok(entries) = fs::read_dir(hwmon_dir)
|
|
{
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let power1_cap = path.join("power1_cap");
|
|
let name_path = path.join("name");
|
|
|
|
if power1_cap.exists() {
|
|
// Try to verify this is actually the CPU and not a generic sensor
|
|
if let Ok(name) = fs::read_to_string(&name_path)
|
|
&& name.trim().contains("amd")
|
|
{
|
|
match fs::write(&power1_cap, microwatts.to_string()) {
|
|
Ok(_) => {
|
|
log::debug!("Set AMD hwmon power1_cap to {}uW", microwatts);
|
|
success = true;
|
|
}
|
|
Err(e) => log::debug!("Failed to write AMD hwmon: {}", e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
success
|
|
}
|
|
fn set_platform_profile(profile: &str) {
|
|
let platform_profile_path = Path::new("/sys/firmware/acpi/platform_profile");
|
|
if platform_profile_path.exists() {
|
|
match fs::write(platform_profile_path, profile) {
|
|
Ok(_) => info!("Set ACPI platform profile to '{}'", profile),
|
|
Err(e) => error!("Failed to set ACPI platform profile: {}", e),
|
|
}
|
|
} else {
|
|
log::debug!("ACPI platform profile not available on this system");
|
|
}
|
|
}
|
|
|
|
pub fn execute(command: &PowerCommands) {
|
|
if try_call_daemon(command) {
|
|
println!("Request handled by lapctld daemon.");
|
|
return;
|
|
}
|
|
|
|
match command {
|
|
PowerCommands::Performance => {
|
|
println!("Setting power profile to Performance");
|
|
set_platform_profile("performance");
|
|
set_cpu_governor("performance");
|
|
set_energy_performance_preference("performance");
|
|
println!("Operation completed successfully.");
|
|
}
|
|
PowerCommands::Balanced => {
|
|
println!("Setting power profile to Balanced");
|
|
set_platform_profile("balanced");
|
|
// Schedutil is the modern balanced default. Fallback isn't critical since cpufreq
|
|
// usually rejects invalid profiles, and standard Intel/AMD systems use EPP now anyway.
|
|
set_cpu_governor("schedutil");
|
|
set_energy_performance_preference("balance_performance");
|
|
println!("Operation completed successfully.");
|
|
}
|
|
PowerCommands::BatterySave => {
|
|
println!("Setting power profile to Battery Saver");
|
|
set_platform_profile("low-power");
|
|
set_cpu_governor("powersave");
|
|
set_energy_performance_preference("power");
|
|
println!("Operation completed successfully.");
|
|
}
|
|
PowerCommands::LimitTdp { watts } => {
|
|
println!("Setting CPU Package TDP Limit to {} Watts...", watts);
|
|
|
|
let mut applied = false;
|
|
if set_intel_rapl_limit(*watts) {
|
|
applied = true;
|
|
println!("Successfully applied Intel RAPL boundaries (PL1/PL2).");
|
|
}
|
|
if set_amd_hwmon_limit(*watts) {
|
|
applied = true;
|
|
println!("Successfully applied AMD HWMon power capping.");
|
|
}
|
|
|
|
if applied {
|
|
println!("Operation completed successfully.");
|
|
} else {
|
|
error!(
|
|
"Hardware does not support dynamic powercap limits via intel-rapl or standard hwmon."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|