File indexing completed on 2024-09-15 12:01:08
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kuiserverv2jobtracker.h" 0009 #include "kuiserverv2jobtracker_p.h" 0010 0011 #include "jobviewv3iface.h" 0012 #include "debug.h" 0013 0014 #include <KJob> 0015 0016 #include <QtGlobal> 0017 #include <QDBusConnection> 0018 #include <QDBusPendingCallWatcher> 0019 #include <QDBusPendingReply> 0020 #include <QGuiApplication> 0021 #include <QTimer> 0022 #include <QHash> 0023 #include <QVariantMap> 0024 0025 Q_GLOBAL_STATIC(KSharedUiServerV2Proxy, serverProxy) 0026 0027 struct JobView 0028 { 0029 QTimer *delayTimer = nullptr; 0030 org::kde::JobViewV3 *jobView = nullptr; 0031 QVariantMap currentState; 0032 QVariantMap pendingUpdates; 0033 }; 0034 0035 class KUiServerV2JobTrackerPrivate 0036 { 0037 public: 0038 KUiServerV2JobTrackerPrivate(KUiServerV2JobTracker *parent) 0039 : q(parent) 0040 { 0041 updateTimer.setInterval(0); 0042 updateTimer.setSingleShot(true); 0043 QObject::connect(&updateTimer, &QTimer::timeout, q, [this] { 0044 sendAllUpdates(); 0045 }); 0046 } 0047 0048 KUiServerV2JobTracker *const q; 0049 0050 void sendAllUpdates(); 0051 void sendUpdate(JobView &view); 0052 void scheduleUpdate(KJob *job, const QString &key, const QVariant &value); 0053 0054 void updateDestUrl(KJob *job); 0055 0056 void requestView(KJob *job, const QString &desktopEntry); 0057 0058 QHash<KJob *, JobView> jobViews; 0059 QTimer updateTimer; 0060 0061 QMetaObject::Connection serverRegisteredConnection; 0062 }; 0063 0064 void KUiServerV2JobTrackerPrivate::scheduleUpdate(KJob *job, const QString &key, const QVariant &value) 0065 { 0066 auto &view = jobViews[job]; 0067 view.currentState[key] = value; 0068 view.pendingUpdates[key] = value; 0069 0070 if (!updateTimer.isActive()) { 0071 updateTimer.start(); 0072 } 0073 } 0074 0075 void KUiServerV2JobTrackerPrivate::sendAllUpdates() 0076 { 0077 for (auto it = jobViews.begin(), end = jobViews.end(); it != end; ++it) { 0078 sendUpdate(it.value()); 0079 } 0080 } 0081 0082 void KUiServerV2JobTrackerPrivate::sendUpdate(JobView &view) 0083 { 0084 if (!view.jobView) { 0085 return; 0086 } 0087 0088 const QVariantMap updates = view.pendingUpdates; 0089 if (updates.isEmpty()) { 0090 return; 0091 } 0092 0093 view.jobView->update(updates); 0094 view.pendingUpdates.clear(); 0095 } 0096 0097 void KUiServerV2JobTrackerPrivate::updateDestUrl(KJob *job) 0098 { 0099 scheduleUpdate(job, QStringLiteral("destUrl"), job->property("destUrl").toString()); 0100 } 0101 0102 void KUiServerV2JobTrackerPrivate::requestView(KJob *job, const QString &desktopEntry) 0103 { 0104 QPointer<KJob> jobGuard = job; 0105 auto &view = jobViews[job]; 0106 0107 QVariantMap hints = view.currentState; 0108 // Tells Plasma to show the job view right away, since the delay is always handled on our side 0109 hints.insert(QStringLiteral("immediate"), true); 0110 // Must not clear currentState as only Plasma 5.22+ will use properties from "hints", 0111 // there must still be a full update() call for earlier versions! 0112 0113 if (job->isFinishedNotificationHidden()) { 0114 hints.insert(QStringLiteral("transient"), true); 0115 } 0116 0117 auto reply = serverProxy()->uiserver()->requestView(desktopEntry, job->capabilities(), hints); 0118 0119 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, q); 0120 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher, jobGuard, job] { 0121 QDBusPendingReply<QDBusObjectPath> reply = *watcher; 0122 watcher->deleteLater(); 0123 0124 if (reply.isError()) { 0125 qCWarning(KJOBWIDGETS) << "Failed to register job with KUiServerV2JobTracker" << reply.error().message(); 0126 jobViews.remove(job); 0127 return; 0128 } 0129 0130 const QString viewObjectPath = reply.value().path(); 0131 auto *jobView = new org::kde::JobViewV3(QStringLiteral("org.kde.JobViewServer"), viewObjectPath, QDBusConnection::sessionBus()); 0132 0133 auto &view = jobViews[job]; 0134 0135 if (jobGuard) { 0136 QObject::connect(jobView, &org::kde::JobViewV3::cancelRequested, job, [job] { 0137 job->kill(KJob::EmitResult); 0138 }); 0139 QObject::connect(jobView, &org::kde::JobViewV3::suspendRequested, job, &KJob::suspend); 0140 QObject::connect(jobView, &org::kde::JobViewV3::resumeRequested, job, &KJob::resume); 0141 0142 view.jobView = jobView; 0143 } 0144 0145 // Now send the full current job state over 0146 jobView->update(view.currentState); 0147 // which also contains all pending updates 0148 view.pendingUpdates.clear(); 0149 0150 // Job was deleted or finished in the meantime 0151 if (!jobGuard || view.currentState.value(QStringLiteral("terminated")).toBool()) { 0152 const uint errorCode = view.currentState.value(QStringLiteral("errorCode")).toUInt(); 0153 const QString errorMessage = view.currentState.value(QStringLiteral("errorMessage")).toString(); 0154 0155 jobView->terminate(errorCode, errorMessage, QVariantMap() /*hints*/); 0156 delete jobView; 0157 0158 jobViews.remove(job); 0159 } 0160 }); 0161 } 0162 0163 KUiServerV2JobTracker::KUiServerV2JobTracker(QObject *parent) 0164 : KJobTrackerInterface(parent) 0165 , d(new KUiServerV2JobTrackerPrivate(this)) 0166 { 0167 qDBusRegisterMetaType<qulonglong>(); 0168 } 0169 0170 KUiServerV2JobTracker::~KUiServerV2JobTracker() 0171 { 0172 if (!d->jobViews.isEmpty()) { 0173 qCWarning(KJOBWIDGETS) << "A KUiServerV2JobTracker instance contains" 0174 << d->jobViews.size() << "stalled jobs"; 0175 } 0176 0177 delete d; 0178 } 0179 0180 void KUiServerV2JobTracker::registerJob(KJob *job) 0181 { 0182 if (d->jobViews.contains(job)) { 0183 return; 0184 } 0185 0186 QString desktopEntry = job->property("desktopFileName").toString(); 0187 if (desktopEntry.isEmpty()) { 0188 desktopEntry = QGuiApplication::desktopFileName(); 0189 } 0190 0191 if (desktopEntry.isEmpty()) { 0192 qCWarning(KJOBWIDGETS) << "Cannot register a job with KUiServerV2JobTracker without QGuiApplication::desktopFileName"; 0193 return; 0194 } 0195 0196 // Watch the server registering/unregistering and re-register the jobs as needed 0197 if (!d->serverRegisteredConnection) { 0198 d->serverRegisteredConnection = connect(serverProxy(), &KSharedUiServerV2Proxy::serverRegistered, this, [this]() { 0199 const auto staleViews = d->jobViews; 0200 0201 // Delete the old views, remove the old struct but keep the state, 0202 // register the job again (which checks for presence, hence removing first) 0203 // and then restore its previous state, which is safe because the DBus 0204 // is async and is only processed once event loop returns 0205 for (auto it = staleViews.begin(), end = staleViews.end(); it != end; ++it) { 0206 QPointer<KJob> jobGuard = it.key(); 0207 const JobView &view = it.value(); 0208 0209 const auto oldState = view.currentState; 0210 0211 // It is possible that the KJob has been deleted already so do not 0212 // use or deference if marked as terminated 0213 if (oldState.value(QStringLiteral("terminated")).toBool()) { 0214 const uint errorCode = oldState.value(QStringLiteral("errorCode")).toUInt(); 0215 const QString errorMessage = oldState.value(QStringLiteral("errorMessage")).toString(); 0216 0217 if (view.jobView) { 0218 view.jobView->terminate(errorCode, errorMessage, QVariantMap() /*hints*/); 0219 } 0220 0221 delete view.jobView; 0222 d->jobViews.remove(it.key()); 0223 } else { 0224 delete view.jobView; 0225 d->jobViews.remove(it.key()); // must happen before registerJob 0226 0227 if (jobGuard) { 0228 registerJob(jobGuard); 0229 0230 d->jobViews[jobGuard].currentState = oldState; 0231 } 0232 } 0233 } 0234 }); 0235 } 0236 0237 // Send along current job state 0238 if (job->isSuspended()) { 0239 suspended(job); 0240 } 0241 if (job->error()) { 0242 d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error())); 0243 d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText()); 0244 } 0245 for (int i = KJob::Bytes; i <= KJob::Items; ++i) { 0246 const auto unit = static_cast<KJob::Unit>(i); 0247 0248 if (job->processedAmount(unit) > 0) { 0249 processedAmount(job, unit, job->processedAmount(unit)); 0250 } 0251 if (job->totalAmount(unit) > 0) { 0252 totalAmount(job, unit, job->totalAmount(unit)); 0253 } 0254 } 0255 if (job->percent() > 0) { 0256 percent(job, job->percent()); 0257 } 0258 d->updateDestUrl(job); 0259 0260 if (job->property("immediateProgressReporting").toBool()) { 0261 d->requestView(job, desktopEntry); 0262 } else { 0263 QPointer<KJob> jobGuard = job; 0264 0265 QTimer *delayTimer = new QTimer(); 0266 delayTimer->setSingleShot(true); 0267 connect(delayTimer, &QTimer::timeout, this, [this, job, jobGuard, desktopEntry] { 0268 auto &view = d->jobViews[job]; 0269 if (view.delayTimer) { 0270 view.delayTimer->deleteLater(); 0271 view.delayTimer = nullptr; 0272 } 0273 0274 if (jobGuard) { 0275 d->requestView(job, desktopEntry); 0276 } 0277 }); 0278 0279 d->jobViews[job].delayTimer = delayTimer; 0280 delayTimer->start(500); 0281 } 0282 0283 KJobTrackerInterface::registerJob(job); 0284 } 0285 0286 void KUiServerV2JobTracker::unregisterJob(KJob *job) 0287 { 0288 KJobTrackerInterface::unregisterJob(job); 0289 finished(job); 0290 } 0291 0292 void KUiServerV2JobTracker::finished(KJob *job) 0293 { 0294 d->updateDestUrl(job); 0295 0296 // send all pending updates before terminating to ensure state is correct 0297 auto &view = d->jobViews[job]; 0298 d->sendUpdate(view); 0299 0300 if (view.delayTimer) { 0301 delete view.delayTimer; 0302 d->jobViews.remove(job); 0303 } else if (view.jobView) { 0304 view.jobView->terminate(static_cast<uint>(job->error()), 0305 job->error() ? job->errorText() : QString(), 0306 QVariantMap() /*hints*/); 0307 delete view.jobView; 0308 d->jobViews.remove(job); 0309 } else { 0310 // Remember that the job finished in the meantime and 0311 // terminate the JobView once it arrives 0312 d->scheduleUpdate(job, QStringLiteral("terminated"), true); 0313 if (job->error()) { 0314 d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error())); 0315 d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText()); 0316 } 0317 } 0318 } 0319 0320 void KUiServerV2JobTracker::suspended(KJob *job) 0321 { 0322 d->scheduleUpdate(job, QStringLiteral("suspended"), true); 0323 } 0324 0325 void KUiServerV2JobTracker::resumed(KJob *job) 0326 { 0327 d->scheduleUpdate(job, QStringLiteral("suspended"), false); 0328 } 0329 0330 void KUiServerV2JobTracker::description(KJob *job, const QString &title, 0331 const QPair<QString, QString> &field1, 0332 const QPair<QString, QString> &field2) 0333 { 0334 d->scheduleUpdate(job, QStringLiteral("title"), title); 0335 0336 d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), field1.first); 0337 d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), field1.second); 0338 0339 d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), field2.first); 0340 d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), field2.second); 0341 } 0342 0343 void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &plain, const QString &rich) 0344 { 0345 Q_UNUSED(rich); 0346 d->scheduleUpdate(job, QStringLiteral("infoMessage"), plain); 0347 } 0348 0349 void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount) 0350 { 0351 switch (unit) { 0352 case KJob::Bytes: 0353 d->scheduleUpdate(job, QStringLiteral("totalBytes"), amount); 0354 break; 0355 case KJob::Files: 0356 d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount); 0357 break; 0358 case KJob::Directories: 0359 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount); 0360 break; 0361 case KJob::Items: 0362 d->scheduleUpdate(job, QStringLiteral("totalItems"), amount); 0363 break; 0364 case KJob::UnitsCount: 0365 Q_UNREACHABLE(); 0366 break; 0367 } 0368 } 0369 0370 void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount) 0371 { 0372 switch (unit) { 0373 case KJob::Bytes: 0374 d->scheduleUpdate(job, QStringLiteral("processedBytes"), amount); 0375 break; 0376 case KJob::Files: 0377 d->scheduleUpdate(job, QStringLiteral("processedFiles"), amount); 0378 break; 0379 case KJob::Directories: 0380 d->scheduleUpdate(job, QStringLiteral("processedDirectories"), amount); 0381 break; 0382 case KJob::Items: 0383 d->scheduleUpdate(job, QStringLiteral("processedItems"), amount); 0384 break; 0385 case KJob::UnitsCount: 0386 Q_UNREACHABLE(); 0387 break; 0388 } 0389 } 0390 0391 void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent) 0392 { 0393 d->scheduleUpdate(job, QStringLiteral("percent"), static_cast<uint>(percent)); 0394 } 0395 0396 void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed) 0397 { 0398 d->scheduleUpdate(job, QStringLiteral("speed"), static_cast<qulonglong>(speed)); 0399 } 0400 0401 KSharedUiServerV2Proxy::KSharedUiServerV2Proxy() 0402 : m_uiserver(new org::kde::JobViewServerV2(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus())) 0403 , m_watcher(new QDBusServiceWatcher(QStringLiteral("org.kde.JobViewServer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange)) 0404 { 0405 connect(m_watcher.get(), &QDBusServiceWatcher::serviceOwnerChanged, this, &KSharedUiServerV2Proxy::uiserverOwnerChanged); 0406 0407 // cleanup early enough to avoid issues with dbus at application exit 0408 // see e.g. https://phabricator.kde.org/D2545 0409 qAddPostRoutine([]() { 0410 serverProxy->m_uiserver.reset(); 0411 serverProxy->m_watcher.reset(); 0412 }); 0413 } 0414 0415 KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy() 0416 { 0417 0418 } 0419 0420 org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver() 0421 { 0422 return m_uiserver.get(); 0423 } 0424 0425 void KSharedUiServerV2Proxy::uiserverOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) 0426 { 0427 Q_UNUSED(serviceName); 0428 Q_UNUSED(oldOwner); 0429 0430 if (!newOwner.isEmpty()) { // registered 0431 Q_EMIT serverRegistered(); 0432 } else if (newOwner.isEmpty()) { // unregistered 0433 Q_EMIT serverUnregistered(); 0434 } 0435 } 0436 0437 #include "moc_kuiserverv2jobtracker.cpp" 0438 #include "moc_kuiserverv2jobtracker_p.cpp"