diff --git a/Cargo.lock b/Cargo.lock index 2982a85e6..0c5ff6301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,6 +834,8 @@ dependencies = [ "cosmic-text", "egui", "egui_plot", + "futures-executor", + "futures-util", "i18n-embed", "i18n-embed-fl", "iced_tiny_skia", diff --git a/Cargo.toml b/Cargo.toml index e08779342..db3bf2f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,8 @@ reis = { version = "0.5", features = ["calloop"] } clap_lex = "0.7" parking_lot = "0.12.4" logind-zbus = { version = "5.3.2", optional = true } +futures-executor = { version = "0.3.31", features = ["thread-pool"] } +futures-util = "0.3.31" [dependencies.id_tree] branch = "feature/copy_clone" diff --git a/src/dbus/a11y_keyboard_monitor.rs b/src/dbus/a11y_keyboard_monitor.rs new file mode 100644 index 000000000..84d972940 --- /dev/null +++ b/src/dbus/a11y_keyboard_monitor.rs @@ -0,0 +1,271 @@ +// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.freedesktop.a11y.xml +// +// TODO: Restrict protocol acccess? +// TODO remove client when not connected to server + +use futures_executor::ThreadPool; +use smithay::backend::input::KeyState; +use std::collections::HashMap; +use std::collections::HashSet; +use std::sync::OnceLock; +use std::sync::{Arc, Mutex}; +use xkbcommon::xkb::{self, Keysym}; +use zbus::message::Header; +use zbus::names::UniqueName; +use zbus::object_server::SignalEmitter; + +// As defined in at-spi2-core +const ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START: u32 = 15; + +#[derive(PartialEq, Eq, Debug)] +struct KeyGrab { + pub mods: u32, + pub virtual_mods: HashSet, + pub key: Keysym, +} + +impl KeyGrab { + fn new(virtual_mods: &[Keysym], key: Keysym, raw_mods: u32) -> Self { + let mods = raw_mods & ((1 << ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START) - 1); + let virtual_mods = virtual_mods + .iter() + .copied() + .enumerate() + .filter(|(i, _)| { + raw_mods & (1 << (ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START + *i as u32)) != 0 + }) + .map(|(_, x)| x) + .collect(); + Self { + mods, + virtual_mods, + key, + } + } +} + +#[derive(Debug, Default)] +struct Client { + grabbed: bool, + watched: bool, + virtual_mods: HashSet, + key_grabs: Vec, +} + +#[derive(Debug, Default)] +struct Clients(HashMap, Client>); + +impl Clients { + fn get(&mut self, name: &UniqueName<'_>) -> &mut Client { + self.0.entry(name.to_owned()).or_default() + } + + fn remove(&mut self, name: &UniqueName<'_>) -> bool { + self.0.remove(&name.to_owned()).is_some() + } +} + +#[derive(Debug)] +pub struct A11yKeyboardMonitorState { + executor: ThreadPool, + clients: Arc>, + active_virtual_mods: HashSet, + conn: Arc>, +} + +impl A11yKeyboardMonitorState { + pub fn new(executor: &ThreadPool) -> Self { + let clients = Arc::new(Mutex::new(Clients::default())); + let clients_clone = clients.clone(); + let conn_cell = Arc::new(OnceLock::new()); + let conn_cell_clone = conn_cell.clone(); + executor.spawn_ok(async move { + match serve(clients_clone).await { + Ok(conn) => { + conn_cell_clone.set(conn).unwrap(); + } + Err(err) => { + tracing::error!("Failed to serve `org.freedesktop.a11y.Manager`: {err}"); + } + } + }); + Self { + executor: executor.clone(), + clients, + active_virtual_mods: HashSet::new(), + conn: conn_cell, + } + } + + pub fn has_virtual_mod(&self, keysym: Keysym) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.virtual_mods.contains(&keysym)) + } + + pub fn add_active_virtual_mod(&mut self, keysym: Keysym) { + self.active_virtual_mods.insert(keysym); + } + + pub fn remove_active_virtual_mod(&mut self, keysym: Keysym) -> bool { + self.active_virtual_mods.remove(&keysym) + } + + pub fn has_keyboard_grab(&self) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.grabbed) + } + + /// Key grab exists for mods, key, with active virtual mods + pub fn has_key_grab(&self, mods: u32, key: Keysym) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .flat_map(|client| &client.key_grabs) + .any(|grab| { + grab.mods == mods + && grab.virtual_mods == self.active_virtual_mods + && grab.key == key + }) + } + + pub fn key_event( + &self, + modifiers: &smithay::input::keyboard::ModifiersState, + keysym: &smithay::input::keyboard::KeysymHandle, + state: smithay::backend::input::KeyState, + ) { + let Some(conn) = self.conn.get() else { + return; + }; + + // Test if any client is watching key input + if !self + .clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.watched) + { + return; + } + + let signal_context = SignalEmitter::new(conn, "/org/freedesktop/a11y/Manager").unwrap(); + let released = match state { + KeyState::Pressed => false, + KeyState::Released => true, + }; + let unichar = { + let xkb = keysym.xkb().lock().unwrap(); + unsafe { xkb.state() }.key_get_utf32(keysym.raw_code()) + }; + let future = KeyboardMonitor::key_event( + signal_context, + released, + modifiers.serialized.layout_effective, + keysym.modified_sym().raw(), + unichar, + keysym.raw_code().raw() as u16, + ); + self.executor.spawn_ok(async { + future.await; + }); + } +} + +struct KeyboardMonitor { + clients: Arc>, +} + +#[zbus::interface(name = "org.freedesktop.a11y.KeyboardMonitor")] +impl KeyboardMonitor { + fn grab_keyboard(&mut self, #[zbus(header)] header: Header<'_>) { + if let Some(sender) = header.sender() { + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).grabbed = true; + eprintln!("grab keyboard by {}", sender); + } + } + + fn ungrab_keyboard(&mut self, #[zbus(header)] header: Header<'_>) { + if let Some(sender) = header.sender() { + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).grabbed = false; + eprintln!("ungrab keyboard by {}", sender); + } + } + + fn watch_keyboard(&mut self, #[zbus(header)] header: Header<'_>) { + if let Some(sender) = header.sender() { + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).watched = true; + eprintln!("watch keyboard by {}", sender); + } + } + + fn unwatch_keyboard(&mut self, #[zbus(header)] header: Header<'_>) { + if let Some(sender) = header.sender() { + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).watched = false; + eprintln!("unwatch keyboard by {}", sender); + } + } + + fn set_key_grabs( + &self, + #[zbus(header)] header: Header<'_>, + virtual_mods: Vec, + keystrokes: Vec<(u32, u32)>, + ) { + let virtual_mods = virtual_mods + .into_iter() + .map(Keysym::from) + .collect::>(); + let key_grabs = keystrokes + .into_iter() + .map(|(k, mods)| KeyGrab::new(&virtual_mods, Keysym::from(k), mods)) + .collect::>(); + + if let Some(sender) = header.sender() { + let mut clients = self.clients.lock().unwrap(); + let client = clients.get(sender); + eprintln!( + "key grabs set by {}: {:?}", + sender, + (&virtual_mods, &key_grabs) + ); + client.virtual_mods = virtual_mods.into_iter().collect::>(); + client.key_grabs = key_grabs; + } + } + + // TODO signal + #[zbus(signal)] + async fn key_event( + ctx: SignalEmitter<'_>, + released: bool, + state: u32, + keysym: u32, + unichar: u32, + keycode: u16, + ) -> zbus::Result<()>; +} + +async fn serve(clients: Arc>) -> zbus::Result { + let keyboard_monitor = KeyboardMonitor { clients }; + zbus::connection::Builder::session()? + .name("org.freedesktop.a11y.Manager")? + .serve_at("/org/freedesktop/a11y/Manager", keyboard_monitor)? + .build() + .await +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index e587e89b6..7eb5eb51f 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -5,18 +5,25 @@ use crate::{ use anyhow::{Context, Result}; use calloop::{InsertError, LoopHandle, RegistrationToken}; use cosmic_comp_config::output::comp::OutputState; +use futures_executor::{block_on, ThreadPool}; +use futures_util::stream::StreamExt; use std::collections::HashMap; use tracing::{error, warn}; use zbus::blocking::{fdo::DBusProxy, Connection}; +pub mod a11y_keyboard_monitor; #[cfg(feature = "systemd")] pub mod logind; +mod name_owners; mod power; -pub fn init(evlh: &LoopHandle<'static, State>) -> Result> { +pub fn init( + evlh: &LoopHandle<'static, State>, + executor: &ThreadPool, +) -> Result> { let mut tokens = Vec::new(); - match power::init() { + match block_on(power::init()) { Ok(power_daemon) => { let (tx, rx) = calloop::channel::channel(); @@ -58,29 +65,17 @@ pub fn init(evlh: &LoopHandle<'static, State>) -> Result> .with_context(|| "Failed to add channel to event_loop")?; // start helper thread - let result = std::thread::Builder::new() - .name("system76-power-hotplug".to_string()) - .spawn(move || { - if let Ok(mut msg_iter) = power_daemon.receive_hot_plug_detect() { - while let Some(msg) = msg_iter.next() { - if tx.send(msg).is_err() { - break; - } + executor.spawn_ok(async move { + if let Ok(mut msg_iter) = power_daemon.receive_hot_plug_detect().await { + while let Some(msg) = msg_iter.next().await { + if tx.send(msg).is_err() { + break; } } - }) - .with_context(|| "Failed to start helper thread"); - - match result { - Ok(_handle) => { - tokens.push(token); - // detach thread } - Err(err) => { - evlh.remove(token); - return Err(err); - } - } + }); + + tokens.push(token); } Err(err) => { tracing::info!(?err, "Failed to connect to com.system76.PowerDaemon"); @@ -110,3 +105,21 @@ pub fn ready(common: &Common) -> Result<()> { Ok(()) } + +/* +/// Serve interfaces on session socket +/// +/// (Currently only the a11y keyboard monitor interface) +fn serve_interfaces(executor: &ThreadPool) { + let executor_clone = executor.clone(); + executor.spawn_ok(async move { + serve_interfaces_inner(&executor_clone); + }); +} + +async fn serve_interfaces_inner(executor: &ThreadPool, a11y_clients: Arc>) -> zbus::Result<()> { + let conn = zbus::Connection::session().await?; + name_owners::NameOwners::new(&conn, executor).await?; + Ok(()) +} +*/ diff --git a/src/dbus/name_owners.rs b/src/dbus/name_owners.rs new file mode 100644 index 000000000..6c9b12789 --- /dev/null +++ b/src/dbus/name_owners.rs @@ -0,0 +1,107 @@ +use futures_executor::ThreadPool; +use futures_util::StreamExt; +use std::{ + collections::HashMap, + future::{poll_fn, Future}, + sync::{Arc, Mutex, Weak}, + task::{Context, Poll, Waker}, +}; +use zbus::{ + fdo, + names::{BusName, UniqueName, WellKnownName}, +}; + +#[derive(Debug)] +struct Inner { + name_owners: HashMap, UniqueName<'static>>, + stream: fdo::NameOwnerChangedStream, + // Waker from `update_task` is stored, so that task will still be woken after + // polling elsewhere. + waker: Waker, + enforce: bool, +} + +impl Drop for Inner { + fn drop(&mut self) { + // XXX shouldn't wake until Arc has no refs? Race. + self.waker.wake_by_ref(); + } +} + +impl Inner { + /// Process all events so far on `stream`, and update `name_owners`. + fn update_if_needed(&mut self) { + let mut context = Context::from_waker(&self.waker); + while let Poll::Ready(val) = self.stream.poll_next_unpin(&mut context) { + let val = val.unwrap(); + let args = val.args().unwrap(); + if let BusName::WellKnown(name) = args.name { + if let Some(owner) = &*args.new_owner { + self.name_owners.insert(name.to_owned(), owner.to_owned()); + } else { + self.name_owners.remove(&name.to_owned()); + } + } + } + } +} + +/// This task polls the steam regularly, to make sure events on the stream aren't just +/// buffered indefinitely if `check_owner` is never called. +fn update_task(inner: Weak>) -> impl Future { + poll_fn(move |context| { + if let Some(inner) = inner.upgrade() { + let mut inner = inner.lock().unwrap(); + inner.waker = context.waker().clone(); + inner.update_if_needed(); + // Nothing to do now until waker is invoked + Poll::Pending + } else { + // All strong references have been dropped, so task has nothing left to do. + Poll::Ready(()) + } + }) +} + +/// Track which DBus unique names own which well-known names, so protocols can be restricted to +/// only certain names. +/// +/// Enforcement can be disabled by setting `COSMIC_ENFORCE_DBUS_OWNERS`. +#[derive(Clone, Debug)] +pub struct NameOwners(Arc>); + +impl NameOwners { + pub async fn new(connection: &zbus::Connection, executor: &ThreadPool) -> zbus::Result { + let dbus = fdo::DBusProxy::new(connection).await?; + let stream = dbus.receive_name_owner_changed().await?; + + let enforce = crate::utils::env::bool_var("COSMIC_ENFORCE_DBUS_OWNERS").unwrap_or(true); + + let inner = Arc::new(Mutex::new(Inner { + name_owners: HashMap::new(), + stream, + waker: Waker::noop().clone(), + enforce, + })); + + if enforce { + executor.spawn_ok(update_task(Arc::downgrade(&inner))); + } + + Ok(NameOwners(inner)) + } + + /// Check if the unique name `name` owns at least one of the well-known names in `allowed_names`. + pub fn check_owner(&self, name: &UniqueName<'_>, allowed_names: &[WellKnownName<'_>]) -> bool { + let mut inner = self.0.lock().unwrap(); + if !inner.enforce { + return true; + } + // Make sure latest events from stream have been processed + inner.update_if_needed(); + + allowed_names + .iter() + .any(|n| inner.name_owners.get(n) == Some(name)) + } +} diff --git a/src/dbus/power.rs b/src/dbus/power.rs index 9d9d37b62..346b8107e 100644 --- a/src/dbus/power.rs +++ b/src/dbus/power.rs @@ -18,7 +18,7 @@ //! //! …consequently `zbus-xmlgen` did not generate code for the above interfaces. -use zbus::blocking::Connection; +use zbus::Connection; #[zbus::proxy( interface = "com.system76.PowerDaemon", @@ -79,9 +79,9 @@ pub trait PowerDaemon { fn power_profile_switch(&self, profile: &str) -> zbus::Result<()>; } -pub fn init() -> anyhow::Result> { - let conn = Connection::system()?; - let proxy = PowerDaemonProxyBlocking::new(&conn)?; - proxy.0.introspect()?; +pub async fn init() -> anyhow::Result> { + let conn = Connection::system().await?; + let proxy = PowerDaemonProxy::new(&conn).await?; + proxy.0.introspect().await?; Ok(proxy) } diff --git a/src/input/mod.rs b/src/input/mod.rs index 9c1f9c2d9..146ff5c41 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1598,12 +1598,16 @@ impl State { .unwrap_or(false) }); + // TODO update keyboard monitor self.common.atspi_ei.input( modifiers, &handle, event.state(), event.time() as u64 * 1000, ); + self.common + .a11y_keyboard_monitor_state + .key_event(modifiers, &handle, event.state()); // Leave move overview mode, if any modifier was released if let Some(Trigger::KeyboardMove(action_modifiers)) = @@ -1768,11 +1772,15 @@ impl State { } if event.state() == KeyState::Released { - let removed = self + let mut removed = self .common .atspi_ei .active_virtual_mods .remove(&event.key_code()); + removed |= self + .common + .a11y_keyboard_monitor_state + .remove_active_virtual_mod(handle.modified_sym()); // If `Caps_Lock` is a virtual modifier, and is in locked state, clear it if removed && handle.modified_sym() == Keysym::Caps_Lock { if (modifiers.serialized.locked & 2) != 0 { @@ -1798,16 +1806,23 @@ impl State { } } } else if event.state() == KeyState::Pressed - && self + && (self .common .atspi_ei .virtual_mods .contains(&event.key_code()) + || self + .common + .a11y_keyboard_monitor_state + .has_virtual_mod(handle.modified_sym())) { self.common .atspi_ei .active_virtual_mods .insert(event.key_code()); + self.common + .a11y_keyboard_monitor_state + .add_active_virtual_mod(handle.modified_sym()); tracing::debug!( "active virtual mods: {:?}", @@ -1843,10 +1858,15 @@ impl State { } if self.common.atspi_ei.has_keyboard_grab() + || self.common.a11y_keyboard_monitor_state.has_keyboard_grab() || self .common .atspi_ei .has_key_grab(modifiers.serialized.layout_effective, event.key_code()) + || self + .common + .a11y_keyboard_monitor_state + .has_key_grab(modifiers.serialized.layout_effective, handle.modified_sym()) { return FilterResult::Intercept(None); } diff --git a/src/state.rs b/src/state.rs index fb1dff3cb..380d03edc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,6 +8,7 @@ use crate::{ x11::X11State, }, config::{CompOutputConfig, Config, ScreenFilter}, + dbus::a11y_keyboard_monitor::A11yKeyboardMonitorState, input::{gestures::GestureState, PointerFocusState}, shell::{grabs::SeatMoveGrabState, CosmicSurface, SeatExt, Shell}, utils::prelude::OutputExt, @@ -33,6 +34,7 @@ use crate::{ use anyhow::Context; use calloop::RegistrationToken; use cosmic_comp_config::output::comp::{OutputConfig, OutputState}; +use futures_executor::ThreadPool; use i18n_embed::{ fluent::{fluent_language_loader, FluentLanguageLoader}, DesktopLanguageRequester, @@ -198,6 +200,7 @@ pub struct Common { pub display_handle: DisplayHandle, pub event_loop_handle: LoopHandle<'static, State>, pub event_loop_signal: LoopSignal, + pub async_executor: ThreadPool, pub popups: PopupManager, pub shell: Arc>, @@ -239,6 +242,7 @@ pub struct Common { pub xdg_decoration_state: XdgDecorationState, pub overlap_notify_state: OverlapNotifyState, pub a11y_state: A11yState, + pub a11y_keyboard_monitor_state: A11yKeyboardMonitorState, // shell-related wayland state pub xdg_shell_state: XdgShellState, @@ -681,12 +685,16 @@ impl State { ); let workspace_state = WorkspaceState::new(dh, client_is_privileged); - if let Err(err) = crate::dbus::init(&handle) { + let async_executor = ThreadPool::builder().pool_size(1).create().unwrap(); + + if let Err(err) = crate::dbus::init(&handle, &async_executor) { tracing::warn!(?err, "Failed to initialize dbus handlers"); } let a11y_state = A11yState::new::(dh, client_is_privileged); + let a11y_keyboard_monitor_state = A11yKeyboardMonitorState::new(&async_executor); + // TODO: Restrict to only specific client? let atspi_state = AtspiState::new::(dh, |_| true); @@ -697,6 +705,7 @@ impl State { display_handle: dh.clone(), event_loop_handle: handle, event_loop_signal: signal, + async_executor, popups: PopupManager::default(), shell, @@ -745,6 +754,7 @@ impl State { xdg_foreign_state, workspace_state, a11y_state, + a11y_keyboard_monitor_state, xwayland_scale: None, xwayland_state: None, xwayland_shell_state,