From 032ec7a7685ee015b368b72529407f783205c5d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ga=C3=ABl=20Bonithon?= <gael@xfce.org>
Date: Sat, 3 Feb 2024 16:58:10 +0100
Subject: [PATCH] wayland: Use ext-session-lock protocol

---
 .gitignore                        |   5 +
 Makefile.am                       |   1 +
 configure.ac                      |  14 ++
 protocols/Makefile.am             |  65 ++++++
 protocols/ext-session-lock-v1.xml | 328 ++++++++++++++++++++++++++++++
 src/Makefile.am                   |  16 ++
 src/gs-manager.c                  |  41 +++-
 src/gs-session-lock-manager.c     | 235 +++++++++++++++++++++
 src/gs-session-lock-manager.h     |  40 ++++
 9 files changed, 744 insertions(+), 1 deletion(-)
 create mode 100644 protocols/Makefile.am
 create mode 100644 protocols/ext-session-lock-v1.xml
 create mode 100644 src/gs-session-lock-manager.c
 create mode 100644 src/gs-session-lock-manager.h

diff --git a/.gitignore b/.gitignore
index af62531..0c008ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,8 @@ xfce4-screensaver-*/
 *.desktop
 *.gmo
 *.html
+*.la
+*.lo
 *.m4
 *.o
 *.pot
@@ -23,6 +25,7 @@ xfce4-screensaver-*/
 *marshal.h
 *~
 config.*
+!config.xsl
 stamp-*
 
 # File Match
@@ -56,6 +59,8 @@ po/insert-header.sin
 po/quot.sed
 po/remove-potcdate.sed
 po/remove-potcdate.sin
