1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kuiserverv2jobtracker.h"
9#include "kuiserverv2jobtracker_p.h"
10
11#include "jobviewv3iface.h"
12#include "debug.h"
13
14#include <KJob>
15
16#include <QtGlobal>
17#include <QDBusConnection>
18#include <QDBusPendingCallWatcher>
19#include <QDBusPendingReply>
20#include <QGuiApplication>
21#include <QTimer>
22#include <QHash>
23#include <QVariantMap>
24
25Q_GLOBAL_STATIC(KSharedUiServerV2Proxy, serverProxy)
26
27struct JobView
28{
29 QTimer *delayTimer = nullptr;
30 org::kde::JobViewV3 *jobView = nullptr;
31 QVariantMap currentState;
32 QVariantMap pendingUpdates;
33};
34
35class KUiServerV2JobTrackerPrivate
36{
37public:
38 KUiServerV2JobTrackerPrivate(KUiServerV2JobTracker *parent)
39 : q(parent)
40 {
41 updateTimer.setInterval(0);
42 updateTimer.setSingleShot(true);
43 QObject::connect(sender: &updateTimer, signal: &QTimer::timeout, context: q, slot: [this] {
44 sendAllUpdates();
45 });
46 }
47
48 KUiServerV2JobTracker *const q;
49
50 void sendAllUpdates();
51 void sendUpdate(JobView &view);
52 void scheduleUpdate(KJob *job, const QString &key, const QVariant &value);
53
54 void updateDestUrl(KJob *job);
55
56 void requestView(KJob *job, const QString &desktopEntry);
57
58 QHash<KJob *, JobView> jobViews;
59 QTimer updateTimer;
60
61 QMetaObject::Connection serverRegisteredConnection;
62};
63
64void KUiServerV2JobTrackerPrivate::scheduleUpdate(KJob *job, const QString &key, const QVariant &value)
65{
66 auto &view = jobViews[job];
67 view.currentState[key] = value;
68 view.pendingUpdates[key] = value;
69
70 if (!updateTimer.isActive()) {
71 updateTimer.start();
72 }
73}
74
75void KUiServerV2JobTrackerPrivate::sendAllUpdates()
76{
77 for (auto it = jobViews.begin(), end = jobViews.end(); it != end; ++it) {
78 sendUpdate(view&: it.value());
79 }
80}
81
82void KUiServerV2JobTrackerPrivate::sendUpdate(JobView &view)
83{
84 if (!view.jobView) {
85 return;
86 }
87
88 const QVariantMap updates = view.pendingUpdates;
89 if (updates.isEmpty()) {
90 return;
91 }
92
93 view.jobView->update(properties: updates);
94 view.pendingUpdates.clear();
95}
96
97void KUiServerV2JobTrackerPrivate::updateDestUrl(KJob *job)
98{
99 scheduleUpdate(job, QStringLiteral("destUrl"), value: job->property(name: "destUrl").toString());
100}
101
102void KUiServerV2JobTrackerPrivate::requestView(KJob *job, const QString &desktopEntry)
103{
104 QPointer<KJob> jobGuard = job;
105 auto &view = jobViews[job];
106
107 QVariantMap hints = view.currentState;
108 // Tells Plasma to show the job view right away, since the delay is always handled on our side
109 hints.insert(QStringLiteral("immediate"), value: true);
110 // Must not clear currentState as only Plasma 5.22+ will use properties from "hints",
111 // there must still be a full update() call for earlier versions!
112
113 if (job->isFinishedNotificationHidden()) {
114 hints.insert(QStringLiteral("transient"), value: true);
115 }
116
117 auto reply = serverProxy()->uiserver()->requestView(desktopEntry, capabilities: job->capabilities(), hints);
118
119 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, q);
120 QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: q, slot: [this, watcher, jobGuard, job] {
121 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
122 watcher->deleteLater();
123
124 if (reply.isError()) {
125 qCWarning(KJOBWIDGETS) << "Failed to register job with KUiServerV2JobTracker" << reply.error().message();
126 jobViews.remove(key: job);
127 return;
128 }
129
130 const QString viewObjectPath = reply.value().path();
131 auto *jobView = new org::kde::JobViewV3(QStringLiteral("org.kde.JobViewServer"), viewObjectPath, QDBusConnection::sessionBus());
132
133 auto &view = jobViews[job];
134
135 if (jobGuard) {
136 QObject::connect(sender: jobView, signal: &org::kde::JobViewV3::cancelRequested, context: job, slot: [job] {
137 job->kill(verbosity: KJob::EmitResult);
138 });
139 QObject::connect(sender: jobView, signal: &org::kde::JobViewV3::suspendRequested, context: job, slot: &KJob::suspend);
140 QObject::connect(sender: jobView, signal: &org::kde::JobViewV3::resumeRequested, context: job, slot: &KJob::resume);
141
142 view.jobView = jobView;
143 }
144
145 // Now send the full current job state over
146 jobView->update(properties: view.currentState);
147 // which also contains all pending updates
148 view.pendingUpdates.clear();
149
150 // Job was deleted or finished in the meantime
151 if (!jobGuard || view.currentState.value(QStringLiteral("terminated")).toBool()) {
152 const uint errorCode = view.currentState.value(QStringLiteral("errorCode")).toUInt();
153 const QString errorMessage = view.currentState.value(QStringLiteral("errorMessage")).toString();
154
155 jobView->terminate(errorCode, errorMessage, hints: QVariantMap() /*hints*/);
156 delete jobView;
157
158 jobViews.remove(key: job);
159 }
160 });
161}
162
163KUiServerV2JobTracker::KUiServerV2JobTracker(QObject *parent)
164 : KJobTrackerInterface(parent)
165 , d(new KUiServerV2JobTrackerPrivate(this))
166{
167 qDBusRegisterMetaType<qulonglong>();
168}
169
170KUiServerV2JobTracker::~KUiServerV2JobTracker()
171{
172 if (!d->jobViews.isEmpty()) {
173 qCWarning(KJOBWIDGETS) << "A KUiServerV2JobTracker instance contains"
174 << d->jobViews.size() << "stalled jobs";
175 }
176}
177
178void KUiServerV2JobTracker::registerJob(KJob *job)
179{
180 if (d->jobViews.contains(key: job)) {
181 return;
182 }
183
184 QString desktopEntry = job->property(name: "desktopFileName").toString();
185 if (desktopEntry.isEmpty()) {
186 desktopEntry = QGuiApplication::desktopFileName();
187 }
188
189 if (desktopEntry.isEmpty()) {
190 qCWarning(KJOBWIDGETS) << "Cannot register a job with KUiServerV2JobTracker without QGuiApplication::desktopFileName";
191 return;
192 }
193
194 // Watch the server registering/unregistering and re-register the jobs as needed
195 if (!d->serverRegisteredConnection) {
196 d->serverRegisteredConnection = connect(sender: serverProxy(), signal: &KSharedUiServerV2Proxy::serverRegistered, context: this, slot: [this]() {
197 const auto staleViews = d->jobViews;
198
199 // Delete the old views, remove the old struct but keep the state,
200 // register the job again (which checks for presence, hence removing first)
201 // and then restore its previous state, which is safe because the DBus
202 // is async and is only processed once event loop returns
203 for (auto it = staleViews.begin(), end = staleViews.end(); it != end; ++it) {
204 QPointer<KJob> jobGuard = it.key();
205 const JobView &view = it.value();
206
207 const auto oldState = view.currentState;
208
209 // It is possible that the KJob has been deleted already so do not
210 // use or deference if marked as terminated
211 if (oldState.value(QStringLiteral("terminated")).toBool()) {
212 const uint errorCode = oldState.value(QStringLiteral("errorCode")).toUInt();
213 const QString errorMessage = oldState.value(QStringLiteral("errorMessage")).toString();
214
215 if (view.jobView) {
216 view.jobView->terminate(errorCode, errorMessage, hints: QVariantMap() /*hints*/);
217 }
218
219 delete view.jobView;
220 d->jobViews.remove(key: it.key());
221 } else {
222 delete view.jobView;
223 d->jobViews.remove(key: it.key()); // must happen before registerJob
224
225 if (jobGuard) {
226 registerJob(job: jobGuard);
227
228 d->jobViews[jobGuard].currentState = oldState;
229 }
230 }
231 }
232 });
233 }
234
235 // Send along current job state
236 if (job->isSuspended()) {
237 suspended(job);
238 }
239 if (job->error()) {
240 d->scheduleUpdate(job, QStringLiteral("errorCode"), value: static_cast<uint>(job->error()));
241 d->scheduleUpdate(job, QStringLiteral("errorMessage"), value: job->errorText());
242 }
243 for (int i = KJob::Bytes; i <= KJob::Items; ++i) {
244 const auto unit = static_cast<KJob::Unit>(i);
245
246 if (job->processedAmount(unit) > 0) {
247 processedAmount(job, unit, amount: job->processedAmount(unit));
248 }
249 if (job->totalAmount(unit) > 0) {
250 totalAmount(job, unit, amount: job->totalAmount(unit));
251 }
252 }
253 if (job->percent() > 0) {
254 percent(job, percent: job->percent());
255 }
256 d->updateDestUrl(job);
257
258 if (job->property(name: "immediateProgressReporting").toBool()) {
259 d->requestView(job, desktopEntry);
260 } else {
261 QPointer<KJob> jobGuard = job;
262
263 QTimer *delayTimer = new QTimer();
264 delayTimer->setSingleShot(true);
265 connect(sender: delayTimer, signal: &QTimer::timeout, context: this, slot: [this, job, jobGuard, desktopEntry] {
266 if (jobGuard) {
267 auto &view = d->jobViews[job];
268 if (view.delayTimer) {
269 view.delayTimer->deleteLater();
270 view.delayTimer = nullptr;
271 }
272 d->requestView(job, desktopEntry);
273 }
274 });
275
276 d->jobViews[job].delayTimer = delayTimer;
277 delayTimer->start(msec: 500);
278 }
279
280 KJobTrackerInterface::registerJob(job);
281}
282
283void KUiServerV2JobTracker::unregisterJob(KJob *job)
284{
285 KJobTrackerInterface::unregisterJob(job);
286 finished(job);
287}
288
289void KUiServerV2JobTracker::finished(KJob *job)
290{
291 d->updateDestUrl(job);
292
293 // send all pending updates before terminating to ensure state is correct
294 auto &view = d->jobViews[job];
295 d->sendUpdate(view);
296
297 if (view.delayTimer) {
298 delete view.delayTimer;
299 d->jobViews.remove(key: job);
300 } else if (view.jobView) {
301 view.jobView->terminate(errorCode: static_cast<uint>(job->error()),
302 errorMessage: job->error() ? job->errorText() : QString(),
303 hints: QVariantMap() /*hints*/);
304 delete view.jobView;
305 d->jobViews.remove(key: job);
306 } else {
307 // Remember that the job finished in the meantime and
308 // terminate the JobView once it arrives
309 d->scheduleUpdate(job, QStringLiteral("terminated"), value: true);
310 if (job->error()) {
311 d->scheduleUpdate(job, QStringLiteral("errorCode"), value: static_cast<uint>(job->error()));
312 d->scheduleUpdate(job, QStringLiteral("errorMessage"), value: job->errorText());
313 }
314 }
315}
316
317void KUiServerV2JobTracker::suspended(KJob *job)
318{
319 d->scheduleUpdate(job, QStringLiteral("suspended"), value: true);
320}
321
322void KUiServerV2JobTracker::resumed(KJob *job)
323{
324 d->scheduleUpdate(job, QStringLiteral("suspended"), value: false);
325}
326
327void KUiServerV2JobTracker::description(KJob *job, const QString &title,
328 const QPair<QString, QString> &field1,
329 const QPair<QString, QString> &field2)
330{
331 d->scheduleUpdate(job, QStringLiteral("title"), value: title);
332
333 d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), value: field1.first);
334 d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), value: field1.second);
335
336 d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), value: field2.first);
337 d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), value: field2.second);
338}
339
340void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &message)
341{
342 d->scheduleUpdate(job, QStringLiteral("infoMessage"), value: message);
343}
344
345void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount)
346{
347 switch (unit) {
348 case KJob::Bytes:
349 d->scheduleUpdate(job, QStringLiteral("totalBytes"), value: amount);
350 break;
351 case KJob::Files:
352 d->scheduleUpdate(job, QStringLiteral("totalFiles"), value: amount);
353 break;
354 case KJob::Directories:
355 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), value: amount);
356 break;
357 case KJob::Items:
358 d->scheduleUpdate(job, QStringLiteral("totalItems"), value: amount);
359 break;
360 case KJob::UnitsCount:
361 Q_UNREACHABLE();
362 break;
363 }
364}
365
366void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount)
367{
368 switch (unit) {
369 case KJob::Bytes:
370 d->scheduleUpdate(job, QStringLiteral("elapsedTime"), value: job->elapsedTime());
371 d->scheduleUpdate(job, QStringLiteral("processedBytes"), value: amount);
372 break;
373 case KJob::Files:
374 d->scheduleUpdate(job, QStringLiteral("processedFiles"), value: amount);
375 break;
376 case KJob::Directories:
377 d->scheduleUpdate(job, QStringLiteral("processedDirectories"), value: amount);
378 break;
379 case KJob::Items:
380 d->scheduleUpdate(job, QStringLiteral("processedItems"), value: amount);
381 break;
382 case KJob::UnitsCount:
383 Q_UNREACHABLE();
384 break;
385 }
386}
387
388void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent)
389{
390 d->scheduleUpdate(job, QStringLiteral("percent"), value: static_cast<uint>(percent));
391}
392
393void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed)
394{
395 d->scheduleUpdate(job, QStringLiteral("speed"), value: static_cast<qulonglong>(speed));
396}
397
398KSharedUiServerV2Proxy::KSharedUiServerV2Proxy()
399 : m_uiserver(new org::kde::JobViewServerV2(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus()))
400 , m_watcher(new QDBusServiceWatcher(QStringLiteral("org.kde.JobViewServer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange))
401{
402 connect(sender: m_watcher.get(), signal: &QDBusServiceWatcher::serviceOwnerChanged, context: this, slot: &KSharedUiServerV2Proxy::uiserverOwnerChanged);
403
404 // cleanup early enough to avoid issues with dbus at application exit
405 // see e.g. https://phabricator.kde.org/D2545
406 qAddPostRoutine([]() {
407 serverProxy->m_uiserver.reset();
408 serverProxy->m_watcher.reset();
409 });
410}
411
412KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy()
413{
414
415}
416
417org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver()
418{
419 return m_uiserver.get();
420}
421
422void KSharedUiServerV2Proxy::uiserverOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
423{
424 Q_UNUSED(serviceName);
425 Q_UNUSED(oldOwner);
426
427 if (!newOwner.isEmpty()) { // registered
428 Q_EMIT serverRegistered();
429 } else if (newOwner.isEmpty()) { // unregistered
430 Q_EMIT serverUnregistered();
431 }
432}
433
434#include "moc_kuiserverv2jobtracker.cpp"
435#include "moc_kuiserverv2jobtracker_p.cpp"
436

source code of kjobwidgets/src/kuiserverv2jobtracker.cpp