Merge branch 'endrift/remote-interface' into 'master'

Draft: Add support for remote interfaces

See merge request holo/steamos-manager!23
This commit is contained in:
Vicki Pfau 2025-07-11 18:38:54 -07:00
commit ac306da8d0
10 changed files with 911 additions and 19 deletions

10
Cargo.lock generated
View file

@ -1196,6 +1196,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"speech-dispatcher", "speech-dispatcher",
"steamos-manager-macros",
"steamos-manager-proxy", "steamos-manager-proxy",
"strum", "strum",
"sysinfo", "sysinfo",
@ -1212,6 +1213,15 @@ dependencies = [
"zbus_xml", "zbus_xml",
] ]
[[package]]
name = "steamos-manager-macros"
version = "25.6.1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "steamos-manager-proxy" name = "steamos-manager-proxy"
version = "25.6.1" version = "25.6.1"

View file

@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["steamos-manager", "steamos-manager-proxy"] members = ["steamos-manager", "steamos-manager-macros", "steamos-manager-proxy"]
[profile.release] [profile.release]
strip="symbols" strip="symbols"

View file

@ -298,6 +298,27 @@
</interface> </interface>
<!--
com.steampowered.SteamOSManager1.RemoteInterface1
@short_description: TKTK
-->
<interface name="com.steampowered.SteamOSManager1.RemoteInterface1">
<method name="RegisterInterface">
<arg type="s" name="interface" direction="in"/>
<arg type="o" name="object" direction="in"/>
<arg type="b" name="registered" direction="out"/>
</method>
<method name="UnregisterInterface">
<arg type="s" name="interface" direction="in"/>
<arg type="b" name="unregistered" direction="out"/>
</method>
<property name="RemoteInterfaces" type="as" access="read"/>
</interface>
<!-- <!--
com.steampowered.SteamOSManager1.ScreenReader1 com.steampowered.SteamOSManager1.ScreenReader1
@short_description: Optional interface for managing a screen reader. @short_description: Optional interface for managing a screen reader.

View file

@ -0,0 +1,12 @@
[package]
name = "steamos-manager-macros"
version = "25.6.1"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
syn = "2.0"
proc-macro2 = "1.0"

View file

