Implement native InputCapture with private EIS receiver transport
Add consent, zones and outside-edge barriers, connection-scoped leases, bounded input queues and keyboard/pointer forwarding. Restore local input on release, locking, topology or keymap changes, receiver loss and emergency escape. Preserve held-key state and distinguish finger stops from absent scroll axes. Validation: independent review cleared; 5 native InputCapture unit tests; 32 portal tests; official D-Bus contract checks on a private bus; real installed libei receiver interoperability test. Live physical barrier and consent interaction remain to be checked in a desktop session.
This commit is contained in:
1 parent
baca852ee9
commit
d26e1511d1
22 files changed
+2349
-11
No files matched your search
+10
-2
@@ -54,12 +54,15 @@ pub(crate) struct Cancel {
|
||||
impl Cancel {
|
||||
pub(crate) fn stop(&self) {
|
||||
self.stopped.store(true, Ordering::SeqCst);
|
||||
self.changed.notify_one();
|
||||
self.changed.notify_waiters();
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(&self) {
|
||||
let changed = self.changed.notified();
|
||||
tokio::pin!(changed);
|
||||
changed.as_mut().enable();
|
||||
if !self.stopped.load(Ordering::SeqCst) {
|
||||
self.changed.notified().await;
|
||||
changed.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -912,6 +915,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let lockdown = crate::lockdown::Lockdown::new();
|
||||
let shortcuts = crate::shortcuts::GlobalShortcuts::new(requests.clone());
|
||||
let inhibit = crate::inhibit::Inhibit::default();
|
||||
let input_capture = crate::input_capture::InputCapture::new(requests.clone());
|
||||
let connection = zbus::connection::Builder::session()?
|
||||
.name("org.freedesktop.impl.portal.desktop.ferese")?
|
||||
.serve_at(PATH, backend.clone())?
|
||||
@@ -923,6 +927,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.serve_at(PATH, lockdown.clone())?
|
||||
.serve_at(PATH, shortcuts.clone())?
|
||||
.serve_at(PATH, inhibit.clone())?
|
||||
.serve_at(PATH, input_capture.clone())?
|
||||
.serve_at(
|
||||
"/org/ferese/ScreenRecorder",
|
||||
RecorderControl(backend.clone()),
|
||||
@@ -950,6 +955,9 @@ 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;
|
||||
input_capture
|
||||
.revoke_stale(&connection, owner.as_deref())
|
||||
.await;
|
||||
|
||||
let stale: Vec<_> = backend
|
||||
.sessions
|
||||
|
||||
@@ -72,6 +72,15 @@ impl Bridge {
|
||||
.map_err(|error| error.to_string())?
|
||||
}
|
||||
|
||||
pub(crate) async fn capture_watch(&self, session: u64) -> Result<Json, String> {
|
||||
self.request(
|
||||
"input-capture-watch",
|
||||
serde_json::json!({"session":session}),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn closed(&self) {
|
||||
use std::io::Read;
|
||||
let Ok(reader) = self.0.shutdown.try_clone() else {
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
os::{
|
||||
fd::{AsFd, OwnedFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use calloop::{EventLoop, LoopSignal, PostAction, channel};
|
||||
use reis::{
|
||||
calloop::{EisRequestSource, EisRequestSourceEvent},
|
||||
eis,
|
||||
enumflags2::BitFlags,
|
||||
request::{Connection, Device, DeviceCapability, EisRequest},
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::backend::Cancel;
|
||||
|
||||
pub(crate) struct Worker {
|
||||
sender: channel::SyncSender<Vec<Value>>,
|
||||
signal: LoopSignal,
|
||||
pub(crate) ready: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub(crate) fn start(
|
||||
capabilities: u32,
|
||||
keymap: &str,
|
||||
cancel: Arc<Cancel>,
|
||||
) -> Result<(Self, OwnedFd), String> {
|
||||
if keymap.is_empty() || keymap.len() > 256 * 1024 {
|
||||
return Err("Invalid captured keyboard map".into());
|
||||
}
|
||||
let mut keymap_file = tempfile::tempfile().map_err(|e| e.to_string())?;
|
||||
keymap_file
|
||||
.write_all(keymap.as_bytes())
|
||||
.and_then(|_| keymap_file.write_all(&[0]))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let keymap_size = keymap.len() as u32 + 1;
|
||||
let (server, client) = UnixStream::pair().map_err(|e| e.to_string())?;
|
||||
let ready = Arc::new(AtomicBool::new(false));
|
||||
let thread_ready = ready.clone();
|
||||
let (startup, started) = std::sync::mpsc::sync_channel(1);
|
||||
std::thread::Builder::new()
|
||||
.name("ferese-input-eis".into())
|
||||
.spawn(move || {
|
||||
struct Finish(Arc<Cancel>);
|
||||
impl Drop for Finish {
|
||||
fn drop(&mut self) {
|
||||
self.0.stop();
|
||||
}
|
||||
}
|
||||
let _finish = Finish(cancel);
|
||||
let run = || {
|
||||
let context = eis::Context::new(server).map_err(|e| e.to_string())?;
|
||||
let mut event_loop =
|
||||
EventLoop::<State>::try_new().map_err(|e| e.to_string())?;
|
||||
let signal = event_loop.get_signal();
|
||||
let (sender, receiver) = channel::sync_channel(4);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(receiver, |event, _, state| match event {
|
||||
channel::Event::Msg(events) => state.events(events),
|
||||
channel::Event::Closed => state.signal.stop(),
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(
|
||||
EisRequestSource::new(context, 1),
|
||||
|event, connection, state| {
|
||||
let action = match event {
|
||||
Ok(event) => state.request(connection, event),
|
||||
Err(_) => {
|
||||
state.signal.stop();
|
||||
PostAction::Remove
|
||||
}
|
||||
};
|
||||
if connection.flush().is_err() {
|
||||
state.signal.stop();
|
||||
}
|
||||
Ok(action)
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut state = State {
|
||||
signal: signal.clone(),
|
||||
ready: thread_ready.clone(),
|
||||
capabilities,
|
||||
keymap: keymap_file,
|
||||
keymap_size,
|
||||
connection: None,
|
||||
keyboard: None,
|
||||
pointer: None,
|
||||
activation: None,
|
||||
keyboard_ready: false,
|
||||
pointer_ready: false,
|
||||
};
|
||||
startup
|
||||
.send(Ok((sender, signal)))
|
||||
.map_err(|_| "Input transport startup cancelled".to_owned())?;
|
||||
let _ = event_loop.run(None, &mut state, |_| {});
|
||||
if let Some(connection) = state.connection.take() {
|
||||
connection
|
||||
.disconnected(eis::connection::DisconnectReason::Disconnected, None);
|
||||
}
|
||||
state.ready.store(false, Ordering::SeqCst);
|
||||
Ok::<(), String>(())
|
||||
};
|
||||
if let Err(error) = run() {
|
||||
let _ = startup.send(Err(error));
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (sender, signal) = started
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.map_err(|_| "Input transport startup timed out".to_owned())??;
|
||||
Ok((
|
||||
Self {
|
||||
sender,
|
||||
signal,
|
||||
ready,
|
||||
},
|
||||
client.into(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, events: Vec<Value>) -> Result<(), String> {
|
||||
self.sender
|
||||
.try_send(events)
|
||||
.map_err(|_| "Input transport is closed or stalled".into())
|
||||
}
|
||||
|
||||
pub(crate) fn stop(&self) {
|
||||
self.signal.stop();
|
||||
self.signal.wakeup();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Worker {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
signal: LoopSignal,
|
||||
ready: Arc<AtomicBool>,
|
||||
capabilities: u32,
|
||||
keymap: File,
|
||||
keymap_size: u32,
|
||||
connection: Option<Connection>,
|
||||
keyboard: Option<Device>,
|
||||
pointer: Option<Device>,
|
||||
activation: Option<u32>,
|
||||
keyboard_ready: bool,
|
||||
pointer_ready: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn request(&mut self, connection: &Connection, event: EisRequestSourceEvent) -> PostAction {
|
||||
match event {
|
||||
EisRequestSourceEvent::Connected => {
|
||||
if connection.context_type() != eis::handshake::ContextType::Receiver {
|
||||
connection.disconnected(
|
||||
eis::connection::DisconnectReason::Mode,
|
||||
Some("Input capture requires a receiver context"),
|
||||
);
|
||||
self.signal.stop();
|
||||
return PostAction::Remove;
|
||||
}
|
||||
let mut capabilities = BitFlags::<DeviceCapability>::empty();
|
||||
if self.capabilities & 1 != 0 {
|
||||
capabilities |= DeviceCapability::Keyboard;
|
||||
}
|
||||
if self.capabilities & 2 != 0 {
|
||||
capabilities |= DeviceCapability::Pointer
|
||||
| DeviceCapability::Button
|
||||
| DeviceCapability::Scroll;
|
||||
}
|
||||
let _ = connection.add_seat(Some("Ferese input capture"), capabilities);
|
||||
self.connection = Some(connection.clone());
|
||||
}
|
||||
EisRequestSourceEvent::Request(EisRequest::Bind(request)) => {
|
||||
let keyboard = request.capabilities.contains(DeviceCapability::Keyboard)
|
||||
&& self.capabilities & 1 != 0;
|
||||
let pointer = request.capabilities.contains(DeviceCapability::Pointer)
|
||||
&& self.capabilities & 2 != 0;
|
||||
if !keyboard && let Some(device) = self.keyboard.take() {
|
||||
device.remove();
|
||||
self.keyboard_ready = false;
|
||||
}
|
||||
if !pointer && let Some(device) = self.pointer.take() {
|
||||
device.remove();
|
||||
self.pointer_ready = false;
|
||||
}
|
||||
if keyboard && self.keyboard.is_none() {
|
||||
let device = request.seat.add_device(
|
||||
Some("Ferese keyboard"),
|
||||
eis::device::DeviceType::Virtual,
|
||||
DeviceCapability::Keyboard.into(),
|
||||
|device| {
|
||||
if let Some(keyboard) = device.interface::<eis::Keyboard>() {
|
||||
keyboard.keymap(
|
||||
eis::keyboard::KeymapType::Xkb,
|
||||
self.keymap_size,
|
||||
self.keymap.as_fd(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
self.keyboard_ready = device.device().version() < 3;
|
||||
device.resumed();
|
||||
self.keyboard = Some(device);
|
||||
}
|
||||
if pointer && self.pointer.is_none() {
|
||||
let caps = request.capabilities
|
||||
& (DeviceCapability::Pointer
|
||||
| DeviceCapability::Button
|
||||
| DeviceCapability::Scroll);
|
||||
let device = request.seat.add_device(
|
||||
Some("Ferese pointer"),
|
||||
eis::device::DeviceType::Virtual,
|
||||
caps,
|
||||
|_| {},
|
||||
);
|
||||
self.pointer_ready = device.device().version() < 3;
|
||||
device.resumed();
|
||||
self.pointer = Some(device);
|
||||
}
|
||||
let ready = self.keyboard.is_some() || self.pointer.is_some();
|
||||
self.ready.store(
|
||||
ready
|
||||
&& (self.keyboard.is_none() || self.keyboard_ready)
|
||||
&& (self.pointer.is_none() || self.pointer_ready),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
if self.activation.is_some() || !ready {
|
||||
self.signal.stop();
|
||||
}
|
||||
}
|
||||
EisRequestSourceEvent::Request(EisRequest::Ready(request)) => {
|
||||
if self
|
||||
.keyboard
|
||||
.as_ref()
|
||||
.is_some_and(|device| device.device() == request.device.device())
|
||||
{
|
||||
self.keyboard_ready = true;
|
||||
} else if self
|
||||
.pointer
|
||||
.as_ref()
|
||||
.is_some_and(|device| device.device() == request.device.device())
|
||||
{
|
||||
self.pointer_ready = true;
|
||||
} else {
|
||||
self.signal.stop();
|
||||
return PostAction::Remove;
|
||||
}
|
||||
self.ready.store(
|
||||
(self.keyboard.is_none() || self.keyboard_ready)
|
||||
&& (self.pointer.is_none() || self.pointer_ready),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
EisRequestSourceEvent::Request(
|
||||
EisRequest::Disconnect | EisRequest::DeviceClosed(_),
|
||||
) => {
|
||||
self.signal.stop();
|
||||
return PostAction::Remove;
|
||||
}
|
||||
EisRequestSourceEvent::Request(_) => {
|
||||
connection.disconnected(
|
||||
eis::connection::DisconnectReason::Mode,
|
||||
Some("Input capture does not accept injected input"),
|
||||
);
|
||||
self.signal.stop();
|
||||
return PostAction::Remove;
|
||||
}
|
||||
}
|
||||
PostAction::Continue
|
||||
}
|
||||
|
||||
fn events(&mut self, events: Vec<Value>) {
|
||||
for event in events {
|
||||
match event["type"].as_str() {
|
||||
Some("activated") => {
|
||||
let activation = event["activation_id"].as_u64().unwrap_or(0) as u32;
|
||||
self.activation = Some(activation);
|
||||
for device in [&self.keyboard, &self.pointer].into_iter().flatten() {
|
||||
device.start_emulating(activation);
|
||||
}
|
||||
}
|
||||
Some("deactivated" | "disabled" | "closed") => {
|
||||
if self.activation.take().is_some() {
|
||||
for device in [&self.keyboard, &self.pointer].into_iter().flatten() {
|
||||
device.stop_emulating();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("keys") if self.activation.is_some() => {
|
||||
if let Some(device) = &self.keyboard
|
||||
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
||||
{
|
||||
for key in event["keys"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_u64)
|
||||
{
|
||||
keyboard.key(key as u32, eis::keyboard::KeyState::Press);
|
||||
}
|
||||
device.frame(now());
|
||||
}
|
||||
}
|
||||
Some("key") if self.activation.is_some() => {
|
||||
if let Some(device) = &self.keyboard
|
||||
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
||||
{
|
||||
keyboard.key(
|
||||
event["key"].as_u64().unwrap_or(0) as u32,
|
||||
if event["pressed"] == true {
|
||||
eis::keyboard::KeyState::Press
|
||||
} else {
|
||||
eis::keyboard::KeyState::Released
|
||||
},
|
||||
);
|
||||
device.frame(now());
|
||||
}
|
||||
}
|
||||
Some("motion" | "button" | "scroll") if self.activation.is_some() => {
|
||||
if let Some(device) = &self.pointer {
|
||||
match event["type"].as_str() {
|
||||
Some("motion") => {
|
||||
if let Some(pointer) = device.interface::<eis::Pointer>() {
|
||||
pointer.motion_relative(
|
||||
event["x"].as_f64().unwrap_or(0.0) as f32,
|
||||
event["y"].as_f64().unwrap_or(0.0) as f32,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("button") => {
|
||||
if let Some(button) = device.interface::<eis::Button>() {
|
||||
button.button(
|
||||
event["button"].as_u64().unwrap_or(0) as u32,
|
||||
if event["pressed"] == true {
|
||||
eis::button::ButtonState::Press
|
||||
} else {
|
||||
eis::button::ButtonState::Released
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("scroll") => {
|
||||
if let Some(scroll) = device.interface::<eis::Scroll>() {
|
||||
let x = event["x"].as_f64().unwrap_or(0.0) as f32;
|
||||
let y = event["y"].as_f64().unwrap_or(0.0) as f32;
|
||||
if !event["v120_x"].is_null() || !event["v120_y"].is_null() {
|
||||
scroll.scroll_discrete(
|
||||
event["v120_x"].as_f64().unwrap_or(0.0) as i32,
|
||||
event["v120_y"].as_f64().unwrap_or(0.0) as i32,
|
||||
);
|
||||
} else if x != 0.0 || y != 0.0 {
|
||||
scroll.scroll(x, y);
|
||||
}
|
||||
let stop_x = event["stop_x"] == true;
|
||||
let stop_y = event["stop_y"] == true;
|
||||
if stop_x || stop_y {
|
||||
scroll.scroll_stop(stop_x as u32, stop_y as u32, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
device.frame(now());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
if let Some(connection) = &self.connection
|
||||
&& connection.flush().is_err()
|
||||
{
|
||||
self.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn now() -> u64 {
|
||||
let mut time = libc::timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
};
|
||||
// SAFETY: the initialized output pointer covers a timespec allocation.
|
||||
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } == 0 {
|
||||
time.tv_sec as u64 * 1_000_000 + time.tv_nsec as u64 / 1_000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
process::{Command, Stdio},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires Python and the system libei receiver library"]
|
||||
fn libei_receives_keymap_motion_keyboard_and_explicit_axis_stop() {
|
||||
let cancel = Arc::new(Cancel::default());
|
||||
let map = "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; xkb_types { include \"complete\" }; xkb_compatibility { include \"complete\" }; xkb_symbols { include \"pc+us+inet(evdev)\" }; };";
|
||||
let (worker, fd) = Worker::start(3, map, cancel.clone()).unwrap();
|
||||
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../scripts/tests/fixtures/eis-receiver.py");
|
||||
let mut client = Command::new("python3")
|
||||
.arg(fixture)
|
||||
.stdin(Stdio::from(fd))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !worker.ready.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
if !worker.ready.load(Ordering::SeqCst) {
|
||||
let _ = client.kill();
|
||||
let output = client.wait_with_output().unwrap();
|
||||
panic!(
|
||||
"Receiver did not bind: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
worker
|
||||
.send(vec![
|
||||
json!({"type":"activated", "activation_id":1}),
|
||||
json!({"type":"motion", "x":10.0, "y":-5.0}),
|
||||
json!({"type":"motion", "x":10.0, "y":-5.0}),
|
||||
json!({"type":"key", "key":30, "pressed":true}),
|
||||
json!({"type":"key", "key":30, "pressed":false}),
|
||||
json!({"type":"scroll", "x":0.0, "y":4.0}),
|
||||
json!({"type":"scroll", "x":0.0, "y":0.0, "stop_x":false, "stop_y":true}),
|
||||
json!({"type":"deactivated"}),
|
||||
])
|
||||
.unwrap();
|
||||
let output = client.wait_with_output().unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("\"keymap\": true"));
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while !cancel.stopped.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(
|
||||
cancel.stopped.load(Ordering::SeqCst),
|
||||
"Receiver disconnect must revoke capture"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,784 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedFd, OwnedObjectPath, Value},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Cancel, Options, authorize},
|
||||
bridge::Bridge,
|
||||
desktop::Requests,
|
||||
eis::Worker,
|
||||
};
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
starting: bool,
|
||||
started: bool,
|
||||
id: u64,
|
||||
capabilities: u32,
|
||||
keymap: String,
|
||||
bridge: Option<Bridge>,
|
||||
watch: Option<Bridge>,
|
||||
worker: Option<Worker>,
|
||||
}
|
||||
|
||||
struct Session {
|
||||
owner: String,
|
||||
app: String,
|
||||
cancel: Arc<Cancel>,
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct InputCapture {
|
||||
sessions: Arc<Mutex<HashMap<String, Arc<Session>>>>,
|
||||
requests: Requests,
|
||||
}
|
||||
|
||||
impl InputCapture {
|
||||
pub(crate) fn new(requests: Requests) -> Self {
|
||||
Self {
|
||||
requests,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn session(
|
||||
&self,
|
||||
connection: &Connection,
|
||||
header: &Header<'_>,
|
||||
path: &OwnedObjectPath,
|
||||
app: &str,
|
||||
) -> zbus::fdo::Result<Arc<Session>> {
|
||||
let owner = authorize(connection, header).await?;
|
||||
self.sessions
|
||||
.lock()
|
||||
.await
|
||||
.get(path.as_str())
|
||||
.filter(|session| session.owner == owner && session.app == app)
|
||||
.cloned()
|
||||
.ok_or_else(|| invalid("Unknown input capture session"))
|
||||
}
|
||||
|
||||
async fn end(&self, connection: &Connection, path: &str) {
|
||||
let session = self.sessions.lock().await.remove(path);
|
||||
if let Some(session) = session {
|
||||
session.cancel.stop();
|
||||
let state = session.state.lock().await;
|
||||
if let Some(worker) = &state.worker {
|
||||
worker.stop();
|
||||
}
|
||||
if let Some(bridge) = &state.bridge {
|
||||
bridge.close();
|
||||
}
|
||||
if let Some(watch) = &state.watch {
|
||||
watch.close();
|
||||
}
|
||||
if let Ok(emitter) = SignalEmitter::new(connection, path) {
|
||||
let _ = SessionObject::closed(&emitter).await;
|
||||
}
|
||||
let connection = connection.clone();
|
||||
let path = path.to_owned();
|
||||
tokio::spawn(async move {
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<SessionObject, _>(path)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_stale(&self, connection: &Connection, owner: Option<&str>) {
|
||||
let stale = self
|
||||
.sessions
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.filter_map(|(path, session)| {
|
||||
(Some(session.owner.as_str()) != owner).then_some(path.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for path in stale {
|
||||
self.end(connection, &path).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn native(session: &Session) -> zbus::fdo::Result<(Bridge, u64)> {
|
||||
let state = session.state.lock().await;
|
||||
if !state.started || session.cancel.stopped.load(Ordering::SeqCst) {
|
||||
return Err(invalid("Start the session first"));
|
||||
}
|
||||
Ok((
|
||||
state
|
||||
.bridge
|
||||
.clone()
|
||||
.ok_or_else(|| invalid("Missing capture connection"))?,
|
||||
state.id,
|
||||
))
|
||||
}
|
||||
|
||||
async fn watch(
|
||||
self,
|
||||
connection: Connection,
|
||||
path: OwnedObjectPath,
|
||||
session: Arc<Session>,
|
||||
bridge: Bridge,
|
||||
id: u64,
|
||||
) {
|
||||
loop {
|
||||
let response = tokio::select! {
|
||||
_ = session.cancel.wait() => break,
|
||||
result = bridge.capture_watch(id) => result,
|
||||
};
|
||||
let Ok(response) = response else {
|
||||
break;
|
||||
};
|
||||
let Some(events) = response["events"].as_array() else {
|
||||
break;
|
||||
};
|
||||
let state = session.state.lock().await;
|
||||
if session.cancel.stopped.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let transport = state
|
||||
.worker
|
||||
.as_ref()
|
||||
.map(|worker| worker.send(events.clone()));
|
||||
if transport.is_some_and(|result| result.is_err()) {
|
||||
break;
|
||||
}
|
||||
let mut closed = false;
|
||||
let Ok(emitter) = SignalEmitter::new(&connection, PATH) else {
|
||||
break;
|
||||
};
|
||||
for event in events {
|
||||
let result = match event["type"].as_str() {
|
||||
Some("activated") => {
|
||||
let mut options = Options::new();
|
||||
options.insert(
|
||||
"activation_id".into(),
|
||||
(event["activation_id"].as_u64().unwrap_or(0) as u32).into(),
|
||||
);
|
||||
options.insert(
|
||||
"barrier_id".into(),
|
||||
(event["barrier_id"].as_u64().unwrap_or(0) as u32).into(),
|
||||
);
|
||||
if let Some(position) = event["cursor_position"]
|
||||
.as_array()
|
||||
.filter(|position| position.len() == 2)
|
||||
{
|
||||
if let Ok(position) = Value::from((
|
||||
position[0].as_f64().unwrap_or(0.0),
|
||||
position[1].as_f64().unwrap_or(0.0),
|
||||
))
|
||||
.try_to_owned()
|
||||
{
|
||||
options.insert("cursor_position".into(), position);
|
||||
}
|
||||
}
|
||||
Self::activated(&emitter, path.clone(), options).await
|
||||
}
|
||||
Some("deactivated") => {
|
||||
Self::deactivated(
|
||||
&emitter,
|
||||
path.clone(),
|
||||
Options::from([(
|
||||
"activation_id".into(),
|
||||
(event["activation_id"].as_u64().unwrap_or(0) as u32).into(),
|
||||
)]),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some("disabled") => {
|
||||
Self::disabled(&emitter, path.clone(), Options::new()).await
|
||||
}
|
||||
Some("zones") => {
|
||||
Self::zones_changed(&emitter, path.clone(), Options::new()).await
|
||||
}
|
||||
Some("closed") => {
|
||||
closed = true;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
if result.is_err() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
if closed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.end(&connection, path.as_str()).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(message: &str) -> zbus::fdo::Error {
|
||||
zbus::fdo::Error::InvalidArgs(message.into())
|
||||
}
|
||||
|
||||
fn failed(error: String) -> zbus::fdo::Error {
|
||||
zbus::fdo::Error::Failed(error)
|
||||
}
|
||||
|
||||
fn capabilities(options: &Options) -> zbus::fdo::Result<u32> {
|
||||
let requested = options
|
||||
.get("capabilities")
|
||||
.ok_or_else(|| invalid("Missing requested capabilities"))?;
|
||||
let requested =
|
||||
u32::try_from(requested).map_err(|_| invalid("Invalid requested capabilities"))?;
|
||||
if requested == 0 || requested & 3 == 0 {
|
||||
return Err(invalid("No supported capabilities requested"));
|
||||
}
|
||||
Ok(requested & 3)
|
||||
}
|
||||
|
||||
#[zbus::interface(name = "org.freedesktop.impl.portal.InputCapture")]
|
||||
impl InputCapture {
|
||||
#[zbus(property, name = "version")]
|
||||
fn version(&self) -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn supported_capabilities(&self) -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
async fn create_session2(
|
||||
&self,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<Options> {
|
||||
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(invalid("Invalid session"));
|
||||
}
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
if sessions.len() >= 8 || sessions.contains_key(session_handle.as_str()) {
|
||||
return Err(invalid("Session limit reached or duplicate session"));
|
||||
}
|
||||
if !connection
|
||||
.object_server()
|
||||
.at(
|
||||
&session_handle,
|
||||
SessionObject {
|
||||
backend: self.clone(),
|
||||
path: session_handle.to_string(),
|
||||
owner: owner.clone(),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(invalid("Session object already exists"));
|
||||
}
|
||||
sessions.insert(
|
||||
session_handle.to_string(),
|
||||
Arc::new(Session {
|
||||
owner,
|
||||
app: app_id,
|
||||
cancel: Arc::default(),
|
||||
state: Mutex::default(),
|
||||
}),
|
||||
);
|
||||
Ok(Options::new())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_session(
|
||||
&self,
|
||||
handle: OwnedObjectPath,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
parent_window: String,
|
||||
options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
capabilities(&options)?;
|
||||
self.create_session2(
|
||||
session_handle.clone(),
|
||||
app_id.clone(),
|
||||
Options::new(),
|
||||
connection,
|
||||
header.clone(),
|
||||
)
|
||||
.await?;
|
||||
let result = self
|
||||
.start(
|
||||
handle,
|
||||
session_handle.clone(),
|
||||
app_id,
|
||||
parent_window,
|
||||
options,
|
||||
connection,
|
||||
header,
|
||||
)
|
||||
.await;
|
||||
let (response, mut results) = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
self.end(connection, session_handle.as_str()).await;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
if response == 0 {
|
||||
results.insert(
|
||||
"session_id".into(),
|
||||
Value::from(session_handle.to_string())
|
||||
.try_to_owned()
|
||||
.map_err(|e| failed(e.to_string()))?,
|
||||
);
|
||||
}
|
||||
Ok((response, results))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn start(
|
||||
&self,
|
||||
handle: OwnedObjectPath,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
parent_window: String,
|
||||
options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let caps = capabilities(&options)?;
|
||||
crate::restore::persist_mode(&options)?;
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
if state.starting || state.started {
|
||||
self.requests.end(connection, &handle).await;
|
||||
return Err(invalid("Input capture can only be started once"));
|
||||
}
|
||||
state.starting = true;
|
||||
}
|
||||
let title = match caps {
|
||||
1 => "Share keyboard input?",
|
||||
2 => "Share pointer input?",
|
||||
_ => "Share keyboard and pointer?",
|
||||
};
|
||||
let consent = crate::desktop::consent(
|
||||
&app_id,
|
||||
&parent_window,
|
||||
title,
|
||||
"Allow input capture at the screen edges this app configures. Captured input is sent to this app until it releases control. Press Ctrl+Alt+Escape to return control to Ferese.",
|
||||
"Allow input capture",
|
||||
None,
|
||||
);
|
||||
let approved = tokio::select! {
|
||||
_ = cancel.wait() => Ok(false),
|
||||
_ = session.cancel.wait() => Ok(false),
|
||||
result = consent => result,
|
||||
};
|
||||
let result = if approved == Ok(true)
|
||||
&& !cancel.stopped.load(Ordering::SeqCst)
|
||||
&& !session.cancel.stopped.load(Ordering::SeqCst)
|
||||
{
|
||||
async {
|
||||
let bridge = Bridge::connect()?;
|
||||
let reply = bridge
|
||||
.call("input-capture-register", json!({"capabilities":caps}))
|
||||
.await?;
|
||||
let id = reply["session"]
|
||||
.as_u64()
|
||||
.ok_or("Missing capture session ID")?;
|
||||
let keymap = reply["keymap"]
|
||||
.as_str()
|
||||
.ok_or("Missing keyboard map")?
|
||||
.to_owned();
|
||||
let watch = Bridge::connect()?;
|
||||
let mut state = session.state.lock().await;
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
{
|
||||
bridge.close();
|
||||
return Err("Input capture approval was cancelled".to_owned());
|
||||
}
|
||||
state.started = true;
|
||||
state.starting = false;
|
||||
state.id = id;
|
||||
state.capabilities = caps;
|
||||
state.keymap = keymap;
|
||||
state.bridge = Some(bridge);
|
||||
state.watch = Some(watch.clone());
|
||||
tokio::spawn(self.clone().watch(
|
||||
connection.clone(),
|
||||
session_handle.clone(),
|
||||
session.clone(),
|
||||
watch,
|
||||
id,
|
||||
));
|
||||
Ok(Options::from([
|
||||
("capabilities".into(), caps.into()),
|
||||
("clipboard_enabled".into(), false.into()),
|
||||
("persist_mode".into(), 0u32.into()),
|
||||
]))
|
||||
}
|
||||
.await
|
||||
} else {
|
||||
Err(approved.err().unwrap_or_else(|| "Cancelled".into()))
|
||||
};
|
||||
self.requests.end(connection, &handle).await;
|
||||
match result {
|
||||
Ok(results) => Ok((0, results)),
|
||||
Err(error) => {
|
||||
let response = if error == "Cancelled"
|
||||
|| cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
{
|
||||
1
|
||||
} else {
|
||||
eprintln!("ferese input capture: {error}");
|
||||
2
|
||||
};
|
||||
self.end(connection, session_handle.as_str()).await;
|
||||
Ok((response, Options::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_zones(
|
||||
&self,
|
||||
handle: OwnedObjectPath,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
_ = cancel.wait() => None,
|
||||
_ = session.cancel.wait() => None,
|
||||
result = bridge.call("input-capture-zones", json!({"session":id})) => Some(result),
|
||||
};
|
||||
self.requests.end(connection, &handle).await;
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
|| result.is_none()
|
||||
{
|
||||
return Ok((1, Options::new()));
|
||||
}
|
||||
let result = result.unwrap().map_err(failed)?;
|
||||
let zones: Vec<(u32, u32, i32, i32)> = serde_json::from_value(result["zones"].clone())
|
||||
.map_err(|_| invalid("Invalid native capture zones"))?;
|
||||
Ok((
|
||||
0,
|
||||
Options::from([
|
||||
(
|
||||
"zones".into(),
|
||||
Value::from(zones)
|
||||
.try_to_owned()
|
||||
.map_err(|e| failed(e.to_string()))?,
|
||||
),
|
||||
(
|
||||
"zone_set".into(),
|
||||
(result["zone_set"].as_u64().unwrap_or(0) as u32).into(),
|
||||
),
|
||||
]),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn set_pointer_barriers(
|
||||
&self,
|
||||
handle: OwnedObjectPath,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_options: Options,
|
||||
barriers: Vec<Options>,
|
||||
zone_set: u32,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
if barriers.len() > 64 {
|
||||
return Err(invalid("Too many pointer barriers"));
|
||||
}
|
||||
let barriers = barriers
|
||||
.into_iter()
|
||||
.map(|barrier| {
|
||||
let id = u32::try_from(
|
||||
barrier
|
||||
.get("barrier_id")
|
||||
.ok_or_else(|| invalid("Missing barrier ID"))?,
|
||||
)
|
||||
.map_err(|_| invalid("Invalid barrier ID"))?;
|
||||
let position = barrier
|
||||
.get("position")
|
||||
.ok_or_else(|| invalid("Missing barrier position"))?
|
||||
.try_clone()
|
||||
.map_err(|_| invalid("Invalid position"))?;
|
||||
let position: (i32, i32, i32, i32) = position
|
||||
.try_into()
|
||||
.map_err(|_| invalid("Invalid barrier position"))?;
|
||||
Ok(json!({"id":id,"position":[position.0,position.1,position.2,position.3]}))
|
||||
})
|
||||
.collect::<zbus::fdo::Result<Vec<_>>>()?;
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let result = tokio::select! {
|
||||
biased;
|
||||
_ = cancel.wait() => None,
|
||||
_ = session.cancel.wait() => None,
|
||||
result = bridge.call("input-capture-barriers", json!({"session":id,"zone_set":zone_set,"barriers":barriers})) => Some(result),
|
||||
};
|
||||
self.requests.end(connection, &handle).await;
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
|| result.is_none()
|
||||
{
|
||||
self.end(connection, session_handle.as_str()).await;
|
||||
return Ok((1, Options::new()));
|
||||
}
|
||||
let result = result.unwrap().map_err(failed)?;
|
||||
let failed_barriers: Vec<u32> = serde_json::from_value(result["failed_barriers"].clone())
|
||||
.map_err(|_| invalid("Invalid native barrier response"))?;
|
||||
Ok((
|
||||
0,
|
||||
Options::from([(
|
||||
"failed_barriers".into(),
|
||||
Value::from(failed_barriers)
|
||||
.try_to_owned()
|
||||
.map_err(|e| failed(e.to_string()))?,
|
||||
)]),
|
||||
))
|
||||
}
|
||||
|
||||
#[zbus(name = "ConnectToEIS")]
|
||||
async fn connect_to_eis(
|
||||
&self,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<OwnedFd> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
Self::native(&session).await?;
|
||||
let mut state = session.state.lock().await;
|
||||
if state.worker.is_some() {
|
||||
return Err(invalid("EIS has already been connected"));
|
||||
}
|
||||
let capabilities = state.capabilities;
|
||||
let keymap = state.keymap.clone();
|
||||
let cancel = session.cancel.clone();
|
||||
let (worker, fd) =
|
||||
tokio::task::spawn_blocking(move || Worker::start(capabilities, &keymap, cancel))
|
||||
.await
|
||||
.map_err(|e| failed(e.to_string()))?
|
||||
.map_err(failed)?;
|
||||
state.worker = Some(worker);
|
||||
Ok(fd.into())
|
||||
}
|
||||
|
||||
async fn enable(
|
||||
&self,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
let ready = session
|
||||
.state
|
||||
.lock()
|
||||
.await
|
||||
.worker
|
||||
.as_ref()
|
||||
.map(|worker| worker.ready.clone())
|
||||
.ok_or_else(|| invalid("Connect to EIS before enabling input capture"))?;
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||
while !ready.load(Ordering::SeqCst) {
|
||||
if session.cancel.stopped.load(Ordering::SeqCst)
|
||||
|| tokio::time::Instant::now() >= deadline
|
||||
{
|
||||
return Err(invalid("EIS receiver has not bound an input device"));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
bridge
|
||||
.call("input-capture-enable", json!({"session":id}))
|
||||
.await
|
||||
.map_err(failed)?;
|
||||
Ok((0, Options::new()))
|
||||
}
|
||||
|
||||
async fn disable(
|
||||
&self,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
_options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
bridge
|
||||
.call("input-capture-disable", json!({"session":id}))
|
||||
.await
|
||||
.map_err(failed)?;
|
||||
Ok((0, Options::new()))
|
||||
}
|
||||
|
||||
async fn release(
|
||||
&self,
|
||||
session_handle: OwnedObjectPath,
|
||||
app_id: String,
|
||||
options: Options,
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
let activation = options
|
||||
.get("activation_id")
|
||||
.map(u32::try_from)
|
||||
.transpose()
|
||||
.map_err(|_| invalid("Invalid activation ID"))?;
|
||||
let position = options
|
||||
.get("cursor_position")
|
||||
.map(|value| {
|
||||
let position: (f64, f64) = value
|
||||
.try_clone()
|
||||
.map_err(|_| invalid("Invalid cursor position"))?
|
||||
.try_into()
|
||||
.map_err(|_| invalid("Invalid cursor position"))?;
|
||||
if !position.0.is_finite() || !position.1.is_finite() {
|
||||
return Err(invalid("Invalid cursor position"));
|
||||
}
|
||||
Ok(position)
|
||||
})
|
||||
.transpose()?;
|
||||
bridge
|
||||
.call(
|
||||
"input-capture-release",
|
||||
json!({"session":id,"activation_id":activation,"cursor_position":position}),
|
||||
)
|
||||
.await
|
||||
.map_err(failed)?;
|
||||
Ok((0, Options::new()))
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn activated(
|
||||
emitter: &SignalEmitter<'_>,
|
||||
session_handle: OwnedObjectPath,
|
||||
options: Options,
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn deactivated(
|
||||
emitter: &SignalEmitter<'_>,
|
||||
session_handle: OwnedObjectPath,
|
||||
options: Options,
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn disabled(
|
||||
emitter: &SignalEmitter<'_>,
|
||||
session_handle: OwnedObjectPath,
|
||||
options: Options,
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn zones_changed(
|
||||
emitter: &SignalEmitter<'_>,
|
||||
session_handle: OwnedObjectPath,
|
||||
options: Options,
|
||||
) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
struct SessionObject {
|
||||
backend: InputCapture,
|
||||
path: String,
|
||||
owner: String,
|
||||
}
|
||||
|
||||
#[zbus::interface(name = "org.freedesktop.impl.portal.Session")]
|
||||
impl SessionObject {
|
||||
#[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 header.sender().map(|owner| owner.as_str()) != Some(self.owner.as_str()) {
|
||||
return Err(zbus::fdo::Error::AccessDenied("Wrong session owner".into()));
|
||||
}
|
||||
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 requested_capabilities_are_required_and_supported_subset_is_returned() {
|
||||
assert!(capabilities(&Options::new()).is_err());
|
||||
for requested in [0u32, 4, 8] {
|
||||
assert!(
|
||||
capabilities(&Options::from([("capabilities".into(), requested.into())])).is_err()
|
||||
);
|
||||
}
|
||||
for (requested, supported) in [(1u32, 1), (2, 2), (3, 3), (7, 3), (9, 1)] {
|
||||
assert_eq!(
|
||||
capabilities(&Options::from([("capabilities".into(), requested.into())])).unwrap(),
|
||||
supported
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ mod bridge;
|
||||
mod capture;
|
||||
mod consent;
|
||||
mod desktop;
|
||||
mod eis;
|
||||
mod inhibit;
|
||||
mod input_capture;
|
||||
mod lockdown;
|
||||
mod parent;
|
||||
mod permissions;
|
||||
|
||||
Reference in new issue
Block a user