Skip to content

Commit 90f52ee

Browse files
authored
Merge pull request #52 from NotAShelf/notashelf/push-sukuuwyqoxms
watt: initial d-bus interface work
2 parents ca3b1aa + 6c048ba commit 90f52ee

12 files changed

Lines changed: 1397 additions & 267 deletions

File tree

Cargo.lock

Lines changed: 496 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@ clap = { features = [ "derive", "env" ], version = "4.6.1" }
1919
clap-verbosity-flag = "3.0.4"
2020
clap_complete = "4.6.2"
2121
clap_complete_nushell = "4.6.0"
22-
ctrlc = "3.5.2"
2322
env_logger = "0.11.10"
2423
humantime = "2.3.0"
2524
jiff = "0.2.24"
2625
log = "0.4.29"
27-
nix = { features = [ "fs" ], version = "0.31.2" }
26+
nix = { features = [ "fs", "process", "signal" ], version = "0.31.2" }
2827
num_cpus = "1.17.0"
2928
serde = { features = [ "derive" ], version = "1.0.228" }
3029
toml = "1.1.2"
3130
yansi = { features = [ "detect-env", "detect-tty" ], version = "1.0.1" }
31+
zbus = { default-features = false, features = [ "tokio" ], version = "5.13.2" }
32+
tokio = { features = [ "macros", "rt-multi-thread", "signal", "time" ], version = "1.49.0" }
3233

3334
[profile.release]
3435
codegen-units = 1

dbus/net.hadess.PowerProfiles.conf

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
2+
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
3+
<busconfig>
4+
<!-- Allow root to own the PowerProfiles name -->
5+
<policy user="root">
6+
<allow own="net.hadess.PowerProfiles"/>
7+
<allow own="dev.notashelf.Watt"/>
8+
<allow send_destination="net.hadess.PowerProfiles"/>
9+
<allow send_destination="dev.notashelf.Watt"/>
10+
</policy>
11+
12+
<!-- Allow any user to interact with the service -->
13+
<policy context="default">
14+
<allow send_destination="net.hadess.PowerProfiles"/>
15+
<allow send_destination="dev.notashelf.Watt"/>
16+
<allow receive_sender="net.hadess.PowerProfiles"/>
17+
<allow receive_sender="dev.notashelf.Watt"/>
18+
</policy>
19+
</busconfig>

watt/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ path = "main.rs"
1717
anyhow.workspace = true
1818
clap.workspace = true
1919
clap-verbosity-flag.workspace = true
20-
ctrlc.workspace = true
2120
env_logger.workspace = true
2221
humantime.workspace = true
2322
jiff.workspace = true
@@ -27,6 +26,8 @@ num_cpus.workspace = true
2726
serde.workspace = true
2827
toml.workspace = true
2928
yansi.workspace = true
29+
zbus.workspace = true
30+
tokio.workspace = true
3031

3132
[dev-dependencies]
3233
proptest = "1.9.0"

watt/config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ mod expression {
432432
named!(battery_health => "%battery-health");
433433

434434
named!(discharging => "?discharging");
435+
named!(power_profile_preference => "$power-profile-preference");
435436
}
436437

