mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
486 lines
17 KiB
486 lines
17 KiB
/* |
|
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <[email protected]> |
|
|
|
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
|
*/ |
|
|
|
#include "jobsmodel_p.h" |
|
|
|
#include "debug.h" |
|
|
|
#include "job.h" |
|
#include "job_p.h" |
|
|
|
#include "utils_p.h" |
|
|
|
#include "jobviewserveradaptor.h" |
|
#include "jobviewserverv2adaptor.h" |
|
#include "kuiserveradaptor.h" |
|
|
|
#include <QDBusConnection> |
|
#include <QDBusConnectionInterface> |
|
#include <QDBusMessage> |
|
#include <QDBusServiceWatcher> |
|
|
|
#include <KJob> |
|
#include <KLocalizedString> |
|
#include <KService> |
|
|
|
#include <kio/global.h> |
|
|
|
#include <algorithm> |
|
#include <chrono> |
|
|
|
using namespace NotificationManager; |
|
using namespace std::literals::chrono_literals; |
|
|
|
JobsModelPrivate::JobsModelPrivate(QObject *parent) |
|
: QObject(parent) |
|
, m_serviceWatcher(new QDBusServiceWatcher(this)) |
|
, m_compressUpdatesTimer(new QTimer(this)) |
|
{ |
|
m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); |
|
m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); |
|
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &JobsModelPrivate::onServiceUnregistered); |
|
|
|
m_compressUpdatesTimer->setInterval(0); |
|
m_compressUpdatesTimer->setSingleShot(true); |
|
connect(m_compressUpdatesTimer, &QTimer::timeout, this, [this] { |
|
for (auto it = m_pendingDirtyRoles.constBegin(), end = m_pendingDirtyRoles.constEnd(); it != end; ++it) { |
|
Job *job = it.key(); |
|
const QVector<int> roles = it.value(); |
|
const int row = m_jobViews.indexOf(job); |
|
if (row == -1) { |
|
continue; |
|
} |
|
|
|
Q_EMIT jobViewChanged(row, job, roles); |
|
|
|
// This is updated here and not the percentageChanged signal so we also get some batching out of it |
|
if (roles.contains(Notifications::PercentageRole)) { |
|
updateApplicationPercentage(job->desktopEntry()); |
|
} |
|
} |
|
|
|
m_pendingDirtyRoles.clear(); |
|
}); |
|
} |
|
|
|
JobsModelPrivate::~JobsModelPrivate() |
|
{ |
|
QDBusConnection sessionBus = QDBusConnection::sessionBus(); |
|
sessionBus.unregisterService(QStringLiteral("org.kde.JobViewServer")); |
|
sessionBus.unregisterService(QStringLiteral("org.kde.kuiserver")); |
|
sessionBus.unregisterObject(QStringLiteral("/JobViewServer")); |
|
|
|
// Remember which services we had running and clear their progress |
|
QStringList desktopEntries; |
|
for (Job *job : qAsConst(m_jobViews)) { |
|
if (!desktopEntries.contains(job->desktopEntry())) { |
|
desktopEntries.append(job->desktopEntry()); |
|
} |
|
} |
|
|
|
qDeleteAll(m_jobViews); |
|
m_jobViews.clear(); |
|
qDeleteAll(m_pendingJobViews); |
|
m_pendingJobViews.clear(); |
|
|
|
m_pendingDirtyRoles.clear(); |
|
|
|
for (const QString &desktopEntry : desktopEntries) { |
|
updateApplicationPercentage(desktopEntry); |
|
} |
|
} |
|
|
|
bool JobsModelPrivate::init() |
|
{ |
|
if (m_valid) { |
|
return true; |
|
} |
|
|
|
new KuiserverAdaptor(this); |
|
new JobViewServerAdaptor(this); |
|
new JobViewServerV2Adaptor(this); |
|
|
|
QDBusConnection sessionBus = QDBusConnection::sessionBus(); |
|
|
|
if (!sessionBus.registerObject(QStringLiteral("/JobViewServer"), this)) { |
|
qCWarning(NOTIFICATIONMANAGER) << "Failed to register JobViewServer DBus object"; |
|
return false; |
|
} |
|
|
|
// Only the "dbus master" (effectively plasmashell) should be the true owner of job progress reporting |
|
const bool master = Utils::isDBusMaster(); |
|
const auto queueOptions = master ? QDBusConnectionInterface::ReplaceExistingService : QDBusConnectionInterface::DontQueueService; |
|
const auto replacementOptions = master ? QDBusConnectionInterface::DontAllowReplacement : QDBusConnectionInterface::AllowReplacement; |
|
|
|
const QString jobViewServerService = QStringLiteral("org.kde.JobViewServer"); |
|
const QString kuiserverService = QStringLiteral("org.kde.kuiserver"); |
|
|
|
QDBusConnectionInterface *dbusIface = QDBusConnection::sessionBus().interface(); |
|
|
|
if (!master) { |
|
connect(dbusIface, &QDBusConnectionInterface::serviceUnregistered, this, [=](const QString &serviceName) { |
|
// Close all running jobs as we're defunct now |
|
if (serviceName == jobViewServerService || serviceName == kuiserverService) { |
|
qCDebug(NOTIFICATIONMANAGER) << "Lost ownership of" << serviceName << "service"; |
|
|
|
const auto pendingJobs = m_pendingJobViews; |
|
for (Job *job : pendingJobs) { |
|
remove(job); |
|
} |
|
|
|
const auto jobs = m_jobViews; |
|
for (Job *job : jobs) { |
|
// We can keep the finished ones as they're non-interactive anyway |
|
if (job->state() != Notifications::JobStateStopped) { |
|
remove(job); |
|
} |
|
} |
|
|
|
m_valid = false; |
|
Q_EMIT serviceOwnershipLost(); |
|
} |
|
}); |
|
} |
|
|
|
auto registration = dbusIface->registerService(jobViewServerService, queueOptions, replacementOptions); |
|
if (registration.value() == QDBusConnectionInterface::ServiceRegistered) { |
|
qCDebug(NOTIFICATIONMANAGER) << "Registered JobViewServer service on DBus"; |
|
} else { |
|
qCWarning(NOTIFICATIONMANAGER) << "Failed to register JobViewServer service on DBus, is kuiserver running?"; |
|
return false; |
|
} |
|
|
|
registration = dbusIface->registerService(kuiserverService, queueOptions, replacementOptions); |
|
if (registration.value() != QDBusConnectionInterface::ServiceRegistered) { |
|
qCWarning(NOTIFICATIONMANAGER) << "Failed to register org.kde.kuiserver service on DBus, is kuiserver running?"; |
|
return false; |
|
} |
|
|
|
m_valid = true; |
|
return true; |
|
} |
|
|
|
void JobsModelPrivate::registerService(const QString &service, const QString &objectPath) |
|
{ |
|
qCWarning(NOTIFICATIONMANAGER) << "Request to register JobView service" << service << "on" << objectPath; |
|
qCWarning(NOTIFICATIONMANAGER) << "org.kde.kuiserver registerService is deprecated and defunct."; |
|
sendErrorReply(QDBusError::NotSupported, QStringLiteral("kuiserver proxying capabilities are deprecated and defunct.")); |
|
} |
|
|
|
QStringList JobsModelPrivate::jobUrls() const |
|
{ |
|
QStringList jobUrls; |
|
for (Job *job : m_jobViews) { |
|
if (job->state() != Notifications::JobStateStopped && job->destUrl().isValid()) { |
|
jobUrls.append(job->destUrl().toString()); |
|
} |
|
} |
|
for (Job *job : m_pendingJobViews) { |
|
if (job->state() != Notifications::JobStateStopped && job->destUrl().isValid()) { |
|
jobUrls.append(job->destUrl().toString()); |
|
} |
|
} |
|
return jobUrls; |
|
} |
|
|
|
void JobsModelPrivate::emitJobUrlsChanged() |
|
{ |
|
Q_EMIT jobUrlsChanged(jobUrls()); |
|
} |
|
|
|
bool JobsModelPrivate::requiresJobTracker() const |
|
{ |
|
return false; |
|
} |
|
|
|
QStringList JobsModelPrivate::registeredJobContacts() const |
|
{ |
|
return QStringList(); |
|
} |
|
|
|
QDBusObjectPath JobsModelPrivate::requestView(const QString &appName, const QString &appIconName, int capabilities) |
|
{ |
|
QString desktopEntry; |
|
QVariantMap hints; |
|
|
|
QString applicationName = appName; |
|
QString applicationIconName = appIconName; |
|
|
|
// JobViewServerV1 only sends application name, try to look it up as a service |
|
KService::Ptr service = KService::serviceByStorageId(applicationName); |
|
if (!service) { |
|
// HACK :) |
|
service = KService::serviceByStorageId(QLatin1String("org.kde.") + appName); |
|
} |
|
|
|
if (service) { |
|
desktopEntry = service->desktopEntryName(); |
|
applicationName = service->name(); |
|
applicationIconName = service->icon(); |
|
} |
|
|
|
if (!applicationName.isEmpty()) { |
|
hints.insert(QStringLiteral("application-display-name"), applicationName); |
|
} |
|
if (!applicationIconName.isEmpty()) { |
|
hints.insert(QStringLiteral("application-icon-name"), applicationIconName); |
|
} |
|
|
|
return requestView(desktopEntry, capabilities, hints); |
|
} |
|
|
|
QDBusObjectPath JobsModelPrivate::requestView(const QString &desktopEntry, int capabilities, const QVariantMap &hints) |
|
{ |
|
qCDebug(NOTIFICATIONMANAGER) << "JobView requested by" << desktopEntry; |
|
|
|
if (!m_highestJobId) { |
|
++m_highestJobId; |
|
} |
|
|
|
Job *job = new Job(m_highestJobId); |
|
++m_highestJobId; |
|
|
|
QString applicationName = hints.value(QStringLiteral("application-display-name")).toString(); |
|
QString applicationIconName = hints.value(QStringLiteral("application-icon-name")).toString(); |
|
|
|
job->setDesktopEntry(desktopEntry); |
|
|
|
KService::Ptr service = KService::serviceByDesktopName(desktopEntry); |
|
if (service) { |
|
if (applicationName.isEmpty()) { |
|
applicationName = service->name(); |
|
} |
|
if (applicationIconName.isEmpty()) { |
|
applicationIconName = service->icon(); |
|
} |
|
} |
|
|
|
job->setApplicationName(applicationName); |
|
job->setApplicationIconName(applicationIconName); |
|
|
|
// No application name? Try to figure out the process name using the sender's PID |
|
const QString serviceName = message().service(); |
|
if (job->applicationName().isEmpty()) { |
|
qCInfo(NOTIFICATIONMANAGER) << "JobView request from" << serviceName << "didn't contain any identification information, this is an application bug!"; |
|
|
|
QDBusReply<uint> pidReply = connection().interface()->servicePid(serviceName); |
|
if (pidReply.isValid()) { |
|
const auto pid = pidReply.value(); |
|
|
|
const QString processName = Utils::processNameFromPid(pid); |
|
if (!processName.isEmpty()) { |
|
qCDebug(NOTIFICATIONMANAGER) << "Resolved JobView request to be from" << processName; |
|
job->setApplicationName(processName); |
|
} |
|
} |
|
} |
|
|
|
job->setSuspendable(capabilities & KJob::Suspendable); |
|
job->setKillable(capabilities & KJob::Killable); |
|
|
|
connect(job->d, &JobPrivate::showRequested, this, [this, job] { |
|
if (job->state() == Notifications::JobStateStopped) { |
|
// Stop finished or canceled in the meantime, remove |
|
qCDebug(NOTIFICATIONMANAGER) << "By the time we wanted to show JobView" << job->id() << "from" << job->applicationName() |
|
<< ", it was already stopped"; |
|
remove(job); |
|
return; |
|
} |
|
|
|
const int pendingRow = m_pendingJobViews.indexOf(job); |
|
Q_ASSERT(pendingRow > -1); |
|
m_pendingJobViews.removeAt(pendingRow); |
|
|
|
const int newRow = m_jobViews.count(); |
|
Q_EMIT jobViewAboutToBeAdded(newRow, job); |
|
m_jobViews.append(job); |
|
Q_EMIT jobViewAdded(newRow, job); |
|
updateApplicationPercentage(job->desktopEntry()); |
|
}); |
|
|
|
m_pendingJobViews.append(job); |
|
|
|
if (hints.value(QStringLiteral("immediate")).toBool()) { |
|
// Slightly delay showing the job so that the first update() call with a |
|
// summary will be shown atomically to the user. |
|
job->d->delayedShow(50ms, JobPrivate::ShowCondition::OnTimeout | JobPrivate::ShowCondition::OnSummary | JobPrivate::ShowCondition::OnTermination); |
|
} else { |
|
// Delay showing a job view to avoid showing really short stat jobs and other useless stuff. |
|
job->d->delayedShow(500ms, JobPrivate::ShowCondition::OnTimeout); |
|
} |
|
|
|
if (hints.value(QStringLiteral("transient")).toBool()) { |
|
job->setTransient(true); |
|
} |
|
|
|
m_jobServices.insert(job, serviceName); |
|
m_serviceWatcher->addWatchedService(serviceName); |
|
|
|
// Apply initial properties |
|
job->d->update(hints); |
|
|
|
connect(job, &Job::updatedChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::UpdatedRole); |
|
}); |
|
connect(job, &Job::summaryChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::SummaryRole); |
|
}); |
|
connect(job, &Job::textChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::BodyRole); |
|
}); |
|
connect(job, &Job::stateChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::JobStateRole); |
|
// Timeout and Closable depend on state, signal a change for those, too |
|
scheduleUpdate(job, Notifications::TimeoutRole); |
|
scheduleUpdate(job, Notifications::ClosableRole); |
|
|
|
if (job->state() == Notifications::JobStateStopped) { |
|
unwatchJob(job); |
|
updateApplicationPercentage(job->desktopEntry()); |
|
emitJobUrlsChanged(); |
|
} |
|
}); |
|
connect(job, &Job::percentageChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::PercentageRole); |
|
}); |
|
connect(job, &Job::errorChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::JobErrorRole); |
|
}); |
|
connect(job, &Job::expiredChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::ExpiredRole); |
|
}); |
|
connect(job, &Job::dismissedChanged, this, [this, job] { |
|
scheduleUpdate(job, Notifications::DismissedRole); |
|
}); |
|
|
|
connect(job, &Job::destUrlChanged, this, &JobsModelPrivate::emitJobUrlsChanged); |
|
|
|
connect(job->d, &JobPrivate::closed, this, [this, job] { |
|
remove(job); |
|
}); |
|
|
|
if (!connection().interface()->isServiceRegistered(serviceName)) { |
|
qCWarning(NOTIFICATIONMANAGER) << "Service that requested the view wasn't registered anymore by the time the request was being processed"; |
|
QMetaObject::invokeMethod( |
|
this, |
|
[this, serviceName] { |
|
onServiceUnregistered(serviceName); |
|
}, |
|
Qt::QueuedConnection); |
|
} |
|
|
|
return job->d->objectPath(); |
|
} |
|
|
|
void JobsModelPrivate::remove(Job *job) |
|
{ |
|
const int activeRow = m_jobViews.indexOf(job); |
|
const int pendingRow = m_pendingJobViews.indexOf(job); |
|
|
|
Job *jobToBeRemoved = nullptr; |
|
|
|
if (activeRow > -1) { |
|
Q_EMIT jobViewAboutToBeRemoved(activeRow); |
|
jobToBeRemoved = m_jobViews.takeAt(activeRow); |
|
} else if (pendingRow > -1) { |
|
jobToBeRemoved = m_pendingJobViews.takeAt(pendingRow); |
|
} |
|
Q_ASSERT(jobToBeRemoved); |
|
|
|
m_pendingDirtyRoles.remove(jobToBeRemoved); |
|
|
|
const QString desktopEntry = jobToBeRemoved->desktopEntry(); |
|
|
|
unwatchJob(jobToBeRemoved); |
|
|
|
delete jobToBeRemoved; |
|
if (activeRow > -1) { |
|
Q_EMIT jobViewRemoved(activeRow); |
|
} |
|
|
|
updateApplicationPercentage(desktopEntry); |
|
} |
|
|
|
void JobsModelPrivate::removeAt(int row) |
|
{ |
|
Q_ASSERT(row >= 0 && row < m_jobViews.count()); |
|
remove(m_jobViews.at(row)); |
|
} |
|
|
|
// This will forward overall application process via Unity API. |
|
// This way users of that like Task Manager and Latte Dock still get basic job information. |
|
void JobsModelPrivate::updateApplicationPercentage(const QString &desktopEntry) |
|
{ |
|
if (desktopEntry.isEmpty()) { |
|
return; |
|
} |
|
|
|
int jobsPercentages = 0; |
|
int jobsCount = 0; |
|
|
|
for (int i = 0; i < m_jobViews.count(); ++i) { |
|
Job *job = m_jobViews.at(i); |
|
if (job->state() == Notifications::JobStateStopped || job->desktopEntry() != desktopEntry) { |
|
continue; |
|
} |
|
|
|
jobsPercentages += job->percentage(); |
|
++jobsCount; |
|
} |
|
|
|
int percentage = 0; |
|
if (jobsCount > 0) { |
|
percentage = jobsPercentages / jobsCount; |
|
} |
|
|
|
const QVariantMap properties = {{QStringLiteral("count-visible"), jobsCount > 0}, |
|
{QStringLiteral("count"), jobsCount}, |
|
{QStringLiteral("progress-visible"), jobsCount > 0}, |
|
{QStringLiteral("progress"), percentage / 100.0}, |
|
// so Task Manager knows this is a job progress and can ignore it if disabled in settings |
|
{QStringLiteral("proxied-for"), QStringLiteral("kuiserver")}}; |
|
|
|
QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/org/kde/notificationmanager/jobs"), |
|
QStringLiteral("com.canonical.Unity.LauncherEntry"), |
|
QStringLiteral("Update")); |
|
message.setArguments({QStringLiteral("application://") + desktopEntry, properties}); |
|
QDBusConnection::sessionBus().send(message); |
|
} |
|
|
|
void JobsModelPrivate::unwatchJob(Job *job) |
|
{ |
|
const QString serviceName = m_jobServices.take(job); |
|
// Check if there's any jobs left for this service, otherwise stop watching it |
|
auto it = std::find_if(m_jobServices.constBegin(), m_jobServices.constEnd(), [&serviceName](const QString &item) { |
|
return item == serviceName; |
|
}); |
|
if (it == m_jobServices.constEnd()) { |
|
m_serviceWatcher->removeWatchedService(serviceName); |
|
} |
|
} |
|
|
|
void JobsModelPrivate::onServiceUnregistered(const QString &serviceName) |
|
{ |
|
qCDebug(NOTIFICATIONMANAGER) << "JobView service unregistered" << serviceName; |
|
|
|
const QList<Job *> jobs = m_jobServices.keys(serviceName); |
|
for (Job *job : jobs) { |
|
// Mark all non-finished jobs as failed |
|
if (job->state() == Notifications::JobStateStopped) { |
|
continue; |
|
} |
|
|
|
job->d->terminate(KIO::ERR_OWNER_DIED, i18n("Application closed unexpectedly."), {} /*hints*/); |
|
} |
|
|
|
Q_ASSERT(!m_serviceWatcher->watchedServices().contains(serviceName)); |
|
} |
|
|
|
void JobsModelPrivate::scheduleUpdate(Job *job, Notifications::Roles role) |
|
{ |
|
m_pendingDirtyRoles[job].append(role); |
|
m_compressUpdatesTimer->start(); |
|
}
|
|
|