diff --git a/Cargo.lock b/Cargo.lock index ccb2bfb..bc112d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + [[package]] name = "anyhow" version = "1.0.82" @@ -250,6 +256,44 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.59", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -486,6 +530,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -886,6 +936,7 @@ name = "steamos-manager" version = "24.4.1" dependencies = [ "anyhow", + "clap", "inotify", "nix", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 8b892d1..f16357e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ strip="symbols" [dependencies] anyhow = "1" +clap = { version = "4.5", default-features = false, features = ["derive", "help", "std", "usage"] } inotify = { version = "0.10", default-features = false, features = ["stream"] } nix = { version = "0.28", default-features = false, features = ["fs"] } tokio = { version = "1", default-features = false, features = ["fs", "io-util", "macros", "rt-multi-thread", "signal", "sync"] } diff --git a/com.steampowered.SteamOSManager1.xml b/com.steampowered.SteamOSManager1.xml index 6121e85..3748618 100644 --- a/com.steampowered.SteamOSManager1.xml +++ b/com.steampowered.SteamOSManager1.xml @@ -252,6 +252,17 @@ --> + + + diff --git a/data/com.steampowered.SteamOSManager1.conf b/data/system/com.steampowered.SteamOSManager1.conf similarity index 100% rename from data/com.steampowered.SteamOSManager1.conf rename to data/system/com.steampowered.SteamOSManager1.conf diff --git a/data/system/com.steampowered.SteamOSManager1.service b/data/system/com.steampowered.SteamOSManager1.service new file mode 100644 index 0000000..792ea8d --- /dev/null +++ b/data/system/com.steampowered.SteamOSManager1.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=com.steampowered.SteamOSManager1 +Exec=/usr/lib/steamos-manager -r +User=root +SystemdService=steamos-manager.service diff --git a/data/steamos-manager.service b/data/system/steamos-manager.service similarity index 87% rename from data/steamos-manager.service rename to data/system/steamos-manager.service index 1675fc8..1e21630 100644 --- a/data/steamos-manager.service +++ b/data/system/steamos-manager.service @@ -7,7 +7,7 @@ After=steamos-log-submitter.service Type=dbus BusName=com.steampowered.SteamOSManager1 Environment=RUST_LOG='INFO' -ExecStart=/usr/lib/steamos-manager +ExecStart=/usr/lib/steamos-manager -r Restart=on-failure RestartSec=1 diff --git a/data/com.steampowered.SteamOSManager1.service b/data/user/com.steampowered.SteamOSManager1.service similarity index 99% rename from data/com.steampowered.SteamOSManager1.service rename to data/user/com.steampowered.SteamOSManager1.service index c0683ca..fccfb36 100644 --- a/data/com.steampowered.SteamOSManager1.service +++ b/data/user/com.steampowered.SteamOSManager1.service @@ -3,3 +3,4 @@ Name=com.steampowered.SteamOSManager1 Exec=/usr/lib/steamos-manager User=root SystemdService=steamos-manager.service + diff --git a/data/user/steamos-manager.service b/data/user/steamos-manager.service new file mode 100644 index 0000000..c147e40 --- /dev/null +++ b/data/user/steamos-manager.service @@ -0,0 +1,11 @@ +[Unit] +Description=SteamOS Manager Daemon +After=gamescope.service + +[Service] +Type=dbus +BusName=com.steampowered.SteamOSManager1 +Environment=RUST_LOG='INFO' +ExecStart=/usr/lib/steamos-manager +Restart=on-failure +RestartSec=1 diff --git a/src/cec.rs b/src/cec.rs new file mode 100644 index 0000000..9372f35 --- /dev/null +++ b/src/cec.rs @@ -0,0 +1,102 @@ +/* + * Copyright © 2023 Collabora Ltd. + * Copyright © 2024 Valve Software + * Copyright © 2024 Igalia S.L. + * + * SPDX-License-Identifier: MIT + */ + +use anyhow::Result; +use std::fmt; +use zbus::Connection; + +use crate::systemd::{daemon_reload, EnableState, SystemdUnit}; + +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum HdmiCecState { + Disabled = 0, + ControlOnly = 1, + ControlAndWake = 2, +} + +impl TryFrom for HdmiCecState { + type Error = &'static str; + fn try_from(v: u32) -> Result { + match v { + x if x == HdmiCecState::Disabled as u32 => Ok(HdmiCecState::Disabled), + x if x == HdmiCecState::ControlOnly as u32 => Ok(HdmiCecState::ControlOnly), + x if x == HdmiCecState::ControlAndWake as u32 => Ok(HdmiCecState::ControlAndWake), + _ => Err("No enum match for value {v}"), + } + } +} + +impl fmt::Display for HdmiCecState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HdmiCecState::Disabled => write!(f, "Disabled"), + HdmiCecState::ControlOnly => write!(f, "ControlOnly"), + HdmiCecState::ControlAndWake => write!(f, "ControlAndWake"), + } + } +} + +pub struct HdmiCecControl<'dbus> { + plasma_rc_unit: SystemdUnit<'dbus>, + wakehook_unit: SystemdUnit<'dbus>, + connection: Connection, +} + +impl<'dbus> HdmiCecControl<'dbus> { + pub async fn new(connection: &Connection) -> Result> { + Ok(HdmiCecControl { + plasma_rc_unit: SystemdUnit::new( + connection.clone(), + "plasma-remotecontrollers.service", + ) + .await?, + wakehook_unit: SystemdUnit::new(connection.clone(), "wakehook.service").await?, + connection: connection.clone(), + }) + } + + pub async fn get_enabled_state(&self) -> Result { + Ok(match self.plasma_rc_unit.enabled().await? { + EnableState::Enabled | EnableState::Static => { + match self.wakehook_unit.enabled().await? { + EnableState::Enabled | EnableState::Static => HdmiCecState::ControlAndWake, + _ => HdmiCecState::ControlOnly, + } + } + _ => HdmiCecState::Disabled, + }) + } + + pub async fn set_enabled_state(&self, state: HdmiCecState) -> Result<()> { + match state { + HdmiCecState::Disabled => { + self.plasma_rc_unit.mask().await?; + self.plasma_rc_unit.stop().await?; + self.wakehook_unit.mask().await?; + self.wakehook_unit.stop().await?; + daemon_reload(&self.connection).await?; + } + HdmiCecState::ControlOnly => { + self.wakehook_unit.mask().await?; + self.wakehook_unit.stop().await?; + self.plasma_rc_unit.unmask().await?; + daemon_reload(&self.connection).await?; + self.plasma_rc_unit.start().await?; + } + HdmiCecState::ControlAndWake => { + self.plasma_rc_unit.unmask().await?; + self.wakehook_unit.unmask().await?; + daemon_reload(&self.connection).await?; + self.plasma_rc_unit.start().await?; + self.wakehook_unit.start().await?; + } + }; + + Ok(()) + } +} diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..4fcccd5 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,92 @@ +/* + * Copyright © 2023 Collabora Ltd. + * Copyright © 2024 Valve Software + * + * SPDX-License-Identifier: MIT + */ + +use anyhow::{anyhow, ensure, Result}; +use tokio::signal::unix::{signal, Signal, SignalKind}; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::registry::LookupSpan; +use zbus::connection::Connection; + +use crate::sls::{LogLayer, LogReceiver}; +use crate::{reload, Service}; + +pub struct Daemon { + services: JoinSet>, + token: CancellationToken, + sigterm: Signal, + sigquit: Signal, +} + +impl Daemon { + pub async fn new LookupSpan<'a>>( + subscriber: S, + connection: Connection, + ) -> Result { + let services = JoinSet::new(); + let token = CancellationToken::new(); + + let log_receiver = LogReceiver::new(connection.clone()).await?; + let remote_logger = LogLayer::new(&log_receiver).await; + let subscriber = subscriber.with(remote_logger); + tracing::subscriber::set_global_default(subscriber)?; + + let sigterm = signal(SignalKind::terminate())?; + let sigquit = signal(SignalKind::quit())?; + + let mut daemon = Daemon { + services, + token, + sigterm, + sigquit, + }; + daemon.add_service(log_receiver); + + Ok(daemon) + } + + pub fn add_service(&mut self, service: S) { + let token = self.token.clone(); + self.services + .spawn(async move { service.start(token).await }); + } + + pub async fn run(&mut self) -> Result<()> { + ensure!( + !self.services.is_empty(), + "Can't run a daemon with no services attached." + ); + + let mut res = tokio::select! { + e = self.services.join_next() => match e.unwrap() { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(e), + Err(e) => Err(e.into()) + }, + _ = tokio::signal::ctrl_c() => Ok(()), + e = self.sigterm.recv() => e.ok_or(anyhow!("SIGTERM machine broke")), + _ = self.sigquit.recv() => Err(anyhow!("Got SIGQUIT")), + e = reload() => e, + } + .inspect_err(|e| error!("Encountered error running: {e}")); + self.token.cancel(); + + info!("Shutting down"); + + while let Some(service_res) = self.services.join_next().await { + res = match service_res { + Ok(Err(e)) => Err(e), + Err(e) => Err(e.into()), + _ => continue, + }; + } + + res.inspect_err(|e| error!("Encountered error: {e}")) + } +} diff --git a/src/main.rs b/src/main.rs index 6b76e56..b7e444c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,66 +5,75 @@ * SPDX-License-Identifier: MIT */ -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; +use clap::Parser; +use std::future::Future; use std::path::{Path, PathBuf}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::signal::unix::{signal, SignalKind}; -use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; -use tracing_subscriber::prelude::*; -use tracing_subscriber::{fmt, Registry}; -use zbus::connection::Connection; -use zbus::ConnectionBuilder; - -use crate::ds_inhibit::Inhibitor; -use crate::sls::ftrace::Ftrace; -use crate::sls::{LogLayer, LogReceiver}; +use tracing::{info, warn}; +mod cec; +mod daemon; mod ds_inhibit; mod hardware; mod manager; mod power; mod process; +mod root; mod sls; mod systemd; +mod user; +mod user_manager; mod wifi; #[cfg(test)] mod testing; +const API_VERSION: u32 = 8; + trait Service where - Self: Sized, + Self: Sized + Send, { const NAME: &'static str; - async fn run(&mut self) -> Result<()>; + fn run(&mut self) -> impl Future> + Send; - async fn shutdown(&mut self) -> Result<()> { - Ok(()) + fn shutdown(&mut self) -> impl Future> + Send { + async { Ok(()) } } - async fn start(mut self, token: CancellationToken) -> Result<()> { - info!("Starting {}", Self::NAME); - let res = tokio::select! { - r = self.run() => r, - _ = token.cancelled() => Ok(()), - }; - if res.is_err() { - warn!( - "{} encountered an error: {}", - Self::NAME, - res.as_ref().unwrap_err() - ); - token.cancel(); + fn start(mut self, token: CancellationToken) -> impl Future> + Send { + async move { + info!("Starting {}", Self::NAME); + let res = tokio::select! { + r = self.run() => r, + _ = token.cancelled() => Ok(()), + }; + if res.is_err() { + warn!( + "{} encountered an error: {}", + Self::NAME, + res.as_ref().unwrap_err() + ); + token.cancel(); + } + info!("Shutting down {}", Self::NAME); + self.shutdown().await.and(res) } - info!("Shutting down {}", Self::NAME); - self.shutdown().await.and(res) } } +#[derive(Parser)] +struct Args { + /// Run the root manager daemon + #[arg(short, long)] + root: bool, +} + #[cfg(not(test))] pub fn path>(path: S) -> PathBuf { PathBuf::from(path.as_ref()) @@ -147,79 +156,14 @@ pub fn zbus_to_zbus_fdo(error: zbus::Error) -> zbus::fdo::Error { } } -async fn create_connection() -> Result { - let connection = ConnectionBuilder::system()? - .name("com.steampowered.SteamOSManager1")? - .build() - .await?; - let manager = manager::SteamOSManager::new(connection.clone()).await?; - connection - .object_server() - .at("/com/steampowered/SteamOSManager1", manager) - .await?; - Ok(connection) -} - #[tokio::main] -async fn main() -> Result<()> { - // This daemon is responsible for creating a dbus api that steam client can use to do various OS - // level things. It implements com.steampowered.SteamOSManager1.Manager interface - - let stdout_log = fmt::layer(); - let subscriber = Registry::default().with(stdout_log); - - let connection = match create_connection().await { - Ok(c) => c, - Err(e) => { - let _guard = tracing::subscriber::set_default(subscriber); - error!("Error connecting to DBus: {}", e); - bail!(e); - } - }; - - let mut services = JoinSet::new(); - let token = CancellationToken::new(); - - let mut log_receiver = LogReceiver::new(connection.clone()).await?; - let remote_logger = LogLayer::new(&log_receiver).await; - let subscriber = subscriber.with(remote_logger); - tracing::subscriber::set_global_default(subscriber)?; - - let mut sigterm = signal(SignalKind::terminate())?; - let mut sigquit = signal(SignalKind::quit())?; - - let ftrace = Ftrace::init(connection.clone()).await?; - services.spawn(ftrace.start(token.clone())); - - let inhibitor = Inhibitor::init().await?; - services.spawn(inhibitor.start(token.clone())); - - let mut res = tokio::select! { - e = log_receiver.run() => e, - e = services.join_next() => match e.unwrap() { - Ok(Ok(())) => Ok(()), - Ok(Err(e)) => Err(e), - Err(e) => Err(e.into()) - }, - _ = tokio::signal::ctrl_c() => Ok(()), - e = sigterm.recv() => e.ok_or(anyhow!("SIGTERM machine broke")), - _ = sigquit.recv() => Err(anyhow!("Got SIGQUIT")), - e = reload() => e, +pub async fn main() -> Result<()> { + let args = Args::parse(); + if args.root { + root::daemon().await + } else { + user::daemon().await } - .inspect_err(|e| error!("Encountered error running: {e}")); - token.cancel(); - - info!("Shutting down"); - - while let Some(service_res) = services.join_next().await { - res = match service_res { - Ok(Err(e)) => Err(e), - Err(e) => Err(e.into()), - _ => continue, - }; - } - - res.inspect_err(|e| error!("Encountered error: {e}")) } #[cfg(test)] diff --git a/src/manager.rs b/src/manager.rs index 7cfc7f7..2a69fa8 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -22,7 +22,7 @@ use crate::wifi::{ get_wifi_backend, get_wifi_power_management_state, set_wifi_backend, set_wifi_debug_mode, set_wifi_power_management_state, WifiBackend, WifiDebugMode, WifiPowerManagement, }; -use crate::{to_zbus_error, to_zbus_fdo_error}; +use crate::{to_zbus_error, to_zbus_fdo_error, API_VERSION}; #[derive(PartialEq, Debug, Copy, Clone)] #[repr(u32)] @@ -55,8 +55,6 @@ const ALS_INTEGRATION_PATH: &str = "/sys/devices/platform/AMDI0010:00/i2c-0/i2c- #[interface(name = "com.steampowered.SteamOSManager1.Manager")] impl SteamOSManager { - const API_VERSION: u32 = 7; - async fn prepare_factory_reset(&self) -> u32 { // Run steamos factory reset script and return true on success let res = run_script("/usr/bin/steamos-factory-reset-config", &[""]).await; @@ -327,7 +325,7 @@ impl SteamOSManager { /// A version property. #[zbus(property(emits_changed_signal = "const"))] async fn version(&self) -> u32 { - SteamOSManager::API_VERSION + API_VERSION } } @@ -335,7 +333,7 @@ impl SteamOSManager { mod test { use super::*; use crate::{power, testing}; - use std::collections::{HashMap, HashSet}; + use std::collections::HashMap; use std::iter::zip; use tokio::fs::{create_dir_all, read, write}; use zbus::{Connection, ConnectionBuilder, Interface}; @@ -472,7 +470,7 @@ mod test { async fn version() { let test = start("Version").await; let proxy = VersionProxy::new(&test.connection).await.unwrap(); - assert_eq!(proxy.version().await, Ok(SteamOSManager::API_VERSION)); + assert_eq!(proxy.version().await, Ok(API_VERSION)); } fn collect_methods<'a>(methods: &'a [Method<'a>]) -> HashMap> { @@ -517,9 +515,7 @@ mod test { assert_eq!(remote_interface.len(), 1); let remote_interface = remote_interface[0]; let remote_methods = collect_methods(remote_interface.methods()); - let remote_method_names: HashSet<&String> = remote_methods.keys().collect(); let remote_properties = collect_properties(remote_interface.properties()); - let remote_property_names: HashSet<&String> = remote_properties.keys().collect(); let local_interface_string = read("com.steampowered.SteamOSManager1.xml") .await @@ -534,13 +530,11 @@ mod test { assert_eq!(local_interface.len(), 1); let local_interface = local_interface[0]; let local_methods = collect_methods(local_interface.methods()); - let local_method_names: HashSet<&String> = local_methods.keys().collect(); let local_properties = collect_properties(local_interface.properties()); - let local_property_names: HashSet<&String> = local_properties.keys().collect(); - for key in local_method_names.union(&remote_method_names) { - let local_method = local_methods.get(*key).expect(key); - let remote_method = remote_methods.get(*key).expect(key); + for key in remote_methods.keys() { + let local_method = local_methods.get(key).expect(key); + let remote_method = remote_methods.get(key).expect(key); assert_eq!(local_method.name(), remote_method.name()); assert_eq!(local_method.args().len(), remote_method.args().len()); @@ -552,9 +546,9 @@ mod test { } } - for key in local_property_names.union(&remote_property_names) { - let local_property = local_properties.get(*key).expect(key); - let remote_property = remote_properties.get(*key).expect(key); + for key in remote_properties.keys() { + let local_property = local_properties.get(key).expect(key); + let remote_property = remote_properties.get(key).expect(key); assert_eq!(local_property.name(), remote_property.name()); assert_eq!(local_property.ty(), remote_property.ty()); diff --git a/src/root.rs b/src/root.rs new file mode 100644 index 0000000..525a8be --- /dev/null +++ b/src/root.rs @@ -0,0 +1,57 @@ +/* + * Copyright © 2023 Collabora Ltd. + * Copyright © 2024 Valve Software + * + * SPDX-License-Identifier: MIT + */ + +use anyhow::{bail, Result}; +use tracing::error; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, Registry}; +use zbus::connection::Connection; +use zbus::ConnectionBuilder; + +use crate::daemon::Daemon; +use crate::ds_inhibit::Inhibitor; +use crate::manager; +use crate::sls::ftrace::Ftrace; + +async fn create_connection() -> Result { + let connection = ConnectionBuilder::system()? + .name("com.steampowered.SteamOSManager1")? + .build() + .await?; + let manager = manager::SteamOSManager::new(connection.clone()).await?; + connection + .object_server() + .at("/com/steampowered/SteamOSManager1", manager) + .await?; + Ok(connection) +} + +pub async fn daemon() -> Result<()> { + // This daemon is responsible for creating a dbus api that steam client can use to do various OS + // level things. It implements com.steampowered.SteamOSManager1.Manager interface + + let stdout_log = fmt::layer(); + let subscriber = Registry::default().with(stdout_log); + + let connection = match create_connection().await { + Ok(c) => c, + Err(e) => { + let _guard = tracing::subscriber::set_default(subscriber); + error!("Error connecting to DBus: {}", e); + bail!(e); + } + }; + let mut daemon = Daemon::new(subscriber, connection.clone()).await?; + + let ftrace = Ftrace::init(connection.clone()).await?; + daemon.add_service(ftrace); + + let inhibitor = Inhibitor::init().await?; + daemon.add_service(inhibitor); + + daemon.run().await +} diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..f90a3c0 --- /dev/null +++ b/src/user.rs @@ -0,0 +1,58 @@ +/* + * Copyright © 2023 Collabora Ltd. + * Copyright © 2024 Valve Software + * + * SPDX-License-Identifier: MIT + */ + +use anyhow::{bail, Result}; +use tracing::error; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, Registry}; +use zbus::connection::Connection; +use zbus::ConnectionBuilder; + +use crate::daemon::Daemon; +use crate::user_manager::SteamOSManagerUser; + +async fn create_connection(system_conn: &Connection) -> Result { + let connection = ConnectionBuilder::session()? + .name("com.steampowered.SteamOSManager1")? + .build() + .await?; + let manager = SteamOSManagerUser::new(connection.clone(), system_conn).await?; + connection + .object_server() + .at("/com/steampowered/SteamOSManager1", manager) + .await?; + Ok(connection) +} + +pub async fn daemon() -> Result<()> { + // This daemon is responsible for creating a dbus api that steam client can use to do various OS + // level things. It implements com.steampowered.SteamOSManager1.Manager interface + + let stdout_log = fmt::layer(); + let subscriber = Registry::default().with(stdout_log); + + let system = match Connection::system().await { + Ok(c) => c, + Err(e) => { + let _guard = tracing::subscriber::set_default(subscriber); + error!("Error connecting to DBus: {}", e); + bail!(e); + } + }; + let _session = match create_connection(&system).await { + Ok(c) => c, + Err(e) => { + let _guard = tracing::subscriber::set_default(subscriber); + error!("Error connecting to DBus: {}", e); + bail!(e); + } + }; + + let mut daemon = Daemon::new(subscriber, system).await?; + + daemon.run().await +} diff --git a/src/user_manager.rs b/src/user_manager.rs new file mode 100644 index 0000000..a3d1f59 --- /dev/null +++ b/src/user_manager.rs @@ -0,0 +1,372 @@ +/* + * Copyright © 2023 Collabora Ltd. + * Copyright © 2024 Valve Software + * Copyright © 2024 Igalia S.L. + * + * SPDX-License-Identifier: MIT + */ + +use anyhow::Result; +use tracing::error; +use zbus::proxy::Builder; +use zbus::zvariant::Fd; +use zbus::{interface, Connection, Proxy, SignalContext}; + +use crate::cec::{HdmiCecControl, HdmiCecState}; +use crate::{to_zbus_error, to_zbus_fdo_error, zbus_to_zbus_fdo, API_VERSION}; + +macro_rules! method { + ($self:expr, $method:expr, $($args:expr),+) => { + $self.proxy + .call($method, &($($args,)*)) + .await + .map_err(zbus_to_zbus_fdo) + }; + ($self:expr, $method:expr) => { + $self.proxy + .call($method, &()) + .await + .map_err(zbus_to_zbus_fdo) + }; +} + +macro_rules! getter { + ($self:expr, $prop:expr) => { + $self + .proxy + .get_property($prop) + .await + .map_err(zbus_to_zbus_fdo) + }; +} + +macro_rules! setter { + ($self:expr, $prop:expr, $value:expr) => { + $self + .proxy + .set_property($prop, $value) + .await + .map_err(|e| zbus::Error::FDO(Box::new(e))) + }; +} + +pub struct SteamOSManagerUser { + proxy: Proxy<'static>, + hdmi_cec: HdmiCecControl<'static>, +} + +impl SteamOSManagerUser { + pub async fn new(connection: Connection, system_conn: &Connection) -> Result { + Ok(SteamOSManagerUser { + hdmi_cec: HdmiCecControl::new(&connection).await?, + proxy: Builder::new(system_conn) + .destination("com.steampowered.SteamOSManager1")? + .path("/com/steampowered/SteamOSManager1")? + .interface("com.steampowered.SteamOSManager1.Manager")? + .cache_properties(zbus::CacheProperties::No) + .build() + .await?, + }) + } +} + +#[interface(name = "com.steampowered.SteamOSManager1.Manager")] +impl SteamOSManagerUser { + #[zbus(property(emits_changed_signal = "const"))] + async fn version(&self) -> u32 { + API_VERSION + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn hdmi_cec_state(&self) -> zbus::fdo::Result { + match self.hdmi_cec.get_enabled_state().await { + Ok(state) => Ok(state as u32), + Err(e) => Err(to_zbus_fdo_error(e)), + } + } + + #[zbus(property)] + async fn set_hdmi_cec_state(&self, state: u32) -> zbus::Result<()> { + let state = match HdmiCecState::try_from(state) { + Ok(state) => state, + Err(err) => return Err(zbus::fdo::Error::InvalidArgs(err.to_string()).into()), + }; + self.hdmi_cec + .set_enabled_state(state) + .await + .inspect_err(|message| error!("Error setting CEC state: {message}")) + .map_err(to_zbus_error) + } + + async fn prepare_factory_reset(&self) -> zbus::fdo::Result { + method!(self, "PrepareFactoryReset") + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn wifi_power_management_state(&self) -> zbus::fdo::Result { + getter!(self, "WifiPowerManagementState") + } + + #[zbus(property)] + async fn set_wifi_power_management_state(&self, state: u32) -> zbus::Result<()> { + setter!(self, "WifiPowerManagementState", state) + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn fan_control_state(&self) -> zbus::fdo::Result { + getter!(self, "FanControlState") + } + + #[zbus(property)] + async fn set_fan_control_state(&self, state: u32) -> zbus::Result<()> { + setter!(self, "SetFanControlState", state) + } + + #[zbus(property(emits_changed_signal = "const"))] + async fn hardware_currently_supported(&self) -> zbus::fdo::Result { + getter!(self, "HardwareCurrentlySupported") + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn als_calibration_gain(&self) -> zbus::fdo::Result { + getter!(self, "AlsCalibrationGain") + } + + async fn get_als_integration_time_file_descriptor(&self) -> zbus::fdo::Result { + let m = self + .proxy + .call_method::<&str, ()>("GetAlsIntegrationTimeFileDescriptor", &()) + .await + .map_err(zbus_to_zbus_fdo)?; + match m.body().deserialize::() { + Ok(fd) => fd.try_to_owned().map_err(to_zbus_fdo_error), + Err(e) => Err(zbus_to_zbus_fdo(e)), + } + } + + async fn update_bios(&self) -> zbus::fdo::Result<()> { + method!(self, "UpdateBios") + } + + async fn update_dock(&self) -> zbus::fdo::Result<()> { + method!(self, "UpdateDock") + } + + async fn trim_devices(&self) -> zbus::fdo::Result<()> { + method!(self, "TrimDevices") + } + + async fn format_device( + &self, + device: &str, + label: &str, + validate: bool, + ) -> zbus::fdo::Result<()> { + method!(self, "FormatDevice", device, label, validate) + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn gpu_performance_level(&self) -> zbus::fdo::Result { + getter!(self, "GpuPerformanceLevel") + } + + #[zbus(property)] + async fn set_gpu_performance_level(&self, level: u32) -> zbus::Result<()> { + setter!(self, "GpuPerformanceLevel", level) + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn manual_gpu_clock(&self) -> zbus::fdo::Result { + getter!(self, "ManualGpuClock") + } + + #[zbus(property)] + async fn set_manual_gpu_clock(&self, clocks: u32) -> zbus::Result<()> { + setter!(self, "ManualGpuClock", clocks) + } + + #[zbus(property(emits_changed_signal = "const"))] + async fn manual_gpu_clock_min(&self) -> zbus::fdo::Result { + getter!(self, "ManualGpuClockMin") + } + + #[zbus(property(emits_changed_signal = "const"))] + async fn manual_gpu_clock_max(&self) -> zbus::fdo::Result { + getter!(self, "ManualGpuClockMax") + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn tdp_limit(&self) -> zbus::fdo::Result { + getter!(self, "TdpLimit") + } + + #[zbus(property)] + async fn set_tdp_limit(&self, limit: u32) -> zbus::Result<()> { + setter!(self, "TdpLimit", limit) + } + + #[zbus(property(emits_changed_signal = "const"))] + async fn tdp_limit_min(&self) -> zbus::fdo::Result { + getter!(self, "TdpLimitMin") + } + + #[zbus(property(emits_changed_signal = "const"))] + async fn tdp_limit_max(&self) -> zbus::fdo::Result { + getter!(self, "TdpLimitMax") + } + + #[zbus(property)] + async fn wifi_debug_mode_state(&self) -> zbus::fdo::Result { + getter!(self, "WifiDebugModeState") + } + + async fn set_wifi_debug_mode( + &self, + mode: u32, + buffer_size: u32, + #[zbus(signal_context)] ctx: SignalContext<'_>, + ) -> zbus::fdo::Result<()> { + method!(self, "SetWifiDebugMode", mode, buffer_size)?; + self.wifi_debug_mode_state_changed(&ctx) + .await + .map_err(zbus_to_zbus_fdo)?; + Ok(()) + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn wifi_backend(&self) -> zbus::fdo::Result { + getter!(self, "WifiBackend") + } + + #[zbus(property)] + async fn set_wifi_backend(&self, backend: u32) -> zbus::Result<()> { + setter!(self, "WifiBackend", backend) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::testing; + use std::collections::{HashMap, HashSet}; + use std::iter::zip; + use tokio::fs::read; + use zbus::{Connection, ConnectionBuilder, Interface}; + use zbus_xml::{Method, Node, Property}; + + struct TestHandle { + _handle: testing::TestHandle, + connection: Connection, + } + + async fn start(name: &str) -> TestHandle { + let handle = testing::start(); + let connection = ConnectionBuilder::session() + .unwrap() + .name(format!("com.steampowered.SteamOSManager1.UserTest.{name}")) + .unwrap() + .build() + .await + .unwrap(); + let manager = SteamOSManagerUser::new(connection.clone(), &connection) + .await + .unwrap(); + connection + .object_server() + .at("/com/steampowered/SteamOSManager1", manager) + .await + .expect("object_server at"); + + TestHandle { + _handle: handle, + connection, + } + } + + fn collect_methods<'a>(methods: &'a [Method<'a>]) -> HashMap> { + let mut map = HashMap::new(); + for method in methods.iter() { + map.insert(method.name().to_string(), method); + } + map + } + + fn collect_properties<'a>(props: &'a [Property<'a>]) -> HashMap> { + let mut map = HashMap::new(); + for prop in props.iter() { + map.insert(prop.name().to_string(), prop); + } + map + } + + #[tokio::test] + async fn interface_matches() { + let test = start("Interface").await; + + let manager_ref = test + .connection + .object_server() + .interface::<_, SteamOSManagerUser>("/com/steampowered/SteamOSManager1") + .await + .expect("interface"); + let manager = manager_ref.get().await; + let mut remote_interface_string = String::from( + "", + ); + manager.introspect_to_writer(&mut remote_interface_string, 0); + remote_interface_string.push_str(""); + let remote_interfaces = + Node::from_reader::<&[u8]>(remote_interface_string.as_bytes()).expect("from_reader"); + let remote_interface: Vec<_> = remote_interfaces + .interfaces() + .iter() + .filter(|iface| iface.name() == "com.steampowered.SteamOSManager1.Manager") + .collect(); + assert_eq!(remote_interface.len(), 1); + let remote_interface = remote_interface[0]; + let remote_methods = collect_methods(remote_interface.methods()); + let remote_method_names: HashSet<&String> = remote_methods.keys().collect(); + let remote_properties = collect_properties(remote_interface.properties()); + let remote_property_names: HashSet<&String> = remote_properties.keys().collect(); + + let local_interface_string = read("com.steampowered.SteamOSManager1.xml") + .await + .expect("read"); + let local_interfaces = + Node::from_reader::<&[u8]>(local_interface_string.as_ref()).expect("from_reader"); + let local_interface: Vec<_> = local_interfaces + .interfaces() + .iter() + .filter(|iface| iface.name() == "com.steampowered.SteamOSManager1.Manager") + .collect(); + assert_eq!(local_interface.len(), 1); + let local_interface = local_interface[0]; + let local_methods = collect_methods(local_interface.methods()); + let local_method_names: HashSet<&String> = local_methods.keys().collect(); + let local_properties = collect_properties(local_interface.properties()); + let local_property_names: HashSet<&String> = local_properties.keys().collect(); + + for key in local_method_names.union(&remote_method_names) { + let local_method = local_methods.get(*key).expect(key); + let remote_method = remote_methods.get(*key).expect(key); + + assert_eq!(local_method.name(), remote_method.name()); + assert_eq!(local_method.args().len(), remote_method.args().len()); + for (local_arg, remote_arg) in + zip(local_method.args().iter(), remote_method.args().iter()) + { + assert_eq!(local_arg.direction(), remote_arg.direction()); + assert_eq!(local_arg.ty(), remote_arg.ty()); + } + } + + for key in local_property_names.union(&remote_property_names) { + let local_property = local_properties.get(*key).expect(key); + let remote_property = remote_properties.get(*key).expect(key); + + assert_eq!(local_property.name(), remote_property.name()); + assert_eq!(local_property.ty(), remote_property.ty()); + assert_eq!(local_property.access(), remote_property.access()); + } + } +}