Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
634 changes: 607 additions & 27 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ edition = "2024" # K
license = "MPL-2.0"
readme = true
repository = "https://github.com/notashelf/watt"
rust-version = "1.88"
rust-version = "1.91.1"
version = "1.0.0"

[workspace.dependencies]
Expand All @@ -26,3 +26,6 @@ num_cpus = "1.17.0"
serde = { features = [ "derive" ], version = "1.0.228" }
toml = "0.9.8"
yansi = { features = [ "detect-env", "detect-tty" ], version = "1.0.1" }
zbus = { version = "5.13.2", default-features = false, features = [ "tokio" ] }
tokio = { version = "1.49.0", features = [ "rt-multi-thread", "macros", "signal", "time" ] }
async-trait = "0.1.89"
19 changes: 19 additions & 0 deletions dbus/net.hadess.PowerProfiles.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Allow root to own the PowerProfiles name -->
<policy user="root">
<allow own="net.hadess.PowerProfiles"/>
<allow own="dev.notashelf.Watt"/>
<allow send_destination="net.hadess.PowerProfiles"/>
<allow send_destination="dev.notashelf.Watt"/>
</policy>

<!-- Allow any user to interact with the service -->
<policy context="default">
<allow send_destination="net.hadess.PowerProfiles"/>
<allow send_destination="dev.notashelf.Watt"/>
<allow receive_sender="net.hadess.PowerProfiles"/>
<allow receive_sender="dev.notashelf.Watt"/>
</policy>
</busconfig>
3 changes: 3 additions & 0 deletions watt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ num_cpus.workspace = true
serde.workspace = true
toml.workspace = true
yansi.workspace = true
zbus.workspace = true
tokio.workspace = true
async-trait.workspace = true

