mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-15 10:46:41 -04:00
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:
commit
ac306da8d0
10 changed files with 911 additions and 19 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -1196,6 +1196,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"speech-dispatcher",
|
||||
"steamos-manager-macros",
|
||||
"steamos-manager-proxy",
|
||||
"strum",
|
||||
"sysinfo",
|
||||
|
@ -1212,6 +1213,15 @@ dependencies = [
|
|||
"zbus_xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "steamos-manager-macros"
|
||||
version = "25.6.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "steamos-manager-proxy"
|
||||
version = "25.6.1"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["steamos-manager", "steamos-manager-proxy"]
|
||||
members = ["steamos-manager", "steamos-manager-macros", "steamos-manager-proxy"]
|
||||
|
||||
[profile.release]
|
||||
strip="symbols"
|
||||
|
|
|
@ -298,6 +298,27 @@
|
|||
|
||||
</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
|
||||
@short_description: Optional interface for managing a screen reader.
|
||||
|
|
12
steamos-manager-macros/Cargo.toml
Normal file
12
steamos-manager-macros/Cargo.toml
Normal 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"
|
418
steamos-manager-macros/src/lib.rs
Normal file
418
steamos-manager-macros/src/lib.rs
Normal 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()
|
||||
}
|
|
@ -25,6 +25,7 @@ mod hdmi_cec1;
|
|||
mod low_power_mode1;
|
||||
mod manager2;
|
||||
mod performance_profile1;
|
||||
mod remote_interface1;
|
||||
mod screenreader0;
|
||||
mod storage1;
|
||||
mod tdp_limit1;
|
||||
|
@ -44,6 +45,7 @@ pub use crate::hdmi_cec1::HdmiCec1Proxy;
|
|||
pub use crate::low_power_mode1::LowPowerMode1Proxy;
|
||||
pub use crate::manager2::Manager2Proxy;
|
||||
pub use crate::performance_profile1::PerformanceProfile1Proxy;
|
||||
pub use crate::remote_interface1::RemoteInterface1Proxy;
|
||||
pub use crate::screenreader0::ScreenReader0Proxy;
|
||||
pub use crate::storage1::Storage1Proxy;
|
||||
pub use crate::tdp_limit1::TdpLimit1Proxy;
|
||||
|
|
35
steamos-manager-proxy/src/remote_interface1.rs
Normal file
35
steamos-manager-proxy/src/remote_interface1.rs
Normal 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>>;
|
||||
}
|
|
@ -21,6 +21,7 @@ regex = "1"
|
|||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
speech-dispatcher = "0.16"
|
||||
steamos-manager-macros = { path = "../steamos-manager-macros" }
|
||||
steamos-manager-proxy = { path = "../steamos-manager-proxy" }
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
sysinfo = "0.35"
|
||||
|
|
|
@ -11,12 +11,18 @@ use std::collections::HashMap;
|
|||
use tokio::fs::try_exists;
|
||||
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::{spawn, JoinHandle};
|
||||
use tokio_stream::StreamExt;
|
||||
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::zvariant::Fd;
|
||||
use zbus::{fdo, interface, zvariant, Connection, ObjectServer, Proxy};
|
||||
use zbus::zvariant::{Fd, ObjectPath};
|
||||
use zbus::{interface, zvariant, Connection, ObjectServer, Proxy};
|
||||
|
||||
use steamos_manager_macros::remote;
|
||||
|
||||
use crate::cec::{HdmiCecControl, HdmiCecState};
|
||||
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_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::wifi::{
|
||||
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 {
|
||||
proxy: Proxy<'static>,
|
||||
_job_manager: UnboundedSender<JobManagerCommand>,
|
||||
|
@ -157,6 +254,19 @@ struct PerformanceProfile1 {
|
|||
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 {
|
||||
screen_reader: OrcaManager<'static>,
|
||||
}
|
||||
|
@ -270,7 +380,7 @@ impl BatteryChargeLimit1 {
|
|||
const DEFAULT_SUGGESTED_MINIMUM_LIMIT: i32 = 10;
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.BatteryChargeLimit1")]
|
||||
#[remote(name = "com.steampowered.SteamOSManager1.BatteryChargeLimit1")]
|
||||
impl BatteryChargeLimit1 {
|
||||
#[zbus(property)]
|
||||
async fn max_charge_level(&self) -> fdo::Result<i32> {
|
||||
|
@ -322,7 +432,7 @@ impl CpuScaling1 {
|
|||
#[zbus(property)]
|
||||
async fn set_cpu_scaling_governor(
|
||||
&self,
|
||||
governor: String,
|
||||
governor: &str,
|
||||
#[zbus(signal_emitter)] ctx: SignalEmitter<'_>,
|
||||
) -> zbus::Result<()> {
|
||||
let _: () = self
|
||||
|
@ -333,14 +443,14 @@ impl CpuScaling1 {
|
|||
}
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.FactoryReset1")]
|
||||
#[remote(name = "com.steampowered.SteamOSManager1.FactoryReset1")]
|
||||
impl FactoryReset1 {
|
||||
async fn prepare_factory_reset(&self, flags: u32) -> fdo::Result<u32> {
|
||||
method!(self, "PrepareFactoryReset", flags)
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.FanControl1")]
|
||||
#[remote(name = "com.steampowered.SteamOSManager1.FanControl1")]
|
||||
impl FanControl1 {
|
||||
#[zbus(property)]
|
||||
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 {
|
||||
#[zbus(property(emits_changed_signal = "const"))]
|
||||
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 {
|
||||
#[zbus(property(emits_changed_signal = "const"))]
|
||||
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 {
|
||||
#[zbus(property(emits_changed_signal = "const"))]
|
||||
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() {
|
||||
let manager = manager.clone();
|
||||
let _ = manager.send(TdpManagerCommand::UpdateDownloadMode);
|
||||
tokio::spawn(async move {
|
||||
spawn(async move {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
manager.send(TdpManagerCommand::IsActive(tx))?;
|
||||
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 {
|
||||
async fn new(connection: &Connection) -> Result<ScreenReader0> {
|
||||
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 {
|
||||
async fn format_device(
|
||||
&mut self,
|
||||
|
@ -820,14 +1046,14 @@ impl TdpLimit1 {
|
|||
}
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.UpdateBios1")]
|
||||
#[remote(name = "com.steampowered.SteamOSManager1.UpdateBios1")]
|
||||
impl UpdateBios1 {
|
||||
async fn update_bios(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
|
||||
job_method!(self, "UpdateBios")
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.UpdateDock1")]
|
||||
#[remote(name = "com.steampowered.SteamOSManager1.UpdateDock1")]
|
||||
impl UpdateDock1 {
|
||||
async fn update_dock(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
|
||||
job_method!(self, "UpdateDock")
|
||||
|
@ -1049,7 +1275,7 @@ async fn create_device_interfaces(
|
|||
}
|
||||
|
||||
let object_server = object_server.clone();
|
||||
tokio::spawn(async move {
|
||||
spawn(async move {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
manager.send(TdpManagerCommand::IsActive(tx))?;
|
||||
if rx.await?? {
|
||||
|
@ -1110,6 +1336,7 @@ pub(crate) async fn create_interfaces(
|
|||
proxy: proxy.clone(),
|
||||
channel: daemon,
|
||||
};
|
||||
let remote_interface = RemoteInterface1::default();
|
||||
let screen_reader = ScreenReader0::new(&session).await?;
|
||||
let wifi_power_management = WifiPowerManagement1 {
|
||||
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, remote_interface).await?;
|
||||
|
||||
if try_exists(path("/usr/bin/orca")).await? {
|
||||
object_server.at(MANAGER_PATH, screen_reader).await?;
|
||||
|
@ -1192,9 +1420,11 @@ mod test {
|
|||
FormatDeviceConfig, PlatformConfig, ResetConfig, ScriptConfig, ServiceConfig, StorageConfig,
|
||||
};
|
||||
use crate::power::TdpLimitingMethod;
|
||||
use crate::proxy::RemoteInterface1Proxy;
|
||||
use crate::systemd::test::{MockManager, MockUnit};
|
||||
use crate::{path, power, testing};
|
||||
|
||||
use anyhow::{anyhow, ensure};
|
||||
use std::num::NonZeroU32;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
|
@ -1203,10 +1433,9 @@ mod test {
|
|||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||
use tokio::time::sleep;
|
||||
use zbus::object_server::Interface;
|
||||
use zbus::Connection;
|
||||
|
||||
struct TestHandle {
|
||||
_handle: testing::TestHandle,
|
||||
handle: testing::TestHandle,
|
||||
connection: Connection,
|
||||
_rx_job: UnboundedReceiver<JobManagerCommand>,
|
||||
rx_tdp: Option<UnboundedReceiver<TdpManagerCommand>>,
|
||||
|
@ -1324,7 +1553,7 @@ mod test {
|
|||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
Ok(TestHandle {
|
||||
_handle: handle,
|
||||
handle,
|
||||
connection,
|
||||
_rx_job: rx_job,
|
||||
rx_tdp,
|
||||
|
@ -1603,6 +1832,17 @@ mod test {
|
|||
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]
|
||||
async fn interface_matches_storage1() {
|
||||
let test = start(all_platform_config(), all_device_config())
|
||||
|
@ -1746,4 +1986,149 @@ mod test {
|
|||
.await
|
||||
.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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,14 @@ impl TestHandle {
|
|||
pub async fn dbus_address(&self) -> Option<Address> {
|
||||
(*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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue