File indexing completed on 2024-04-21 03:56:18
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 0178 void KUiServerV2JobTracker::registerJob(KJob *job) 0179 { 0180 if (d->jobViews.contains(job)) { 0181 return; 0182 } 0183 0184 QString desktopEntry = job->property("desktopFileName").toString(); 0185 if (desktopEntry.isEmpty()) { 0186 desktopEntry = QGuiApplication::desktopFileName(); 0187 } 0188 0189 if (desktopEntry.isEmpty()) { 0190 qCWarning(KJOBWIDGETS) << "Cannot register a job with KUiServerV2JobTracker without QGuiApplication::desktopFileName"; 0191 return; 0192 } 0193 0194 // Watch the server registering/unregistering and re-register the jobs as needed 0195 if (!d->serverRegisteredConnection) { 0196 d->serverRegisteredConnection = connect(serverProxy(), &KSharedUiServerV2Proxy::serverRegistered, this, [this]() { 0197 const auto staleViews = d->jobViews; 0198 0199 // Delete the old views, remove the old struct but keep the state, 0200 // register the job again (which checks for presence, hence removing first) 0201 // and then restore its previous state, which is safe because the DBus 0202 // is async and is only processed once event loop returns 0203 for (auto it = staleViews.begin(), end = staleViews.end(); it != end; ++it) { 0204 QPointer<KJob> jobGuard = it.key(); 0205 const JobView &view = it.value(); 0206 0207 const auto oldState = view.currentState; 0208 0209 // It is possible that the KJob has been deleted already so do not 0210 // use or deference if marked as terminated 0211 if (oldState.value(QStringLiteral("terminated")).toBool()) { 0212 const uint errorCode = oldState.value(QStringLiteral("errorCode")).toUInt(); 0213 const QString errorMessage = oldState.value(QStringLiteral("errorMessage")).toString(); 0214 0215 if (view.jobView) { 0216 view.jobView->terminate(errorCode, errorMessage, QVariantMap() /*hints*/); 0217 } 0218 0219 delete view.jobView; 0220 d->jobViews.remove(it.key()); 0221 } else { 0222 delete view.jobView; 0223 d->jobViews.remove(it.key()); // must happen before registerJob 0224 0225 if (jobGuard) { 0226 registerJob(jobGuard); 0227 0228 d->jobViews[jobGuard].currentState = oldState; 0229 } 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 &message) 0342 { 0343 d->scheduleUpdate(job, QStringLiteral("infoMessage"), message); 0344 } 0345 0346 void KUiServerV2JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount) 0347 { 0348 switch (unit) { 0349 case KJob::Bytes: 0350 d->scheduleUpdate(job, QStringLiteral("totalBytes"), amount); 0351 break; 0352 case KJob::Files: 0353 d->scheduleUpdate(job, QStringLiteral("totalFiles"), amount); 0354 break; 0355 case KJob::Directories: 0356 d->scheduleUpdate(job, QStringLiteral("totalDirectories"), amount); 0357 break; 0358 case KJob::Items: 0359 d->scheduleUpdate(job, QStringLiteral("totalItems"), amount); 0360 break; 0361 case KJob::UnitsCount: 0362 Q_UNREACHABLE(); 0363 break; 0364 } 0365 } 0366 0367 void KUiServerV2JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount) 0368 { 0369 switch (unit) { 0370 case KJob::Bytes: 0371 d->scheduleUpdate(job, QStringLiteral("processedBytes"), amount); 0372 break; 0373 case KJob::Files: 0374 d->scheduleUpdate(job, QStringLiteral("processedFiles"), amount); 0375 break; 0376 case KJob::Directories: 0377 d->scheduleUpdate(job, QStringLiteral("processedDirectories"), amount); 0378 break; 0379 case KJob::Items: 0380 d->scheduleUpdate(job, QStringLiteral("processedItems"), amount); 0381 break; 0382 case KJob::UnitsCount: 0383 Q_UNREACHABLE(); 0384 break; 0385 } 0386 } 0387 0388 void KUiServerV2JobTracker::percent(KJob *job, unsigned long percent) 0389 { 0390 d->scheduleUpdate(job, QStringLiteral("percent"), static_cast<uint>(percent)); 0391 } 0392 0393 void KUiServerV2JobTracker::speed(KJob *job, unsigned long speed) 0394 { 0395 d->scheduleUpdate(job, QStringLiteral("speed"), static_cast<qulonglong>(speed)); 0396 } 0397 0398 KSharedUiServerV2Proxy::KSharedUiServerV2Proxy() 0399 : m_uiserver(new org::kde::JobViewServerV2(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QDBusConnection::sessionBus())) 0400 , m_watcher(new QDBusServiceWatcher(QStringLiteral("org.kde.JobViewServer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange)) 0401 { 0402 connect(m_watcher.get(), &QDBusServiceWatcher::serviceOwnerChanged, this, &KSharedUiServerV2Proxy::uiserverOwnerChanged); 0403 0404 // cleanup early enough to avoid issues with dbus at application exit 0405 // see e.g. https://phabricator.kde.org/D2545 0406 qAddPostRoutine([]() { 0407 serverProxy->m_uiserver.reset(); 0408 serverProxy->m_watcher.reset(); 0409 }); 0410 } 0411 0412 KSharedUiServerV2Proxy::~KSharedUiServerV2Proxy() 0413 { 0414 0415 } 0416 0417 org::kde::JobViewServerV2 *KSharedUiServerV2Proxy::uiserver() 0418 { 0419 return m_uiserver.get(); 0420 } 0421 0422 void KSharedUiServerV2Proxy::uiserverOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) 0423 { 0424 Q_UNUSED(serviceName); 0425 Q_UNUSED(oldOwner); 0426 0427 if (!newOwner.isEmpty()) { // registered 0428 Q_EMIT serverRegistered(); 0429 } else if (newOwner.isEmpty()) { // unregistered 0430 Q_EMIT serverUnregistered(); 0431 } 0432 } 0433 0434 #include "moc_kuiserverv2jobtracker.cpp" 0435 #include "moc_kuiserverv2jobtracker_p.cpp"