Files
fluxer/fluxer_desktop/native/linux-portals/src/background.rs
T

207 lines
6.2 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(target_os = "linux")]
use std::{collections::HashMap, time::Duration};
#[cfg(target_os = "linux")]
use futures_lite::{FutureExt, StreamExt, future};
#[cfg(target_os = "linux")]
use zbus::{
MatchRule, MessageStream, Proxy,
message::Type as MessageType,
zvariant::{OwnedObjectPath, Value},
};
#[cfg(target_os = "linux")]
use crate::portal::{REQUEST_INTERFACE, mint_token, request_path};
pub const PORTAL_DESTINATION: &str = "org.freedesktop.portal.Desktop";
pub const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
pub const BACKGROUND_INTERFACE: &str = "org.freedesktop.portal.Background";
#[cfg(target_os = "linux")]
pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(5 * 60);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RequestOptions {
pub reason: Option<String>,
pub autostart: bool,
pub commandline: Vec<String>,
pub dbus_activatable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestResult {
pub response: u32,
pub background: bool,
pub autostart: bool,
}
impl RequestResult {
pub fn cancelled(&self) -> bool {
self.response != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BackgroundError {
DbusError,
PortalTimeout,
InvalidReply,
SendFailed,
}
impl std::fmt::Display for BackgroundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::DbusError => "DbusError",
Self::PortalTimeout => "PortalTimeout",
Self::InvalidReply => "InvalidReply",
Self::SendFailed => "SendFailed",
};
f.write_str(s)
}
}
#[cfg(target_os = "linux")]
pub fn request_background(options: RequestOptions) -> Result<RequestResult, BackgroundError> {
future::block_on(request_background_async(options))
}
#[cfg(target_os = "linux")]
async fn request_background_async(
options: RequestOptions,
) -> Result<RequestResult, BackgroundError> {
let conn = zbus::Connection::session()
.await
.map_err(|_| BackgroundError::DbusError)?;
let unique_owned = conn
.unique_name()
.ok_or(BackgroundError::DbusError)?
.to_owned();
let unique_name = unique_owned.as_str().to_string();
let handle_token = mint_token("fluxer_bg");
let expected_path = request_path(&unique_name, &handle_token);
let rule = MatchRule::builder()
.msg_type(MessageType::Signal)
.interface(REQUEST_INTERFACE)
.map_err(|_| BackgroundError::DbusError)?
.member("Response")
.map_err(|_| BackgroundError::DbusError)?
.path(expected_path.clone())
.map_err(|_| BackgroundError::DbusError)?
.build();
let mut stream = MessageStream::for_match_rule(rule, &conn, Some(8))
.await
.map_err(|_| BackgroundError::DbusError)?;
send_call(&conn, &handle_token, &options, &expected_path).await?;
loop {
let timeout = async {
async_io::Timer::after(REQUEST_TIMEOUT).await;
None::<zbus::Result<zbus::Message>>
};
match stream.next().or(timeout).await {
Some(Ok(message)) => {
if let Some(parsed) = parse_request_response(&message) {
return Ok(parsed);
}
}
Some(Err(_)) => return Err(BackgroundError::DbusError),
None => return Err(BackgroundError::PortalTimeout),
}
}
}
#[cfg(target_os = "linux")]
fn parse_request_response(message: &zbus::Message) -> Option<RequestResult> {
let body = message.body();
let (response, results): (u32, HashMap<String, zbus::zvariant::OwnedValue>) =
body.deserialize().ok()?;
Some(RequestResult {
response,
background: bool_result(&results, "background").unwrap_or(false),
autostart: bool_result(&results, "autostart").unwrap_or(false),
})
}
#[cfg(target_os = "linux")]
fn bool_result(results: &HashMap<String, zbus::zvariant::OwnedValue>, key: &str) -> Option<bool> {
results
.get(key)
.and_then(|value| bool_from_value(crate::kwin::value_of_owned(value)))
}
#[cfg(target_os = "linux")]
fn bool_from_value(value: &Value<'_>) -> Option<bool> {
match value {
Value::Bool(v) => Some(*v),
Value::Value(inner) => bool_from_value(inner),
_ => None,
}
}
#[cfg(target_os = "linux")]
async fn send_call(
conn: &zbus::Connection,
handle_token: &str,
options: &RequestOptions,
expected_path: &str,
) -> Result<(), BackgroundError> {
let proxy = Proxy::new(conn, PORTAL_DESTINATION, PORTAL_PATH, BACKGROUND_INTERFACE)
.await
.map_err(|_| BackgroundError::DbusError)?;
let mut vardict: HashMap<&str, Value<'_>> = HashMap::new();
vardict.insert("handle_token", Value::new(handle_token));
vardict.insert("autostart", Value::new(options.autostart));
if let Some(reason) = options.reason.as_deref() {
vardict.insert("reason", Value::new(reason));
}
if !options.commandline.is_empty() {
let commandline: Vec<&str> = options.commandline.iter().map(String::as_str).collect();
vardict.insert("commandline", Value::new(commandline));
}
if options.dbus_activatable {
vardict.insert("dbus-activatable", Value::new(true));
}
let reply_path: OwnedObjectPath = proxy
.call("RequestBackground", &("", vardict))
.await
.map_err(|_| BackgroundError::SendFailed)?;
if !reply_path.as_str().is_empty() && reply_path.as_str() != expected_path {
return Err(BackgroundError::InvalidReply);
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn request_background(_options: RequestOptions) -> Result<RequestResult, BackgroundError> {
Err(BackgroundError::DbusError)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_request_does_not_autostart() {
let opts = RequestOptions::default();
assert!(!opts.autostart);
assert!(opts.commandline.is_empty());
}
#[test]
fn nonzero_response_is_cancelled() {
let result = RequestResult {
response: 1,
background: false,
autostart: false,
};
assert!(result.cancelled());
}
}