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

source code of kjobwidgets/src/kuiserverv2jobtracker.cpp