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 }