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] {
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{
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) {
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 {
262
263 QTimer *delayTimer = new QTimer();
264 delayTimer->setSingleShot(true);
265 connect(delayTimer, &QTimer::timeout, this, [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(500);
279 }
280
282}
283
289
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(job);
301 } else if (view.jobView) {
302 view.jobView->terminate(static_cast<uint>(job->error()),
303 job->error() ? job->errorText() : QString(),
304 QVariantMap() /*hints*/);
305 delete view.jobView;
306 d->jobViews.remove(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"), true);
311 if (job->error()) {
312 d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error()));
313 d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText());
314 }
315 }
316}
317
318void KUiServerV2JobTracker::suspended(KJob *job)
319{
320 d->scheduleUpdate(job, QStringLiteral("suspended"), true);
321}
322
323void KUiServerV2JobTracker::resumed(KJob *job)
324{
325 d->scheduleUpdate(job, QStringLiteral("suspended"), 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"), title);
333
334 d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), field1.first);
335 d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), field1.second);
336
337 d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), field2.first);
338 d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), field2.second);
339}
340
341void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &message)
342{
343 d->scheduleUpdate(job, QStringLiteral("infoMessage"), 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"), amount);
351 break;
352 case KJob::Files:
353 d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount);
354 break;
356 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount);
357 break;
358 case KJob::Items:
359 d->scheduleUpdate(job, QStringLiteral("totalItems"), amount);
360 break;
361 case KJob::UnitsCount:
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"), 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:
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()
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)
const QList< QKeySequence > & end()
QDBusConnection sessionBus()
void finished(QDBusPendingCallWatcher *self)
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
T qobject_cast(QObject *object)
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 Tue Mar 26 2024 11:13:04 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.