File indexing completed on 2024-04-28 16:54:34

0001 /*
0002     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "jobsmodel_p.h"
0008 
0009 #include "debug.h"
0010 
0011 #include "job.h"
0012 #include "job_p.h"
0013 
0014 #include "utils_p.h"
0015 
0016 #include "jobviewserveradaptor.h"
0017 #include "jobviewserverv2adaptor.h"
0018 #include "kuiserveradaptor.h"
0019 
0020 #include <QDBusConnection>
0021 #include <QDBusConnectionInterface>
0022 #include <QDBusMessage>
0023 #include <QDBusServiceWatcher>
0024 
0025 #include <KJob>
0026 #include <KLocalizedString>
0027 #include <KService>
0028 
0029 #include <kio/global.h>
0030 
0031 #include <algorithm>
0032 #include <chrono>
0033 
0034 using namespace NotificationManager;
0035 using namespace std::literals::chrono_literals;
0036 
0037 JobsModelPrivate::JobsModelPrivate(QObject *parent)
0038     : QObject(parent)
0039     , m_serviceWatcher(new QDBusServiceWatcher(this))
0040     , m_compressUpdatesTimer(new QTimer(this))
0041 {
0042     m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
0043     m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
0044     connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &JobsModelPrivate::onServiceUnregistered);
0045 
0046     m_compressUpdatesTimer->setInterval(0);
0047     m_compressUpdatesTimer->setSingleShot(true);
0048     connect(m_compressUpdatesTimer, &QTimer::timeout, this, [this] {
0049         for (auto it = m_pendingDirtyRoles.constBegin(), end = m_pendingDirtyRoles.constEnd(); it != end; ++it) {
0050             Job *job = it.key();
0051             const QVector<int> roles = it.value();
0052             const int row = m_jobViews.indexOf(job);
0053             if (row == -1) {
0054                 continue;
0055             }
0056 
0057             Q_EMIT jobViewChanged(row, job, roles);
0058 
0059             // This is updated here and not the percentageChanged signal so we also get some batching out of it
0060             if (roles.contains(Notifications::PercentageRole)) {
0061                 updateApplicationPercentage(job->desktopEntry());
0062             }
0063         }
0064 
0065         m_pendingDirtyRoles.clear();
0066     });
0067 }
0068 
0069 JobsModelPrivate::~JobsModelPrivate()
0070 {
0071     QDBusConnection sessionBus = QDBusConnection::sessionBus();
0072     sessionBus.unregisterService(QStringLiteral("org.kde.JobViewServer"));
0073     sessionBus.unregisterService(QStringLiteral("org.kde.kuiserver"));
0074     sessionBus.unregisterObject(QStringLiteral("/JobViewServer"));
0075 
0076     // Remember which services we had running and clear their progress
0077     QStringList desktopEntries;
0078     for (Job *job : qAsConst(m_jobViews)) {
0079         if (!desktopEntries.contains(job->desktopEntry())) {
0080             desktopEntries.append(job->desktopEntry());
0081         }
0082     }
0083 
0084     qDeleteAll(m_jobViews);
0085     m_jobViews.clear();
0086     qDeleteAll(m_pendingJobViews);
0087     m_pendingJobViews.clear();
0088 
0089     m_pendingDirtyRoles.clear();
0090 
0091     for (const QString &desktopEntry : desktopEntries) {
0092         updateApplicationPercentage(desktopEntry);
0093     }
0094 }
0095 
0096 bool JobsModelPrivate::init()
0097 {
0098     if (m_valid) {
0099         return true;
0100     }
0101 
0102     new KuiserverAdaptor(this);
0103     new JobViewServerAdaptor(this);
0104     new JobViewServerV2Adaptor(this);
0105 
0106     QDBusConnection sessionBus = QDBusConnection::sessionBus();
0107 
0108     if (!sessionBus.registerObject(QStringLiteral("/JobViewServer"), this)) {
0109         qCWarning(NOTIFICATIONMANAGER) << "Failed to register JobViewServer DBus object";
0110         return false;
0111     }
0112 
0113     // Only the "dbus master" (effectively plasmashell) should be the true owner of job progress reporting
0114     const bool master = Utils::isDBusMaster();
0115     const auto queueOptions = master ? QDBusConnectionInterface::ReplaceExistingService : QDBusConnectionInterface::DontQueueService;
0116     const auto replacementOptions = master ? QDBusConnectionInterface::DontAllowReplacement : QDBusConnectionInterface::AllowReplacement;
0117 
0118     const QString jobViewServerService = QStringLiteral("org.kde.JobViewServer");
0119     const QString kuiserverService = QStringLiteral("org.kde.kuiserver");
0120 
0121     QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface();
0122 
0123     if (!master) {
0124         connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, [=](const QString &serviceName) {
0125             // Close all running jobs as we're defunct now
0126             if (serviceName == jobViewServerService || serviceName == kuiserverService) {
0127                 qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service";
0128 
0129                 const auto pendingJobs = m_pendingJobViews;
0130                 for (Job *job : pendingJobs) {
0131                     remove(job);
0132                 }
0133 
0134                 const auto jobs = m_jobViews;
0135                 for (Job *job : jobs) {
0136                     // We can keep the finished ones as they're non-interactive anyway
0137                     if (job->state() != Notifications::JobStateStopped) {
0138                         remove(job);
0139                     }
0140                 }
0141 
0142                 m_valid = false;
0143                 Q_EMIT serviceOwnershipLost();
0144             }
0145         });
0146     }
0147 
0148     auto registration = dbusIface->registerService(jobViewServerService, queueOptions, replacementOptions);
0149     if (registration.value() == QDBusConnectionInterface::ServiceRegistered) {
0150         qCDebug(NOTIFICATIONMANAGER) << "Registered JobViewServer service on DBus";
0151     } else {
0152         qCWarning(NOTIFICATIONMANAGER) << "Failed to register JobViewServer service on DBus, is kuiserver running?";
0153         return false;
0154     }
0155 
0156     registration = dbusIface->registerService(kuiserverService, queueOptions, replacementOptions);
0157     if (registration.value() != QDBusConnectionInterface::ServiceRegistered) {
0158         qCWarning(NOTIFICATIONMANAGER) << "Failed to register org.kde.kuiserver service on DBus, is kuiserver running?";
0159         return false;
0160     }
0161 
0162     m_valid = true;
0163     return true;
0164 }
0165 
0166 void JobsModelPrivate::registerService(const QString &service, const QString &objectPath)
0167 {
0168     qCWarning(NOTIFICATIONMANAGER) << "Request to register JobView service" << service << "on" << objectPath;
0169     qCWarning(NOTIFICATIONMANAGER) << "org.kde.kuiserver registerService is deprecated and defunct.";
0170     sendErrorReply(QDBusError::NotSupported, QStringLiteral("kuiserver proxying capabilities are deprecated and defunct."));
0171 }
0172 
0173 QStringList JobsModelPrivate::jobUrls() const
0174 {
0175     QStringList jobUrls;
0176     for (Job *job : m_jobViews) {
0177         if (job->state() != Notifications::JobStateStopped && job->destUrl().isValid()) {
0178             jobUrls.append(job->destUrl().toString());
0179         }
0180     }
0181     for (Job *job : m_pendingJobViews) {
0182         if (job->state() != Notifications::JobStateStopped && job->destUrl().isValid()) {
0183             jobUrls.append(job->destUrl().toString());
0184         }
0185     }
0186     return jobUrls;
0187 }
0188 
0189 void JobsModelPrivate::emitJobUrlsChanged()
0190 {
0191     Q_EMIT jobUrlsChanged(jobUrls());
0192 }
0193 
0194 bool JobsModelPrivate::requiresJobTracker() const
0195 {
0196     return false;
0197 }
0198 
0199 QStringList JobsModelPrivate::registeredJobContacts() const
0200 {
0201     return QStringList();
0202 }
0203 
0204 QDBusObjectPath JobsModelPrivate::requestView(const QString &appName, const QString &appIconName, int capabilities)
0205 {
0206     QString desktopEntry;
0207     QVariantMap hints;
0208 
0209     QString applicationName = appName;
0210     QString applicationIconName = appIconName;
0211 
0212     // JobViewServerV1 only sends application name, try to look it up as a service
0213     KService::Ptr service = KService::serviceByStorageId(applicationName);
0214     if (!service) {
0215         // HACK :)
0216         service = KService::serviceByStorageId(QLatin1String("org.kde.") + appName);
0217     }
0218 
0219     if (service) {
0220         desktopEntry = service->desktopEntryName();
0221         applicationName = service->name();
0222         applicationIconName = service->icon();
0223     }
0224 
0225     if (!applicationName.isEmpty()) {
0226         hints.insert(QStringLiteral("application-display-name"), applicationName);
0227     }
0228     if (!applicationIconName.isEmpty()) {
0229         hints.insert(QStringLiteral("application-icon-name"), applicationIconName);
0230     }
0231 
0232     return requestView(desktopEntry, capabilities, hints);
0233 }
0234 
0235 QDBusObjectPath JobsModelPrivate::requestView(const QString &desktopEntry, int capabilities, const QVariantMap &hints)
0236 {
0237     qCDebug(NOTIFICATIONMANAGER) << "JobView requested by" << desktopEntry;
0238 
0239     if (!m_highestJobId) {
0240         ++m_highestJobId;
0241     }
0242 
0243     Job *job = new Job(m_highestJobId);
0244     ++m_highestJobId;
0245 
0246     QString applicationName = hints.value(QStringLiteral("application-display-name")).toString();
0247     QString applicationIconName = hints.value(QStringLiteral("application-icon-name")).toString();
0248 
0249     job->setDesktopEntry(desktopEntry);
0250 
0251     KService::Ptr service = KService::serviceByDesktopName(desktopEntry);
0252     if (service) {
0253         if (applicationName.isEmpty()) {
0254             applicationName = service->name();
0255         }
0256         if (applicationIconName.isEmpty()) {
0257             applicationIconName = service->icon();
0258         }
0259     }
0260 
0261     job->setApplicationName(applicationName);
0262     job->setApplicationIconName(applicationIconName);
0263 
0264     // No application name? Try to figure out the process name using the sender's PID
0265     const QString serviceName = message().service();
0266     if (job->applicationName().isEmpty()) {
0267         qCInfo(NOTIFICATIONMANAGER) << "JobView request from" << serviceName << "didn't contain any identification information, this is an application bug!";
0268 
0269         QDBusReply<uint> pidReply = connection().interface()->servicePid(serviceName);
0270         if (pidReply.isValid()) {
0271             const auto pid = pidReply.value();
0272 
0273             const QString processName = Utils::processNameFromPid(pid);
0274             if (!processName.isEmpty()) {
0275                 qCDebug(NOTIFICATIONMANAGER) << "Resolved JobView request to be from" << processName;
0276                 job->setApplicationName(processName);
0277             }
0278         }
0279     }
0280 
0281     job->setSuspendable(capabilities & KJob::Suspendable);
0282     job->setKillable(capabilities & KJob::Killable);
0283 
0284     connect(job->d, &JobPrivate::showRequested, this, [this, job] {
0285         if (job->state() == Notifications::JobStateStopped) {
0286             // Stop finished or canceled in the meantime, remove
0287             qCDebug(NOTIFICATIONMANAGER) << "By the time we wanted to show JobView" << job->id() << "from" << job->applicationName()
0288                                          << ", it was already stopped";
0289             remove(job);
0290             return;
0291         }
0292 
0293         const int pendingRow = m_pendingJobViews.indexOf(job);
0294         Q_ASSERT(pendingRow > -1);
0295         m_pendingJobViews.removeAt(pendingRow);
0296 
0297         const int newRow = m_jobViews.count();
0298         Q_EMIT jobViewAboutToBeAdded(newRow, job);
0299         m_jobViews.append(job);
0300         Q_EMIT jobViewAdded(newRow, job);
0301         updateApplicationPercentage(job->desktopEntry());
0302     });
0303 
0304     m_pendingJobViews.append(job);
0305 
0306     if (hints.value(QStringLiteral("immediate")).toBool()) {
0307         // Slightly delay showing the job so that the first update() call with a
0308         // summary will be shown atomically to the user.
0309         job->d->delayedShow(50ms, JobPrivate::ShowCondition::OnTimeout | JobPrivate::ShowCondition::OnSummary | JobPrivate::ShowCondition::OnTermination);
0310     } else {
0311         // Delay showing a job view to avoid showing really short stat jobs and other useless stuff.
0312         job->d->delayedShow(500ms, JobPrivate::ShowCondition::OnTimeout);
0313     }
0314 
0315     if (hints.value(QStringLiteral("transient")).toBool()) {
0316         job->setTransient(true);
0317     }
0318 
0319     m_jobServices.insert(job, serviceName);
0320     m_serviceWatcher->addWatchedService(serviceName);
0321 
0322     // Apply initial properties
0323     job->d->update(hints);
0324 
0325     connect(job, &Job::updatedChanged, this, [this, job] {
0326         scheduleUpdate(job, Notifications::UpdatedRole);
0327     });
0328     connect(job, &Job::summaryChanged, this, [this, job] {
0329         scheduleUpdate(job, Notifications::SummaryRole);
0330     });
0331     connect(job, &Job::textChanged, this, [this, job] {
0332         scheduleUpdate(job, Notifications::BodyRole);
0333         scheduleUpdate(job, Qt::AccessibleDescriptionRole);
0334     });
0335     connect(job, &Job::stateChanged, this, [this, job] {
0336         scheduleUpdate(job, Notifications::JobStateRole);
0337         // Timeout and Closable depend on state, signal a change for those, too
0338         scheduleUpdate(job, Notifications::TimeoutRole);
0339         scheduleUpdate(job, Notifications::ClosableRole);
0340 
0341         if (job->state() == Notifications::JobStateStopped) {
0342             unwatchJob(job);
0343             updateApplicationPercentage(job->desktopEntry());
0344             emitJobUrlsChanged();
0345         }
0346     });
0347     connect(job, &Job::percentageChanged, this, [this, job] {
0348         scheduleUpdate(job, Notifications::PercentageRole);
0349     });
0350     connect(job, &Job::errorChanged, this, [this, job] {
0351         scheduleUpdate(job, Notifications::JobErrorRole);
0352     });
0353     connect(job, &Job::expiredChanged, this, [this, job] {
0354         scheduleUpdate(job, Notifications::ExpiredRole);
0355     });
0356     connect(job, &Job::dismissedChanged, this, [this, job] {
0357         scheduleUpdate(job, Notifications::DismissedRole);
0358     });
0359 
0360     connect(job, &Job::destUrlChanged, this, &JobsModelPrivate::emitJobUrlsChanged);
0361 
0362     connect(job->d, &JobPrivate::closed, this, [this, job] {
0363         remove(job);
0364     });
0365 
0366     if (!connection().interface()->isServiceRegistered(serviceName)) {
0367         qCWarning(NOTIFICATIONMANAGER) << "Service that requested the view wasn't registered anymore by the time the request was being processed";
0368         QMetaObject::invokeMethod(
0369             this,
0370             [this, serviceName] {
0371                 onServiceUnregistered(serviceName);
0372             },
0373             Qt::QueuedConnection);
0374     }
0375 
0376     return job->d->objectPath();
0377 }
0378 
0379 void JobsModelPrivate::remove(Job *job)
0380 {
0381     const int activeRow = m_jobViews.indexOf(job);
0382     const int pendingRow = m_pendingJobViews.indexOf(job);
0383 
0384     Job *jobToBeRemoved = nullptr;
0385 
0386     if (activeRow > -1) {
0387         Q_EMIT jobViewAboutToBeRemoved(activeRow);
0388         jobToBeRemoved = m_jobViews.takeAt(activeRow);
0389     } else if (pendingRow > -1) {
0390         jobToBeRemoved = m_pendingJobViews.takeAt(pendingRow);
0391     }
0392     Q_ASSERT(jobToBeRemoved);
0393 
0394     m_pendingDirtyRoles.remove(jobToBeRemoved);
0395 
0396     const QString desktopEntry = jobToBeRemoved->desktopEntry();
0397 
0398     unwatchJob(jobToBeRemoved);
0399 
0400     delete jobToBeRemoved;
0401     if (activeRow > -1) {
0402         Q_EMIT jobViewRemoved(activeRow);
0403     }
0404 
0405     updateApplicationPercentage(desktopEntry);
0406 }
0407 
0408 void JobsModelPrivate::removeAt(int row)
0409 {
0410     Q_ASSERT(row >= 0 && row < m_jobViews.count());
0411     remove(m_jobViews.at(row));
0412 }
0413 
0414 // This will forward overall application process via Unity API.
0415 // This way users of that like Task Manager and Latte Dock still get basic job information.
0416 void JobsModelPrivate::updateApplicationPercentage(const QString &desktopEntry)
0417 {
0418     if (desktopEntry.isEmpty()) {
0419         return;
0420     }
0421 
0422     int jobsPercentages = 0;
0423     int jobsCount = 0;
0424 
0425     for (int i = 0; i < m_jobViews.count(); ++i) {
0426         Job *job = m_jobViews.at(i);
0427         if (job->state() == Notifications::JobStateStopped || job->desktopEntry() != desktopEntry) {
0428             continue;
0429         }
0430 
0431         jobsPercentages += job->percentage();
0432         ++jobsCount;
0433     }
0434 
0435     int percentage = 0;
0436     if (jobsCount > 0) {
0437         percentage = jobsPercentages / jobsCount;
0438     }
0439 
0440     const QVariantMap properties = {{QStringLiteral("count-visible"), jobsCount > 0},
0441                                     {QStringLiteral("count"), jobsCount},
0442                                     {QStringLiteral("progress-visible"), jobsCount > 0},
0443                                     {QStringLiteral("progress"), percentage / 100.0},
0444                                     // so Task Manager knows this is a job progress and can ignore it if disabled in settings
0445                                     {QStringLiteral("proxied-for"), QStringLiteral("kuiserver")}};
0446 
0447     QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/org/kde/notificationmanager/jobs"),
0448                                                       QStringLiteral("com.canonical.Unity.LauncherEntry"),
0449                                                       QStringLiteral("Update"));
0450     message.setArguments({QStringLiteral("application://") + desktopEntry, properties});
0451     QDBusConnection::sessionBus().send(message);
0452 }
0453 
0454 void JobsModelPrivate::unwatchJob(Job *job)
0455 {
0456     const QString serviceName = m_jobServices.take(job);
0457     // Check if there's any jobs left for this service, otherwise stop watching it
0458     auto it = std::find_if(m_jobServices.constBegin(), m_jobServices.constEnd(), [&serviceName](const QString &item) {
0459         return item == serviceName;
0460     });
0461     if (it == m_jobServices.constEnd()) {
0462         m_serviceWatcher->removeWatchedService(serviceName);
0463     }
0464 }
0465 
0466 void JobsModelPrivate::onServiceUnregistered(const QString &serviceName)
0467 {
0468     qCDebug(NOTIFICATIONMANAGER) << "JobView service unregistered" << serviceName;
0469 
0470     const QList<Job *> jobs = m_jobServices.keys(serviceName);
0471     for (Job *job : jobs) {
0472         // Mark all non-finished jobs as failed
0473         if (job->state() == Notifications::JobStateStopped) {
0474             continue;
0475         }
0476 
0477         job->d->terminate(KIO::ERR_OWNER_DIED, i18n("Application closed unexpectedly."), {} /*hints*/);
0478     }
0479 
0480     Q_ASSERT(!m_serviceWatcher->watchedServices().contains(serviceName));
0481 }
0482 
0483 void JobsModelPrivate::scheduleUpdate(Job *job, int role)
0484 {
0485     m_pendingDirtyRoles[job].append(role);
0486     m_compressUpdatesTimer->start();
0487 }