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"