mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
374 lines
12 KiB
Rust
374 lines
12 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use std::time::Duration;
|
|
|
|
pub const PORTAL_DESTINATION: &str = "org.freedesktop.portal.Desktop";
|
|
pub const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
|
|
pub const INHIBIT_INTERFACE: &str = "org.freedesktop.portal.Inhibit";
|
|
pub const SESSION_INTERFACE: &str = "org.freedesktop.portal.Session";
|
|
pub const BUS_NAME: &str = "org.freedesktop.DBus";
|
|
pub const BUS_PATH: &str = "/org/freedesktop/DBus";
|
|
pub const SETUP_TIMEOUT: Duration = Duration::from_secs(30);
|
|
pub const SHORT_TIMEOUT: Duration = Duration::from_secs(2);
|
|
pub const QUERY_END: u32 = 2;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MonitorEvent {
|
|
ScreenLocked,
|
|
ScreenUnlocked,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct LockTracker {
|
|
locked: bool,
|
|
}
|
|
|
|
impl LockTracker {
|
|
pub fn update(&mut self, screensaver_active: bool) -> Option<MonitorEvent> {
|
|
if screensaver_active == self.locked {
|
|
return None;
|
|
}
|
|
self.locked = screensaver_active;
|
|
Some(if screensaver_active {
|
|
MonitorEvent::ScreenLocked
|
|
} else {
|
|
MonitorEvent::ScreenUnlocked
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub use worker::SessionMonitor;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
mod worker {
|
|
use std::{cell::RefCell, collections::HashMap, sync::Arc, thread};
|
|
|
|
use async_channel::{Receiver, Sender};
|
|
use async_io::Timer;
|
|
use futures_lite::{FutureExt, StreamExt};
|
|
use zbus::{
|
|
Connection, MatchRule, Message, MessageStream,
|
|
message::Type as MessageType,
|
|
names::OwnedUniqueName,
|
|
zvariant::{DynamicType, OwnedObjectPath, OwnedValue, Value},
|
|
};
|
|
|
|
use super::*;
|
|
use crate::portal::{REQUEST_INTERFACE, mint_token, request_path};
|
|
|
|
type EventSink = Arc<dyn Fn(MonitorEvent) + Send + Sync + 'static>;
|
|
type Results = HashMap<String, OwnedValue>;
|
|
|
|
pub struct SessionMonitor {
|
|
shutdown: Sender<()>,
|
|
}
|
|
|
|
impl SessionMonitor {
|
|
pub fn spawn(sink: EventSink) -> std::io::Result<Self> {
|
|
let (shutdown, shutdown_rx) = async_channel::bounded(1);
|
|
thread::Builder::new()
|
|
.name("fluxer-linux-portals-session-monitor".to_string())
|
|
.spawn(move || async_io::block_on(run(sink, shutdown_rx)))?;
|
|
Ok(Self { shutdown })
|
|
}
|
|
|
|
pub fn close(&self) {
|
|
self.shutdown.close();
|
|
}
|
|
}
|
|
|
|
impl Drop for SessionMonitor {
|
|
fn drop(&mut self) {
|
|
self.close();
|
|
}
|
|
}
|
|
|
|
struct Monitor {
|
|
owner: OwnedUniqueName,
|
|
handle: OwnedObjectPath,
|
|
}
|
|
|
|
enum Next {
|
|
Owner(Message),
|
|
State(Message),
|
|
End,
|
|
}
|
|
|
|
async fn run(sink: EventSink, shutdown: Receiver<()>) {
|
|
let Ok(conn) = Connection::session().await else {
|
|
return;
|
|
};
|
|
let current = RefCell::new(None::<Monitor>);
|
|
watch(&conn, &sink, ¤t)
|
|
.or(async {
|
|
let _ = shutdown.recv().await;
|
|
})
|
|
.await;
|
|
if let Some(monitor) = current.take() {
|
|
let _ = call(
|
|
&conn,
|
|
PORTAL_DESTINATION,
|
|
monitor.handle.as_str(),
|
|
SESSION_INTERFACE,
|
|
"Close",
|
|
&(),
|
|
SHORT_TIMEOUT,
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
async fn watch(conn: &Connection, sink: &EventSink, current: &RefCell<Option<Monitor>>) {
|
|
let Ok(mut owners) = subscribe(conn, owner_rule()).await else {
|
|
return;
|
|
};
|
|
let Ok(mut states) = subscribe(conn, state_rule()).await else {
|
|
return;
|
|
};
|
|
let mut tracker = LockTracker::default();
|
|
let mut attempt = true;
|
|
loop {
|
|
if attempt {
|
|
attempt = false;
|
|
let created = create_monitor(conn).await;
|
|
current.replace(created);
|
|
}
|
|
let next = async {
|
|
match owners.next().await {
|
|
Some(Ok(message)) => Next::Owner(message),
|
|
_ => Next::End,
|
|
}
|
|
}
|
|
.or(async {
|
|
match states.next().await {
|
|
Some(Ok(message)) => Next::State(message),
|
|
_ => Next::End,
|
|
}
|
|
})
|
|
.await;
|
|
match next {
|
|
Next::End => return,
|
|
Next::Owner(message) => {
|
|
let Some(new_owner) = new_portal_owner(&message) else {
|
|
continue;
|
|
};
|
|
let unchanged = current
|
|
.borrow()
|
|
.as_ref()
|
|
.is_some_and(|monitor| monitor.owner.as_str() == new_owner);
|
|
if unchanged {
|
|
continue;
|
|
}
|
|
current.replace(None);
|
|
attempt = !new_owner.is_empty();
|
|
}
|
|
Next::State(message) => {
|
|
let Some((handle, active, session_state)) =
|
|
state_change(&message, current.borrow().as_ref())
|
|
else {
|
|
continue;
|
|
};
|
|
if session_state == QUERY_END {
|
|
let _ = call(
|
|
conn,
|
|
PORTAL_DESTINATION,
|
|
PORTAL_PATH,
|
|
INHIBIT_INTERFACE,
|
|
"QueryEndResponse",
|
|
&(&handle,),
|
|
SHORT_TIMEOUT,
|
|
)
|
|
.await;
|
|
}
|
|
if let Some(event) = tracker.update(active) {
|
|
sink(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn subscribe(
|
|
conn: &Connection,
|
|
rule: zbus::Result<MatchRule<'static>>,
|
|
) -> zbus::Result<MessageStream> {
|
|
MessageStream::for_match_rule(rule?, conn, None).await
|
|
}
|
|
|
|
fn owner_rule() -> zbus::Result<MatchRule<'static>> {
|
|
Ok(MatchRule::builder()
|
|
.msg_type(MessageType::Signal)
|
|
.sender(BUS_NAME)?
|
|
.interface(BUS_NAME)?
|
|
.member("NameOwnerChanged")?
|
|
.path(BUS_PATH)?
|
|
.arg(0, PORTAL_DESTINATION)?
|
|
.build())
|
|
}
|
|
|
|
fn state_rule() -> zbus::Result<MatchRule<'static>> {
|
|
Ok(MatchRule::builder()
|
|
.msg_type(MessageType::Signal)
|
|
.sender(PORTAL_DESTINATION)?
|
|
.interface(INHIBIT_INTERFACE)?
|
|
.member("StateChanged")?
|
|
.path(PORTAL_PATH)?
|
|
.build())
|
|
}
|
|
|
|
fn new_portal_owner(message: &Message) -> Option<String> {
|
|
let header = message.header();
|
|
if header
|
|
.sender()
|
|
.is_none_or(|sender| sender.as_str() != BUS_NAME)
|
|
{
|
|
return None;
|
|
}
|
|
let (_name, _old, new): (String, String, String) = message.body().deserialize().ok()?;
|
|
Some(new)
|
|
}
|
|
|
|
fn state_change(
|
|
message: &Message,
|
|
monitor: Option<&Monitor>,
|
|
) -> Option<(OwnedObjectPath, bool, u32)> {
|
|
let monitor = monitor?;
|
|
if message.header().sender() != Some(monitor.owner.inner()) {
|
|
return None;
|
|
}
|
|
let (handle, state): (OwnedObjectPath, Results) = message.body().deserialize().ok()?;
|
|
if handle != monitor.handle {
|
|
return None;
|
|
}
|
|
let active = matches!(
|
|
state.get("screensaver-active").map(unbox),
|
|
Some(Value::Bool(true))
|
|
);
|
|
let session_state = state
|
|
.get("session-state")
|
|
.and_then(|value| crate::kwin::integer_from_variant(unbox(value)))
|
|
.unwrap_or(0);
|
|
Some((handle, active, session_state))
|
|
}
|
|
|
|
fn unbox(value: &OwnedValue) -> &Value<'_> {
|
|
let mut value = crate::kwin::value_of_owned(value);
|
|
while let Value::Value(inner) = value {
|
|
value = inner;
|
|
}
|
|
value
|
|
}
|
|
|
|
async fn create_monitor(conn: &Connection) -> Option<Monitor> {
|
|
let owner_reply = call(
|
|
conn,
|
|
BUS_NAME,
|
|
BUS_PATH,
|
|
BUS_NAME,
|
|
"GetNameOwner",
|
|
&(PORTAL_DESTINATION,),
|
|
SHORT_TIMEOUT,
|
|
)
|
|
.await?;
|
|
let owner: OwnedUniqueName = owner_reply.body().deserialize().ok()?;
|
|
let unique_name = conn.unique_name()?.to_string();
|
|
let token = mint_token("fluxer_sm");
|
|
let session_token = mint_token("fluxer_sm_session");
|
|
let namespace = request_path(&unique_name, "");
|
|
let rule = MatchRule::builder()
|
|
.msg_type(MessageType::Signal)
|
|
.sender(PORTAL_DESTINATION)
|
|
.and_then(|rule| rule.interface(REQUEST_INTERFACE))
|
|
.and_then(|rule| rule.member("Response"))
|
|
.and_then(|rule| rule.path_namespace(namespace.trim_end_matches('/').to_string()))
|
|
.map(|rule| rule.build());
|
|
let mut responses = subscribe(conn, rule).await.ok()?;
|
|
let mut options: HashMap<&str, Value<'_>> = HashMap::new();
|
|
options.insert("handle_token", Value::new(token.as_str()));
|
|
options.insert("session_handle_token", Value::new(session_token.as_str()));
|
|
let reply = call(
|
|
conn,
|
|
PORTAL_DESTINATION,
|
|
PORTAL_PATH,
|
|
INHIBIT_INTERFACE,
|
|
"CreateMonitor",
|
|
&("", options),
|
|
SETUP_TIMEOUT,
|
|
)
|
|
.await?;
|
|
let request = reply
|
|
.body()
|
|
.deserialize::<OwnedObjectPath>()
|
|
.ok()
|
|
.map(|path| path.to_string())
|
|
.unwrap_or_else(|| request_path(&unique_name, &token));
|
|
let response = async {
|
|
while let Some(Ok(message)) = responses.next().await {
|
|
let header = message.header();
|
|
if header.sender() != Some(owner.inner())
|
|
|| header.path().map(|path| path.as_str()) != Some(request.as_str())
|
|
{
|
|
continue;
|
|
}
|
|
return message.body().deserialize::<(u32, Results)>().ok();
|
|
}
|
|
None
|
|
}
|
|
.or(async {
|
|
Timer::after(SETUP_TIMEOUT).await;
|
|
None
|
|
})
|
|
.await;
|
|
let (code, results) = response?;
|
|
if code != 0 {
|
|
return None;
|
|
}
|
|
let handle = match results.get("session_handle").map(unbox)? {
|
|
Value::Str(path) => OwnedObjectPath::try_from(path.as_str()).ok()?,
|
|
Value::ObjectPath(path) => OwnedObjectPath::from(path.to_owned()),
|
|
_ => return None,
|
|
};
|
|
Some(Monitor { owner, handle })
|
|
}
|
|
|
|
async fn call<B>(
|
|
conn: &Connection,
|
|
destination: &str,
|
|
path: &str,
|
|
interface: &str,
|
|
method: &str,
|
|
body: &B,
|
|
timeout: Duration,
|
|
) -> Option<Message>
|
|
where
|
|
B: zbus::export::serde::Serialize + DynamicType,
|
|
{
|
|
async {
|
|
conn.call_method(Some(destination), path, Some(interface), method, body)
|
|
.await
|
|
.ok()
|
|
}
|
|
.or(async {
|
|
Timer::after(timeout).await;
|
|
None
|
|
})
|
|
.await
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn lock_tracker_reports_only_transitions() {
|
|
let mut tracker = LockTracker::default();
|
|
assert_eq!(tracker.update(false), None);
|
|
assert_eq!(tracker.update(true), Some(MonitorEvent::ScreenLocked));
|
|
assert_eq!(tracker.update(true), None);
|
|
assert_eq!(tracker.update(false), Some(MonitorEvent::ScreenUnlocked));
|
|
assert_eq!(tracker.update(false), None);
|
|
}
|
|
}
|