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 auto &view = d->jobViews[job];
267 if (view.delayTimer) {
268 view.delayTimer->deleteLater();
269 view.delayTimer = nullptr;
270 }
271
272 if (jobGuard) {
273 d->requestView(job, desktopEntry);
274 }
275 });
276
277 d->jobViews[job].delayTimer = delayTimer;
278 delayTimer->start(msec: 500);
279 }
280
281 KJobTrackerInterface::registerJob(job);
282}
283
284void KUiServerV2JobTracker::unregisterJob(KJob *job)
285{
286 KJobTrackerInterface::unregisterJob(job);
287 finished(job);
288}
289
290void KUiServerV2JobTracker::finished(KJob *job)
291{
292 d->updateDestUrl(job);
293
294 // send all pending updates before terminating to ensure state is correct
295 auto &view = d->jobViews[job];
296 d->sendUpdate(view);
297
298 if (view.delayTimer) {
299 delete view.delayTimer;
300 d->jobViews.remove(key: job);
301 } else if (view.jobView) {
302 view.jobView->terminate(errorCode: static_cast<uint>(job->error()),
303 errorMessage: job->error() ? job->errorText() : QString(),
304 hints: QVariantMap() /*hints*/);
305 delete view.jobView;
306 d->jobViews.remove(key: job);
307 } else {
308 // Remember that the job finished in the meantime and
309 // terminate the JobView once it arrives
310 d->scheduleUpdate(job, QStringLiteral("terminated"), value: true);
311 if (job->error()) {
312 d->scheduleUpdate(job, QStringLiteral("errorCode"), value: static_cast<uint>(job->error()));
313 d->scheduleUpdate(job, QStringLiteral("errorMessage"), value: job->errorText());
314 }
315 }
316}
317
318void KUiServerV2JobTracker::suspended(KJob *job)
319{
320 d->scheduleUpdate(job, QStringLiteral("suspended"), value: true);
321}
322
323void KUiServerV2JobTracker::resumed(KJob *job)
324{
325 d->scheduleUpdate(job, QStringLiteral("suspended"), value: false);
326}
327
328void KUiServerV2JobTracker::description(KJob *job, const QString &title,
329 const QPair<QString, QString> &field1,
330 const QPair<QString, QString> &field2)
331{
332 d->scheduleUpdate(job, QStringLiteral("title"), value: title);
333
334 d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), value: field1.first);
335 d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), value: field1.second);
336
337 d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), value: field2.first);
338 d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), value: field2.second);
339}
340
341void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &message)
342{
343 d->scheduleUpdate(job, QStringLiteral("infoMessage"), value: message);
344}
345
346void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount)
347{
348 switch (unit) {
349 case KJob::Bytes:
350 d->scheduleUpdate(job, QStringLiteral("totalBytes"), value: amount);
351 break;
352 case KJob::Files:
353 d->scheduleUpdate(job, QStringLiteral("totalFiles"), value: amount);
354 break;
355 case KJob::Directories:
356 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), value: amount);
357 break;
358 case KJob::Items:
359 d->scheduleUpdate(job, QStringLiteral("totalItems"), value: amount);
360 break;
361 case KJob::UnitsCount:
362 Q_UNREACHABLE();
363 break;
364 }
365}
366
367void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount)
368{
369 switch (unit) {
370 case KJob::Bytes:
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