1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2025 Kai Uwe Broulik <kde@broulik.de>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include <config-kjobwidgets.h>
9
10#include "kinhibitionjobtracker.h"
11#include "kinhibitionjobtracker_p.h"
12
13#include "debug.h"
14
15#include <KJob>
16
17#include <QCoreApplication>
18#include <QTimer>
19#include <QVariant>
20
21#ifdef Q_OS_WINDOWS
22#include <windows.h>
23#elif HAVE_QTDBUS
24#include <KSandbox>
25
26#include <QDBusConnection>
27#include <QDBusPendingCallWatcher>
28#include <QDBusPendingReply>
29
30#include "inhibit_interface.h"
31#include "portal_inhibit_interface.h"
32#include "portal_request_interface.h"
33
34static constexpr QLatin1String g_portalServiceName{"org.freedesktop.portal.Desktop"};
35static constexpr QLatin1String g_portalInhibitObjectPath{"/org/freedesktop/portal/desktop"};
36
37static constexpr QLatin1String g_inhibitServiceName{"org.freedesktop.PowerManagement.Inhibit"};
38static constexpr QLatin1String g_inhibitObjectPath{"/org/freedesktop/PowerManagement/Inhibit"};
39#endif
40
41KInhibitionJobTrackerPrivate::KInhibitionJobTrackerPrivate(KInhibitionJobTracker *q)
42 : q(q)
43{
44}
45
46void KInhibitionJobTrackerPrivate::inhibit(KJob *job)
47{
48 if (timers.contains(key: job) || inhibitions.contains(key: job)) {
49 return;
50 }
51
52 qCDebug(KJOBWIDGETS) << "Inhibition scheduled in 10s...";
53 // Avoid DBus traffic for short-lived jobs.
54 QTimer *timer = new QTimer(job);
55 QObject::connect(sender: timer, signal: &QObject::destroyed, context: q, slot: [this, job] {
56 timers.remove(key: job);
57 });
58 timer->setSingleShot(true);
59 timer->setInterval(10000);
60 timer->callOnTimeout(args: q, args: [this, job] {
61 doInhibit(job);
62 });
63 timer->start();
64 timers.insert(key: job, value: timer);
65}
66
67void KInhibitionJobTrackerPrivate::doInhibit(KJob *job)
68{
69 QString appName = job->property(name: "desktopFileName").toString();
70 if (appName.isEmpty()) {
71 // desktopFileName is in QGuiApplication but we're in Core here.
72 appName = QCoreApplication::instance()->property(name: "desktopFileName").toString();
73 }
74 if (appName.isEmpty()) {
75 appName = QCoreApplication::applicationName();
76 }
77
78 QString reason = reasons.value(key: job);
79 if (reason.isEmpty()) {
80 reason = QCoreApplication::translate(context: "KInhibitionJobTracker", key: "Job in progress");
81 }
82
83 auto *inhibition = new Inhibition(appName, reason);
84 Q_ASSERT(!inhibitions.contains(job));
85 inhibitions.insert(key: job, value: inhibition);
86}
87
88Inhibition::Inhibition(const QString &appName, const QString &reason, QObject *parent)
89 : QObject(parent)
90 , m_appName(appName)
91 , m_reason(reason)
92{
93 inhibit();
94}
95
96Inhibition::~Inhibition()
97{
98 uninhibit();
99}
100
101void Inhibition::inhibit()
102{
103#ifdef Q_OS_WINDOWS
104 qCDebug(KJOBWIDGETS) << "Requesting power management inhibition with reason" << m_reason;
105 REASON_CONTEXT context{};
106 context.Version = POWER_REQUEST_CONTEXT_VERSION;
107 context.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING;
108 context.Reason.SimpleReasonString = reinterpret_cast<LPWSTR>(const_cast<ushort *>(m_reason.utf16()));
109
110 HANDLE handle = PowerCreateRequest(&context);
111 if (handle == INVALID_HANDLE_VALUE) {
112 qWarning(KJOBWIDGETS).nospace() << "Failed to create inhibition request with reason " << m_reason << ": " << GetLastError();
113 } else if (!PowerSetRequest(handle, PowerRequestExecutionRequired)) {
114 qWarning(KJOBWIDGETS).nospace() << "Failed to set inhibition request with reason " << m_reason << ": " << GetLastError();
115 CloseHandle(handle);
116 } else {
117 m_handle = handle;
118 }
119#elif HAVE_QTDBUS
120 if (KSandbox::isInside()) {
121 qCDebug(KJOBWIDGETS) << "Sending portal inhibition with reason" << m_reason;
122 org::freedesktop::portal::Inhibit inhibitInterface{g_portalServiceName, g_portalInhibitObjectPath, QDBusConnection::sessionBus()};
123 QVariantMap args;
124 if (!m_reason.isEmpty()) {
125 args.insert(QStringLiteral("reason"), value: m_reason);
126 }
127 auto call = inhibitInterface.Inhibit(window: QString() /* window */, flags: 4 /* Suspend */, options: args);
128 // This is not parented to the job, so we can properly clean up the inhibiton
129 // should the inhibition be destroyed before the inhibition has been processed.
130 auto *watcher = new QDBusPendingCallWatcher(call);
131 QPointer<Inhibition> guard(this);
132 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: watcher, slot: [this, guard, watcher, reason = m_reason] {
133 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
134
135 if (reply.isError()) {
136 qCWarning(KJOBWIDGETS).nospace() << "Failed to inhibit suspend with reason " << reason << ": " << reply.error().message();
137 } else {
138 const QDBusObjectPath requestPath = reply.value();
139
140 // By the time the inhibition returned, the inhibition was already gone. Uninhibit again.
141 if (!guard) {
142 org::freedesktop::portal::Request requestInterface{g_portalServiceName, requestPath.path(), QDBusConnection::sessionBus()};
143 requestInterface.Close();
144 } else {
145 m_portalInhibitionRequest = requestPath;
146 }
147 }
148
149 watcher->deleteLater();
150 });
151 } else {
152 qCDebug(KJOBWIDGETS) << "Sending freedesktop inhibition with reason" << m_reason;
153 org::freedesktop::PowerManagement::Inhibit inhibitInterface{g_inhibitServiceName, g_inhibitObjectPath, QDBusConnection::sessionBus()};
154 auto call = inhibitInterface.Inhibit(application: m_appName, reason: m_reason);
155 auto *watcher = new QDBusPendingCallWatcher(call);
156 QPointer<Inhibition> guard(this);
157 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: watcher, slot: [this, guard, watcher, appName = m_appName, reason = m_reason] {
158 QDBusPendingReply<uint> reply = *watcher;
159
160 if (reply.isError()) {
161 qCWarning(KJOBWIDGETS).nospace() << "Failed to inhibit suspend for " << appName << " with reason " << reason << ": " << reply.error().message();
162 } else {
163 const uint cookie = reply.value();
164
165 if (!guard) {
166 org::freedesktop::PowerManagement::Inhibit inhibitInterface{g_inhibitServiceName, g_inhibitObjectPath, QDBusConnection::sessionBus()};
167 inhibitInterface.UnInhibit(cookie);
168 } else {
169 m_inhibitionCookie = cookie;
170 }
171 }
172
173 watcher->deleteLater();
174 });
175 }
176#else
177 qCDebug(KJOBWIDGETS) << "KInhibitionJobTracker is not supported on this platform";
178#endif
179}
180
181void Inhibition::uninhibit()
182{
183#ifdef Q_OS_WINDOWS
184 if (m_handle != INVALID_HANDLE_VALUE) {
185 if (!PowerClearRequest(m_handle, PowerRequestExecutionRequired)) {
186 qWarning(KJOBWIDGETS) << "Failed to clear inhibition request:" << GetLastError();
187 }
188 CloseHandle(m_handle);
189 m_handle = INVALID_HANDLE_VALUE;
190 }
191#elif HAVE_QTDBUS
192 if (!m_portalInhibitionRequest.path().isEmpty()) {
193 org::freedesktop::portal::Request requestInterface{g_portalServiceName, m_portalInhibitionRequest.path(), QDBusConnection::sessionBus()};
194 auto call = requestInterface.Close();
195 auto *watcher = new QDBusPendingCallWatcher(call, this);
196 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, watcher] {
197 QDBusPendingReply<> reply = *watcher;
198
199 if (reply.isError()) {
200 qCWarning(KJOBWIDGETS) << "Failed to uninhibit suspend:" << reply.error().message();
201 } else {
202 m_portalInhibitionRequest = QDBusObjectPath();
203 }
204
205 watcher->deleteLater();
206 });
207 } else if (m_inhibitionCookie) {
208 org::freedesktop::PowerManagement::Inhibit inhibitInterface{g_inhibitServiceName, g_inhibitObjectPath, QDBusConnection::sessionBus()};
209 const int cookie = *m_inhibitionCookie;
210 auto call = inhibitInterface.UnInhibit(cookie);
211 auto *watcher = new QDBusPendingCallWatcher(call, this);
212 connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, watcher, cookie] {
213 QDBusPendingReply<> reply = *watcher;
214
215 if (reply.isError()) {
216 qCWarning(KJOBWIDGETS).nospace() << "Failed to uninhibit suspend for cookie" << cookie << ": " << reply.error().message();
217 } else {
218 m_inhibitionCookie.reset();
219 }
220
221 watcher->deleteLater();
222 });
223 }
224#endif
225}
226
227KInhibitionJobTracker::KInhibitionJobTracker(QObject *parent)
228 : KJobTrackerInterface(parent)
229 , d(std::make_unique<KInhibitionJobTrackerPrivate>(args: this))
230{
231}
232
233KInhibitionJobTracker::~KInhibitionJobTracker()
234{
235 if (!d->inhibitions.isEmpty()) {
236 qCWarning(KJOBWIDGETS) << "A KInhibitionJobTracker instance contains" << d->inhibitions.size() << "stalled jobs";
237 qDeleteAll(c: d->inhibitions);
238 }
239}
240
241void KInhibitionJobTracker::registerJob(KJob *job)
242{
243 if (d->inhibitions.contains(key: job)) {
244 return;
245 }
246
247 if (!job->isSuspended() && !job->error()) {
248 d->inhibit(job);
249 }
250
251 KJobTrackerInterface::registerJob(job);
252}
253
254void KInhibitionJobTracker::unregisterJob(KJob *job)
255{
256 KJobTrackerInterface::unregisterJob(job);
257 finished(job);
258}
259
260void KInhibitionJobTracker::finished(KJob *job)
261{
262 delete d->inhibitions.take(key: job);
263 delete d->timers.take(key: job);
264 d->reasons.remove(key: job);
265}
266
267void KInhibitionJobTracker::suspended(KJob *job)
268{
269 delete d->inhibitions.take(key: job);
270 delete d->timers.take(key: job);
271}
272
273void KInhibitionJobTracker::resumed(KJob *job)
274{
275 d->inhibit(job);
276}
277
278void KInhibitionJobTracker::description(KJob *job, const QString &title, const QPair<QString, QString> &field1, const QPair<QString, QString> &field2)
279{
280 Q_UNUSED(field1);
281 Q_UNUSED(field2);
282 d->reasons.insert(key: job, value: title);
283 // Not recreating the inhibition just to update the title,
284 // lifting an inhibition after some time might trigger any action
285 // that was suppressed by it.
286}
287
288#include "moc_kinhibitionjobtracker.cpp"
289#include "moc_kinhibitionjobtracker_p.cpp"
290

source code of kjobwidgets/src/kinhibitionjobtracker.cpp