@ -0,0 +1,418 @@
/*
* Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software
* Copyright © 2024 Igalia S.L.
*
* SPDX-License-Identifier: MIT
*/
use proc_macro::TokenStream;
use proc_macro2::{Group, Literal, TokenStream as TokenStream2, TokenTree};
use quote::{format_ident, quote, ToTokens};
use std::collections::HashMap;
use syn::parse::{self, Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{
self, parse_macro_input, Attribute, FnArg, GenericArgument, Ident, ImplItem, ItemImpl, Meta,
PathArguments, ReturnType, Type,
};
#[derive(Debug)]
struct Interface {
name: String,
properties: Vec<Property>,
methods: Vec<Method>,
}
#[derive(Debug)]
struct Method {
name: Ident,
args: Vec<Type>,
ret: Option<Type>,
}
#[derive(Debug)]
struct Property {
name: Ident,
attr: Attribute,
emits_changed: bool,
ty: Type,
setter: bool,
}
fn clean_return_type(ty: Type) -> Type {
match ty {
Type::Path(ref path) => {
if let Some(tail) = path.path.segments.last() {
if tail.ident == "Result" {
match &tail.arguments {
PathArguments::None => ty,
PathArguments::AngleBracketed(args) => match args.args.first() {
Some(GenericArgument::Type(ty)) => ty.clone(),
_ => todo!(),
},
PathArguments::Parenthesized(_) => todo!("parenthesized return type"),
}
} else {
ty
}
} else {
todo!("no tail");
}
}
other => todo!("unimplemented return type {other:?}"),
}
}
fn parse_kv_pairs(group: Group) -> parse::Result<HashMap<String, Literal>> {
let mut tokens = group.stream().into_iter();
let mut kv = HashMap::new();
loop {
let prop = match tokens.next() {
Some(TokenTree::Ident(prop)) => prop,
Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => continue,
Some(token) => {
return Err(syn::Error::new(token.span(), "expected `,` or identifier"));
}
None => break,
};
let value = {
match tokens.next() {
Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => (),
Some(token) => {
return Err(syn::Error::new(token.span(), "expected `=`"));
}
None => {
return Err(syn::Error::new(group.span_close(), "expected `=`"));
}
}
match tokens.next() {
Some(TokenTree::Literal(lit)) => lit,
Some(token) => {
return Err(syn::Error::new(token.span(), "expected string"));
}
None => {
return Err(syn::Error::new(group.span_close(), "expected string"));
}
}
};
let prop_str = prop.to_string();
if kv.insert(prop_str, value).is_some() {
return Err(syn::Error::new(
prop.span(),
format!("duplicate key \"{prop}\""),
));
}
}
Ok(kv)
}
impl Parse for Interface {
fn parse(input: ParseStream<'_>) -> parse::Result<Interface> {
let iface_impl: ItemImpl = input.parse()?;
let Type::Path(path) = *iface_impl.self_ty else {
return Err(syn::Error::new(input.span(), "Invalid name identifier"));
};
let name = path.path.require_ident()?;
let mut properties = Vec::new();
let mut methods = Vec::new();
for item in iface_impl.items {
let ImplItem::Fn(fn_item) = item else {
continue;
};
let mut prop_attr = None;
let mut emits_changed = true;
for attr in fn_item.attrs {
let Meta::List(ref list) = attr.meta else {
continue;
};
if list.path.require_ident()? != "zbus" {
continue;
}
let mut tokens = list.tokens.clone().into_iter();
let first = tokens.next();
match first {
Some(TokenTree::Ident(ident)) if ident == "property" => {
prop_attr = Some(attr);
if let Some(TokenTree::Group(group)) = tokens.next() {
let kv = parse_kv_pairs(group)?;
match kv.get("emits_changed_signal") {
None => emits_changed = true,
Some(val) if val.to_string() == "true" => emits_changed = true,
_ => emits_changed = false,
}
}
}
Some(TokenTree::Ident(ident)) if ident == "signal" => {
todo!("signals not implemented")
}
other => todo!("unknown attribute {other:?}"),
}
}
let sig = fn_item.sig;
let name = sig.ident;
let inputs = sig.inputs;
if !matches!(inputs.first(), Some(FnArg::Receiver(_))) {
return Err(syn::Error::new(
sig.paren_token.span.open(),
"expected `self`",
));
}
if let Some(attr) = prop_attr {
let setter = name.to_string().starts_with("set_");
let ty = if setter {
let mut ty = None;
emits_changed = false;
'input: for input in inputs.into_iter().skip(1) {
let span = input.span();
let FnArg::Typed(fty) = input else {
continue;
};
for attr in &fty.attrs {
let Meta::List(ref list) = attr.meta else {
continue;
};
let Some(ident) = list.path.get_ident() else {
continue;
};
if ident == "zbus" {
continue 'input;
}
}
if ty.is_some() {
return Err(syn::Error::new(span, "unexpected argument type"));
}
ty = Some(*fty.ty);
}
ty.unwrap()
} else {
if inputs.len() != 1 {
return Err(syn::Error::new(
sig.paren_token.span.join(),
"expected 1 argument",
));
}
let ReturnType::Type(_, ret) = sig.output else {
return Err(syn::Error::new(sig.fn_token.span, "expected return value"));
};
clean_return_type(*ret)
};
properties.push(Property {
name,
attr,
setter,
ty,
emits_changed,
});
} else {
let ret = match sig.output {
ReturnType::Type(_, ret) => Some(clean_return_type(*ret)),
ReturnType::Default => None,
};
let args = inputs
.into_iter()
.skip(1)
.map(|arg| {
let FnArg::Typed(ty) = arg else {
panic!();
};
*ty.ty
})
.collect();
methods.push(Method { name, args, ret });
}
}
Ok(Interface {
name: name.to_string(),
methods,
properties,
})
}
}
impl ToTokens for Interface {
fn to_tokens(&self, stream: &mut TokenStream2) {
let mut substream = TokenStream2::new();
let mut signals = Vec::new();
for prop in self.properties.iter() {
prop.to_tokens(&mut substream);
if prop.emits_changed {
signals.push(format_ident!("{}_changed", prop.name.clone()));
}
}
for method in self.methods.iter() {
method.to_tokens(&mut substream);
}
let name = format_ident!("{}", self.name);
let struct_name: Ident = format_ident!("{}Remote", self.name);
let proxy_name: Ident = format_ident!("{}Proxy", self.name);
let receivers: Vec<Ident> = signals
.iter()
.map(|name| format_ident!("receive_{name}"))
.collect();
stream.extend(quote! {
impl #struct_name {
#substream
}
struct #struct_name {
proxy: #proxy_name<'static>,
signal_task: JoinHandle<Result<()>>,
interlock: Option<oneshot::Sender<()>>,
}
impl #struct_name {
pub async fn new(
destination: &BusName<'static>,
path: ObjectPath<'static>,
connection: &Connection
)
-> fdo::Result<#struct_name> {
let proxy = #proxy_name::builder(connection)
.path(path)?
.destination(destination)?
.build()
.await?;
let (signal_task, interlock) = #struct_name::signal_task(proxy.clone(), connection.clone())
.await
.map_err(to_zbus_fdo_error)?;
Ok(#struct_name {
proxy,
signal_task,
interlock: Some(interlock),
})
}
fn remote(&self) -> &BusName<'_> {
self.proxy.inner().destination()
}
async fn signal_task(
proxy: #proxy_name<'static>,
connection: Connection
) -> Result<(JoinHandle<Result<()>>, oneshot::Sender<()>)> {
let (tx1, rx1) = oneshot::channel();
let (tx2, rx2) = oneshot::channel();
let handle = spawn(async move {
let object_server = connection.object_server();
let dbus_proxy = DBusProxy::new(&connection).await?;
let mut name_changed_receiver = dbus_proxy.receive_name_owner_changed().await?;
#(let mut #receivers = proxy.#receivers().await;)*
// This should never fail. If it does, something has gone very wrong.
tx1.send(()).unwrap();
rx2.await?;
let mut interface = object_server
.interface::<_, #struct_name>(MANAGER_PATH)
.await?;
let emitter = interface.signal_emitter();
loop {
tokio::select! {
Some(changed) = name_changed_receiver.next() => {
match changed.args() {
Ok(args) => {
if args.name() != proxy.inner().destination() {
continue;
}
if args.new_owner().is_none() {
let manager = object_server
.interface::<_, RemoteInterface1>(MANAGER_PATH)
.await?;
let emitter = manager.signal_emitter();
manager
.get_mut()
.await
.unregister_interface_impl(
Self::name().as_str(),
None,
&connection,
emitter
)
.await?;
}
},
Err(e) => error!("Error receiving signal: {e}"),
}
},
#(Some(val) = #receivers.next() => {
if let Err(e) = interface.get().await.#signals(&emitter).await {
error!("Error receiving signal: {e}");
};
},)*
}
}
});
rx1.await?;
Ok((handle, tx2))
}
}
impl Drop for #struct_name {
fn drop(&mut self) {
self.signal_task.abort();
}
}
impl RemoteInterface for #name {
type Remote = #struct_name;
}
});
}
}
impl ToTokens for Method {
fn to_tokens(&self, stream: &mut TokenStream2) {
let name = &self.name;
let args = &self.args;
let ret = &self.ret;
let arg_names: Vec<Ident> = (0..args.len()).map(|i| format_ident!("arg{i}")).collect();
stream.extend(quote! {
async fn #name(&self #(, #arg_names: #args)*) -> fdo::Result<#ret> {
self.proxy.#name(#(#arg_names),*).await.map_err(zbus_to_zbus_fdo)
}
});
}
}
impl ToTokens for Property {
fn to_tokens(&self, stream: &mut TokenStream2) {
let attr = &self.attr;
let ty = &self.ty;
let name = &self.name;
if self.setter {
stream.extend(quote! {
#attr
async fn #name(&self, arg: #ty) -> zbus::Result<()> {
self.proxy.#name(arg).await
}
});
} else {
stream.extend(quote! {
#attr
async fn #name(&self) -> fdo::Result<#ty> {
Ok(self.proxy.#name().await?)
}
});
}
}
}
#[proc_macro_attribute]
pub fn remote(attr: TokenStream, input: TokenStream) -> TokenStream {
let attr: TokenStream2 = attr.into();
let imp: TokenStream2 = input.clone().into();
let iface = parse_macro_input!(input as Interface);
let out = quote! {
#[interface(#attr)]
#iface
#[interface(#attr)]
#imp
};
out.into()
}

