Implement native inhibition and acknowledged session-ending confirmation
This commit is contained in:
1 parent
cc441c049e
commit
29a71e9974
27 files changed
+1915
-142
No files matched your search
@@ -670,6 +670,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let background = crate::permissions::Background::new(requests.clone());
|
||||
let lockdown = crate::lockdown::Lockdown::new();
|
||||
let shortcuts = crate::shortcuts::GlobalShortcuts::new(requests.clone());
|
||||
let inhibit = crate::inhibit::Inhibit::default();
|
||||
let connection = zbus::connection::Builder::session()?
|
||||
.name("org.freedesktop.impl.portal.desktop.ferese")?
|
||||
.serve_at(PATH, backend.clone())?
|
||||
@@ -680,6 +681,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.serve_at(PATH, background.clone())?
|
||||
.serve_at(PATH, lockdown.clone())?
|
||||
.serve_at(PATH, shortcuts.clone())?
|
||||
.serve_at(PATH, inhibit.clone())?
|
||||
.serve_at(
|
||||
"/org/ferese/ScreenRecorder",
|
||||
RecorderControl(backend.clone()),
|
||||
@@ -689,6 +691,8 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tokio::spawn(settings.watch(connection.clone()));
|
||||
tokio::spawn(background.watch(connection.clone()));
|
||||
tokio::spawn(lockdown.watch(connection.clone()));
|
||||
tokio::spawn(inhibit.clone().watch(connection.clone()));
|
||||
tokio::spawn(inhibit.clone().watch_logind());
|
||||
// Frontend death must revoke every stream, even if Session.Close never arrives.
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
@@ -702,6 +706,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
};
|
||||
requests.revoke_stale(owner.as_deref()).await;
|
||||
shortcuts.revoke_stale(&connection, owner.as_deref()).await;
|
||||
inhibit.revoke_stale(&connection, owner.as_deref()).await;
|
||||
let stale: Vec<_> = backend
|
||||
.sessions
|
||||
.lock()
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use serde_json::Value as Json;
|
||||
use std::{os::unix::net::UnixStream, sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Bridge(Arc<BridgeInner>);
|
||||
|
||||
struct BridgeInner {
|
||||
stream: std::sync::Mutex<UnixStream>,
|
||||
shutdown: UnixStream,
|
||||
}
|
||||
|
||||
impl Bridge {
|
||||
pub(crate) fn connect() -> Result<Self, String> {
|
||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(std::path::PathBuf::from)
|
||||
.ok_or("Missing runtime directory")?
|
||||
.join("ferese/control.sock");
|
||||
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let shutdown = stream.try_clone().map_err(|error| error.to_string())?;
|
||||
Ok(Self(Arc::new(BridgeInner {
|
||||
stream: std::sync::Mutex::new(stream),
|
||||
shutdown,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) async fn call(&self, command: &'static str, args: Json) -> Result<Json, String> {
|
||||
self.request(command, args, false).await
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(&self, since: u64) -> Result<Json, String> {
|
||||
self.request("session-watch", serde_json::json!({"since": since}), true)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn request(&self, command: &'static str, args: Json, wait: bool) -> Result<Json, String> {
|
||||
let bridge = self.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut stream = bridge
|
||||
.0
|
||||
.stream
|
||||
.lock()
|
||||
.map_err(|_| "Compositor connection failed")?;
|
||||
stream
|
||||
.set_read_timeout(if wait {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(2))
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
kind: "command".into(),
|
||||
command: command.into(),
|
||||
args,
|
||||
};
|
||||
ferese_ipc::write_frame(&mut *stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
response.result.ok_or("Missing compositor response".into())
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
}
|
||||
|
||||
pub(crate) async fn closed(&self) {
|
||||
use std::io::Read;
|
||||
let Ok(reader) = self.0.shutdown.try_clone() else {
|
||||
return;
|
||||
};
|
||||
if reader.set_nonblocking(true).is_err() {
|
||||
return;
|
||||
}
|
||||
let Ok(reader) = tokio::io::unix::AsyncFd::new(reader) else {
|
||||
return;
|
||||
};
|
||||
loop {
|
||||
let Ok(mut ready) = reader.readable().await else {
|
||||
return;
|
||||
};
|
||||
match ready.try_io(|reader| {
|
||||
let mut socket = reader.get_ref();
|
||||
socket.read(&mut [0_u8; 1])
|
||||
}) {
|
||||
Ok(_) => return,
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn close(&self) {
|
||||
let _ = self.0.shutdown.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -22,10 +22,20 @@ const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024;
|
||||
type Reply = (u32, Options);
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct Requests(Arc<Mutex<HashMap<String, (String, Arc<Cancel>)>>>);
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Requests(Arc<Mutex<HashMap<String, (String, Arc<Cancel>)>>>, usize);
|
||||
|
||||
impl Default for Requests {
|
||||
fn default() -> Self {
|
||||
Self::with_limit(8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Requests {
|
||||
pub(crate) fn with_limit(limit: usize) -> Self {
|
||||
Self(Default::default(), limit)
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_stale(&self, owner: Option<&str>) {
|
||||
for (request_owner, cancel) in self.0.lock().await.values() {
|
||||
if Some(request_owner.as_str()) != owner {
|
||||
@@ -45,7 +55,7 @@ impl Requests {
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Invalid request path".into()));
|
||||
}
|
||||
let mut pending = self.0.lock().await;
|
||||
if pending.len() >= 8 || pending.contains_key(handle.as_str()) {
|
||||
if pending.len() >= self.1 || pending.contains_key(handle.as_str()) {
|
||||
return Err(zbus::fdo::Error::LimitsExceeded(
|
||||
"Request limit reached".into(),
|
||||
));
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedFd, OwnedObjectPath, Value},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Cancel, Options, authorize},
|
||||
bridge::Bridge,
|
||||
desktop::Requests,
|
||||
};
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
|
||||
struct Monitor {
|
||||
owner: String,
|
||||
cancel: Arc<Cancel>,
|
||||
last: Mutex<Option<(bool, u32, u64, u32)>>,
|
||||
bridge: Bridge,
|
||||
native_owner: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Inhibit {
|
||||
requests: Requests,
|
||||
monitors: Arc<Mutex<HashMap<String, Arc<Monitor>>>>,
|
||||
changed: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl Default for Inhibit {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
requests: Requests::with_limit(24),
|
||||
monitors: Default::default(),
|
||||
changed: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Inhibit {
|
||||
pub(crate) async fn revoke_stale(&self, connection: &Connection, owner: Option<&str>) {
|
||||
self.requests.revoke_stale(owner).await;
|
||||
let stale = self
|
||||
.monitors
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.filter(|(_, monitor)| Some(monitor.owner.as_str()) != owner)
|
||||
.map(|(path, _)| path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for path in stale {
|
||||
self.end(connection, &path).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn end(&self, connection: &Connection, path: &str) {
|
||||
if let Some(monitor) = self.monitors.lock().await.remove(path) {
|
||||
let _state = monitor.last.lock().await;
|
||||
monitor.cancel.stop();
|
||||
monitor.bridge.close();
|
||||
if let Ok(emitter) = SignalEmitter::new(connection, path) {
|
||||
let _ = MonitorObject::closed(&emitter).await;
|
||||
}
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<MonitorObject, _>(path)
|
||||
.await;
|
||||
self.changed.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn watch(self, connection: Connection) {
|
||||
let mut bridge: Option<Bridge> = None;
|
||||
let mut since = None;
|
||||
loop {
|
||||
if self.monitors.lock().await.is_empty() {
|
||||
if let Some(bridge) = bridge.take() {
|
||||
bridge.close();
|
||||
}
|
||||
since = None;
|
||||
self.changed.notified().await;
|
||||
continue;
|
||||
}
|
||||
if bridge.is_none() {
|
||||
bridge = Bridge::connect().ok();
|
||||
}
|
||||
let result = if let Some(active) = &bridge {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.changed.notified() => { active.close(); bridge = None; since = None; continue; }
|
||||
state = async {
|
||||
match since { Some(revision) => active.wait(revision).await, None => active.call("get-session-state", serde_json::json!({})).await }
|
||||
} => state,
|
||||
}
|
||||
} else {
|
||||
Err("Session monitor is unavailable".into())
|
||||
};
|
||||
let monitors = self
|
||||
.monitors
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(path, monitor)| (path.clone(), monitor.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
match result {
|
||||
Ok(state) => {
|
||||
since = state["revision"].as_u64();
|
||||
for (path, monitor) in monitors {
|
||||
if !state["monitor-owners"].as_array().is_some_and(|owners| {
|
||||
owners
|
||||
.iter()
|
||||
.any(|owner| owner.as_u64() == Some(monitor.native_owner))
|
||||
}) {
|
||||
self.end(&connection, &path).await;
|
||||
continue;
|
||||
}
|
||||
let mut last = monitor.last.lock().await;
|
||||
if monitor.cancel.stopped.load(Ordering::SeqCst) {
|
||||
continue;
|
||||
}
|
||||
let transitions = match *last {
|
||||
None => vec![state.clone()],
|
||||
Some((_, _, revision, _)) => {
|
||||
let events =
|
||||
state["transitions"].as_array().cloned().unwrap_or_default();
|
||||
if events
|
||||
.first()
|
||||
.and_then(|event| event["revision"].as_u64())
|
||||
.is_some_and(|first| first > revision.saturating_add(1))
|
||||
{
|
||||
drop(last);
|
||||
self.end(&connection, &path).await;
|
||||
continue;
|
||||
}
|
||||
events
|
||||
.into_iter()
|
||||
.filter(|event| {
|
||||
event["revision"]
|
||||
.as_u64()
|
||||
.is_some_and(|next| next > revision)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
for event in transitions {
|
||||
let locked = event["screensaver-active"].as_bool().unwrap_or(false);
|
||||
let phase = event["session-state"].as_u64().unwrap_or(1) as u32;
|
||||
let revision = event["revision"].as_u64().unwrap_or(0);
|
||||
let token = event["query-token"].as_u64().unwrap_or(0) as u32;
|
||||
let changed = last.as_ref().is_none_or(|old| {
|
||||
old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token)
|
||||
});
|
||||
if changed {
|
||||
let values = HashMap::from([
|
||||
(
|
||||
"screensaver-active".into(),
|
||||
Value::from(locked).try_into().unwrap(),
|
||||
),
|
||||
(
|
||||
"session-state".into(),
|
||||
Value::from(phase).try_into().unwrap(),
|
||||
),
|
||||
]);
|
||||
if let Ok(emitter) = SignalEmitter::new(&connection, PATH) {
|
||||
if Self::state_changed(
|
||||
&emitter,
|
||||
OwnedObjectPath::try_from(path.clone()).unwrap(),
|
||||
values,
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
*last = Some((locked, phase, revision, token));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("ferese-portal: {error}");
|
||||
for (path, _) in monitors {
|
||||
self.end(&connection, &path).await;
|
||||
}
|
||||
if let Some(bridge) = bridge.take() {
|
||||
bridge.close();
|
||||
}
|
||||
since = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn watch_logind(self) {
|
||||
use futures_util::StreamExt;
|
||||
loop {
|
||||
let result: Result<(), String> = async {
|
||||
let connection = Connection::system()
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let proxy = zbus::Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.login1",
|
||||
"/org/freedesktop/login1",
|
||||
"org.freedesktop.login1.Manager",
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut signals = proxy
|
||||
.receive_signal("PrepareForShutdown")
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
while let Some(message) = signals.next().await {
|
||||
let (ending,): (bool,) = message
|
||||
.body()
|
||||
.deserialize()
|
||||
.map_err(|error| error.to_string())?;
|
||||
if let Ok(bridge) = Bridge::connect() {
|
||||
let _ = bridge
|
||||
.call(
|
||||
"logind-session-ending",
|
||||
serde_json::json!({"ending": ending}),
|
||||
)
|
||||
.await;
|
||||
bridge.close();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
eprintln!("ferese-portal: logind monitor unavailable: {error}");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_what(flags: u32) -> zbus::fdo::Result<String> {
|
||||
if flags == 0 || flags & !15 != 0 {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid inhibition flags".into(),
|
||||
));
|
||||
}
|
||||
let mut what = Vec::new();
|
||||
if flags & 1 != 0 {
|
||||
what.push("shutdown");
|
||||
}
|
||||
if flags & 4 != 0 {
|
||||
what.push("sleep");
|
||||
}
|
||||
if flags & 8 != 0 {
|
||||
what.push("idle");
|
||||
}
|
||||
Ok(what.join(":"))
|
||||
}
|
||||
|
||||
async fn login_inhibitor(what: &str, app: &str, reason: &str) -> Result<Option<OwnedFd>, String> {
|
||||
if what.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
let connection = Connection::system()
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let proxy = zbus::Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.login1",
|
||||
"/org/freedesktop/login1",
|
||||
"org.freedesktop.login1.Manager",
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let fd: OwnedFd = proxy
|
||||
.call("Inhibit", &(what, app, reason, "block"))
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(Some(fd))
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "Session inhibition timed out".to_owned())?
|
||||
}
|
||||
|
||||
#[zbus::interface(name = "org.freedesktop.impl.portal.Inhibit")]
|
||||
impl Inhibit {
|
||||
async fn inhibit(
|
||||
&self,
|
||||
handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_window: String,
|
||||
flags: u32,
|
||||
options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
authorize(connection, &header).await?;
|
||||
let what = flags_what(flags)?;
|
||||
let reason = options
|
||||
.get("reason")
|
||||
.map(|value| <&str>::try_from(value).map(str::to_owned))
|
||||
.transpose()
|
||||
.map_err(|_| zbus::fdo::Error::InvalidArgs("Invalid inhibition reason".into()))?
|
||||
.unwrap_or_else(|| "Application requested session inhibition".into());
|
||||
if app_id.len() > 512
|
||||
|| reason.len() > 1024
|
||||
|| app_id.chars().chain(reason.chars()).any(char::is_control)
|
||||
{
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid application or reason".into(),
|
||||
));
|
||||
}
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let bridge = match Bridge::connect() {
|
||||
Ok(bridge) => bridge,
|
||||
Err(error) => {
|
||||
self.requests.end(connection, &handle).await;
|
||||
return Err(zbus::fdo::Error::Failed(error));
|
||||
}
|
||||
};
|
||||
let acquired = tokio::select! {
|
||||
biased;
|
||||
_ = cancel.wait() => Err("Inhibition was cancelled".to_owned()),
|
||||
result = async {
|
||||
let fd = login_inhibitor(&what, &app_id, &reason).await?;
|
||||
bridge.call("portal-inhibit", serde_json::json!({"flags": flags, "app": app_id, "reason": reason})).await?;
|
||||
Ok(fd)
|
||||
} => result,
|
||||
};
|
||||
match acquired {
|
||||
Ok(fd) if !cancel.stopped.load(Ordering::SeqCst) => {
|
||||
let requests = self.requests.clone();
|
||||
let connection = connection.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::select! { _ = cancel.wait() => {}, _ = bridge.closed() => {} }
|
||||
cancel.stop();
|
||||
bridge.close();
|
||||
drop(fd);
|
||||
requests.end(&connection, &handle).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
result => {
|
||||
bridge.close();
|
||||
self.requests.end(connection, &handle).await;
|
||||
Err(zbus::fdo::Error::Failed(
|
||||
result
|
||||
.err()
|
||||
.unwrap_or_else(|| "Inhibition was cancelled".into()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_monitor(
|
||||
&self,
|
||||
handle: OwnedObjectPath,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_window: String,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<u32> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
if !session_handle
|
||||
.as_str()
|
||||
.starts_with(&format!("{PATH}/session/"))
|
||||
|| session_handle.as_str().len() > 512
|
||||
|| app_id.len() > 512
|
||||
{
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid session monitor".into(),
|
||||
));
|
||||
}
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let mut monitors = self.monitors.lock().await;
|
||||
let result = if monitors.len() >= 8
|
||||
|| monitors.contains_key(session_handle.as_str())
|
||||
|| cancel.stopped.load(Ordering::SeqCst)
|
||||
{
|
||||
Ok(2)
|
||||
} else {
|
||||
let attempt = async {
|
||||
let bridge = Bridge::connect().map_err(zbus::fdo::Error::Failed)?;
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
_ = cancel.wait() => Err("Monitor creation was cancelled".into()),
|
||||
result = bridge.call("portal-monitor-register", serde_json::json!({})) => result,
|
||||
};
|
||||
let native = match result { Ok(native) => native, Err(error) => { bridge.close(); return Err(zbus::fdo::Error::Failed(error)); } };
|
||||
let Some(native_owner) = native["monitor-owner"].as_u64() else {
|
||||
bridge.close();
|
||||
return Err(zbus::fdo::Error::Failed("Missing monitor owner".into()));
|
||||
};
|
||||
let exported = connection.object_server().at(&session_handle, MonitorObject {
|
||||
backend: self.clone(), path: session_handle.to_string(), owner: owner.clone(), cancel: cancel.clone(),
|
||||
}).await;
|
||||
match exported {
|
||||
Ok(true) if !cancel.stopped.load(Ordering::SeqCst) => {
|
||||
monitors.insert(session_handle.to_string(), Arc::new(Monitor {
|
||||
owner, cancel: cancel.clone(), last: Mutex::new(None), bridge,
|
||||
native_owner,
|
||||
}));
|
||||
self.changed.notify_one();
|
||||
Ok(0)
|
||||
}
|
||||
result => {
|
||||
bridge.close();
|
||||
if matches!(result, Ok(true)) { let _ = connection.object_server().remove::<MonitorObject, _>(&session_handle).await; }
|
||||
result.map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 }).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}.await;
|
||||
attempt
|
||||
};
|
||||
drop(monitors);
|
||||
self.requests.end(connection, &handle).await;
|
||||
result
|
||||
}
|
||||
|
||||
async fn query_end_response(
|
||||
&self,
|
||||
session_handle: OwnedObjectPath,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
let monitor = self
|
||||
.monitors
|
||||
.lock()
|
||||
.await
|
||||
.get(session_handle.as_str())
|
||||
.filter(|monitor| monitor.owner == owner)
|
||||
.cloned()
|
||||
.ok_or_else(|| zbus::fdo::Error::AccessDenied("Not your session monitor".into()))?;
|
||||
let last = monitor.last.lock().await;
|
||||
if monitor.cancel.stopped.load(Ordering::SeqCst) {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Session monitor closed".into(),
|
||||
));
|
||||
}
|
||||
if let Some((_, 2, _, token)) = *last {
|
||||
monitor
|
||||
.bridge
|
||||
.call("portal-monitor-ack", serde_json::json!({"token": token}))
|
||||
.await
|
||||
.map_err(zbus::fdo::Error::Failed)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn state_changed(
|
||||
emitter: &SignalEmitter<'_>,
|
||||
session_handle: OwnedObjectPath,
|
||||
state: Options,
|
||||
) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
struct MonitorObject {
|
||||
backend: Inhibit,
|
||||
path: String,
|
||||
owner: String,
|
||||
cancel: Arc<Cancel>,
|
||||
}
|
||||
|
||||
#[zbus::interface(name = "org.freedesktop.impl.portal.Session")]
|
||||
impl MonitorObject {
|
||||
#[zbus(property, name = "version")]
|
||||
fn version(&self) -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
async fn close(
|
||||
&self,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
if authorize(connection, &header).await? != self.owner {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Not your session monitor".into(),
|
||||
));
|
||||
}
|
||||
self.cancel.stop();
|
||||
self.backend.end(connection, &self.path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn closed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn logind_flags_are_validated_and_mapped_independently() {
|
||||
assert_eq!(flags_what(1 | 4 | 8).unwrap(), "shutdown:sleep:idle");
|
||||
assert_eq!(flags_what(2).unwrap(), "");
|
||||
assert!(flags_what(0).is_err());
|
||||
assert!(flags_what(16).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
mod backend;
|
||||
mod bridge;
|
||||
mod capture;
|
||||
mod consent;
|
||||
mod desktop;
|
||||
mod inhibit;
|
||||
mod lockdown;
|
||||
mod parent;
|
||||
mod permissions;
|
||||
|
||||
+13
-65
@@ -1,9 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap, io::Read, os::unix::net::UnixStream, process::Stdio, sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{collections::HashMap, io::Read, process::Stdio, sync::Arc, time::Duration};
|
||||
|
||||
use serde_json::{Value as Json, json};
|
||||
use serde_json::json;
|
||||
use tokio::{io::AsyncWriteExt, sync::Mutex};
|
||||
use zbus::{
|
||||
Connection,
|
||||
@@ -14,6 +11,7 @@ use zbus::{
|
||||
|
||||
use crate::{
|
||||
backend::{Cancel, Options, authorize},
|
||||
bridge::Bridge,
|
||||
consent::{Prompt, ShortcutField},
|
||||
desktop::Requests,
|
||||
};
|
||||
@@ -21,66 +19,6 @@ use crate::{
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
type Shortcuts = Vec<(String, Options)>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Bridge(Arc<BridgeInner>);
|
||||
|
||||
struct BridgeInner {
|
||||
stream: std::sync::Mutex<UnixStream>,
|
||||
shutdown: UnixStream,
|
||||
}
|
||||
|
||||
impl Bridge {
|
||||
fn connect() -> Result<Self, String> {
|
||||
let path = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(std::path::PathBuf::from)
|
||||
.ok_or("Missing runtime directory")?
|
||||
.join("ferese/control.sock");
|
||||
let stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let shutdown = stream.try_clone().map_err(|error| error.to_string())?;
|
||||
Ok(Self(Arc::new(BridgeInner {
|
||||
stream: std::sync::Mutex::new(stream),
|
||||
shutdown,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn call(&self, command: &'static str, args: Json) -> Result<Json, String> {
|
||||
let bridge = self.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut stream = bridge
|
||||
.0
|
||||
.stream
|
||||
.lock()
|
||||
.map_err(|_| "Shortcut connection failed")?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
kind: "command".into(),
|
||||
command: command.into(),
|
||||
args,
|
||||
};
|
||||
ferese_ipc::write_frame(&mut *stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
response.result.ok_or("Missing shortcut response".into())
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
let _ = self.0.shutdown.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
struct Session {
|
||||
owner: String,
|
||||
app: String,
|
||||
@@ -177,6 +115,7 @@ impl GlobalShortcuts {
|
||||
};
|
||||
let mut fields = fields;
|
||||
let mut message = "Choose shortcuts for this application. Leave a shortcut empty to disable it. System shortcuts cannot be overridden.".to_owned();
|
||||
|
||||
loop {
|
||||
let selected = tokio::select! {
|
||||
biased;
|
||||
@@ -187,7 +126,9 @@ impl GlobalShortcuts {
|
||||
let Some(selected) = selected else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
validate_selection(&fields, &selected)?;
|
||||
|
||||
let registration = selected
|
||||
.iter()
|
||||
.filter(|field| !field.trigger.trim().is_empty())
|
||||
@@ -199,6 +140,7 @@ impl GlobalShortcuts {
|
||||
{
|
||||
Ok(_) => {
|
||||
let mut state = session.state.lock().await;
|
||||
|
||||
if session
|
||||
.cancel
|
||||
.stopped
|
||||
@@ -218,6 +160,7 @@ impl GlobalShortcuts {
|
||||
.filter(|field| !field.trigger.trim().is_empty())
|
||||
.map(|field| json!({"id":field.id,"trigger":field.trigger}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if bridge
|
||||
.call("portal-shortcuts-register", json!(previous))
|
||||
.await
|
||||
@@ -226,16 +169,20 @@ impl GlobalShortcuts {
|
||||
bridge.close();
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
state.fields = selected.clone();
|
||||
state.bound = true;
|
||||
state.bridge = Some(bridge.clone());
|
||||
state.revision = state.revision.wrapping_add(1);
|
||||
drop(state);
|
||||
|
||||
if let Err(error) = save(&session.app, &selected) {
|
||||
eprintln!("ferese shortcuts: could not save preferences: {error}");
|
||||
}
|
||||
|
||||
if first {
|
||||
let backend = self.clone();
|
||||
let connection = connection.clone();
|
||||
@@ -244,6 +191,7 @@ impl GlobalShortcuts {
|
||||
backend.watch(connection, path, session, bridge).await;
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(Some(results(&selected)));
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
Reference in new issue
Block a user