QortalOS Brooklyn for Raspberry Pi 4
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

/*
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();
}