File indexing completed on 2023-11-26 04:03:34
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 KJob *job = 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(job); 0223 } else { 0224 delete view.jobView; 0225 d->jobViews.remove(job); // must happen before registerJob 0226 0227 registerJob(job); 0228 0229 d->jobViews[job].currentState = oldState; 0230 } 0231 } 0232 }); 0233 } 0234 0235 // Send along current job state 0236 if (job->isSuspended()) { 0237 suspended(job); 0238 } 0239 if (job->error()) { 0240 d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error())); 0241 d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText()); 0242 } 0243 for (int i = KJob::Bytes; i <= KJob::Items; ++i) { 0244 const auto unit = static_cast<KJob::Unit>(i); 0245 0246 if (job->processedAmount(unit) > 0) { 0247 processedAmount(job, unit, job->processedAmount(unit)); 0248 } 0249 if (job->totalAmount(unit) > 0) { 0250 totalAmount(job, unit, job->totalAmount(unit)); 0251 } 0252 } 0253 if (job->percent() > 0) { 0254 percent(job, job->percent()); 0255 } 0256 d->updateDestUrl(job); 0257 0258 if (job->property("immediateProgressReporting").toBool()) { 0259 d->requestView(job, desktopEntry); 0260 } else { 0261 QPointer<KJob> jobGuard = job; 0262 0263 QTimer *delayTimer = new QTimer(); 0264 delayTimer->setSingleShot(true); 0265 connect(delayTimer, &QTimer::timeout, this, [this, job, jobGuard, desktopEntry] { 0266 auto &view = d->jobViews[job]; 0267 if (view.delayTimer) { 0268 view.delayTimer->deleteLater(); 0269 view.delayTimer = nullptr; 0270 } 0271 0272 if (jobGuard) { 0273 d->requestView(job, desktopEntry); 0274 } 0275 }); 0276 0277 d->jobViews[job].delayTimer = delayTimer; 0278 delayTimer->start(500); 0279 } 0280 0281 KJobTrackerInterface::registerJob(job); 0282 } 0283 0284 void KUiServerV2JobTracker::unregisterJob(KJob *job) 0285 { 0286 KJobTrackerInterface::unregisterJob(job); 0287 finished(job); 0288 } 0289 0290 void KUiServerV2JobTracker::finished(KJob *job) 0291 { 0292 d->updateDestUrl(job); 0293 0294 // send all pending updates before terminating to ensure state is correct 0295 auto &view = d->jobViews[job]; 0296 d->sendUpdate(view); 0297 0298 if (view.delayTimer) { 0299 delete view.delayTimer; 0300 d->jobViews.remove(job); 0301 } else if (view.jobView) { 0302 view.jobView->terminate(static_cast<uint>(job->error()), 0303 job->error() ? job->errorText() : QString(), 0304 QVariantMap() /*hints*/); 0305 delete view.jobView; 0306 d->jobViews.remove(job); 0307 } else { 0308 // Remember that the job finished in the meantime and 0309 // terminate the JobView once it arrives 0310 d->scheduleUpdate(job, QStringLiteral("terminated"), true); 0311 if (job->error()) { 0312 d->scheduleUpdate(job, QStringLiteral("errorCode"), static_cast<uint>(job->error())); 0313 d->scheduleUpdate(job, QStringLiteral("errorMessage"), job->errorText()); 0314 } 0315 } 0316 } 0317 0318 void KUiServerV2JobTracker::suspended(KJob *job) 0319 { 0320 d->scheduleUpdate(job, QStringLiteral("suspended"), true); 0321 } 0322 0323 void KUiServerV2JobTracker::resumed(KJob *job) 0324 { 0325 d->scheduleUpdate(job, QStringLiteral("suspended"), false); 0326 } 0327 0328 void KUiServerV2JobTracker::description(KJob *job, const QString &title, 0329 const QPair<QString, QString> &field1, 0330 const QPair<QString, QString> &field2) 0331 { 0332 d->scheduleUpdate(job, QStringLiteral("title"), title); 0333 0334 d->scheduleUpdate(job, QStringLiteral("descriptionLabel1"), field1.first); 0335 d->scheduleUpdate(job, QStringLiteral("descriptionValue1"), field1.second); 0336 0337 d->scheduleUpdate(job, QStringLiteral("descriptionLabel2"), field2.first); 0338 d->scheduleUpdate(job, QStringLiteral("descriptionValue2"), field2.second); 0339 } 0340 0341 void KUiServerV2JobTracker::infoMessage(KJob *job, const QString &plain, const QString &rich) 0342 { 0343 Q_UNUSED(rich); 0344 d->scheduleUpdate(job, QStringLiteral("infoMessage"), plain); 0345 } 0346 0347 void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount) 0348 { 0349 switch (unit) { 0350 case KJob::Bytes: 0351 d->scheduleUpdate(job, QStringLiteral("totalBytes"), amount); 0352 break; 0353 case KJob::Files: 0354 d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount); 0355 break; 0356 case KJob::Directories: 0357 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount); 0358 break; 0359 case KJob::Items: 0360 d->scheduleUpdate(job, QStringLiteral("totalItems"), amount); 0361 break; 0362 case KJob::UnitsCount: 0363 Q_UNREACHABLE(); 0364 break; 0365 } 0366 } 0367 0368 void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount) 0369 { 0370 switch (unit) { 0371 case KJob::Bytes: 0372 d->scheduleUpdate(job, QStringLiteral("processedBytes"), amount); 0373 break; 0374 case KJob::Files: 0375 d->scheduleUpdate(job, QStringLiteral("processedFiles"), amount); 0376 break; 0377 case KJob::Directories: 0378 d->scheduleUpdate(job, QStringLiteral("processedDirectories"), amount); 0379 break; 0380 case KJob::Items: 0381 d->scheduleUpdate(job, QStringLiteral("processedItems"), amount); 0382 break; 0383 case KJob::UnitsCount: 0384 Q_UNREACHABLE(); 0385 break; 0386 } 0387 } 0388 0389 void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent) 0390 { 0391 d->scheduleUpdate(job, QStringLiteral("percent"), static_cast<uint>(percent)); 0392 } 0393 0394 void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed) 0395 { 0396 d->scheduleUpdate(job, QStringLiteral("speed"), static_cast<qulonglong>(speed)); 0397 } 0398 0399 KSharedUiServerV2Proxy::KSharedUiServerV2Proxy() 0400 : m_uiserver(new org::kde::JobViewServerV2(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus())) 0401 , m_watcher(new QDBusServiceWatcher(QStringLiteral("org.kde.JobViewServer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange)) 0402 { 0403 connect(m_watcher.get(), &QDBusServiceWatcher::serviceOwnerChanged, this, &KSharedUiServerV2Proxy::uiserverOwnerChanged); 0404 0405 // cleanup early enough to avoid issues with dbus at application exit 0406 // see e.g. https://phabricator.kde.org/D2545 0407 qAddPostRoutine([]() { 0408 serverProxy->m_uiserver.reset(); 0409 serverProxy->m_watcher.reset(); 0410 }); 0411 } 0412 0413 KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy() 0414 { 0415 0416 } 0417 0418 org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver() 0419 { 0420 return m_uiserver.get(); 0421 } 0422 0423 void KSharedUiServerV2Proxy::uiserverOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) 0424 { 0425 Q_UNUSED(serviceName); 0426 Q_UNUSED(oldOwner); 0427 0428 if (!newOwner.isEmpty()) { // registered 0429 Q_EMIT serverRegistered(); 0430 } else if (newOwner.isEmpty()) { // unregistered 0431 Q_EMIT serverUnregistered(); 0432 } 0433 } 0434 0435 #include "moc_kuiserverv2jobtracker.cpp" 0436 #include "moc_kuiserverv2jobtracker_p.cpp"