[PATCH] core: Add infrastructure for inhibiting suspend in jobs
authorKai Uwe Broulik <kde@privat.broulik.de>
Sun, 20 Apr 2025 10:48:26 +0000 (12:48 +0200)
committerAurélien COUDERC <coucouf@debian.org>
Sun, 8 Jun 2025 12:42:29 +0000 (14:42 +0200)
This calls the freedesktop Inhibit interface on DBus which will
inhibit suspend (but not display power management/screensaver).
When inside a sandbox it instead calls the XDG Desktop Portal
Inhibit interface.

When a job is destroyed or gets suspended, the inhibition is lifted.
When a job is resumed, `doInhibitSuspend` is called again to re-instate
the inhibition.

It is the job's responsibility to call `doInhibitSuspend` at
the appropriate time (e.g. in doStart/slotStart).

Gbp-Pq: Name upstream_3c3d5904_core-Add-infrastructure-for-inhibiting-suspend-in-jobs.patch

src/core/CMakeLists.txt
src/core/config-kiocore.h.cmake
src/core/job.cpp
src/core/job_p.h
src/core/org.freedesktop.PowerManagement.Inhibit.xml [new file with mode: 0644]
src/core/org.freedesktop.portal.Inhibit.xml [new file with mode: 0644]
src/core/org.freedesktop.portal.Request.xml [new file with mode: 0644]

index c4dcd58459947fc1f57d9aada02e467b19f65dad..6ace75e8284597f857c6fd3ff757f64226d01c0d 100644 (file)
@@ -164,6 +164,11 @@ if (HAVE_QTDBUS)
             PROPERTIES INCLUDE authinfo.h
     )
     qt_add_dbus_interface(kiocore_dbus_SRCS org.kde.KPasswdServer.xml kpasswdserver_interface)
+
+    qt_add_dbus_interface(kiocore_dbus_SRCS org.freedesktop.PowerManagement.Inhibit.xml inhibit_interface)
+
+    qt_add_dbus_interface(kiocore_dbus_SRCS org.freedesktop.portal.Inhibit.xml portal_inhibit_interface)
+    qt_add_dbus_interface(kiocore_dbus_SRCS org.freedesktop.portal.Request.xml portal_request_interface)
 endif()
 
 target_sources(KF6KIOCore PRIVATE
index 1f0bc42f2b51013c7a33a9acdde18bd8117361ee..bd9e7582af3457011a7eab0c1e80bbe6a3277d6b 100644 (file)
@@ -7,6 +7,8 @@
 /* Defined if sys/acl.h exists */
 #cmakedefine01 HAVE_SYS_ACL_H
 
+#cmakedefine01 HAVE_QTDBUS
+
 #define KDE_INSTALL_FULL_LIBEXECDIR_KF "${KDE_INSTALL_FULL_LIBEXECDIR_KF}"
 
 #define KDE_INSTALL_FULL_KIO_PLUGINDIR "${KDE_INSTALL_FULL_PLUGINDIR}/kf6/kio/"
index e8360d468a8aebf5bdf7902585180c54021d6372..4eb037816d6359498019fabba09ef055612a7e29 100644 (file)
 #include <time.h>
 
 #include <KLocalizedString>
+#include <KSandbox>
 #include <KStringHandler>
 
+#include "kiocoredebug.h"
 #include "worker_p.h"
 #include <kio/jobuidelegateextension.h>
 
+#if HAVE_QTDBUS
+#include <QDBusConnection>
+#include <QDBusPendingCallWatcher>
+
+#include "inhibit_interface.h"
+#include "portal_inhibit_interface.h"
+#include "portal_request_interface.h"
+#endif
+
 using namespace KIO;
 
+static constexpr QLatin1String g_portalServiceName{"org.freedesktop.portal.Desktop"};
+static constexpr QLatin1String g_portalInhibitObjectPath{"/org/freedesktop/portal/desktop"};
+
+static constexpr QLatin1String g_inhibitServiceName{"org.freedesktop.PowerManagement.Inhibit"};
+static constexpr QLatin1String g_inhibitObjectPath{"/org/freedesktop/PowerManagement/Inhibit"};
+
 Job::Job()
     : KCompositeJob(nullptr)
     , d_ptr(new JobPrivate)
@@ -89,9 +106,127 @@ static QString url_description_string(const QUrl &url)
 }
 
 KIO::JobPrivate::~JobPrivate()
