KJobWidgets

kuiserverv2jobtracker.cpp
1 /*
2  This file is part of the KDE project
3  SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <[email protected]>
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(&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 
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(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(updates);
94  view.pendingUpdates.clear();
95 }
96 
97 void KUiServerV2JobTrackerPrivate::updateDestUrl(KJob *job)
98 {
99  scheduleUpdate(job, QStringLiteral("destUrl"), job->property("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"), 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  delete d;
178 }
179 
181 {
182  if (d->jobViews.contains(job)) {
183  return;
184  }
185 
186  QString desktopEntry = job->property("desktopFileName").toString();
187  if (desktopEntry.isEmpty()) {
188  desktopEntry = QGuiApplication::desktopFileName();
189  }
190 
191  if (desktopEntry.isEmpty()) {
192  qCWarning(KJOBWIDGETS) << "Cannot register a job with KUiServerV2JobTracker without QGuiApplication::desktopFileName";
193  return;
194  }
195 
196  // Watch the server registering/unregistering and re-register the jobs as needed
197  if (!d->serverRegisteredConnection) {
198  d->serverRegisteredConnection = connect(serverProxy(), &KSharedUiServerV2Proxy::serverRegistered, this, [this]() {
199  const auto staleViews = d->jobViews;
200 
201  // Delete the old views, remove the old struct but keep the state,
202  // register the job again (which checks for presence, hence removing first)
203  // and then restore its previous state, which is safe because the DBus
204  // is async and is only processed once event loop returns
205  for (auto it = staleViews.begin(), end = staleViews.end(); it != end; ++it) {
206  KJob *job = it.key();
207  const JobView &view = it.value();
208 
209  const auto oldState = view.currentState;
210 
211  // It is possible that the KJob has been deleted already so do not
212  // use or deference if marked as terminated
213  if (oldState.value(QStringLiteral("terminated")).toBool()) {
214  const uint errorCode = oldState.value(QStringLiteral("errorCode")).toUInt();
215  const QString errorMessage = oldState.value(QStringLiteral("errorMessage")).toString();
216 
217  if (view.jobView) {
218  view.jobView->terminate(errorCode, errorMessage, QVariantMap() /*hints*/);
219  }
220 
221  delete view.jobView;
222  d->jobViews.remove(job);
223  } else {
224  delete view.jobView;
225  d->jobViews.remove(job); // must happen before registerJob
226 
227  registerJob(job);
228 
229  d->jobViews[job].currentState = oldState;
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  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 
285 {
287  finished(job);
288 }
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 
318 void KUiServerV2JobTracker::suspended(KJob *job)
319 {
320  d->scheduleUpdate(job, QStringLiteral("suspended"), true);
321 }
322 
323 void KUiServerV2JobTracker::resumed(KJob *job)
324 {
325  d->scheduleUpdate(job, QStringLiteral("suspended"), 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"), 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 
341 void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &plain, const QString &rich)
342 {
343  Q_UNUSED(rich);
344  d->scheduleUpdate(job, QStringLiteral("infoMessage"), plain);
345 }
346 
347 void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount)
348 {
349  switch (unit) {
350  case KJob::Bytes:
351  d->scheduleUpdate(job, QStringLiteral("totalBytes"), amount);
352  break;
353  case KJob::Files:
354  d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount);
355  break;
356  case KJob::Directories:
357  d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount);
358  break;
359  case KJob::Items:
360  d->scheduleUpdate(job, QStringLiteral("totalItems"), amount);
361  break;
362  case KJob::UnitsCount:
363  Q_UNREACHABLE();
364  break;
365  }
366 }
367 
368 void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount)
369 {
370  switch (unit) {
371  case KJob::Bytes:
372  d->scheduleUpdate(job, QStringLiteral("processedBytes"), amount);
373  break;
374  case KJob::Files:
375  d->scheduleUpdate(job, QStringLiteral("processedFiles"), amount);
376  break;
377  case KJob::Directories:
378  d->scheduleUpdate(job, QStringLiteral("processedDirectories"), amount);
379  break;
380  case KJob::Items:
381  d->scheduleUpdate(job, QStringLiteral("processedItems"), amount);
382  break;
383  case KJob::UnitsCount:
384  Q_UNREACHABLE();
385  break;
386  }
387 }
388 
389 void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent)
390 {
391  d->scheduleUpdate(job, QStringLiteral("percent"), static_cast<uint>(percent));
392 }
393 
394 void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed)
395 {
396  d->scheduleUpdate(job, QStringLiteral("speed"), static_cast<qulonglong>(speed));
397 }
398 
399 KSharedUiServerV2Proxy::KSharedUiServerV2Proxy()
400  : m_uiserver(new org::kde::JobViewServerV2(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus()))
401  , m_watcher(new QDBusServiceWatcher(QStringLiteral("org.kde.JobViewServer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange))
402 {
403  connect(m_watcher.get(), &QDBusServiceWatcher::serviceOwnerChanged, this, &KSharedUiServerV2Proxy::uiserverOwnerChanged);
404 
405  // cleanup early enough to avoid issues with dbus at application exit
406  // see e.g. https://phabricator.kde.org/D2545
407  qAddPostRoutine([]() {
408  serverProxy->m_uiserver.reset();
409  serverProxy->m_watcher.reset();
410  });
411 }
412 
413 KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy()
414 {
415 
416 }
417 
418 org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver()
419 {
420  return m_uiserver.get();
421 }
422 
423 void KSharedUiServerV2Proxy::uiserverOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
424 {
425  Q_UNUSED(serviceName);
426  Q_UNUSED(oldOwner);
427 
428  if (!newOwner.isEmpty()) { // registered
429  Q_EMIT serverRegistered();
430  } else if (newOwner.isEmpty()) { // unregistered
431  Q_EMIT serverUnregistered();
432  }
433 }
434 
435 #include "moc_kuiserverv2jobtracker.cpp"
436 #include "moc_kuiserverv2jobtracker_p.cpp"
bool resume()
virtual void unregisterJob(KJob *job)
void finished(QDBusPendingCallWatcher *self)
Capabilities capabilities() const
virtual void registerJob(KJob *job)
~KUiServerV2JobTracker() override
Destroys a KJobTrackerInterface.
bool suspend()
void setSingleShot(bool singleShot)
void finished(KJob *job) override
The following slots are inherited from KJobTrackerInterface.
KUiServerV2JobTracker(QObject *parent=nullptr)
Creates a new KJobTrackerInterface.
bool isError() const const
bool kill(KillVerbosity verbosity=Quietly)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) class ControlPrivate
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
void deleteLater()
void start(int msec)
QDBusConnection sessionBus()
void timeout()
bool isEmpty() const const
QString errorText() const
bool isFinishedNotificationHidden() const
T1 value() const const
void unregisterJob(KJob *job) override
Unregister a job from this tracker.
bool toBool() const const
KOPENINGHOURS_EXPORT QString currentState(const OpeningHours &oh)
void processedAmount(KJob *job, KJob::Unit unit, qulonglong amount)
bool isSuspended() const
void totalAmount(KJob *job, KJob::Unit unit, qulonglong amount)
void registerJob(KJob *job) override
Register a new job in this tracker.
void percent(KJob *job, unsigned long percent)
int error() const
void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
QString toString() const const
QVariant property(const char *name) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu Oct 6 2022 04:15:43 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.