+protocols/*.c
+protocols/*.h
 savers/*.desktop.in
 savers/floaters
 savers/popsquares
diff --git a/Makefile.am b/Makefile.am
index 4814836..f32cd84 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -7,6 +7,7 @@ AM_DISTCHECK_CONFIGURE_FLAGS = \
 	--without-systemd
 
 SUBDIRS = \
+	protocols      \
 	src      \
 	savers   \
 	data     \
diff --git a/configure.ac b/configure.ac
index fc1bf88..26ddf08 100644
--- a/configure.ac
+++ b/configure.ac
@@ -61,6 +61,8 @@ m4_define([libxklavier_min_version], [5.2])
 m4_define([libwnck_min_version], [3.20])
 
 m4_define([libwlembed_min_version], [0.0.0])
+m4_define([wayland_min_version], [1.15])
+m4_define([wayland_protocols_min_version], [1.20])
 
 XDT_CHECK_PACKAGE([GLIB], [glib-2.0], [glib_min_version])
 XDT_CHECK_PACKAGE([GIO], [gio-2.0], [glib_min_version])
@@ -89,12 +91,23 @@ XDT_CHECK_OPTIONAL_FEATURE([WAYLAND],
                              XDT_FEATURE_DEPENDENCY([GDK_WAYLAND], [gdk-wayland-3.0], [gtk_min_version])
                              XDT_FEATURE_DEPENDENCY([LIBWLEMBED], [libwlembed-0], [libwlembed_min_version])
                              XDT_FEATURE_DEPENDENCY([LIBWLEMBED_GTK3], [libwlembed-gtk3-0], [libwlembed_min_version])
+                             XDT_FEATURE_DEPENDENCY([WAYLAND_CLIENT], [wayland-client], [wayland_min_version])
+                             XDT_FEATURE_DEPENDENCY([WAYLAND_SCANNER], [wayland-scanner], [wayland_min_version])
+                             XDT_FEATURE_DEPENDENCY([WAYLAND_PROTOCOLS], [wayland-protocols], [wayland_protocols_min_version])
                            ],
                            [the Wayland windowing system])
 if test x"$ENABLE_X11" != x"yes" -a x"$ENABLE_WAYLAND" != x"yes"; then
   AC_MSG_ERROR([Either both X11 and Wayland support was disabled, or required dependencies are missing. One of the two must be enabled.])
 fi
 
+if test x"$ENABLE_WAYLAND" = x"yes"; then
+  WAYLAND_PROTOCOLS_PKGDATADIR=`$PKG_CONFIG --variable=pkgdatadir wayland-protocols`
+  AC_SUBST([WAYLAND_PROTOCOLS_PKGDATADIR])
+fi
+dnl FIXME: Bump wayland_protocols_min_version to 1.25 when it is an acceptable requirement,
+dnl and remove this and protocols/ext-session-lock-v1.xml
+AM_CONDITIONAL([HAVE_SESSION_LOCK], [test -f "$WAYLAND_PROTOCOLS_PKGDATADIR/staging/ext-session-lock/ext-session-lock-v1.xml"])
+
 if test x"$ENABLE_X11" = x"yes"; then
 
   # Check whether to use a xscreensaver hacks configuration directory
@@ -902,6 +915,7 @@ AC_SUBST(themesdir)
 
 AC_CONFIG_FILES([
 Makefile
+protocols/Makefile
 po/Makefile.in
 src/Makefile
 src/xfce4-screensaver.desktop.in
diff --git a/protocols/Makefile.am b/protocols/Makefile.am
new file mode 100644
index 0000000..06041ce
--- /dev/null
+++ b/protocols/Makefile.am
@@ -0,0 +1,65 @@
+NULL =
+
+if ENABLE_WAYLAND
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir) \
+	-DG_LOG_DOMAIN=\"libprotocols\" \
+	$(PLATFORM_CPPFLAGS) \
+	$(NULL)
+
+noinst_LTLIBRARIES = \
+	libprotocols.la
+
+libprotocols_built_sources = \
+	ext-session-lock-v1.c \
+	ext-session-lock-v1-client.h \
+	$(NULL)
+
+nodist_libprotocols_la_SOURCES = \
+	$(libprotocols_built_sources)
+
+libprotocols_la_CFLAGS = \
+	$(WAYLAND_CLIENT_CFLAGS) \
+	$(PLATFORM_CFLAGS) \
+	$(NULL)
+
+libprotocols_la_LDFLAGS = \
+	-no-undefined \
+	$(PLATFORM_LDFLAGS) \
+	$(NULL)
+
+libprotocols_la_LIBADD = \
+	$(WAYLAND_CLIENT_LIBS) \
+	$(NULL)
+
+if HAVE_SESSION_LOCK
+%.c: $(WAYLAND_PROTOCOLS_PKGDATADIR)/staging/ext-session-lock/%.xml
+	$(AM_V_GEN) wayland-scanner private-code $< $@
+
+%-client.h: $(WAYLAND_PROTOCOLS_PKGDATADIR)/staging/ext-session-lock/%.xml
+	$(AM_V_GEN) wayland-scanner client-header $< $@
+else
+%.c: %.xml
+	$(AM_V_GEN) wayland-scanner private-code $< $@
+
+%-client.h: %.xml
+	$(AM_V_GEN) wayland-scanner client-header $< $@
+endif
+
+DISTCLEANFILES = \
+	$(libprotocols_built_sources) \
+	$(NULL)
+
+BUILT_SOURCES = \
+	$(libprotocols_built_sources) \
+	$(NULL)
+
+endif # ENABLE_WAYLAND
+
+EXTRA_DIST =
+
+if !HAVE_SESSION_LOCK
+EXTRA_DIST += \
+	ext-session-lock-v1.xml
+endif
diff --git a/protocols/ext-session-lock-v1.xml b/protocols/ext-session-lock-v1.xml
new file mode 100644
index 0000000..19c12d2
--- /dev/null
+++ b/protocols/ext-session-lock-v1.xml
@@ -0,0 +1,328 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="ext_session_lock_v1">
+  <copyright>
+    Copyright 2021 Isaac Freund
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+  </copyright>
+
+  <description summary="secure session locking with arbitrary graphics">
+    This protocol allows for a privileged Wayland client to lock the session
+    and display arbitrary graphics while the session is locked.
+
+    The compositor may choose to restrict this protocol to a special client
+    launched by the compositor itself or expose it to all privileged clients,
+    this is compositor policy.
+
+    The client is responsible for performing authentication and informing the
+    compositor when the session should be unlocked. If the client dies while
+    the session is locked the session remains locked, possibly permanently
+    depending on compositor policy.
+
+    The key words "must", "must not", "required", "shall", "shall not",
+    "should", "should not", "recommended",  "may", and "optional" in this
+    document are to be interpreted as described in IETF RFC 2119.
+
+    Warning! The protocol described in this file is currently in the
+    testing phase. Backward compatible changes may be added together with
+    the corresponding interface version bump. Backward incompatible changes
+    can only be done by creating a new major version of the extension.
+  </description>
+
+  <interface name="ext_session_lock_manager_v1" version="1">
+    <description summary="used to lock the session">
+      This interface is used to request that the session be locked.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the session lock manager object">
+        This informs the compositor that the session lock manager object will
+        no longer be used. Existing objects created through this interface
+        remain valid.
+      </description>
+    </request>
+
+    <request name="lock">
+      <description summary="attempt to lock the session">
+        This request creates a session lock and asks the compositor to lock the
+        session. The compositor will send either the ext_session_lock_v1.locked
+        or ext_session_lock_v1.finished event on the created object in
+        response to this request.
+      </description>
+      <arg name="id" type="new_id" interface="ext_session_lock_v1"/>
+    </request>
+  </interface>
+
+  <interface name="ext_session_lock_v1" version="1">
+    <description summary="manage lock state and create lock surfaces">
+      In response to the creation of this object the compositor must send
+      either the locked or finished event.
+
+      The locked event indicates that the session is locked. This means
+      that the compositor must stop rendering and providing input to normal
+      clients. Instead the compositor must blank all outputs with an opaque
+      color such that their normal content is fully hidden.
+
+      The only surfaces that should be rendered while the session is locked
+      are the lock surfaces created through this interface and optionally,
+      at the compositor's discretion, special privileged surfaces such as
+      input methods or portions of desktop shell UIs.
+
+      The locked event must not be sent until a new "locked" frame (either
+      from a session lock surface or the compositor blanking the output) has
+      been presented on all outputs and no security sensitive normal/unlocked
+      content is possibly visible.
+
+      The finished event should be sent immediately on creation of this
+      object if the compositor decides that the locked event will not be sent.
+
+      The compositor may wait for the client to create and render session lock
+      surfaces before sending the locked event to avoid displaying intermediate
+      blank frames. However, it must impose a reasonable time limit if
+      waiting and send the locked event as soon as the hard requirements
+      described above can be met if the time limit expires. Clients should
+      immediately create lock surfaces for all outputs on creation of this
+      object to make this possible.
+
+      This behavior of the locked event is required in order to prevent
+      possible race conditions with clients that wish to suspend the system
+      or similar after locking the session. Without these semantics, clients
+      triggering a suspend after receiving the locked event would race with
+      the first "locked" frame being presented and normal/unlocked frames
+      might be briefly visible as the system is resumed if the suspend
+      operation wins the race.
+
+      If the client dies while the session is locked, the compositor must not
+      unlock the session in response. It is acceptable for the session to be
+      permanently locked if this happens. The compositor may choose to continue
+      to display the lock surfaces the client had mapped before it died or
+      alternatively fall back to a solid color, this is compositor policy.
+
+      Compositors may also allow a secure way to recover the session, the
+      details of this are compositor policy. Compositors may allow a new
+      client to create a ext_session_lock_v1 object and take responsibility
+      for unlocking the session, they may even start a new lock client
+      instance automatically.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_destroy" value="0"
+        summary="attempted to destroy session lock while locked"/>
+      <entry name="invalid_unlock" value="1"
+        summary="unlock requested but locked event was never sent"/>
+      <entry name="role" value="2"
+        summary="given wl_surface already has a role"/>
+      <entry name="duplicate_output" value="3"
+        summary="given output already has a lock surface"/>
+      <entry name="already_constructed" value="4"
+        summary="given wl_surface has a buffer attached or committed"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the session lock">
+        This informs the compositor that the lock object will no longer be
+        used. Existing objects created through this interface remain valid.
+
+        After this request is made, lock surfaces created through this object
+        should be destroyed by the client as they will no longer be used by
+        the compositor.
+
+        It is a protocol error to make this request if the locked event was
+        sent, the unlock_and_destroy request must be used instead.
+      </description>
+    </request>
+
+    <event name="locked">
+      <description summary="session successfully locked">
+        This client is now responsible for displaying graphics while the
+        session is locked and deciding when to unlock the session.
+
+        The locked event must not be sent until a new "locked" frame has been
+        presented on all outputs and no security sensitive normal/unlocked
+        content is possibly visible.
+
+        If this event is sent, making the destroy request is a protocol error,
+        the lock object must be destroyed using the unlock_and_destroy request.
+      </description>
+    </event>
+
+    <event name="finished">
+      <description summary="the session lock object should be destroyed">
+        The compositor has decided that the session lock should be destroyed
+        as it will no longer be used by the compositor. Exactly when this
+        event is sent is compositor policy, but it must never be sent more
+        than once for a given session lock object.
+
+        This might be sent because there is already another ext_session_lock_v1
+        object held by a client, or the compositor has decided to deny the
+        request to lock the session for some other reason. This might also
+        be sent because the compositor implements some alternative, secure
+        way to authenticate and unlock the session.
+
+        The finished event should be sent immediately on creation of this
+        object if the compositor decides that the locked event will not
+        be sent.
+
+        If the locked event is sent on creation of this object the finished
+        event may still be sent at some later time in this object's
+        lifetime. This is compositor policy.
+
+        Upon receiving this event, the client should make either the destroy
+        request or the unlock_and_destroy request, depending on whether or
+        not the locked event was received on this object.
+      </description>
+    </event>
+
+    <request name="get_lock_surface">
+      <description summary="create a lock surface for a given output">
+        The client is expected to create lock surfaces for all outputs
+        currently present and any new outputs as they are advertised. These
+        won't be displayed by the compositor unless the lock is successful
+        and the locked event is sent.
+
+        Providing a wl_surface which already has a role or already has a buffer
+        attached or committed is a protocol error, as is attaching/committing
+        a buffer before the first ext_session_lock_surface_v1.configure event.
+
+        Attempting to create more than one lock surface for a given output
+        is a duplicate_output protocol error.
+      </description>
+      <arg name="id" type="new_id" interface="ext_session_lock_surface_v1"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+      <arg name="output" type="object" interface="wl_output"/>
+    </request>
+
+    <request name="unlock_and_destroy" type="destructor">
+      <description summary="unlock the session, destroying the object">
+        This request indicates that the session should be unlocked, for
+        example because the user has entered their password and it has been
+        verified by the client.
+
+        This request also informs the compositor that the lock object will
+        no longer be used and should be destroyed. Existing objects created
+        through this interface remain valid.
+
+        After this request is made, lock surfaces created through this object
+        should be destroyed by the client as they will no longer be used by
+        the compositor.
+
+        It is a protocol error to make this request if the locked event has
+        not been sent. In that case, the lock object must be destroyed using
+        the destroy request.
+
+        Note that a correct client that wishes to exit directly after unlocking
+        the session must use the wl_display.sync request to ensure the server
+        receives and processes the unlock_and_destroy request. Otherwise
+        there is no guarantee that the server has unlocked the session due
+        to the asynchronous nature of the Wayland protocol. For example,
+        the server might terminate the client with a protocol error before
+        it processes the unlock_and_destroy request.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="ext_session_lock_surface_v1" version="1">
+    <description summary="a surface displayed while the session is locked">
+      The client may use lock surfaces to display a screensaver, render a
+      dialog to enter a password and unlock the session, or however else it
+      sees fit.
+
+      On binding this interface the compositor will immediately send the
+      first configure event. After making the ack_configure request in
+      response to this event the client should attach and commit the first
+      buffer. Committing the surface before acking the first configure is a
+      protocol error. Committing the surface with a null buffer at any time
+      is a protocol error.
+
+      The compositor is free to handle keyboard/pointer focus for lock
+      surfaces however it chooses. A reasonable way to do this would be to
+      give the first lock surface created keyboard focus and change keyboard
+      focus if the user clicks on other surfaces.
+    </description>
+
+    <enum name="error">
+      <entry name="commit_before_first_ack" value="0"
+        summary="surface committed before first ack_configure request"/>
+      <entry name="null_buffer" value="1"
+        summary="surface committed with a null buffer"/>
+      <entry name="dimensions_mismatch" value="2"
+        summary="failed to match ack'd width/height"/>
+      <entry name="invalid_serial" value="3"
+        summary="serial provided in ack_configure is invalid"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the lock surface object">
+        This informs the compositor that the lock surface object will no
+        longer be used.
+
+        It is recommended for a lock client to destroy lock surfaces if
+        their corresponding wl_output global is removed.
+
+        If a lock surface on an active output is destroyed before the
+        ext_session_lock_v1.unlock_and_destroy event is sent, the compositor
+        must fall back to rendering a solid color.
+      </description>
+    </request>
+
+    <request name="ack_configure">
+      <description summary="ack a configure event">
+        When a configure event is received, if a client commits the surface
+        in response to the configure event, then the client must make an
+        ack_configure request sometime before the commit request, passing
+        along the serial of the configure event.
+
+        If the client receives multiple configure events before it can
+        respond to one, it only has to ack the last configure event.
+
+        A client is not required to commit immediately after sending an
+        ack_configure request - it may even ack_configure several times
+        before its next surface commit.
+
+        A client may send multiple ack_configure requests before committing,
+        but only the last request sent before a commit indicates which
+        configure event the client really is responding to.
+
+        Sending an ack_configure request consumes the configure event
+        referenced by the given serial, as well as all older configure events
+        sent on this object.
+
+        It is a protocol error to issue multiple ack_configure requests
+        referencing the same configure event or to issue an ack_configure
+        request referencing a configure event older than the last configure
+        event acked for a given lock surface.
+      </description>
+      <arg name="serial" type="uint" summary="serial from the configure event"/>
+    </request>
+
+    <event name="configure">
+      <description summary="the client should resize its surface">
+        This event is sent once on binding the interface and may be sent again
+        at the compositor's discretion, for example if output geometry changes.
+
+        The width and height are in surface-local coordinates and are exact
+        requirements. Failing to match these surface dimensions in the next
+        commit after acking a configure is a protocol error.
+      </description>
+      <arg name="serial" type="uint" summary="serial for use in ack_configure"/>
+      <arg name="width" type="uint"/>
+      <arg name="height" type="uint"/>
+    </event>
+  </interface>
+</protocol>
diff --git a/src/Makefile.am b/src/Makefile.am
index af3c752..87cc5f6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -150,14 +150,22 @@ test_window_LDADD += \
 endif
 
 if ENABLE_WAYLAND
+test_window_SOURCES += \
+	gs-session-lock-manager.c \
+	gs-session-lock-manager.h \
+	$(NULL)
+
 test_window_CFLAGS += \
 	$(LIBWLEMBED_CFLAGS) \
 	$(LIBWLEMBED_GTK3_CFLAGS) \
+	$(WAYLAND_CLIENT_CFLAGS) \
 	$(NULL)
 
 test_window_LDADD += \
+	$(top_builddir)/protocols/libprotocols.la \
 	$(LIBWLEMBED_LIBS) \
 	$(LIBWLEMBED_GTK3_LIBS) \
+	$(WAYLAND_CLIENT_LIBS) \
 	$(NULL)
 endif
 
@@ -319,14 +327,22 @@ xfce4_screensaver_LDADD += \
 endif
 
 if ENABLE_WAYLAND
+xfce4_screensaver_SOURCES += \
+	gs-session-lock-manager.c \
+	gs-session-lock-manager.h \
+	$(NULL)
+
 xfce4_screensaver_CFLAGS += \
 	$(LIBWLEMBED_CFLAGS) \
 	$(LIBWLEMBED_GTK3_CFLAGS) \
+	$(WAYLAND_CLIENT_CFLAGS) \
 	$(NULL)
 
 xfce4_screensaver_LDADD += \
+	$(top_builddir)/protocols/libprotocols.la \
 	$(LIBWLEMBED_LIBS) \
 	$(LIBWLEMBED_GTK3_LIBS) \
+	$(WAYLAND_CLIENT_LIBS) \
 	$(NULL)
 endif
 
diff --git a/src/gs-manager.c b/src/gs-manager.c
index dae0297..2f66055 100644
--- a/src/gs-manager.c
+++ b/src/gs-manager.c
@@ -32,6 +32,7 @@
 #ifdef ENABLE_WAYLAND
 #include <gdk/gdkwayland.h>
 #include <libwlembed-gtk3/libwlembed-gtk3.h>
+#include "gs-session-lock-manager.h"
 #endif
 
 #include "gs-debug.h"
@@ -67,6 +68,7 @@ struct GSManagerPrivate {
 #endif
 #ifdef ENABLE_WAYLAND
     WleEmbeddedCompositor *compositor;
+    GSSessionLockManager *lock_manager;
 #endif
 };
 
@@ -461,6 +463,8 @@ gs_manager_init (GSManager *manager) {
             g_critical ("Failed to create embedded compositor: %s", error->message);
             g_error_free (error);
         }
+
+        manager->priv->lock_manager = gs_session_lock_manager_new ();
     }
 #endif
 }
@@ -821,6 +825,11 @@ gs_manager_create_window_for_monitor (GSManager  *manager,
     gs_window_set_status_message (window, manager->priv->status_message);
     gs_window_set_lock_active (window, manager->priv->lock_active);
     connect_window_signals (manager, window);
+#ifdef ENABLE_WAYLAND
+    if (manager->priv->lock_manager != NULL) {
+        gs_session_lock_manager_add_window (manager->priv->lock_manager, window);
+    }
+#endif
 
     g_hash_table_insert (manager->priv->windows, monitor, window);
 
@@ -831,6 +840,16 @@ gs_manager_create_window_for_monitor (GSManager  *manager,
     return GTK_WIDGET (window);
 }
 
+#ifdef ENABLE_WAYLAND
+static void
+remove_window (gpointer monitor,
+               gpointer window,
+               gpointer data) {
+    GSManager *manager = data;
+    gs_session_lock_manager_remove_window (manager->priv->lock_manager, window);
+}
+#endif
+
 static gboolean
 remove_overlays (GtkWidget *window,
                  cairo_t *cr,
@@ -866,6 +885,11 @@ recreate_windows (GtkWidget *overlay,
     gs_debug("Reconfiguring monitors, recreating windows");
 
     g_hash_table_remove_all (manager->priv->jobs);
+#ifdef ENABLE_WAYLAND
+    if (manager->priv->lock_manager != NULL) {
+        g_hash_table_foreach (manager->priv->windows, remove_window, manager);
+    }
+#endif
     g_hash_table_remove_all (manager->priv->windows);
     manager->priv->n_overlay_signal_received = 0;
 
@@ -945,6 +969,11 @@ gs_manager_destroy_windows (GSManager *manager) {
                                           on_display_monitor_added,
                                           manager);
 
+#ifdef ENABLE_WAYLAND
+    if (manager->priv->lock_manager != NULL) {
+        g_hash_table_foreach (manager->priv->windows, remove_window, manager);
+    }
+#endif
     g_hash_table_remove_all (manager->priv->windows);
     g_list_free_full (manager->priv->overlays, (GDestroyNotify) gtk_widget_destroy);
     manager->priv->overlays = NULL;
@@ -980,6 +1009,9 @@ gs_manager_finalize (GObject *object) {
     if (manager->priv->compositor != NULL) {
         g_object_unref (manager->priv->compositor);
     }
+    if (manager->priv->lock_manager != NULL) {
+        g_object_unref (manager->priv->lock_manager);
+    }
 #endif
 
     G_OBJECT_CLASS (gs_manager_parent_class)->finalize (object);
@@ -1047,7 +1079,9 @@ gs_manager_activate (GSManager *manager) {
 
 #ifdef ENABLE_WAYLAND
     if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) {
-        if (manager->priv->compositor == NULL) {
+        if (manager->priv->compositor == NULL
+            || manager->priv->lock_manager == NULL
+            || !gs_session_lock_manager_lock (manager->priv->lock_manager)) {
             return FALSE;
         }
     }
@@ -1080,6 +1114,11 @@ gs_manager_deactivate (GSManager *manager) {
 #endif
     g_hash_table_remove_all (manager->priv->jobs);
     gs_manager_destroy_windows (manager);
+#ifdef ENABLE_WAYLAND
+    if (manager->priv->lock_manager != NULL) {
+        gs_session_lock_manager_unlock (manager->priv->lock_manager);
+    }
+#endif
 
     /* reset state */
     manager->priv->active = FALSE;
