srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/lock.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-04-08 23:54:00 +0200
committersrdusr <[email protected]>2024-04-08 23:54:00 +0200
commit709333908d5b7d3157165d1811c8bc1795ab0028 (patch)
tree92d5616519f63c1e4743add0294ce552e239681c /crates/wayland/src/lock.rs
parent592bd3716c0aa5a47a5eb43f45f55844de9db382 (diff)
downloadsrdwm-709333908d5b7d3157165d1811c8bc1795ab0028.tar.gz
srdwm-709333908d5b7d3157165d1811c8bc1795ab0028.zip
Wayland: clipboard, session lock, screencopy, multi-monitor; modularize
Implements the protocols that were blocking srdwm-wayland from being a real session, plus multi-monitor, and splits the backend into modules. Everything here was verified by running it against real clients, not just compiled. Protocols - wlr-layer-shell + xdg-output: bars/launchers/notifications. xdg-output is not optional in practice - without it wofi segfaults rather than degrading, since it calls get_xdg_output without null-checking. - Clipboard: wl_data_device_manager, primary selection, and wlr-data-control. Data-control is what `wl-paste --watch cliphist store` needs, as it reads the selection without holding focus. Selection focus now follows keyboard focus, without which a focused window can neither copy nor paste. - ext-session-lock: `locked` gates rendering and input. No key is treated as a WM binding while locked - the config binds Mod4+Return to spawn a terminal, so honouring bindings at a locked screen would defeat the lock. The lock is confirmed only after a client-content-free frame has actually been presented, never at request time. - wlr-screencopy (hand-written; smithay ships no helper) for grim/slurp. Multi-monitor Every connected connector becomes a head with its own buffers, damage tracker and page-flip state, matched by CRTC so differing refresh rates don't gate each other. Outputs are reached through primary_output/output_at/ output_for_wl rather than a single field, which kept the change to ~11 call sites. Session lock creates one lock surface per output and waits for all of them, so a second monitor can't still show the desktop when the locker is told the session is safe. Modes are picked by the PREFERRED flag, not list order, and CRTCs are never double-assigned. Bugs found by testing, not review - Nothing gave a newly-created window Wayland focus: a freshly-opened app received no keystrokes and could not paste until clicked. - Opening a window at a locked screen stole keyboard focus. Caught by counting wl_keyboard.enter delivered to a client launched while locked: 1 before the guard, 0 after. A killed locker correctly leaves it locked. - Reading back the winit EGL window surface destroyed the GL context on the first screencopy capture, taking the compositor down. Root-caused by A/B-ing the same build with only the readback removed; capture now renders an offscreen pass. - Output mode was resent every frame at 60Hz, flooding any client bound to wl_output with duplicate mode/done events. - general.default_layout was defaulted and validated but never read, so setting it did nothing. srdwm is dynamic-first (Windows/macOS style, with drag-to-edge snapping); tiling is one opt-in layout, and that stays true. - .gitignore's unanchored `srdwm` matched any path component of that name, silently excluding the whole crates/srdwm source crate - the binary crate the workspace lists as a member, so a fresh clone could not build. Modularization lib.rs went from ~1260 lines to 78: state, protocols, input, lock, screencopy, winit and udev now each own one responsibility. lock is grouped by feature rather than kind on purpose, since its security invariant spans state, protocol handling and rendering at once. Multi-monitor was verified in the QEMU VM with a two-output virtio-gpu: both heads screendumped at their own resolution showing srdwm's clear colour, and a window forced to global x=1500 landed on head 1 at head-local x=220 while head 0 stayed empty. Known limitation, now documented: the nested winit backend stalls while its window is occluded, because the host stops scheduling frames and eglSwapBuffers blocks.
Diffstat (limited to 'crates/wayland/src/lock.rs')
-rw-r--r--crates/wayland/src/lock.rs171
1 files changed, 171 insertions, 0 deletions
diff --git a/crates/wayland/src/lock.rs b/crates/wayland/src/lock.rs
new file mode 100644
index 0000000..51b6158
--- /dev/null
+++ b/crates/wayland/src/lock.rs
@@ -0,0 +1,171 @@
+//! Session lock (`ext-session-lock-v1`).
+//!
+//! Kept as its own module because the security-relevant invariant spans
+//! state, protocol handling *and* rendering: while [`SessionLock::locked`]
+//! is set, no client surface other than the lock surface may be drawn or
+//! receive input. The render helpers here are the "drawn" half; the input
+//! half is the locked-session branches in [`crate::input`], and the focus
+//! half is the guard in [`crate::state::CompState::set_keyboard_focus`].
+
+use smithay::backend::renderer::element::surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement};
+use smithay::backend::renderer::element::Kind;
+use smithay::output::Output;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::wayland::session_lock::{LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker};
+
+use crate::input::dwindow_wl_surface;
+use std::collections::{HashMap, HashSet};
+
+use crate::state::CompState;
+
+/// Session-lock runtime state (`ext-session-lock-v1`).
+///
+/// The security-relevant invariant is that `locked` gates *both* rendering
+/// and input: while it is set, no client surface other than a lock surface
+/// is drawn or receives events. `pending_confirm` exists because the
+/// protocol requires the compositor to confirm the lock only *after* a frame
+/// with no client content has actually been presented - confirming earlier
+/// would tell the locker "the screen is safe" while the user's windows were
+/// still on screen. So `lock()` only stashes the confirmation here, and the
+/// render paths call `confirm_lock_if_presented()` once such a frame is out.
+///
+/// With multiple monitors the locker creates **one lock surface per
+/// output**, and the confirmation must wait for *every* output to have both
+/// a surface and a presented frame - otherwise a second monitor could still
+/// be showing the desktop at the moment the locker is told the session is
+/// locked.
+#[derive(Default)]
+pub(crate) struct SessionLock {
+ pub(crate) locked: bool,
+ /// Lock surface per output, keyed by `Output::name()`.
+ pub(crate) surfaces: HashMap<String, LockSurface>,
+ pub(crate) pending_confirm: Option<SessionLocker>,
+ /// Outputs that have presented a client-content-free frame since the
+ /// lock request, keyed the same way.
+ pub(crate) presented: HashSet<String>,
+}
+
+impl CompState {
+ /// Records that `output` has presented a lock frame, and confirms the
+ /// lock once *every* output has done so. Called from both backends right
+ /// after a frame is presented. See `SessionLock::pending_confirm`.
+ pub(crate) fn confirm_lock_if_presented(&mut self, output: &Output) {
+ if !self.lock.locked || self.lock.pending_confirm.is_none() {
+ return;
+ }
+ self.lock.presented.insert(output.name());
+
+ // Every output must be both covered by a lock surface and have shown
+ // a frame of it. An output still missing its surface means the locker
+ // hasn't got to it yet - keep waiting rather than confirm early.
+ let all_covered = self
+ .outputs()
+ .all(|o| self.lock.surfaces.contains_key(&o.name()) && self.lock.presented.contains(&o.name()));
+ if !all_covered {
+ return;
+ }
+ if let Some(confirm) = self.lock.pending_confirm.take() {
+ log::info!("session lock: all {} output(s) presented a cleared frame, confirming lock", self.outputs.len());
+ confirm.lock();
+ }
+ }
+
+ /// The lock surface covering `output`, if we are locked and the locker
+ /// has created one for it. Used by the render paths and input routing.
+ pub(crate) fn lock_surface_for(&self, output: &Output) -> Option<&WlSurface> {
+ if !self.lock.locked {
+ return None;
+ }
+ self.lock.surfaces.get(&output.name()).map(|s| s.wl_surface())
+ }
+
+ /// Any lock surface at all - used by input routing, which needs a
+ /// keyboard-focus target rather than a per-output one.
+ pub(crate) fn any_lock_surface(&self) -> Option<&WlSurface> {
+ if !self.lock.locked {
+ return None;
+ }
+ self.lock.surfaces.values().next().map(|s| s.wl_surface())
+ }
+}
+
+impl SessionLockHandler for CompState {
+ fn lock_state(&mut self) -> &mut SessionLockManagerState {
+ &mut self.session_lock_state
+ }
+
+ fn lock(&mut self, confirmation: SessionLocker) {
+ log::info!("session lock: locking");
+ self.lock.locked = true;
+ self.lock.pending_confirm = Some(confirmation);
+ // Drop keyboard focus off whatever client had it immediately, so no
+ // keystroke can reach a normal client in the window between the lock
+ // request and the lock surface being mapped. Focus moves to the lock
+ // surface in `new_surface` once it exists.
+ self.set_keyboard_focus(None);
+ }
+
+ fn unlock(&mut self) {
+ log::info!("session lock: unlocking");
+ self.lock.locked = false;
+ self.lock.surfaces.clear();
+ self.lock.presented.clear();
+ self.lock.pending_confirm = None;
+ // Hand focus back to whatever srdwm considers the focused window.
+ let surface = self
+ .wm
+ .borrow()
+ .focused_id()
+ .and_then(|id| self.id_to_window.get(&id).cloned())
+ .and_then(|w| dwindow_wl_surface(&w));
+ self.set_keyboard_focus(surface);
+ }
+
+ fn new_surface(&mut self, surface: LockSurface, wl_output: WlOutput) {
+ // The locker creates one surface per output and names which; size it
+ // to that output specifically, since monitors differ in resolution.
+ let Some(entry) = self.output_for_wl(&wl_output) else {
+ log::warn!("session lock: lock surface for an output we don't drive; ignoring");
+ return;
+ };
+ let name = entry.output.name();
+ let size = entry.size();
+ surface.with_pending_state(|state| {
+ state.size = Some((size.w as u32, size.h as u32).into());
+ });
+ surface.send_configure();
+ // Store before focusing: `set_keyboard_focus`'s locked-session guard
+ // checks the focus target *against* the stored lock surfaces, so
+ // setting it afterwards would make the guard reject this very
+ // surface.
+ let wl_surface = surface.wl_surface().clone();
+ self.lock.surfaces.insert(name, surface);
+ self.set_keyboard_focus(Some(wl_surface));
+ }
+}
+
+/// Render elements for a locked session: the lock surface alone, at the
+/// output origin. Deliberately *not* built from the `Space` or the
+/// `LayerMap` - while locked, nothing else may reach the screen, so the
+/// caller pairs this with an opaque black clear color. Shared by both
+/// backends (winit and udev), which differ only in their renderer type.
+/// Takes the surface rather than `&CompState` so the udev backend can call
+/// it while already holding a `&mut` borrow of its own `UdevOutput`.
+pub(crate) fn lock_render_elements<R>(lock_surface: Option<&WlSurface>, renderer: &mut R) -> Vec<WaylandSurfaceRenderElement<R>>
+where
+ R: smithay::backend::renderer::Renderer + smithay::backend::renderer::ImportAll,
+ R::TextureId: Clone + 'static,
+{
+ let Some(surface) = lock_surface else { return Vec::new() };
+ render_elements_from_surface_tree(renderer, surface, (0, 0), 1.0, 1.0, Kind::Unspecified)
+}
+
+/// Frame callbacks for the lock surface. Without these the locker never
+/// gets to draw a second frame - no cursor blink, no password-dot
+/// feedback, no failed-attempt shake.
+pub(crate) fn send_lock_frame(lock_surface: Option<&WlSurface>, output: &Output, time: std::time::Duration) {
+ if let Some(surface) = lock_surface {
+ smithay::desktop::utils::send_frames_surface_tree(surface, output, time, None, |_, _| Some(output.clone()));
+ }
+}