[dev-dependencies]
proptest = "1.9.0"
12 changes: 12 additions & 0 deletions watt/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ mod expression {
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");

named!(discharging => "?discharging");
named!(power_profile_preference => "$power-profile-preference");
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -458,6 +459,9 @@ pub enum Expression {
#[serde(with = "expression::discharging")]
Discharging,

#[serde(with = "expression::power_profile_preference")]
PowerProfilePreference,

Boolean(bool),

Number(f64),
Expand Down Expand Up @@ -621,6 +625,8 @@ pub struct EvalState<'peripherals, 'context> {

pub discharging: bool,

pub power_profile_preference: crate::profile::PowerProfile,

pub context: EvalContext<'context>,

pub cpus: &'peripherals HashSet<Arc<cpu::Cpu>>,
Expand Down Expand Up @@ -758,6 +764,10 @@ impl Expression {

Discharging => Boolean(state.discharging),

PowerProfilePreference => {
String(state.power_profile_preference.as_str().to_owned())
},

literal @ (Boolean(_) | Number(_) | String(_)) => literal.clone(),

List(items) => {
Expand Down Expand Up @@ -1050,6 +1060,7 @@ mod tests {
power_supply_charge: Some(0.8),
power_supply_discharge_rate: Some(10.0),
discharging: false,
power_profile_preference: crate::profile::PowerProfile::Balanced,
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
power_supplies: &power_supplies,
Expand Down Expand Up @@ -1134,6 +1145,7 @@ mod tests {
power_supply_charge: Some(0.8),
power_supply_discharge_rate: Some(10.0),
discharging: false,
power_profile_preference: crate::profile::PowerProfile::Balanced,
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
power_supplies: &power_supplies,
Expand Down
11 changes: 9 additions & 2 deletions watt/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ impl Cpu {
self.has_cpufreq =
fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));

log::trace!("CPU {number} has cpufreq: {has_cpufreq}", number = self.number, has_cpufreq = self.has_cpufreq);
log::trace!(
"CPU {number} has cpufreq: {has_cpufreq}",
number = self.number,
has_cpufreq = self.has_cpufreq
);

if self.has_cpufreq {
self.scan_governor()?;
Expand Down Expand Up @@ -459,7 +463,10 @@ impl Cpu {

self.governor = Some(governor.to_owned());

log::info!("CPU {number} governor set to {governor}", number = self.number);
log::info!(
"CPU {number} governor set to {governor}",
number = self.number
);

Ok(())
}
Expand Down
3 changes: 3 additions & 0 deletions watt/dbus/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod ppd;
pub mod server;
pub mod watt;
178 changes: 178 additions & 0 deletions watt/dbus/ppd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::{
collections::HashMap,
sync::Arc,
};

use tokio::sync::RwLock;
use zbus::{
fdo,
interface,
object_server::SignalEmitter,
zvariant::Value,
};

use crate::{
profile::{
PowerProfile,
ProfileHold,
},
system::DaemonState,
};

pub struct PowerProfilesInterface {
state: Arc<RwLock<DaemonState>>,
}

impl PowerProfilesInterface {
pub fn new(state: Arc<RwLock<DaemonState>>) -> Self {
Self { state }
}
}

#[interface(name = "net.hadess.PowerProfiles")]
impl PowerProfilesInterface {
// Properties
#[zbus(property)]
async fn active_profile(&self) -> String {
let state = self.state.read().await;
state.profile.get_effective_profile().as_str().to_owned()
}

#[zbus(property)]
async fn set_active_profile(&self, profile: &str) -> zbus::Result<()> {
let profile = match PowerProfile::from_str(profile) {
Some(profile) => profile,
None => {
return Err(zbus::Error::from(fdo::Error::InvalidArgs(format!(
"invalid profile: {profile}, valid: performance, balanced, \
power-saver"
))));
},
};

let mut state = self.state.write().await;
state.profile.set_preference(profile);

log::info!(
"D-Bus: active profile set to {profile}",
profile = profile.as_str()
);

Ok(())
}

#[zbus(property)]
async fn profiles(&self) -> Vec<HashMap<String, Value<'_>>> {
PowerProfile::all()
.iter()
.map(|profile| {
let mut map = HashMap::new();
map.insert("Profile".to_owned(), Value::from(profile.as_str()));
map.insert("Driver".to_owned(), Value::from("watt"));
map.insert("CpuDriver".to_owned(), Value::from("unknown"));
map
})
.collect()
}

#[zbus(property)]
async fn actions(&self) -> Vec<String> {
Vec::new()
}

#[zbus(property)]
async fn performance_degraded(&self) -> String {
let state = self.state.read().await;
state.performance_degraded.clone().unwrap_or_default()
}

#[zbus(property)]
async fn performance_inhibited(&self) -> String {
let state = self.state.read().await;
match state.profile.get_holds().first() {
Some(hold) => hold.reason.clone(),
None => String::new(),
}
}

#[zbus(property)]
async fn active_profile_holds(&self) -> Vec<HashMap<String, Value<'_>>> {
let state = self.state.read().await;
state
.profile
.get_holds()
.into_iter()
.map(|hold: ProfileHold| {
let mut map = HashMap::new();
map.insert("Profile".to_owned(), Value::from(hold.profile.as_str()));
map.insert("Reason".to_owned(), Value::from(hold.reason));
map
.insert("ApplicationId".to_owned(), Value::from(hold.application_id));
map
})
.collect()
}

async fn hold_profile(
&self,
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
profile: String,
reason: String,
application_id: String,
) -> fdo::Result<u32> {
let profile = match PowerProfile::from_str(&profile) {
Some(profile) => profile,
None => {
return Err(fdo::Error::InvalidArgs(format!(
"invalid profile: {profile}"
)));
},
};

let mut state = self.state.write().await;
let cookie = state.profile.add_hold(profile, reason, application_id);

log::info!("D-Bus profile hold added, cookie={cookie}");

// Emit property change signals
drop(state); // release lock before emitting signals

// Log signal failures but don't fail the operation.
// State was already mutated.
if let Err(e) = self.active_profile_holds_changed(&signal_emitter).await {
log::warn!("failed to emit ActiveProfileHolds change signal: {e}");
}

if let Err(e) = self.active_profile_changed(&signal_emitter).await {
log::warn!("failed to emit ActiveProfile change signal: {e}");
}

Ok(cookie)
}

async fn release_profile(
&self,
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
cookie: u32,
) -> fdo::Result<()> {
let mut state = self.state.write().await;
state
.profile
.release_hold(cookie)
.map_err(|error| fdo::Error::Failed(error.to_string()))?;

log::info!("D-Bus profile hold released, cookie={cookie}");

drop(state);

if let Err(e) = self.active_profile_holds_changed(&signal_emitter).await {
log::warn!("Failed to emit ActiveProfileHolds change signal: {e}");
}

if let Err(e) = self.active_profile_changed(&signal_emitter).await {
log::warn!("Failed to emit ActiveProfile change signal: {e}");
}

Ok(())
}
}
59 changes: 59 additions & 0 deletions watt/dbus/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::{
future,
sync::Arc,
time::Duration,
};

use tokio::sync::RwLock;
use zbus::connection;

use crate::system::DaemonState;

pub async fn start_dbus_server(
state: Arc<RwLock<DaemonState>>,
) -> zbus::Result<()> {
log::info!("starting D-Bus server...");

let mut attempt: u32 = 0;
loop {
match try_start(state.clone()).await {
Ok(()) => return Ok(()),
Err(e) => {
attempt += 1;
log::error!("D-Bus server error on attempt {attempt}: {e}");

if attempt >= 5 {
log::error!("D-Bus server failed after {attempt} attempts, bailing");
return Err(e);
}

let delay = Duration::from_secs(2 * attempt as u64);
log::info!(
"retrying D-Bus in {delay_secs}s",
delay_secs = delay.as_secs()
);
tokio::time::sleep(delay).await;
},
}
}
}

async fn try_start(state: Arc<RwLock<DaemonState>>) -> zbus::Result<()> {
let ppd = crate::dbus::ppd::PowerProfilesInterface::new(state.clone());
let watt = crate::dbus::watt::WattInterface::new(state);

let _connection = connection::Builder::system()?
.name("net.hadess.PowerProfiles")?
.name("dev.notashelf.Watt")?
.serve_at("/net/hadess/PowerProfiles", ppd)?
.serve_at("/dev/notashelf/Watt", watt)?
.build()
.await?;

log::info!("D-Bus server started");

// Block forever to keep the D-Bus server alive
loop {
future::pending::<()>().await;
}
}
Loading