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.h>
15 
16 #include <QDBusConnection>
17 #include <QDBusPendingCallWatcher>
18 #include <QDBusPendingReply>
19 #include <QGuiApplication>
20 #include <QTimer>
21 #include <QHash>
22 #include <QVariantMap>
23 
24 Q_GLOBAL_STATIC(KSharedUiServerV2Proxy, serverProxy)
25 
26 struct JobView
27 {
28  QTimer *delayTimer = nullptr;
29  org::kde::JobViewV3 *jobView = nullptr;
30  QVariantMap currentState;
31  QVariantMap pendingUpdates;
32 };
33 
34 class KUiServerV2JobTrackerPrivate
35 {
36 public:
37  KUiServerV2JobTrackerPrivate(KUiServerV2JobTracker *parent)
38  : q(parent)
39  {
40  updateTimer.setInterval(0);
41  updateTimer.setSingleShot(true);
42  QObject::connect(&updateTimer, &QTimer::timeout, q, [this] {
43  sendAllUpdates();
44  });
45  }
46 
47  KUiServerV2JobTracker *const q;
48 
49  void sendAllUpdates();
50  void sendUpdate(JobView &view);
51  void scheduleUpdate(KJob *job, const QString &key, const QVariant &value);
52 
53  void updateDestUrl(KJob *job);
54 
55  void requestView(KJob *job, const QString &desktopEntry);
56 
57  QHash<KJob *, JobView> jobViews;
58  QTimer updateTimer;
59 
60  QMetaObject::Connection serverRegisteredConnection;
61 };
62 
63 void KUiServerV2JobTrackerPrivate::scheduleUpdate(KJob *job, const QString &key, const QVariant &value)
64 {
65  auto &view = jobViews[job];
66  view.currentState[key] = value;
67  view.pendingUpdates[key] = value;
68 
69  if (!updateTimer.isActive()) {
70  updateTimer.start();
71  }
72 }
73 
74 void KUiServerV2JobTrackerPrivate::sendAllUpdates()
75 {
76  for (auto it = jobViews.begin(), end = jobViews.end(); it != end; ++it) {
77  sendUpdate(it.value());
78  }
79 }
80 
81 void KUiServerV2JobTrackerPrivate::sendUpdate(JobView &view)
82 {
83  if (!view.jobView) {
84  return;
85  }
86 
87  const QVariantMap updates = view.pendingUpdates;
88  if (updates.isEmpty()) {
89  return;
90  }
91 
92  view.jobView->update(updates);
93  view.pendingUpdates.clear();
94 }
95 
96 void KUiServerV2JobTrackerPrivate::updateDestUrl(KJob *job)
97 {
98  const QVariant destUrl = job->property("destUrl");
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  auto reply = serverProxy()->uiserver()->requestView(desktopEntry, job->capabilities(), hints);
114 
115  QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, q);
116  QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher, jobGuard, job] {
117  QDBusPendingReply<QDBusObjectPath> reply = *watcher;
118  watcher->deleteLater();
119 
120  if (reply.isError()) {
121  qCWarning(KJOBWIDGETS) << "Failed to register job with KUiServerV2JobTracker" << reply.error().message();
122  jobViews.remove(job);
123  return;
124  }
125 
126  const QString viewObjectPath = reply.value().path();
127  auto *jobView = new org::kde::JobViewV3(QStringLiteral("org.kde.JobViewServer"), viewObjectPath, QDBusConnection::sessionBus());
128 
129  auto &view = jobViews[job];
130 
131  if (jobGuard) {
132  QObject::connect(jobView, &org::kde::JobViewV3::cancelRequested, job, [job] {
133  job->kill(KJob::EmitResult);
134  });
135  QObject::connect(jobView, &org::kde::JobViewV3::suspendRequested, job, &KJob::suspend);
136  QObject::connect(jobView, &org::kde::JobViewV3::resumeRequested, job, &KJob::resume);
137 
138  view.jobView = jobView;
139  }
140 
141  // Now send the full current job state over
142  jobView->update(view.currentState);
143  // which also contains all pending updates
144  view.pendingUpdates.clear();
145 
146  // Job was deleted or finished in the meantime
147  if (!jobGuard || view.currentState.value(QStringLiteral("terminated")).toBool()) {
148  const uint errorCode = view.currentState.value(QStringLiteral("errorCode")).toUInt();
149  const QString errorMessage = view.currentState.value(QStringLiteral("errorMessage")).toString();
150 
151  jobView->terminate(errorCode, errorMessage, QVariantMap() /*hints*/);
152  delete jobView;
153 
154  jobViews.remove(job);
155  }
156  });
157 }
158 
160  : KJobTrackerInterface(parent)
161  , d(new KUiServerV2JobTrackerPrivate(this))
162 {
163  qDBusRegisterMetaType<qulonglong>();
164 }
165 
167 {
168  if (!d->jobViews.isEmpty()) {
169  qCWarning(KJOBWIDGETS) << "A KUiServerV2JobTracker instance contains"
170  << d->jobViews.size() << "stalled jobs";
171  }
172 
173  delete d;
174 }
175 
177 {
178  if (d->jobViews.contains(job)) {
179  return;
180  }
181 
182  QString desktopEntry = job->property("desktopFileName").toString();
183  if (desktopEntry.isEmpty()) {
184  desktopEntry = QGuiApplication::desktopFileName();
185  }
186 
187  if (desktopEntry.isEmpty()) {
188  qCWarning(KJOBWIDGETS) << "Cannot register a job with KUiServerV2JobTracker without QGuiApplication::desktopFileName";
189  return;
190  }
191 
192  // Watch the server registering/unregistering and re-register the jobs as needed
193  if (!d->serverRegisteredConnection) {
194  d->serverRegisteredConnection = connect(serverProxy(), &KSharedUiServerV2Proxy::serverRegistered, this, [this]() {
195  const auto staleViews = d->jobViews;
196 
197  // Delete the old views, remove the old struct but keep the state,
198  // register the job again (which checks for presence, hence removing first)
199  // and then restore its previous state, which is safe because the DBus
200  // is async and is only processed once event loop returns
201  for (auto it = staleViews.begin(), end = staleViews.end(); it != end; ++it) {
202  KJob *job = it.key();
203  const JobView &view = it.value();
204 
205  const auto oldState = view.currentState;
206 
207  delete view.jobView;
208  d->jobViews.remove(job);
209 
210  registerJob(job);
211 
212  d->jobViews[job].currentState = oldState;
213  }
214  });
215  }
216 
217  // Send along current job state
218  if (job->isSuspended()) {
219  suspended(job);
220  }
221  if (job->error()) {
222  d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error()));
223  d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText());
224  }
225  for (int i = KJob::Bytes; i <= KJob::Items; ++i) {
226  const auto unit = static_cast<KJob::Unit>(i);
227 
228  if (job->processedAmount(unit) > 0) {
229  processedAmount(job, unit, job->processedAmount(unit));
230  }
231  if (job->totalAmount(unit) > 0) {
232  totalAmount(job, unit, job->totalAmount(unit));
233  }
234  }
235  if (job->percent() > 0) {
236  percent(job, job->percent());
237  }
238  d->updateDestUrl(job);
239 
240  if (job->property("immediateProgressReporting").toBool()) {
241  d->requestView(job, desktopEntry);
242  } else {
243  QPointer<KJob> jobGuard = job;
244 
245  QTimer *delayTimer = new QTimer();
246  delayTimer->setSingleShot(true);
247  connect(delayTimer, &QTimer::timeout, this, [this, job, jobGuard, desktopEntry] {
248  auto &view = d->jobViews[job];
249  if (view.delayTimer) {
250  view.delayTimer->deleteLater();
251  view.delayTimer = nullptr;
252  }
253 
254  if (jobGuard) {
255  d->requestView(job, desktopEntry);
256  }
257  });
258 
259  d->jobViews[job].delayTimer = delayTimer;
260  delayTimer->start(500);
261  }
262 
264 }
265 
267 {
269  finished(job);
270 }
271 
273 {
274  d->updateDestUrl(job);
275 
276  // send all pending updates before terminating to ensure state is correct
277  auto &view = d->jobViews[job];
278  d->sendUpdate(view);
279 
280  if (view.delayTimer) {
281  delete view.delayTimer;
282  d->jobViews.remove(job);
283  } else if (view.jobView) {
284  view.jobView->terminate(static_cast<uint>(job->error()),
285  job->error() ? job->errorText() : QString(),
286  QVariantMap() /*hints*/);
287  delete view.jobView;
288  d->jobViews.remove(job);
289  } else {
290  // Remember that the job finished in the meantime and
291  // terminate the JobView once it arrives
292  d->scheduleUpdate(job, QStringLiteral("terminated"), true);
293  if (job->error()) {
294  d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error()));
295  d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText());
296  }
297  }
298 }
299 
300 void KUiServerV2JobTracker::suspended(KJob *job)
301 {
302  d->scheduleUpdate(job, QStringLiteral("suspended"), true);
303 }
304 
305 void KUiServerV2JobTracker::resumed(KJob *job)
306 {
307  d->scheduleUpdate(job, QStringLiteral("suspended"), false);
308 }
309 
310 void KUiServerV2JobTracker::description(KJob *job, const QString &title,
311  const QPair<QString, QString> &field1,
312  const QPair<QString, QString> &field2)
313 {
314  d->scheduleUpdate(job, QStringLiteral("title"), title);
315 
316  d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), field1.first);
317  d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), field1.second);
318 
319  d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), field2.first);
320  d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), field2.second);
321 }
322 
323 void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &plain, const QString &rich)
324 {
325  Q_UNUSED(rich);
326  d->scheduleUpdate(job, QStringLiteral("infoMessage"), plain);
327 }
328 
329 void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount)
330 {
331  switch (unit) {
332  case KJob::Bytes:
333  d->scheduleUpdate(job, QStringLiteral("totalBytes"), amount);
334  break;
335  case KJob::Files:
336  d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount);
337  break;
338  case KJob::Directories:
339  d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount);
340  break;
341  case KJob::Items:
342  d->scheduleUpdate(job, QStringLiteral("totalItems"), amount);
343  break;
344  }
345 }
346 
347 void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount)
348 {
349  switch (unit) {
350  case KJob::Bytes:
351  d->scheduleUpdate(job, QStringLiteral("processedBytes"), amount);
352  break;
353  case KJob::Files:
354  d->scheduleUpdate(job, QStringLiteral("processedFiles"), amount);
355  break;
356  case KJob::Directories:
357  d->scheduleUpdate(job, QStringLiteral("processedDirectories"), amount);
358  break;
359  case KJob::Items:
360  d->scheduleUpdate(job, QStringLiteral("processedItems"), amount);
361  break;
362  }
363 }
364 
365 void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent)
366 {
367  d->scheduleUpdate(job, QStringLiteral("percent"), static_cast<uint>(percent));
368 }
369 
370 void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed)
371 {
372  d->scheduleUpdate(job, QStringLiteral("speed"), static_cast<qulonglong>(speed));
373 }
374 
375 KSharedUiServerV2Proxy::KSharedUiServerV2Proxy()
376  : m_uiserver(new org::kde::JobViewServerV2(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus()))
377  , m_watcher(new QDBusServiceWatcher(QStringLiteral("org.kde.JobViewServer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange))
378 {
379  connect(m_watcher.get(), &QDBusServiceWatcher::serviceOwnerChanged, this, &KSharedUiServerV2Proxy::uiserverOwnerChanged);
380 
381  // cleanup early enough to avoid issues with dbus at application exit
382  // see e.g. https://phabricator.kde.org/D2545
383  qAddPostRoutine([]() {
384  serverProxy->m_uiserver.reset();
385  serverProxy->m_watcher.reset();
386  });
387 }
388 
389 KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy()
390 {
391 
392 }
393 
394 org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver()
395 {
396  return m_uiserver.get();
397 }
398 
399 void KSharedUiServerV2Proxy::uiserverOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
400 {
401  Q_UNUSED(serviceName);
402  Q_UNUSED(oldOwner);
403 
404  if (!newOwner.isEmpty()) { // registered
405  Q_EMIT serverRegistered();
406  } else if (newOwner.isEmpty()) { // unregistered
407  Q_EMIT serverUnregistered();
408  }
409 }
410 
411 #include "moc_kuiserverv2jobtracker.cpp"
412 #include "moc_kuiserverv2jobtracker_p.cpp"
bool kill(KillVerbosity verbosity=Quietly)
void totalAmount(KJob *job, KJob::Unit unit, qulonglong amount)
void finished(QDBusPendingCallWatcher *self)
The interface to implement to track the progresses of a job.
QString message() const const
void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
QDBusConnection sessionBus()
QString & remove(int position, int n)
KUiServerV2JobTracker(QObject *parent=nullptr)
Creates a new KJobTrackerInterface.
QString desktopFileName()
void timeout()
bool suspend()
void processedAmount(KJob *job, KJob::Unit unit, qulonglong amount)
~KUiServerV2JobTracker() override
Destroys a KJobTrackerInterface.
QVariant property(const char *name) const const
KOPENINGHOURS_EXPORT QString currentState(const OpeningHours &oh)
void percent(KJob *job, unsigned long percent)
bool isEmpty() const const
void deleteLater()
QDBusError error() const const
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
void clear()
T1 value() const const
bool resume()
void finished(KJob *job) override
The following slots are inherited from KJobTrackerInterface.
void registerJob(KJob *job) override
Register a new job in this tracker.
if(recurs()&&!first)
void unregisterJob(KJob *job) override
Unregister a job from this tracker.
const QList< QKeySequence > & end()
bool isSuspended() const
bool toBool() const const
void start(int msec)
virtual void unregisterJob(KJob *job)
virtual void registerJob(KJob *job)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString toString() const const
Capabilities capabilities() const
Q_EMITQ_EMIT
QString errorText() const
bool isError() const const
int error() const
void setSingleShot(bool singleShot)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Tue May 11 2021 22:48:07 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.