+{
+    uninhibitSuspend();
+}
+
+void JobPrivate::doInhibitSuspend()
 {
 }
 
+void JobPrivate::inhibitSuspend(const QString &reason)
+{
+#if HAVE_QTDBUS
+    if (KSandbox::isInside()) {
+        Q_ASSERT(m_portalInhibitionRequest.path().isEmpty());
+
+        org::freedesktop::portal::Inhibit inhibitInterface{g_portalServiceName, g_portalInhibitObjectPath, QDBusConnection::sessionBus()};
+        QVariantMap args;
+        if (!reason.isEmpty()) {
+            args.insert(QStringLiteral("reason"), reason);
+        }
+        auto call = inhibitInterface.Inhibit(QString() /* TODO window. */, 4 /* Suspend */, args);
+        // This is not parented to the job, so we can properly clean up the inhibiton
+        // should the job finish before the inhibition has been processed.
+        auto *watcher = new QDBusPendingCallWatcher(call);
+        QPointer<Job> guard(q_ptr);
+        QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [this, guard, watcher, reason] {
+            QDBusPendingReply<QDBusObjectPath> reply = *watcher;
+
+            if (reply.isError()) {
+                qCWarning(KIO_CORE).nospace() << "Failed to inhibit suspend with reason " << reason << ": " << reply.error().message();
+            } else {
+                const QDBusObjectPath requestPath = reply.value();
+
+                // By the time the inhibition returned, the job was already gone. Uninhibit again.
+                if (!guard) {
+                    org::freedesktop::portal::Request requestInterface{g_portalServiceName, requestPath.path(), QDBusConnection::sessionBus()};
+                    requestInterface.Close();
+                } else {
+                    m_portalInhibitionRequest = requestPath;
+                }
+            }
+
+            watcher->deleteLater();
+        });
+    } else {
+        Q_ASSERT(!m_inhibitionCookie);
+
+        QString appName = q_ptr->property("desktopFileName").toString();
+        if (appName.isEmpty()) {
+            // desktopFileName is in QGuiApplication but we're in KIO Core here.
+            appName = QCoreApplication::instance()->property("desktopFileName").toString();
+        }
+        if (appName.isEmpty()) {
+            appName = QCoreApplication::applicationName();
+        }
+
+        org::freedesktop::PowerManagement::Inhibit inhibitInterface{g_inhibitServiceName, g_inhibitObjectPath, QDBusConnection::sessionBus()};
+        auto call = inhibitInterface.Inhibit(appName, reason);
+        auto *watcher = new QDBusPendingCallWatcher(call);
+        QPointer<Job> guard(q_ptr);
+        QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [this, guard, watcher, appName, reason] {
+            QDBusPendingReply<uint> reply = *watcher;
+
+            if (reply.isError()) {
+                qCWarning(KIO_CORE).nospace() << "Failed to inhibit suspend for " << appName << " with reason " << reason << ": " << reply.error().message();
+            } else {
+                const uint cookie = reply.value();
+
+                if (!guard) {
+                    org::freedesktop::PowerManagement::Inhibit inhibitInterface{g_inhibitServiceName, g_inhibitObjectPath, QDBusConnection::sessionBus()};
+                    inhibitInterface.UnInhibit(cookie);
+                } else {
+                    m_inhibitionCookie = cookie;
+                }
+            }
+
+            watcher->deleteLater();
+        });
+    }
+#else
+    Q_UNUSED(reason)
+#endif
+}
+
+void JobPrivate::uninhibitSuspend()
+{
+#if HAVE_QTDBUS
+    if (!m_portalInhibitionRequest.path().isEmpty()) {
+        org::freedesktop::portal::Request requestInterface{g_portalServiceName, m_portalInhibitionRequest.path(), QDBusConnection::sessionBus()};
+        auto call = requestInterface.Close();
+        auto *watcher = new QDBusPendingCallWatcher(call, q_ptr);
+        QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q_ptr, [this, watcher] {
+            QDBusPendingReply<> reply = *watcher;
+
+            if (reply.isError()) {
+                qCWarning(KIO_CORE) << "Failed to uninhibit suspend:" << reply.error().message();
+            } else {
+                m_portalInhibitionRequest = QDBusObjectPath();
+            }
+
+            watcher->deleteLater();
+        });
+    } else if (m_inhibitionCookie) {
+        org::freedesktop::PowerManagement::Inhibit inhibitInterface{g_inhibitServiceName, g_inhibitObjectPath, QDBusConnection::sessionBus()};
+        const int cookie = *m_inhibitionCookie;
+        auto call = inhibitInterface.UnInhibit(cookie);
+        auto *watcher = new QDBusPendingCallWatcher(call, q_ptr);
+        QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q_ptr, [this, watcher, cookie] {
+            QDBusPendingReply<> reply = *watcher;
+
+            if (reply.isError()) {
+                qCWarning(KIO_CORE).nospace() << "Failed to uninhibit suspend for cookie" << cookie << ": " << reply.error().message();
+            } else {
+                m_inhibitionCookie.reset();
+            }
+
+            watcher->deleteLater();
+        });
+    }
+#endif
+}
+
 void JobPrivate::emitMoving(KIO::Job *job, const QUrl &src, const QUrl &dest)
 {
     static const QString s_title = i18nc("@title job", "Moving");
@@ -172,7 +307,7 @@ bool Job::doSuspend()
             return false;
         }
     }
