File indexing completed on 2024-02-18 15:02:51

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"