KJobWidgets

kuiserverv2jobtracker.cpp
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(&updateTimer, &QTimer::timeout, q, [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(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(updates);
94 view.pendingUpdates.clear();
95}
96
97void KUiServerV2JobTrackerPrivate::updateDestUrl(KJob *job)
98{
99 scheduleUpdate(job, QStringLiteral("destUrl"), job->property("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"), 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"), true);
115 }
116
117 auto reply = serverProxy()->uiserver()->requestView(desktopEntry, job->capabilities(), hints);
118
119 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, q);
120 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [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(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(jobView, &org::kde::JobViewV3::cancelRequested, job, [job] {
137 job->kill(KJob::EmitResult);
138 });
139 QObject::connect(jobView, &org::kde::JobViewV3::suspendRequested, job, &KJob::suspend);
140 QObject::connect(jobView, &org::kde::JobViewV3::resumeRequested, job, &KJob::resume);
141
142 view.jobView = jobView;
143 }
144
145 // Now send the full current job state over
146 jobView->update(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, QVariantMap() /*hints*/);
156 delete jobView;
157
158 jobViews.remove(job);
159 }
160 });
161}
162
165 , d(new KUiServerV2JobTrackerPrivate(this))
166{
167 qDBusRegisterMetaType<qulonglong>();
168}
169
171{
172 if (!d->jobViews.isEmpty()) {
173 qCWarning(KJOBWIDGETS) << "A KUiServerV2JobTracker instance contains"
174 << d->jobViews.size() << "stalled jobs";
175 }
176}
177
179{
180 if (d->jobViews.contains(job)) {
181 return;
182 }
183
184 QString desktopEntry = job->property("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(serverProxy(), &KSharedUiServerV2Proxy::serverRegistered, this, [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, QVariantMap() /*hints*/);
217 }
218
219 delete view.jobView;
220 d->jobViews.remove(it.key());
221 } else {
222 delete view.jobView;
223 d->jobViews.remove(it.key()); // must happen before registerJob
224
225 if (jobGuard) {
226 registerJob(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"), static_cast<uint>(job->error()));
241 d->scheduleUpdate(job, QStringLiteral("errorMessage"), 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, job->processedAmount(unit));
248 }
249 if (job->totalAmount(unit) > 0) {
250 totalAmount(job, unit, job->totalAmount(unit));
251 }
252 }
253 if (job->percent() > 0) {
254 percent(job, job->percent());
255 }
256 d->updateDestUrl(job);
257
258 if (job->property("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(delayTimer, &QTimer::timeout, this, [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(500);
278 }
279
281}
282
288
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(job);
300 } else if (view.jobView) {
301 view.jobView->terminate(static_cast<uint>(job->error()),
302 job->error() ? job->errorText() : QString(),
303 QVariantMap() /*hints*/);
304 delete view.jobView;
305 d->jobViews.remove(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"), true);
310 if (job->error()) {
311 d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error()));
312 d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText());
313 }
314 }
315}
316
317void KUiServerV2JobTracker::suspended(KJob *job)
318{
319 d->scheduleUpdate(job, QStringLiteral("suspended"), true);
320}
321
322void KUiServerV2JobTracker::resumed(KJob *job)
323{
324 d->scheduleUpdate(job, QStringLiteral("suspended"), 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"), title);
332
333 d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), field1.first);
334 d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), field1.second);
335
336 d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), field2.first);
337 d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), field2.second);
338}
339
340void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &message)
341{
342 d->scheduleUpdate(job, QStringLiteral("infoMessage"), 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"), amount);
350 break;
351 case KJob::Files:
352 d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount);
353 break;
355 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount);
356 break;
357 case KJob::Items:
358 d->scheduleUpdate(job, QStringLiteral("totalItems"), 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"), job->elapsedTime());
371 d->scheduleUpdate(job, QStringLiteral("processedBytes"), amount);
372 break;
373 case KJob::Files:
374 d->scheduleUpdate(job, QStringLiteral("processedFiles"), amount);
375 break;
377 d->scheduleUpdate(job, QStringLiteral("processedDirectories"), amount);
378 break;
379 case KJob::Items:
380 d->scheduleUpdate(job, QStringLiteral("processedItems"), 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"), static_cast<uint>(percent));
391}
392
393void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed)
394{
395 d->scheduleUpdate(job, QStringLiteral("speed"), 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(m_watcher.get(), &QDBusServiceWatcher::serviceOwnerChanged, this, &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"
virtual void registerJob(KJob *job)
virtual void unregisterJob(KJob *job)
KJobTrackerInterface(QObject *parent=nullptr)
bool resume()
bool suspend()
qint64 elapsedTime() const
Q_SCRIPTABLE qulonglong totalAmount(Unit unit) const
int error() const
bool isSuspended() const
Q_SCRIPTABLE qulonglong processedAmount(Unit unit) const
bool isFinishedNotificationHidden() const
unsigned long percent() const
Capabilities capabilities() const
QString errorText() const
bool kill(KJob::KillVerbosity verbosity=KJob::Quietly)
void registerJob(KJob *job) override
Register a new job in this tracker.
void unregisterJob(KJob *job) override
Unregister a job from this tracker.
void finished(KJob *job) override
The following slots are inherited from KJobTrackerInterface.
KUiServerV2JobTracker(QObject *parent=nullptr)
Creates a new KJobTrackerInterface.
~KUiServerV2JobTracker() override
Destroys a KJobTrackerInterface.
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QDBusConnection sessionBus()
QString message() const const
void finished(QDBusPendingCallWatcher *self)
QDBusError error() const const
bool isError() const const
typename Select< 0 >::Type value() const const
void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
QObject(QObject *parent)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
QObject * parent() const const
QVariant property(const char *name) const const
bool isEmpty() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
void setSingleShot(bool singleShot)
void start()
void timeout()
bool toBool() const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:52:46 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.