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 | |
25 | Q_GLOBAL_STATIC(KSharedUiServerV2Proxy, serverProxy) |
26 | |
27 | struct JobView |
28 | { |
29 | QTimer *delayTimer = nullptr; |
30 | org::kde::JobViewV3 *jobView = nullptr; |
31 | QVariantMap currentState; |
32 | QVariantMap pendingUpdates; |
33 | }; |
34 | |
35 | class KUiServerV2JobTrackerPrivate |
36 | { |
37 | public: |
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 | |
64 | void 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 | |
75 | void KUiServerV2JobTrackerPrivate::sendAllUpdates() |
76 | { |
77 | for (auto it = jobViews.begin(), end = jobViews.end(); it != end; ++it) { |
78 | sendUpdate(view&: it.value()); |
79 | } |
80 | } |
81 | |
82 | void 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 | |
97 | void KUiServerV2JobTrackerPrivate::updateDestUrl(KJob *job) |
98 | { |
99 | scheduleUpdate(job, QStringLiteral("destUrl" ), value: job->property(name: "destUrl" ).toString()); |
100 | } |
101 | |
102 | void 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 | |
163 | KUiServerV2JobTracker::KUiServerV2JobTracker(QObject *parent) |
164 | : KJobTrackerInterface(parent) |
165 | , d(new KUiServerV2JobTrackerPrivate(this)) |
166 | { |
167 | qDBusRegisterMetaType<qulonglong>(); |
168 | } |
169 | |
170 | KUiServerV2JobTracker::~KUiServerV2JobTracker() |
171 | { |
172 | if (!d->jobViews.isEmpty()) { |
173 | qCWarning(KJOBWIDGETS) << "A KUiServerV2JobTracker instance contains" |
174 | << d->jobViews.size() << "stalled jobs" ; |
175 | } |
176 | } |
177 | |
178 | void 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 | |
284 | void KUiServerV2JobTracker::unregisterJob(KJob *job) |
285 | { |
286 | KJobTrackerInterface::unregisterJob(job); |
287 | finished(job); |
288 | } |
289 | |
290 | void 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 | |
318 | void KUiServerV2JobTracker::suspended(KJob *job) |
319 | { |
320 | d->scheduleUpdate(job, QStringLiteral("suspended" ), value: true); |
321 | } |
322 | |
323 | void KUiServerV2JobTracker::resumed(KJob *job) |
324 | { |
325 | d->scheduleUpdate(job, QStringLiteral("suspended" ), value: false); |
326 | } |
327 | |
328 | void 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 | |
341 | void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &message) |
342 | { |
343 | d->scheduleUpdate(job, QStringLiteral("infoMessage" ), value: message); |
344 | } |
345 | |
346 | void 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 | |
367 | void 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 | |
388 | void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent) |
389 | { |
390 | d->scheduleUpdate(job, QStringLiteral("percent" ), value: static_cast<uint>(percent)); |
391 | } |
392 | |
393 | void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed) |
394 | { |
395 | d->scheduleUpdate(job, QStringLiteral("speed" ), value: static_cast<qulonglong>(speed)); |
396 | } |
397 | |
398 | KSharedUiServerV2Proxy::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 | |
412 | KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy() |
413 | { |
414 | |
415 | } |
416 | |
417 | org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver() |
418 | { |
419 | return m_uiserver.get(); |
420 | } |
421 | |
422 | void 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 | |