View file

@ -25,6 +25,7 @@ mod hdmi_cec1;
mod low_power_mode1; mod low_power_mode1;
mod manager2; mod manager2;
mod performance_profile1; mod performance_profile1;
mod remote_interface1;
mod screenreader0; mod screenreader0;
mod storage1; mod storage1;
mod tdp_limit1; mod tdp_limit1;
@ -44,6 +45,7 @@ pub use crate::hdmi_cec1::HdmiCec1Proxy;
pub use crate::low_power_mode1::LowPowerMode1Proxy; pub use crate::low_power_mode1::LowPowerMode1Proxy;
pub use crate::manager2::Manager2Proxy; pub use crate::manager2::Manager2Proxy;
pub use crate::performance_profile1::PerformanceProfile1Proxy; pub use crate::performance_profile1::PerformanceProfile1Proxy;
pub use crate::remote_interface1::RemoteInterface1Proxy;
pub use crate::screenreader0::ScreenReader0Proxy; pub use crate::screenreader0::ScreenReader0Proxy;
pub use crate::storage1::Storage1Proxy; pub use crate::storage1::Storage1Proxy;
pub use crate::tdp_limit1::TdpLimit1Proxy; pub use crate::tdp_limit1::TdpLimit1Proxy;

View file

@ -0,0 +1,35 @@
//! # D-Bus interface proxy for: `com.steampowered.SteamOSManager1.RemoteInterface1`
//!
//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data.
//! Source: `com.steampowered.SteamOSManager1.xml`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the [Writing a client proxy] section of the zbus
//! documentation.
//!
//!
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
use zbus::proxy;
#[proxy(
interface = "com.steampowered.SteamOSManager1.RemoteInterface1",
default_service = "com.steampowered.SteamOSManager1",
default_path = "/com/steampowered/SteamOSManager1",
assume_defaults = true
)]
pub trait RemoteInterface1 {
/// RegisterInterface method
fn register_interface(
&self,
interface: &str,
object: &zbus::zvariant::ObjectPath<'_>,
) -> zbus::Result<bool>;
/// UnregisterInterface method
fn unregister_interface(&self, interface: &str) -> zbus::Result<bool>;
/// RemoteInterfaces property
#[zbus(property)]
fn remote_interfaces(&self) -> zbus::Result<Vec<String>>;
}