-
+    d_ptr->uninhibitSuspend();
     return true;
 }
 
@@ -183,7 +318,7 @@ bool Job::doResume()
             return false;
         }
     }
-
+    d_ptr->doInhibitSuspend();
     return true;
 }
 
index e9eab0cf6939b00a32865ae8cb16542dd5b179f8..e8191198cd196e3cd369746878a10d1bc64dbbeb 100644 (file)
@@ -12,6 +12,8 @@
 #ifndef KIO_JOB_P_H
 #define KIO_JOB_P_H
 
+#include "config-kiocore.h"
+
 #include "commands_p.h"
 #include "global.h"
 #include "jobtracker.h"
 #include <kio/jobuidelegateextension.h>
 #include <kio/jobuidelegatefactory.h>
 
+#if HAVE_QTDBUS
+#include <QDBusObjectPath>
+
+#include <optional>
+#endif
+
 /* clang-format off */
 #define KIO_ARGS \
     QByteArray packedArgs; \
@@ -84,6 +92,10 @@ public:
     MetaData m_outgoingMetaData;
     JobUiDelegateExtension *m_uiDelegateExtension;
     Job *q_ptr;
+#if HAVE_QTDBUS
+    std::optional<uint> m_inhibitionCookie; // fdo.
+    QDBusObjectPath m_portalInhibitionRequest; // portal.
+#endif
     // For privilege operation
     bool m_privilegeExecutionEnabled;
     QString m_title, m_message;
@@ -92,6 +104,10 @@ public:
     QByteArray privilegeOperationData();
     void slotSpeed(KJob *job, unsigned long speed);
 
+    void inhibitSuspend(const QString &reason);
+    void uninhibitSuspend();
+    virtual void doInhibitSuspend();
+
     static void emitMoving(KIO::Job *, const QUrl &src, const QUrl &dest);
     static void emitRenaming(KIO::Job *, const QUrl &src, const QUrl &dest);
     static void emitCopying(KIO::Job *, const QUrl &src, const QUrl &dest);
