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
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
164 : KJobTrackerInterface(parent)
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)
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)
The interface to implement to track the progresses of a job.
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)
iterator begin()
iterator end()
bool remove(const Key &key)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
QVariant property(const char *name) const const
bool isEmpty() const const
void setInterval(int msec)
bool isActive() const const
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-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:04:20 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.