diff --git a/src/gs-session-lock-manager.c b/src/gs-session-lock-manager.c
new file mode 100644
index 0000000..b1fea42
--- /dev/null
+++ b/src/gs-session-lock-manager.c
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2024 Gaël Bonithon <gael@xfce.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include <gdk/gdk.h>
+#include <gdk/gdkwayland.h>
+
+#include "gs-session-lock-manager.h"
+#include "protocols/ext-session-lock-v1-client.h"
+#include "gs-debug.h"
+
+static void     gs_session_lock_manager_finalize   (GObject        *object);
+
+static void registry_global (void *data, struct wl_registry *registry, uint32_t id, const char *interface, uint32_t version);
+static void registry_global_remove (void *data, struct wl_registry *registry, uint32_t id);
+static void lock_locked (void *data, struct ext_session_lock_v1 *lock);
+static void lock_finished (void *data, struct ext_session_lock_v1 *lock);
+static void surface_configure (void *data, struct ext_session_lock_surface_v1 *surface, uint32_t serial, uint32_t width, uint32_t height);
+
+struct _GSSessionLockManager {
+    GObject __parent__;
+
+    struct wl_registry *wl_registry;
+    struct ext_session_lock_manager_v1 *wl_manager;
+    struct ext_session_lock_v1 *wl_lock;
+    GHashTable *lock_surfaces;
+    gboolean locked;
+};
+
+static const struct wl_registry_listener registry_listener = {
+    .global = registry_global,
+    .global_remove = registry_global_remove,
+};
+
+static const struct ext_session_lock_v1_listener lock_listener = {
+    .locked = lock_locked,
+    .finished = lock_finished,
+};
+
+static const struct ext_session_lock_surface_v1_listener surface_listener = {
+    .configure = surface_configure,
+};
+
+
+
+G_DEFINE_TYPE (GSSessionLockManager, gs_session_lock_manager, G_TYPE_OBJECT)
+
+
+
+static void
+gs_session_lock_manager_class_init (GSSessionLockManagerClass *klass) {
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+    object_class->finalize = gs_session_lock_manager_finalize;
+}
+
+static void
+gs_session_lock_manager_init (GSSessionLockManager *manager) {
+    struct wl_display *wl_display = gdk_wayland_display_get_wl_display (gdk_display_get_default ());
+
+    manager->wl_registry = wl_display_get_registry (wl_display);
+    wl_registry_add_listener (manager->wl_registry, &registry_listener, manager);
+    wl_display_roundtrip (wl_display);
+    if (manager->wl_manager != NULL) {
+        manager->lock_surfaces = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) ext_session_lock_surface_v1_destroy);
+    } else {
+        g_warning ("ext-session-lock-v1 protocol unsupported: xfce4-screensaver will not be able to activate or lock the session");
+    }
+}
+
+static void
+gs_session_lock_manager_finalize (GObject *object) {
+    GSSessionLockManager *manager = GS_SESSION_LOCK_MANAGER (object);
+
+    if (manager->wl_manager != NULL) {
+        if (manager->wl_lock != NULL) {
+            lock_finished (manager, manager->wl_lock);
+        }
+        g_hash_table_destroy (manager->lock_surfaces);
+        ext_session_lock_manager_v1_destroy (manager->wl_manager);
+    }
+    wl_registry_destroy (manager->wl_registry);
+
+    G_OBJECT_CLASS (gs_session_lock_manager_parent_class)->finalize (object);
+}
+
+static void
+registry_global (void *data,
+                 struct wl_registry *registry,
+                 uint32_t id,
+                 const char *interface,
+                 uint32_t version) {
+    GSSessionLockManager *manager = data;
+    if (g_strcmp0 (ext_session_lock_manager_v1_interface.name, interface) == 0) {
+        manager->wl_manager = wl_registry_bind (manager->wl_registry, id, &ext_session_lock_manager_v1_interface,
+                                                MIN ((uint32_t) ext_session_lock_manager_v1_interface.version, version));
+    }
+}
+
+static void
+registry_global_remove (void *data,
+                        struct wl_registry *registry,
+                        uint32_t id) {
+}
+
+static void
+lock_locked (void *data,
+             struct ext_session_lock_v1 *lock) {
+    GSSessionLockManager *manager = data;
+    gs_debug ("Locked event received from compositor");
+    manager->locked = TRUE;
+}
+
+static void
+lock_finished (void *data,
+               struct ext_session_lock_v1 *lock) {
+    GSSessionLockManager *manager = data;
+    gs_debug ("Finished event received from compositor");
+    gs_session_lock_manager_unlock (manager);
+}
+
+static void
+surface_configure (void *data,
+                   struct ext_session_lock_surface_v1 *surface,
+                   uint32_t serial,
+                   uint32_t width,
+                   uint32_t height) {
+    gs_debug ("Configuring lock surface for monitor %s: width %d, height %d",
+              gdk_monitor_get_model (gs_window_get_monitor (data)), width, height);
+    gdk_window_move_resize (gtk_widget_get_window (data), 0, 0, width, height);
+    ext_session_lock_surface_v1_ack_configure (surface, serial);
+}
+
+
+
+GSSessionLockManager *
+gs_session_lock_manager_new (void) {
+    GSSessionLockManager *manager = g_object_new (GS_TYPE_SESSION_LOCK_MANAGER, NULL);
+    if (manager->wl_manager == NULL) {
+        g_object_unref (manager);
+        manager = NULL;
+    }
+    return manager;
+}
+
+gboolean
+gs_session_lock_manager_lock (GSSessionLockManager *manager) {
+    g_return_val_if_fail (GS_IS_SESSION_LOCK_MANAGER (manager), FALSE);
+    g_return_val_if_fail (manager->wl_lock == NULL, FALSE);
+
+    gs_debug ("Locking session");
+    manager->wl_lock = ext_session_lock_manager_v1_lock (manager->wl_manager);
+    ext_session_lock_v1_add_listener (manager->wl_lock, &lock_listener, manager);
+    wl_display_roundtrip (gdk_wayland_display_get_wl_display (gdk_display_get_default ()));
+
+    return manager->wl_lock != NULL;
+}
+
+void
+gs_session_lock_manager_unlock (GSSessionLockManager *manager) {
+    g_return_if_fail (GS_IS_SESSION_LOCK_MANAGER (manager));
+    g_return_if_fail (manager->wl_lock != NULL);
+
+    gs_debug ("Unlocking session");
+    if (manager->locked) {
+        ext_session_lock_v1_unlock_and_destroy (manager->wl_lock);
+        wl_display_roundtrip (gdk_wayland_display_get_wl_display (gdk_display_get_default ()));
+    } else {
+        ext_session_lock_v1_destroy (manager->wl_lock);
+    }
+    g_hash_table_remove_all (manager->lock_surfaces);
+    manager->wl_lock = NULL;
+    manager->locked = FALSE;
+}
+
+static void
+window_realized (GtkWidget *window,
+                 GSSessionLockManager *manager) {
+    struct wl_display *wl_display = gdk_wayland_display_get_wl_display (gdk_display_get_default ());
+    struct wl_surface *wl_surface = gdk_wayland_window_get_wl_surface (gtk_widget_get_window (window));
+    struct wl_output *wl_output = gdk_wayland_monitor_get_wl_output (gs_window_get_monitor (GS_WINDOW (window)));
+    struct ext_session_lock_surface_v1 *wl_lock_surface;
+
+    gs_debug ("Creating lock surface for monitor %s", gdk_monitor_get_model (gs_window_get_monitor (GS_WINDOW (window))));
+    gdk_wayland_window_set_use_custom_surface (gtk_widget_get_window (window));
+    wl_lock_surface = ext_session_lock_v1_get_lock_surface (manager->wl_lock, wl_surface, wl_output);
+    ext_session_lock_surface_v1_add_listener (wl_lock_surface, &surface_listener, window);
+    g_hash_table_insert (manager->lock_surfaces, window, wl_lock_surface);
+    wl_display_roundtrip (wl_display);
+}
+
+void
+gs_session_lock_manager_add_window (GSSessionLockManager *manager,
+                                    GSWindow *window) {
+    const gchar *model;
+
+    g_return_if_fail (GS_IS_SESSION_LOCK_MANAGER (manager));
+    g_return_if_fail (manager->wl_lock != NULL);
+    g_return_if_fail (GS_IS_WINDOW (window));
+    g_return_if_fail (!gtk_widget_get_realized (GTK_WIDGET (window)));
+
+    model = gdk_monitor_get_model (gs_window_get_monitor (window));
+    gs_debug ("Adding window for monitor %s", model);
+    g_signal_connect (window, "realize", G_CALLBACK (window_realized), manager);
+    g_object_set_data_full (G_OBJECT (window), "monitor-model", g_strdup (model), g_free);
+}
+
+void
+gs_session_lock_manager_remove_window (GSSessionLockManager *manager,
+                                       GSWindow *window) {
+    g_return_if_fail (GS_IS_SESSION_LOCK_MANAGER (manager));
+    g_return_if_fail (manager->wl_lock != NULL);
+    g_return_if_fail (GS_IS_WINDOW (window));
+
+    gs_debug ("Removing window for monitor %s", g_object_get_data (G_OBJECT (window), "monitor-model"));
+    /* we can't do this on dispose or destroy or whatever signal unfortunately, it's too
+     * late and causes a protocol error (surface destroyed before its role) */
+    g_hash_table_remove (manager->lock_surfaces, window);
+}
diff --git a/src/gs-session-lock-manager.h b/src/gs-session-lock-manager.h
new file mode 100644
index 0000000..30de8c8
--- /dev/null
+++ b/src/gs-session-lock-manager.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 Gaël Bonithon <gael@xfce.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef SRC_GS_SESSION_LOCK_MANAGER_H_
+#define SRC_GS_SESSION_LOCK_MANAGER_H_
+
+#include <glib-object.h>
+#include "gs-window.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_SESSION_LOCK_MANAGER (gs_session_lock_manager_get_type ())
+G_DECLARE_FINAL_TYPE (GSSessionLockManager, gs_session_lock_manager, GS, SESSION_LOCK_MANAGER, GObject)
+
+GSSessionLockManager    *gs_session_lock_manager_new            (void);
+gboolean                 gs_session_lock_manager_lock           (GSSessionLockManager     *manager);
+void                     gs_session_lock_manager_unlock         (GSSessionLockManager     *manager);
+void                     gs_session_lock_manager_add_window     (GSSessionLockManager     *manager,
+                                                                 GSWindow                 *window);
+void                     gs_session_lock_manager_remove_window  (GSSessionLockManager     *manager,
+                                                                 GSWindow                 *window);
+
+G_END_DECLS
+
+#endif /* SRC_GS_SESSION_LOCK_MANAGER_H_ */
-- 
GitLab