437438
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
@@ -538,6 +539,9 @@ pub enum Expression {
538539
#[serde(with = "expression::discharging")]
539540
Discharging,
540541

542+
#[serde(with = "expression::power_profile_preference")]
543+
PowerProfilePreference,
544+
541545
Boolean(bool),
542546

543547
Number(f64),
@@ -706,6 +710,8 @@ pub struct EvalState<'peripherals, 'context> {
706710

707711
pub discharging: bool,
708712

713+
pub power_profile_preference: crate::profile::PowerProfile,
714+
709715
pub context: EvalContext<'context>,
710716

711717
pub cpus: &'peripherals HashSet<Arc<cpu::Cpu>>,
@@ -932,6 +938,10 @@ impl Expression {
932938

933939
Discharging => Boolean(state.discharging),
934940

941+
PowerProfilePreference => {
942+
String(state.power_profile_preference.as_str().to_owned())
943+
},
944+
935945
literal @ (Boolean(_) | Number(_) | String(_)) => literal.clone(),
936946

937947
List(items) => {
@@ -1229,6 +1239,7 @@ mod tests {
12291239
battery_cycles: Some(100.0),
12301240
battery_health: Some(0.95),
12311241
discharging: false,
1242+
power_profile_preference: crate::profile::PowerProfile::Balanced,
12321243
context: EvalContext::Cpu(&cpu),
12331244
cpus: &cpus,
12341245
power_supplies: &power_supplies,
@@ -1319,6 +1330,7 @@ mod tests {
13191330
battery_cycles: Some(100.0),
13201331
battery_health: Some(0.95),
13211332
discharging: false,
1333+
power_profile_preference: crate::profile::PowerProfile::Balanced,
13221334
context: EvalContext::Cpu(&cpu),
13231335
cpus: &cpus,
13241336
power_supplies: &power_supplies,
@@ -1397,6 +1409,7 @@ mod tests {
13971409
battery_cycles: None,
13981410
battery_health: None,
13991411
discharging: false,
1412+
power_profile_preference: crate::profile::PowerProfile::Balanced,
14001413
context: EvalContext::Cpu(&cpu),
14011414
cpus: &cpus,
14021415
power_supplies: &power_supplies,

watt/dbus/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod ppd;
2+
pub mod server;
3+
pub mod watt;

watt/dbus/ppd.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use std::{
2+
collections::HashMap,
3+
str::FromStr as _,
4+
sync::Arc,
5+
};
6+
7+
use tokio::sync::RwLock;
8+
use zbus::{
9+
fdo,
10+
interface,
11+
object_server::SignalEmitter,
12+
zvariant::Value,
13+
};
14+
15+
use crate::{
16+
profile::PowerProfile,
17+
system::DaemonState,
18+
};
19+
20+
const DRIVER_NAME: &str = "watt";
21+
const CPU_DRIVER_NAME: &str = "watt";
22+
23+
pub struct PowerProfilesInterface {
24+
state: Arc<RwLock<DaemonState>>,
25+
}
26+
27+
impl PowerProfilesInterface {
28+
pub fn new(state: Arc<RwLock<DaemonState>>) -> Self {
29+
Self { state }
30+
}
31+
}
32+
33+
#[interface(name = "net.hadess.PowerProfiles")]
34+
impl PowerProfilesInterface {
35+
// Properties
36+
#[zbus(property)]
37+
async fn active_profile(&self) -> String {
38+
let state = self.state.read().await;
39+
state.active_profile().as_str().to_owned()
40+
}
41+
42+
#[zbus(property)]
43+
async fn set_active_profile(&self, profile: &str) -> zbus::Result<()> {
44+
let profile = match PowerProfile::from_str(profile) {
45+
Ok(profile) => profile,
46+
Err(_) => {
47+
return Err(zbus::Error::from(fdo::Error::InvalidArgs(format!(
48+
"invalid profile: {profile}, valid: performance, balanced, \
49+
power-saver"
50+
))));
51+
},
52+
};
53+
54+
let mut state = self.state.write().await;
55+
state.set_active_profile(profile);
56+
57+
log::info!(
58+
"D-Bus: active profile set to {profile}",
59+
profile = profile.as_str()
60+
);
61+
62+
Ok(())
63+
}
64+
65+
#[zbus(property)]
66+
async fn profiles(&self) -> Vec<HashMap<String, Value<'_>>> {
67+
PowerProfile::all()
68+
.iter()
69+
.map(|profile| {
70+
let mut map = HashMap::new();
71+
map.insert("Profile".to_owned(), Value::from(profile.as_str()));
72+
map.insert("Driver".to_owned(), Value::from(DRIVER_NAME));
73+
map.insert("CpuDriver".to_owned(), Value::from(CPU_DRIVER_NAME));
74+
map
75+
})
76+
.collect()
77+
}
78+
79+
#[zbus(property)]
80+
async fn actions(&self) -> Vec<String> {
81+
Vec::new()
82+
}
83+
84+
#[zbus(property)]
85+
async fn performance_degraded(&self) -> String {
86+
let state = self.state.read().await;
87+
state.performance_degraded().unwrap_or_default().to_owned()
88+
}
89+
90+
#[zbus(property)]
91+
async fn performance_inhibited(&self) -> String {
92+
let state = self.state.read().await;
93+
match state.profile_holds().first() {
94+
Some(hold) => hold.reason.clone(),
95+
None => String::new(),
96+
}
97+
}
98+
99+
#[zbus(property)]
100+
async fn active_profile_holds(&self) -> Vec<HashMap<String, Value<'_>>> {
101+
let state = self.state.read().await;
102+
state
103+
.profile_holds()
104+
.into_iter()
105+
.map(|hold| {
106+
let mut map = HashMap::new();
107+
map.insert("Profile".to_owned(), Value::from(hold.profile.as_str()));
108+
map.insert("Reason".to_owned(), Value::from(hold.reason));
109+
map
110+
.insert("ApplicationId".to_owned(), Value::from(hold.application_id));
111+
map
112+
})
113+
.collect()
114+
}
115+
116+
async fn hold_profile(
117+
&self,
118+
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
119+
profile: String,
120+
reason: String,
121+
application_id: String,
122+
) -> fdo::Result<u32> {
123+
let profile = match PowerProfile::from_str(&profile) {
124+
Ok(profile) => profile,
125+
Err(_) => {
126+
return Err(fdo::Error::InvalidArgs(format!(
127+
"invalid profile: {profile}"
128+
)));
129+
},
130+
};
131+
132+
if profile == PowerProfile::Balanced {
133+
return Err(fdo::Error::InvalidArgs(
134+
"profile holds only support performance and power-saver".to_owned(),
135+
));
136+
}
137+
138+
let mut state = self.state.write().await;
139+
let cookie = state.add_profile_hold(profile, reason, application_id);
140+
141+
log::info!("D-Bus profile hold added, cookie={cookie}");
142+
143+
// Emit property change signals
144+
drop(state); // release lock before emitting signals
145+
146+
// Log signal failures but don't fail the operation.
147+
// State was already mutated.
148+
if let Err(e) = self.active_profile_holds_changed(&signal_emitter).await {
149+
log::warn!("failed to emit ActiveProfileHolds change signal: {e}");
150+
}
151+
152+
if let Err(e) = self.active_profile_changed(&signal_emitter).await {
153+
log::warn!("failed to emit ActiveProfile change signal: {e}");
154+
}
155+
156+
Ok(cookie)
157+
}
158+
159+
async fn release_profile(
160+
&self,
161+
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
162+
cookie: u32,
163+
) -> fdo::Result<()> {
164+
let mut state = self.state.write().await;
165+
state
166+
.release_profile_hold(cookie)
167+
.map_err(|error| fdo::Error::Failed(error.to_string()))?;
168+
169+
log::info!("D-Bus profile hold released, cookie={cookie}");
170+
171+
drop(state);
172+
173+
if let Err(e) = self.active_profile_holds_changed(&signal_emitter).await {
174+
log::warn!("Failed to emit ActiveProfileHolds change signal: {e}");
175+
}
176+
177+
if let Err(e) = self.active_profile_changed(&signal_emitter).await {
178+
log::warn!("Failed to emit ActiveProfile change signal: {e}");
179+
}
180+
181+
Ok(())
182+
}
183+
}

watt/dbus/server.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use std::{
2+
future,
3+
sync::Arc,
4+
time::Duration,
5+
};
6+
7+
use tokio::sync::RwLock;
8+
use zbus::connection;
9+
10+
use crate::system::DaemonState;
11+
12+
pub async fn start(state: Arc<RwLock<DaemonState>>) -> zbus::Result<()> {
13+
log::info!("starting D-Bus server...");
14+
15+
let mut attempt: u32 = 0;
16+
loop {
17+
match try_start(state.clone()).await {
18+
Ok(()) => return Ok(()),
19+
Err(e) => {
20+
attempt += 1;
21+
log::error!("D-Bus server error on attempt {attempt}: {e}");
22+
23+
if attempt >= 5 {
24+
log::error!("D-Bus server failed after {attempt} attempts, bailing");
25+
return Err(e);
26+
}
27+
28+
let delay = Duration::from_secs(2 * attempt as u64);
29+
log::info!(
30+
"retrying D-Bus in {delay_secs}s",
31+
delay_secs = delay.as_secs()
32+
);
33+
tokio::time::sleep(delay).await;
34+
},
35+
}
36+
}
37+
}
38+
39+
async fn try_start(state: Arc<RwLock<DaemonState>>) -> zbus::Result<()> {
40+
let ppd = crate::dbus::ppd::PowerProfilesInterface::new(state.clone());
41+
let watt = crate::dbus::watt::WattInterface::new(state);
42+
43+
let _connection = connection::Builder::system()?
44+
.name("net.hadess.PowerProfiles")?
45+
.name("dev.notashelf.Watt")?
46+
.serve_at("/net/hadess/PowerProfiles", ppd)?
47+
.serve_at("/dev/notashelf/Watt", watt)?
48+
.build()
49+
.await?;
50+
51+
log::info!("D-Bus server started");
52+
53+
// Block forever to keep the D-Bus server alive
54+
loop {
55+
future::pending::<()>().await;
56+
}
57+
}

0 commit comments

Comments
 (0)