srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/lock.rs
diff options
context:
space:
mode:
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()));
+ }
+}