View file

@ -21,6 +21,7 @@ regex = "1"
serde = { version = "1.0", default-features = false, features = ["derive"] } serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
speech-dispatcher = "0.16" speech-dispatcher = "0.16"
steamos-manager-macros = { path = "../steamos-manager-macros" }
steamos-manager-proxy = { path = "../steamos-manager-proxy" } steamos-manager-proxy = { path = "../steamos-manager-proxy" }
strum = { version = "0.27", features = ["derive"] } strum = { version = "0.27", features = ["derive"] }
sysinfo = "0.35" sysinfo = "0.35"

View file

@ -11,12 +11,18 @@ use std::collections::HashMap;
use tokio::fs::try_exists; use tokio::fs::try_exists;
use tokio::sync::mpsc::{Sender, UnboundedSender}; use tokio::sync::mpsc::{Sender, UnboundedSender};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::task::{spawn, JoinHandle};
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tracing::error; use tracing::error;
use zbus::object_server::SignalEmitter; use zbus::fdo::{self, DBusProxy};
use zbus::message::Header;
use zbus::names::{BusName, UniqueName};
use zbus::object_server::{Interface, InterfaceRef, SignalEmitter};
use zbus::proxy::{Builder, CacheProperties}; use zbus::proxy::{Builder, CacheProperties};
use zbus::zvariant::Fd; use zbus::zvariant::{Fd, ObjectPath};
use zbus::{fdo, interface, zvariant, Connection, ObjectServer, Proxy}; use zbus::{interface, zvariant, Connection, ObjectServer, Proxy};
use steamos_manager_macros::remote;
use crate::cec::{HdmiCecControl, HdmiCecState}; use crate::cec::{HdmiCecControl, HdmiCecState};
use crate::daemon::user::Command; use crate::daemon::user::Command;
@ -34,6 +40,11 @@ use crate::power::{
get_gpu_clocks, get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile, get_gpu_clocks, get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile,
get_max_charge_level, get_platform_profile, TdpManagerCommand, get_max_charge_level, get_platform_profile, TdpManagerCommand,
}; };
use crate::proxy::{
BatteryChargeLimit1Proxy, FactoryReset1Proxy, FanControl1Proxy, GpuPerformanceLevel1Proxy,
GpuPowerProfile1Proxy, PerformanceProfile1Proxy, Storage1Proxy, UpdateBios1Proxy,
UpdateDock1Proxy,
};
use crate::screenreader::{OrcaManager, ScreenReaderAction, ScreenReaderMode}; use crate::screenreader::{OrcaManager, ScreenReaderAction, ScreenReaderMode};
use crate::wifi::{ use crate::wifi::{
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend, get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
@ -102,6 +113,92 @@ macro_rules! setter {
}; };
} }
macro_rules! register_interface {
(($self:expr, $name:expr, $object:expr, $bus_name:expr, $connection:expr, $ctxt:expr); $($var:ident: $iface:ident,)*) => {
let object_server = $connection.object_server();
let object = $object.to_owned();
match $name {
$(_ if $name == <$iface as Interface>::name().as_str() => {
if $self.$var.is_some() {
return Ok(false);
}
if object_server
.interface::<_, $iface>(MANAGER_PATH)
.await
.is_ok()
{
return Ok(false);
}
if object_server
.interface::<_, <$iface as RemoteInterface>::Remote>(MANAGER_PATH)
.await
.is_ok()
{
return Ok(false);
}
let remote = <$iface as RemoteInterface>::Remote::new(
&$bus_name.to_owned(),
object,
$connection,
)
.await?;
object_server.at(MANAGER_PATH, remote).await?;
let iface = object_server.interface
::<_, <$iface as RemoteInterface>::Remote>(MANAGER_PATH).await?;
if let Some(interlock) = iface.get_mut().await.interlock.take() {
let _ = interlock.send(());
}
$self.$var = Some(iface);
$self.remote_interfaces_changed(&$ctxt).await?;
Ok(true)
})*
_ => {
Err(fdo::Error::InvalidArgs(format!(
"Unknown interface {}", $name
)))
}
}
};
}
macro_rules! unregister_interface {
(($self:expr, $name:expr, $sender:expr, $connection:expr, $ctxt:expr); $($var:ident: $iface:ident,)*) => {
let object_server = $connection.object_server();
match $name {
$(_ if $name == <$iface as Interface>::name().as_str() => {
let Some(iface) = $self.$var.as_ref() else {
return Ok(false);
};
if let Some(sender) = $sender {
let iface = iface.get().await;
let remote = iface.remote();
if remote != sender {
return Err(fdo::Error::AccessDenied(format!(
"Interface {} is owned by a different remote", $name
)));
}
}
object_server.remove::<$iface, _>(MANAGER_PATH).await?;
$self.$var = None;
$self.remote_interfaces_changed($ctxt).await?;
Ok(true)
})*
_ => {
Err(fdo::Error::InvalidArgs(format!(
"Unknown interface {}", $name
)))
}
}
};
}
trait RemoteInterface {
type Remote: Interface;
}
struct SteamOSManager { struct SteamOSManager {
proxy: Proxy<'static>, proxy: Proxy<'static>,
_job_manager: UnboundedSender<JobManagerCommand>, _job_manager: UnboundedSender<JobManagerCommand>,
@ -157,6 +254,19 @@ struct PerformanceProfile1 {
tdp_limit_manager: Option<UnboundedSender<TdpManagerCommand>>, tdp_limit_manager: Option<UnboundedSender<TdpManagerCommand>>,
} }
#[derive(Default)]
struct RemoteInterface1 {
remote_battery_charge_limit1: Option<InterfaceRef<BatteryChargeLimit1Remote>>,
remote_factory_reset1: Option<InterfaceRef<FactoryReset1Remote>>,
remote_fan_control1: Option<InterfaceRef<FanControl1Remote>>,
remote_gpu_performance_level1: Option<InterfaceRef<GpuPerformanceLevel1Remote>>,
remote_gpu_power_profile1: Option<InterfaceRef<GpuPowerProfile1Remote>>,
remote_performance_profile1: Option<InterfaceRef<PerformanceProfile1Remote>>,
remote_storage1: Option<InterfaceRef<Storage1Remote>>,
remote_update_bios1: Option<InterfaceRef<UpdateBios1Remote>>,
remote_update_dock1: Option<InterfaceRef<UpdateDock1Remote>>,
}
struct ScreenReader0 { struct ScreenReader0 {
screen_reader: OrcaManager<'static>, screen_reader: OrcaManager<'static>,
} }
@ -270,7 +380,7 @@ impl BatteryChargeLimit1 {
const DEFAULT_SUGGESTED_MINIMUM_LIMIT: i32 = 10; const DEFAULT_SUGGESTED_MINIMUM_LIMIT: i32 = 10;
} }
#[interface(name = "com.steampowered.SteamOSManager1.BatteryChargeLimit1")] #[remote(name = "com.steampowered.SteamOSManager1.BatteryChargeLimit1")]
impl BatteryChargeLimit1 { impl BatteryChargeLimit1 {
#[zbus(property)] #[zbus(property)]
async fn max_charge_level(&self) -> fdo::Result<i32> { async fn max_charge_level(&self) -> fdo::Result<i32> {
@ -322,7 +432,7 @@ impl CpuScaling1 {
#[zbus(property)] #[zbus(property)]
async fn set_cpu_scaling_governor( async fn set_cpu_scaling_governor(
&self, &self,
governor: String, governor: &str,
#[zbus(signal_emitter)] ctx: SignalEmitter<'_>, #[zbus(signal_emitter)] ctx: SignalEmitter<'_>,
) -> zbus::Result<()> { ) -> zbus::Result<()> {
let _: () = self let _: () = self
@ -333,14 +443,14 @@ impl CpuScaling1 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.FactoryReset1")] #[remote(name = "com.steampowered.SteamOSManager1.FactoryReset1")]
impl FactoryReset1 { impl FactoryReset1 {
async fn prepare_factory_reset(&self, flags: u32) -> fdo::Result<u32> { async fn prepare_factory_reset(&self, flags: u32) -> fdo::Result<u32> {
method!(self, "PrepareFactoryReset", flags) method!(self, "PrepareFactoryReset", flags)
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.FanControl1")] #[remote(name = "com.steampowered.SteamOSManager1.FanControl1")]
impl FanControl1 { impl FanControl1 {
#[zbus(property)] #[zbus(property)]
async fn fan_control_state(&self) -> fdo::Result<u32> { async fn fan_control_state(&self) -> fdo::Result<u32> {
@ -358,7 +468,7 @@ impl FanControl1 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.GpuPerformanceLevel1")] #[remote(name = "com.steampowered.SteamOSManager1.GpuPerformanceLevel1")]
impl GpuPerformanceLevel1 { impl GpuPerformanceLevel1 {
#[zbus(property(emits_changed_signal = "const"))] #[zbus(property(emits_changed_signal = "const"))]
async fn available_gpu_performance_levels(&self) -> fdo::Result<Vec<String>> { async fn available_gpu_performance_levels(&self) -> fdo::Result<Vec<String>> {
@ -425,7 +535,7 @@ impl GpuPerformanceLevel1 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.GpuPowerProfile1")] #[remote(name = "com.steampowered.SteamOSManager1.GpuPowerProfile1")]
impl GpuPowerProfile1 { impl GpuPowerProfile1 {
#[zbus(property(emits_changed_signal = "const"))] #[zbus(property(emits_changed_signal = "const"))]
async fn available_gpu_power_profiles(&self) -> fdo::Result<Vec<String>> { async fn available_gpu_power_profiles(&self) -> fdo::Result<Vec<String>> {
@ -547,7 +657,7 @@ impl Manager2 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.PerformanceProfile1")] #[remote(name = "com.steampowered.SteamOSManager1.PerformanceProfile1")]
impl PerformanceProfile1 { impl PerformanceProfile1 {
#[zbus(property(emits_changed_signal = "const"))] #[zbus(property(emits_changed_signal = "const"))]
async fn available_performance_profiles(&self) -> fdo::Result<Vec<String>> { async fn available_performance_profiles(&self) -> fdo::Result<Vec<String>> {
@ -590,7 +700,7 @@ impl PerformanceProfile1 {
if let Some(manager) = self.tdp_limit_manager.as_ref() { if let Some(manager) = self.tdp_limit_manager.as_ref() {
let manager = manager.clone(); let manager = manager.clone();
let _ = manager.send(TdpManagerCommand::UpdateDownloadMode); let _ = manager.send(TdpManagerCommand::UpdateDownloadMode);
tokio::spawn(async move { spawn(async move {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
manager.send(TdpManagerCommand::IsActive(tx))?; manager.send(TdpManagerCommand::IsActive(tx))?;
if rx.await?? { if rx.await?? {
@ -624,6 +734,122 @@ impl PerformanceProfile1 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.RemoteInterface1")]
impl RemoteInterface1 {
async fn register_interface(
&mut self,
iface: &str,
object: ObjectPath<'_>,
#[zbus(header)] header: Header<'_>,
#[zbus(connection)] connection: &Connection,
#[zbus(signal_emitter)] ctxt: SignalEmitter<'_>,
) -> fdo::Result<bool> {
let Some(sender) = header.sender() else {
return Err(fdo::Error::InvalidArgs(String::from("Sender missing")));
};
let bus_name = BusName::Unique(sender.to_owned());
self.register_interface_impl(iface, object, &bus_name, connection, &ctxt)
.await
}
async fn unregister_interface(
&mut self,
iface: &str,
#[zbus(header)] header: Header<'_>,
#[zbus(connection)] connection: &Connection,
#[zbus(signal_emitter)] ctxt: SignalEmitter<'_>,
) -> fdo::Result<bool> {
let sender = header.sender();
if sender.is_none() {
return Err(fdo::Error::InvalidArgs(String::from("Sender missing")));
};
self.unregister_interface_impl(iface, sender, connection, &ctxt)
.await
}
#[zbus(property)]
async fn remote_interfaces(&self) -> Vec<String> {
let mut ifaces = Vec::new();
if self.remote_battery_charge_limit1.is_some() {
ifaces.push(BatteryChargeLimit1::name().to_string());
}
if self.remote_factory_reset1.is_some() {
ifaces.push(FactoryReset1::name().to_string());
}
if self.remote_fan_control1.is_some() {
ifaces.push(FanControl1::name().to_string());
}
if self.remote_gpu_performance_level1.is_some() {
ifaces.push(GpuPerformanceLevel1::name().to_string());
}
if self.remote_gpu_power_profile1.is_some() {
ifaces.push(GpuPowerProfile1::name().to_string());
}
if self.remote_performance_profile1.is_some() {
ifaces.push(PerformanceProfile1::name().to_string());
}
if self.remote_storage1.is_some() {
ifaces.push(Storage1::name().to_string());
}
if self.remote_update_bios1.is_some() {
ifaces.push(UpdateBios1::name().to_string());
}
if self.remote_update_dock1.is_some() {
ifaces.push(UpdateDock1::name().to_string());
}
ifaces
}
}
impl RemoteInterface1 {
async fn register_interface_impl(
&mut self,
iface: &str,
object: ObjectPath<'_>,
bus_name: &BusName<'_>,
connection: &Connection,
ctxt: &SignalEmitter<'_>,
) -> fdo::Result<bool> {
register_interface! {
(self, iface, object, bus_name, connection, ctxt);
remote_battery_charge_limit1: BatteryChargeLimit1,
remote_factory_reset1: FactoryReset1,
remote_fan_control1: FanControl1,
remote_gpu_performance_level1: GpuPerformanceLevel1,
remote_gpu_power_profile1: GpuPowerProfile1,
remote_performance_profile1: PerformanceProfile1,
remote_storage1: Storage1,
remote_update_bios1: UpdateBios1,
remote_update_dock1: UpdateDock1,
}
}
async fn unregister_interface_impl(
&mut self,
iface: &str,
sender: Option<&UniqueName<'_>>,
connection: &Connection,
ctxt: &SignalEmitter<'_>,
) -> fdo::Result<bool> {
unregister_interface! {
(self, iface, sender, connection, ctxt);
remote_battery_charge_limit1: BatteryChargeLimit1,
remote_factory_reset1: FactoryReset1,
remote_fan_control1: FanControl1,
remote_gpu_performance_level1: GpuPerformanceLevel1,
remote_gpu_power_profile1: GpuPowerProfile1,
remote_performance_profile1: PerformanceProfile1,
remote_storage1: Storage1,
remote_update_bios1: UpdateBios1,
remote_update_dock1: UpdateDock1,
}
}
}
impl ScreenReader0 { impl ScreenReader0 {
async fn new(connection: &Connection) -> Result<ScreenReader0> { async fn new(connection: &Connection) -> Result<ScreenReader0> {
let screen_reader = OrcaManager::new(connection).await?; let screen_reader = OrcaManager::new(connection).await?;
@ -747,7 +973,7 @@ impl ScreenReader0 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.Storage1")] #[remote(name = "com.steampowered.SteamOSManager1.Storage1")]
impl Storage1 { impl Storage1 {
async fn format_device( async fn format_device(
&mut self, &mut self,
@ -820,14 +1046,14 @@ impl TdpLimit1 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.UpdateBios1")] #[remote(name = "com.steampowered.SteamOSManager1.UpdateBios1")]
impl UpdateBios1 { impl UpdateBios1 {
async fn update_bios(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> { async fn update_bios(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
job_method!(self, "UpdateBios") job_method!(self, "UpdateBios")
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.UpdateDock1")] #[remote(name = "com.steampowered.SteamOSManager1.UpdateDock1")]
impl UpdateDock1 { impl UpdateDock1 {
async fn update_dock(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> { async fn update_dock(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
job_method!(self, "UpdateDock") job_method!(self, "UpdateDock")
@ -1049,7 +1275,7 @@ async fn create_device_interfaces(
} }
let object_server = object_server.clone(); let object_server = object_server.clone();
tokio::spawn(async move { spawn(async move {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
manager.send(TdpManagerCommand::IsActive(tx))?; manager.send(TdpManagerCommand::IsActive(tx))?;
if rx.await?? { if rx.await?? {
@ -1110,6 +1336,7 @@ pub(crate) async fn create_interfaces(
proxy: proxy.clone(), proxy: proxy.clone(),
channel: daemon, channel: daemon,
}; };
let remote_interface = RemoteInterface1::default();
let screen_reader = ScreenReader0::new(&session).await?; let screen_reader = ScreenReader0::new(&session).await?;
let wifi_power_management = WifiPowerManagement1 { let wifi_power_management = WifiPowerManagement1 {
proxy: proxy.clone(), proxy: proxy.clone(),
@ -1164,6 +1391,7 @@ pub(crate) async fn create_interfaces(
} }
object_server.at(MANAGER_PATH, manager2).await?; object_server.at(MANAGER_PATH, manager2).await?;
object_server.at(MANAGER_PATH, remote_interface).await?;
if try_exists(path("/usr/bin/orca")).await? { if try_exists(path("/usr/bin/orca")).await? {
object_server.at(MANAGER_PATH, screen_reader).await?; object_server.at(MANAGER_PATH, screen_reader).await?;
@ -1192,9 +1420,11 @@ mod test {
FormatDeviceConfig, PlatformConfig, ResetConfig, ScriptConfig, ServiceConfig, StorageConfig, FormatDeviceConfig, PlatformConfig, ResetConfig, ScriptConfig, ServiceConfig, StorageConfig,
}; };
use crate::power::TdpLimitingMethod; use crate::power::TdpLimitingMethod;
use crate::proxy::RemoteInterface1Proxy;
use crate::systemd::test::{MockManager, MockUnit}; use crate::systemd::test::{MockManager, MockUnit};
use crate::{path, power, testing}; use crate::{path, power, testing};
use anyhow::{anyhow, ensure};
use std::num::NonZeroU32; use std::num::NonZeroU32;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf; use std::path::PathBuf;
@ -1203,10 +1433,9 @@ mod test {
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
use tokio::time::sleep; use tokio::time::sleep;
use zbus::object_server::Interface; use zbus::object_server::Interface;
use zbus::Connection;
struct TestHandle { struct TestHandle {
_handle: testing::TestHandle, handle: testing::TestHandle,
connection: Connection, connection: Connection,
_rx_job: UnboundedReceiver<JobManagerCommand>, _rx_job: UnboundedReceiver<JobManagerCommand>,
rx_tdp: Option<UnboundedReceiver<TdpManagerCommand>>, rx_tdp: Option<UnboundedReceiver<TdpManagerCommand>>,
@ -1324,7 +1553,7 @@ mod test {
sleep(Duration::from_millis(1)).await; sleep(Duration::from_millis(1)).await;
Ok(TestHandle { Ok(TestHandle {
_handle: handle, handle,
connection, connection,
_rx_job: rx_job, _rx_job: rx_job,
rx_tdp, rx_tdp,
@ -1603,6 +1832,17 @@ mod test {
assert!(test_interface_missing::<PerformanceProfile1>(&test.connection).await); assert!(test_interface_missing::<PerformanceProfile1>(&test.connection).await);
} }
#[tokio::test]
async fn interface_matches_remote_interface1() {
let test = start(all_platform_config(), all_device_config())
.await
.expect("start");
assert!(test_interface_matches::<RemoteInterface1>(&test.connection)
.await
.unwrap());
}
#[tokio::test] #[tokio::test]
async fn interface_matches_storage1() { async fn interface_matches_storage1() {
let test = start(all_platform_config(), all_device_config()) let test = start(all_platform_config(), all_device_config())
@ -1746,4 +1986,149 @@ mod test {
.await .await
.unwrap()); .unwrap());
} }
async fn test_remote_interface_added<I: RemoteInterface + Interface>(
test: &TestHandle,
new_conn: &Connection,
) -> Result<()> {
let proxy = RemoteInterface1Proxy::builder(&new_conn)
.destination(
test.connection
.unique_name()
.ok_or(anyhow!("no unique name"))?,
)?
.build()
.await?;
ensure!(test_remote_interface_missing::<I>(&proxy, test).await?);
proxy
.register_interface(
<I as Interface>::name().as_str(),
&ObjectPath::try_from("/foo")?,
)
.await?;
ensure!(!test_remote_interface_missing::<I>(&proxy, test).await?);
Ok(())
}
async fn test_remote_interface_missing<I: RemoteInterface + Interface>(
proxy: &RemoteInterface1Proxy<'_>,
test: &TestHandle,
) -> Result<bool> {
Ok(!proxy
.remote_interfaces()
.await?
.contains(&<I as Interface>::name().to_string())
&& test_interface_missing::<<I as RemoteInterface>::Remote>(&test.connection).await)
}
#[tokio::test]
async fn remote_battery_charge_limit1() {
let test = start(None, None).await.unwrap();
let new_conn = test.handle.new_connection().await.unwrap();
test_remote_interface_added::<BatteryChargeLimit1>(&test, &new_conn)
.await
.unwrap();
let new_conn = test.handle.new_connection().await.unwrap();
let proxy = RemoteInterface1Proxy::builder(&new_conn)
.destination(test.connection.unique_name().unwrap())
.unwrap()
.build()
.await
.unwrap();
assert!(
!test_remote_interface_missing::<BatteryChargeLimit1>(&proxy, &test)
.await
.unwrap()
);
}
#[tokio::test]
async fn remote_battery_charge_limit1_dropped() {
let test = start(None, None).await.unwrap();
let new_conn = test.handle.new_connection().await.unwrap();
test_remote_interface_added::<BatteryChargeLimit1>(&test, &new_conn)
.await
.unwrap();
drop(new_conn);
let new_conn = test.handle.new_connection().await.unwrap();
let proxy = RemoteInterface1Proxy::builder(&new_conn)
.destination(test.connection.unique_name().unwrap())
.unwrap()
.build()
.await
.unwrap();
assert!(
test_remote_interface_missing::<BatteryChargeLimit1>(&proxy, &test)
.await
.unwrap()
);
}
#[tokio::test]
async fn remote_battery_charge_limit1_removed() {
let test = start(None, None).await.unwrap();
let new_conn = test.handle.new_connection().await.unwrap();
test_remote_interface_added::<BatteryChargeLimit1>(&test, &new_conn)
.await
.unwrap();
let proxy = RemoteInterface1Proxy::builder(&new_conn)
.destination(test.connection.unique_name().unwrap())
.unwrap()
.build()
.await
.unwrap();
assert!(proxy
.unregister_interface(BatteryChargeLimit1::name().as_str())
.await
.unwrap());
assert!(
test_remote_interface_missing::<BatteryChargeLimit1>(&proxy, &test)
.await
.unwrap()
);
}
#[tokio::test]
async fn remote_battery_charge_limit1_not_removed() {
let test = start(None, None).await.unwrap();
let new_conn = test.handle.new_connection().await.unwrap();
test_remote_interface_added::<BatteryChargeLimit1>(&test, &new_conn)
.await
.unwrap();
let new_conn = test.handle.new_connection().await.unwrap();
let proxy = RemoteInterface1Proxy::builder(&new_conn)
.destination(test.connection.unique_name().unwrap())
.unwrap()
.build()
.await
.unwrap();
assert!(proxy
.unregister_interface(BatteryChargeLimit1::name().as_str())
.await
.is_err());
assert!(
!test_remote_interface_missing::<BatteryChargeLimit1>(&proxy, &test)
.await
.unwrap()
);
}
} }

View file

@ -185,6 +185,14 @@ impl TestHandle {
pub async fn dbus_address(&self) -> Option<Address> { pub async fn dbus_address(&self) -> Option<Address> {
(*self.test.dbus_address.lock().await).clone() (*self.test.dbus_address.lock().await).clone()
} }
pub async fn new_connection(&self) -> Result<Connection> {
Ok(
Builder::address(self.dbus_address().await.ok_or(anyhow!("No address"))?)?
.build()
.await?,
)
}
} }
impl Drop for TestHandle { impl Drop for TestHandle {