diff --git a/src/core/org.freedesktop.PowerManagement.Inhibit.xml b/src/core/org.freedesktop.PowerManagement.Inhibit.xml
new file mode 100644 (file)
index 0000000..21dfce8
--- /dev/null
@@ -0,0 +1,20 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+  <interface name="org.freedesktop.PowerManagement.Inhibit">
+    <method name="Inhibit">
+      <arg direction="in" type="s" name="application"/>
+      <arg direction="in" type="s" name="reason"/>
+      <arg direction="out" type="u" name="cookie"/>
+    </method>
+    <method name="UnInhibit">
+      <arg direction="in" type="u" name="cookie"/>
+    </method>
+    <signal name="HasInhibitChanged">
+      <arg direction="out" type="b" name="has_inhibit"/>
+    </signal>
+    <method name="HasInhibit">
+      <arg direction="out" type="b" name="has_inhibit"/>
+    </method>
+  </interface>
+</node>
diff --git a/src/core/org.freedesktop.portal.Inhibit.xml b/src/core/org.freedesktop.portal.Inhibit.xml
new file mode 100644 (file)
index 0000000..1ae413e
--- /dev/null
@@ -0,0 +1,173 @@
+<?xml version="1.0"?>
+<!--
+ Copyright (C) 2016 Red Hat, Inc.
+
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+ Author: Matthias Clasen <mclasen@redhat.com>
+-->
+
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+  <!--
+      org.freedesktop.portal.Inhibit:
+      @short_description: Portal for inhibiting session transitions
+
+      This simple interface lets sandboxed applications inhibit the user
+      session from ending, suspending, idling or getting switched away.
+
+      This documentation describes version 3 of this interface.
+  -->
+  <interface name="org.freedesktop.portal.Inhibit">
+    <!--
+        Inhibit:
+        @window: Identifier for the window
+        @flags: Flags identifying what is inhibited
+        @options: Vardict with optional further information
+        @handle: Object path for the :ref:`org.freedesktop.portal.Request` object representing this call
+
+        Inhibits a session status changes. To remove the inhibition,
+        call :ref:`org.freedesktop.portal.Request.Close` on the returned
+        handle.
+
+        The flags determine what changes are inhibited:
+
+        - ``1``: Logout
+        - ``2``: User Switch
+        - ``4``: Suspend
+        - ``8``: Idle
+
+        Supported keys in the @options vardict include:
+
+        * ``handle_token`` (``s``)
+
+          A string that will be used as the last element of the @handle. Must be a valid
+          object path element. See the :ref:`org.freedesktop.portal.Request` documentation for
+          more information about the @handle.
+
+        * ``reason`` (``s``)
+
+          User-visible reason for the inhibition.
+    -->
+    <method name="Inhibit">
+      <arg type="s" name="window" direction="in"/>
+      <arg type="u" name="flags" direction="in"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
+      <arg type="a{sv}" name="options" direction="in"/>
+      <arg type="o" name="handle" direction="out"/>
+    </method>
+
+    <!--
+        CreateMonitor:
+        @window: the parent window
+        @options: Vardict with optional further information
+        @handle: Object path for the :ref:`org.freedesktop.portal.Request` object representing this call
+
+        Creates a monitoring session. While this session is
+        active, the caller will receive StateChanged signals
+        with updates on the session state.
+
+        A successfully created session can at any time be closed using
+        org.freedesktop.portal.Session::Close, or may at any time be closed
+        by the portal implementation, which will be signalled via
+        :ref:`org.freedesktop.portal.Session::Closed`.
+
+        Supported keys in the @options vardict include:
+
+        * ``handle_token`` (``s``)
+
+          A string that will be used as the last element of the @handle. Must be a valid
+          object path element. See the :ref:`org.freedesktop.portal.Request` documentation for
+          more information about the @handle.
+
+        * ``session_handle_token`` (``s``)
+
+          A string that will be used as the last element of the session handle. Must be a valid
+          object path element. See the :ref:`org.freedesktop.portal.Session` documentation for
+          more information about the session handle.
+
+        The following results get returned via the :ref:`org.freedesktop.portal.Request::Response` signal:
+
+        * ``session_handle`` (``s``)
+
+          The session handle. An object path for the
+          :ref:`org.freedesktop.portal.Session` object representing the created
+          session.
+
+          .. note::
+            The ``session_handle`` is an object path that was erroneously implemented
+            as ``s``. For backwards compatibility it will remain this type.
+
+        This method was added in version 2 of this interface.
+    -->
+    <method name="CreateMonitor">
+      <arg type="s" name="window" direction="in"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
+      <arg type="a{sv}" name="options" direction="in"/>
+      <arg type="o" name="handle" direction="out"/>
+    </method>
+
+    <!--
+        StateChanged:
+        @session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
+        @state: Vardict with information about the session state
+
+        The StateChanged signal is sent to active monitoring sessions when
+        the session state changes.
+
+        When the session state changes to 'Query End', clients with active monitoring
+        sessions are expected to respond by calling
+        org.freedesktop.portal.Inhibit.QueryEndResponse() within a second
+        of receiving the StateChanged signal. They may call org.freedesktop.portal.Inhibit.Inhibit()
+        first to inhibit logout, to prevent the session from proceeding to the Ending state.
+
+        The following information may get returned in the @state vardict:
+
+        * ``screensaver-active`` (``b``)
+
+          Whether the screensaver is active.
+
+        * ``session-state`` (``u``)
+
+          The state of the session. This member is new in version 3.
+
+          - ``1``: Running
+          - ``2``: Query End
+          - ``3``: Ending
+
+    -->
+    <signal name="StateChanged">
+      <arg type="o" name="session_handle" direction="out"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
+      <arg type="a{sv}" name="state" direction="out"/>
+    </signal>
+
+    <!--
+      QueryEndResponse:
+      @session_handle: Object path for the :ref:`org.freedesktop.portal.Session` object
+
+      Acknowledges that the caller received the #org.freedesktop.portal.Inhibit::StateChanged
+      signal. This method should be called within one second or receiving a StateChanged
+      signal with the 'Query End' state.
+
+      Since version 3.
+    -->
+    <method name="QueryEndResponse">
+      <arg type="o" name="session_handle" direction="in"/>
+    </method>
+
+    <property name="version" type="u" access="read"/>
+  </interface>
+</node>
diff --git a/src/core/org.freedesktop.portal.Request.xml b/src/core/org.freedesktop.portal.Request.xml
new file mode 100644 (file)
index 0000000..e8a2648
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<!--
+ Copyright (C) 2015 Red Hat, Inc.
+
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+ Author: Alexander Larsson <alexl@redhat.com>
+-->
+
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+  <!--
+      org.freedesktop.portal.Request:
+      @short_description: Shared request interface
+
+      The Request interface is shared by all portal interfaces. When a
+      portal method is called, the reply includes a handle (i.e. object path)
+      for a Request object, which will stay alive for the duration of the
+      user interaction related to the method call.
+
+      The portal indicates that a portal request interaction is over by
+      emitting the #org.freedesktop.portal.Request::Response signal on the
+      Request object.
+
+      The application can abort the interaction calling
+      org.freedesktop.portal.Request.Close() on the Request object.
+
+      Since version 0.9 of xdg-desktop-portal, the handle will be of the form
+
+      ::
+
+        /org/freedesktop/portal/desktop/request/SENDER/TOKEN
+
+
+      where ``SENDER`` is the callers unique name, with the initial ``':'`` removed and
+      all ``'.'`` replaced by ``'_'``, and ``TOKEN`` is a unique token that the caller provided
+      with the handle_token key in the options vardict.
+
+      This change was made to let applications subscribe to the Response signal before
+      making the initial portal call, thereby avoiding a race condition. It is recommended
+      that the caller should verify that the returned handle is what it expected, and update
+      its signal subscription if it isn't. This ensures that applications will work with both
+      old and new versions of xdg-desktop-portal.
+
+      The token that the caller provides should be unique and not guessable. To avoid clashes
+      with calls made from unrelated libraries, it is a good idea to use a per-library prefix
+      combined with a random number.
+  -->
+  <interface name="org.freedesktop.portal.Request">
+
+    <!--
+        Close:
+
+        Closes the portal request to which this object refers and ends all
+        related user interaction (dialogs, etc).
+
+        A Response signal will not be emitted in this case.
+    -->
+    <method name="Close">
+    </method>
+
+    <!--
+        Response:
+        @response: Numeric response
+        @results: Vardict with results. The keys and values in the vardict depend on the request.
+
+        Emitted when the user interaction for a portal request is over.
+
+        The @response indicates how the user interaction ended:
+
+        - 0: Success, the request is carried out
+        - 1: The user cancelled the interaction
+        - 2: The user interaction was ended in some other way
+    -->
+    <signal name="Response">
+      <arg type="u" name="response"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
+      <arg type="a{sv}" name="results"/>
+    </signal>
+  </interface>
+</node>