/* * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez * SPDX-FileCopyrightText: 2013 Lukas Appelhans * SPDX-FileCopyrightText: 2023 Harald Sitter * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "PackageKitBackend.h" #include "AppPackageKitResource.h" #include "LocalFilePKResource.h" #include "PKResolveTransaction.h" #include "PKTransaction.h" #include "PackageKitSourcesBackend.h" #include "PackageKitUpdater.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config-paths.h" #include "libdiscover_backend_packagekit_debug.h" #include "utils.h" #include #include using namespace std::chrono_literals; using namespace Qt::StringLiterals; DISCOVER_BACKEND_PLUGIN(PackageKitBackend) QDebug operator<<(QDebug dbg, const PackageOrAppId &value) { QDebugStateSaver saver(dbg); dbg.nospace(); dbg << "PackageOrAppId("; dbg << "id: " << value.id << ','; dbg << "isPkg: " << value.isPackageName; dbg << ')'; return dbg; } bool operator==(const PackageOrAppId &a, const PackageOrAppId &b) { return a.isPackageName == b.isPackageName && a.id == b.id; } uint qHash(const PackageOrAppId &id, uint seed) { return qHash(id.id, seed) ^ qHash(id.isPackageName, seed); } PackageOrAppId makeAppId(const QString &id) { return {id, false}; } PackageOrAppId makePackageId(const QString &id) { return {id, true}; } PackageOrAppId makeResourceId(PackageKitResource *resource) { auto appstreamResource = qobject_cast(resource); if (appstreamResource) { return {appstreamResource->appstreamId(), false}; } return {resource->packageName(), true}; } template static void setWhenAvailable(const QDBusPendingReply &pending, W func, QObject *parent) { QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, parent); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, parent, [func](QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; func(reply.value()); }); } QString PackageKitBackend::locateService(const QString &filename) { return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1StringView("applications/") + filename); } Delay::Delay() { m_delay.setSingleShot(true); m_delay.setInterval(100); connect(&m_delay, &QTimer::timeout, this, [this] { Q_EMIT perform(m_pkgids); m_pkgids.clear(); }); } PackageKitBackend::PackageKitBackend(QObject *parent) : AbstractResourcesBackend(parent) , m_appdata(new AppStream::Pool) , m_updater(new PackageKitUpdater(this)) , m_refresher(nullptr) , m_isFetching(0) , m_reviews(OdrsReviewsBackend::global()) , m_reportToDistroAction( new DiscoverAction(u"tools-report-bug-symbolic"_s, i18nc("@action:button %1 is the distro name", "Report This Issue to %1", AppStreamIntegration::global()->osRelease()->name()), this)) { connect(m_reportToDistroAction, &DiscoverAction::triggered, this, [] { const auto url = QUrl(AppStreamIntegration::global()->osRelease()->bugReportUrl()); if (!QDesktopServices::openUrl(url)) { qCWarning(LIBDISCOVER_BACKEND_PACKAGEKIT_LOG) << "Failed to open bug report url" << url; } }); QTimer *t = new QTimer(this); connect(t, &QTimer::timeout, this, &PackageKitBackend::checkForUpdates); t->setInterval(60 * 60 * 1000); t->setSingleShot(false); t->start(); connect(&m_details, &Delay::perform, this, &PackageKitBackend::performDetailsFetch); connect(&m_details, &Delay::perform, this, [this](const QSet &pkgids) { PackageKit::Transaction *t = PackageKit::Daemon::getUpdatesDetails(kSetToList(pkgids)); connect(t, &PackageKit::Transaction::updateDetail, this, [this](const QString &packageID, const QStringList &updates, const QStringList &obsoletes, const QStringList &vendorUrls, const QStringList &bugzillaUrls, const QStringList &cveUrls, PackageKit::Transaction::Restart restart, const QString &updateText, const QString &changelog, PackageKit::Transaction::UpdateState state, const QDateTime &issued, const QDateTime &updated) { const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(packageID)); for (auto r : resources) { PackageKitResource *resource = qobject_cast(r); if (resource->containsPackageId(packageID)) { resource->updateDetail(packageID, updates, obsoletes, vendorUrls, bugzillaUrls, cveUrls, restart, updateText, changelog, state, issued, updated); } } }); connect(t, &PackageKit::Transaction::errorCode, this, [this, pkgids](PackageKit::Transaction::Error err, const QString &error) { qWarning() << "PackageKitBackend: Error fetching updates:" << err << error; for (const QString &pkgid : pkgids) { const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(pkgid)); for (auto r : resources) { PackageKitResource *resource = qobject_cast(r); if (resource->containsPackageId(pkgid)) { Q_EMIT resource->changelogFetched(QString()); } } } }); }); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::restartScheduled, this, [this] { m_updater->setNeedsReboot(true); }); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitBackend::checkDaemonRunning); connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { const auto resources = m_packages.packages.values(); m_reviews->emitRatingFetched(this, resources); }); auto proxyWatch = new QFileSystemWatcher(this); proxyWatch->addPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/kioslaverc")); connect(proxyWatch, &QFileSystemWatcher::fileChanged, this, [this]() { KProtocolManager::reparseConfiguration(); updateProxy(); }); SourcesModel::global()->addSourcesBackend(new PackageKitSourcesBackend(this)); reloadPackageList(); acquireFetching(true); setWhenAvailable( PackageKit::Daemon::getTimeSinceAction(PackageKit::Transaction::RoleRefreshCache), [this](uint timeSince) { if (timeSince > 3600) { checkForUpdates(); } else if (!PackageKit::Daemon::global()->offline()->upgradeTriggered()) { fetchUpdates(); } acquireFetching(false); }, this); m_globalHints = QStringList() << QStringLiteral("interactive=true") << QStringLiteral("locale=%1").arg(qEnvironmentVariable("LANG")); PackageKit::Daemon::global()->setHints(m_globalHints); } PackageKitBackend::~PackageKitBackend() { m_threadPool.waitForDone(200); m_threadPool.clear(); } QString proxyFor(KConfigGroup *config, const QString &protocol) { const QString key = protocol + QLatin1String("Proxy"); QString proxyStr(config->readEntry(key)); const int index = proxyStr.lastIndexOf(QLatin1Char(' ')); if (index > -1) { const QStringView portStr = QStringView(proxyStr).right(proxyStr.length() - index - 1); const bool isDigits = std::all_of(portStr.cbegin(), portStr.cend(), [](const QChar c) { return c.isDigit(); }); if (isDigits) { proxyStr = QStringView(proxyStr).left(index) + QLatin1Char(':') + portStr; } else { proxyStr.clear(); } } return proxyStr; } void PackageKitBackend::updateProxy() { if (PackageKit::Daemon::isRunning()) { KConfig kioSettings(QStringLiteral("kioslaverc")); KConfigGroup proxyConfig = kioSettings.group(u"Proxy Settings"_s); bool useProxy = proxyConfig.readEntry("ProxyType", 0) != 0; static bool everHad = useProxy; if (!everHad && !useProxy) { return; } everHad = useProxy; PackageKit::Daemon::global()->setProxy(proxyFor(&proxyConfig, QStringLiteral("http")), proxyFor(&proxyConfig, QStringLiteral("https")), proxyFor(&proxyConfig, QStringLiteral("ftp")), proxyFor(&proxyConfig, QStringLiteral("socks")), {}, {}); } } bool PackageKitBackend::isFetching() const { return m_isFetching; } void PackageKitBackend::acquireFetching(bool f) { if (f) { m_isFetching++; } else { m_isFetching--; } if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) { Q_EMIT fetchingChanged(); if (m_isFetching == 0) { Q_EMIT available(); } } Q_ASSERT(m_isFetching >= 0); } void PackageKitBackend::reloadPackageList() { acquireFetching(true); m_appdata.reset(new AppStream::Pool); const auto loadDone = [this](bool correct) { if (!correct && m_packages.packages.isEmpty()) { QTimer::singleShot(0, this, [this]() { Q_EMIT passiveMessage(i18n("Please make sure that Appstream is properly set up on your system")); }); } // TODO // if (data.components.isEmpty()) { // qCDebug(LIBDISCOVER_BACKEND_PACKAGEKIT_LOG) << "empty appstream db"; // if (PackageKit::Daemon::backendName() == QLatin1String("aptcc") || PackageKit::Daemon::backendName().isEmpty()) { // checkForUpdates(); // } // } if (!m_appstreamInitialized) { m_appstreamInitialized = true; Q_EMIT loadedAppStream(); } acquireFetching(false); const auto distroComponents = m_appdata->componentsById(AppStream::SystemInfo::currentDistroComponentId()); if (distroComponents.isEmpty()) { qWarning() << "PackageKitBackend: No distro component found for" << AppStream::SystemInfo::currentDistroComponentId(); } for (const AppStream::Component &dc : distroComponents) { const auto releases = dc.releasesPlain().entries(); for (const auto &r : releases) { int cmp = AppStream::Utils::vercmpSimple(r.version(), AppStreamIntegration::global()->osRelease()->versionId()); if (cmp == 0) { // Ignore (likely) empty date_eol entries that are parsed as the UNIX Epoch if (r.timestampEol().isNull() || r.timestampEol().toSecsSinceEpoch() == 0) { continue; } if (r.timestampEol() < QDateTime::currentDateTime()) { const QString releaseDate = QLocale().toString(r.timestampEol()); Q_EMIT inlineMessageChanged( QSharedPointer::create(InlineMessage::Warning, QStringLiteral("dialog-warning"), i18nc("%1 is the date as formatted by the locale", "Your operating system ended support on %1. Consider upgrading to a supported version.", releaseDate))); } } } } }; connect(m_appdata.get(), &AppStream::Pool::loadFinished, this, [this, loadDone](bool success) { m_appdataLoaded = true; if (!success) { qWarning() << "PackageKitBackend: Could not open the AppStream metadata pool" << m_appdata->lastError(); } // We do not want to block the GUI for very long, so we load appstream pools via queued // invocation for the post-load state changes. QMetaObject::invokeMethod( this, [loadDone, success] { loadDone(success); }, Qt::QueuedConnection); }); m_appdata->loadAsync(); } AppPackageKitResource *PackageKitBackend::addComponent(const AppStream::Component &component) const { const QStringList pkgNames = component.packageNames(); Q_ASSERT(!pkgNames.isEmpty()); Q_ASSERT(component.kind() != AppStream::Component::KindFirmware); auto appId = makeAppId(component.id()); auto resource = qobject_cast(m_packages.packages.value(appId)); if (!resource) { resource = qobject_cast(m_packagesToAdd.value(appId)); } if (!resource) { resource = new AppPackageKitResource(component, pkgNames.at(0), const_cast(this)); m_packagesToAdd.insert(appId, resource); } for (const auto &pkg : pkgNames) { m_packages.packageToApp[pkg] += component.id(); } return resource; } PKResolveTransaction *PackageKitBackend::resolvePackages(const QStringList &packageNames) { if (packageNames.isEmpty()) { return nullptr; } if (!m_resolveTransaction) { m_resolveTransaction = new PKResolveTransaction(this); connect(m_resolveTransaction, &PKResolveTransaction::allFinished, this, &PackageKitBackend::getPackagesFinished); connect(m_resolveTransaction, &PKResolveTransaction::started, this, [this] { m_resolveTransaction = nullptr; }); } m_resolveTransaction->addPackageNames(packageNames); return m_resolveTransaction; } void PackageKitBackend::fetchUpdates() { if (m_updater->isProgressing()) { return; } PackageKit::Transaction *tUpdates = PackageKit::Daemon::getUpdates(); connect(tUpdates, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesFinished); connect(tUpdates, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageToUpdate); connect(tUpdates, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); m_updatesPackageId.clear(); m_hasSecurityUpdates = false; m_updater->setProgressing(true); Q_EMIT fetchingUpdatesProgressChanged(); } void PackageKitBackend::addPackageArch(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary) { addPackage(info, packageId, summary, true); } void PackageKitBackend::addPackageNotArch(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary) { addPackage(info, packageId, summary, false); } void PackageKitBackend::addPackage(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary, bool arch) { if (PackageKit::Daemon::packageArch(packageId) == QLatin1String("source")) { // We do not add source packages, they make little sense here. If source is needed, // we are going to have to consider that in some other way, some other time // If we do not ignore them here, e.g. openSuse entirely fails at installing applications return; } const QString packageName = PackageKit::Daemon::packageName(packageId); QSet r = resourcesByPackageName(packageName); if (r.isEmpty()) { auto pk = new PackageKitResource(packageName, summary, this); r = {pk}; m_packagesToAdd.insert(makePackageId(packageName), pk); } for (auto resource : std::as_const(r)) { static_cast(resource)->addPackageId(info, packageId, arch); } } void PackageKitBackend::getPackagesFinished() { includePackagesToAdd(); } void PackageKitBackend::includePackagesToAdd() { if (m_packagesToAdd.isEmpty() && m_packagesToDelete.isEmpty()) { return; } acquireFetching(true); for (auto [id, resource] : KeyValueRange(std::as_const(m_packagesToAdd))) { m_packages.packages[id] = resource; } m_packagesToAdd.clear(); for (auto pkResource : std::as_const(m_packagesToDelete)) { const auto pkgs = m_packages.packageToApp.value(pkResource->packageName(), {pkResource->packageName()}); for (const auto &pkg : pkgs) { auto resource = m_packages.packages.take(makePackageId(pkg)); if (resource) { Q_EMIT resourceRemoved(resource); resource->deleteLater(); } } } m_packagesToDelete.clear(); acquireFetching(false); } void PackageKitBackend::transactionError(PackageKit::Transaction::Error, const QString &message) { qWarning() << "Transaction error:" << message << sender(); Q_EMIT passiveMessage(message); } void PackageKitBackend::packageDetails(const PackageKit::Details &details) { const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(details.packageId())); if (resources.isEmpty()) { qWarning() << "PackageKitBackend: Couldn't find package for" << details.packageId(); return; } for (auto resource : resources) { qobject_cast(resource)->setDetails(details); } } QSet PackageKitBackend::resourcesByPackageName(const QString &name) const { return resourcesByPackageNames>(QStringList{name}); } template T PackageKitBackend::resourcesByAppNames(const W &appNames) const { T ret; ret.reserve(appNames.size()); for (const QString &name : appNames) { auto resource = m_packages.packages.value(makeAppId(name)); if (resource) { ret += resource; } } return ret; } template T PackageKitBackend::resourcesByPackageNames(const W &pkgnames) const { T ret; ret.reserve(pkgnames.size()); for (const QString &pkg_name : pkgnames) { const QStringList app_names = m_packages.packageToApp.value(pkg_name, QStringList()); if (app_names.isEmpty()) { auto pkgId = makePackageId(pkg_name); auto resource = qobject_cast(m_packages.packages.value(pkgId)); if (!resource) { resource = m_packagesToAdd.value(pkgId); } if (resource) { ret += resource; } } else { for (const QString &app_id : app_names) { const auto appId = makeAppId(app_id); auto resource = m_packages.packages.value(appId); if (!resource) { resource = m_packagesToAdd.value(appId); } if (resource) { ret += resource; } else { ret += resourcesByComponents(m_appdata->componentsByBundleId(AppStream::Bundle::KindPackage, pkg_name, false)); } } } } return ret; } void PackageKitBackend::checkForUpdates() { if (auto offline = PackageKit::Daemon::global()->offline(); offline->updateTriggered() || offline->upgradeTriggered()) { qCDebug(LIBDISCOVER_BACKEND_PACKAGEKIT_LOG) << "Won't be checking for updates again, the system needs a reboot to apply the fetched offline updates."; return; } if (!m_refresher) { acquireFetching(true); m_updater->clearDistroUpgrade(); m_refresher = PackageKit::Daemon::refreshCache(false); // Limit the cache-age so that we actually download new caches if necessary m_refresher->setHints(globalHints() << QStringLiteral("cache-age=300" /* 5 minutes */)); connect(m_refresher.data(), &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(m_refresher.data(), &PackageKit::Transaction::percentageChanged, this, &PackageKitBackend::fetchingUpdatesProgressChanged); connect(m_refresher.data(), &PackageKit::Transaction::finished, this, [this]() { m_refresher = nullptr; fetchUpdates(); acquireFetching(false); }); } else { qWarning() << "PackageKitBackend: Already resetting"; } Q_EMIT fetchingUpdatesProgressChanged(); } AppStream::ComponentBox PackageKitBackend::componentsById(const QString &id) const { Q_ASSERT(m_appstreamInitialized); auto comps = m_appdata->componentsById(id); if (comps.isEmpty()) { comps = m_appdata->componentsByProvided(AppStream::Provided::KindId, id); } return comps; } static bool needsResolveFilter(const StreamResult &result) { return result.resource->state() == AbstractResource::Broken; }; class PKResultsStream : public ResultsStream { private: PKResultsStream(PackageKitBackend *backend, const QString &name) : ResultsStream(name) , backend(backend) { } PKResultsStream(PackageKitBackend *backend, const QString &name, const QVector &resources) : ResultsStream(name) , backend(backend) { QTimer::singleShot(0, this, [resources, this]() { sendResources(resources); }); } public: template [[nodiscard]] static QPointer create(Args &&...args) { return new PKResultsStream(std::forward(args)...); } void sendResources(const QVector &resources, bool waitForResolved = false) { if (resources.isEmpty()) { finish(); return; } Q_ASSERT(resources.size() == QSet(resources.constBegin(), resources.constEnd()).size()); const auto toResolve = kFilter>(resources, needsResolveFilter); if (!toResolve.isEmpty()) { auto transaction = backend->resolvePackages(kTransform(toResolve, [](const StreamResult &result) { return result.resource->packageName(); })); if (waitForResolved) { Q_ASSERT(transaction); connect(transaction, &QObject::destroyed, this, [this, resources] { Q_EMIT resourcesFound(resources); finish(); }); return; } } Q_EMIT resourcesFound(resources); finish(); } private: PackageKitBackend *const backend; }; PKResultsStream *PackageKitBackend::deferredResultStream(const QString &streamName, std::function callback) { // NOTE: stream is a child of `this` so we don't have guard this backend object separately. auto stream = PKResultsStream::create(this, streamName); // Don't capture variables into a coroutine lambda, pass them in as arguments instead // See https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870 [](PackageKitBackend *self, QPointer stream, std::function callback) -> QCoro::Task<> { if (self->m_appstreamInitialized) { co_await QCoro::sleepFor(0ms); } else { co_await qCoro(self, &PackageKitBackend::loadedAppStream); } if (stream.isNull()) { co_return; } callback(stream); }(this, stream, std::move(callback)); return stream; } ResultsStream *PackageKitBackend::search(const AbstractResourcesBackend::Filters &filter) { // In this method we are copying filters by value into capturing lambdas // to avoid lifetime issues. Also coroutines should not be capturing, so // we use nested lambdas with arguments instead of captures. if (!filter.resourceUrl.isEmpty()) { return findResourceByPackageName(filter.resourceUrl); } else if (!filter.extends.isEmpty()) { return deferredResultStream(u"PackageKitStream-extends"_s, [this, filter = filter](PKResultsStream *stream) { const auto extendingComponents = m_appdata->componentsByExtends(filter.extends); auto resources = resultsByComponents(extendingComponents); stream->sendResources(resources, filter.state != AbstractResource::Broken); }); } else if (filter.state == AbstractResource::Upgradeable) { return new ResultsStream(QStringLiteral("PackageKitStream-upgradeable"), kTransform>(upgradeablePackages())); // No need for it to be a PKResultsStream } else if (filter.state == AbstractResource::Installed) { return deferredResultStream(u"PackageKitStream-installed"_s, [this, filter = filter](PKResultsStream *stream) { loadAllPackages(); const auto toResolve = kFilter>(m_packages.packages, needsResolveFilter); auto installedAndNameFilter = [filter](AbstractResource *resource) { return resource->state() >= AbstractResource::Installed && !qobject_cast(resource)->isCritical() && (resource->name().contains(filter.search, Qt::CaseInsensitive) || resource->packageName().compare(filter.search, Qt::CaseInsensitive) == 0); }; bool furtherSearch = false; if (!toResolve.isEmpty()) { resolvePackages(kTransform(toResolve, [](AbstractResource *resource) { return resource->packageName(); })); connect(m_resolveTransaction, &PKResolveTransaction::allFinished, this, [stream, toResolve, installedAndNameFilter] { const auto resolved = kFilter>(toResolve, installedAndNameFilter); if (!resolved.isEmpty()) { Q_EMIT stream->resourcesFound(kTransform>(resolved, [](auto resource) { return StreamResult(resource, 0); })); } stream->finish(); }); furtherSearch = true; } const auto resolved = kFilter>(m_packages.packages, installedAndNameFilter); if (!resolved.isEmpty()) { QTimer::singleShot(0, stream, [resolved, toResolve, stream]() { if (!resolved.isEmpty()) { Q_EMIT stream->resourcesFound(kTransform>(resolved, [](auto resource) { return StreamResult(resource, 0); })); } if (toResolve.isEmpty()) { stream->finish(); } }); furtherSearch = true; } if (!furtherSearch) { stream->finish(); } }); } else if (filter.search.isEmpty() && !filter.category) { return deferredResultStream(u"PackageKitStream-all"_s, [this](PKResultsStream *stream) { loadAllPackages(); auto resources = kFilter>(m_packages.packages, [](AbstractResource *resource) { auto pkResource = qobject_cast(resource); // Neither PackageKitResource or its subclass AppPackageKitResource can have type == ApplicationSupport return resource->type() != AbstractResource::System && pkResource && !pkResource->isCritical() && !pkResource->extendsItself(); }); stream->sendResources(kTransform>(resources, [](auto resource) { return StreamResult(resource, 0); })); }); } else { return deferredResultStream(u"PackageKitStream-search"_s, [this, filter = filter](PKResultsStream *stream) { auto loadComponents = [](const auto &filter, const auto &appdata) -> QCoro::Task { AppStream::ComponentBox components(AppStream::ComponentBox::FlagNone); if (!filter.search.isEmpty()) { components = appdata->search(filter.search); } else if (filter.category) { components = co_await AppStreamUtils::componentsByCategoriesTask(appdata.get(), filter.category, AppStream::Bundle::KindUnknown); } else { components = appdata->components(); } co_return components; }; QCoro::connect(loadComponents(filter, m_appdata), this, [this, stream, filter](auto &&components) { QSet ids; kFilterInPlace(components, [&ids](const AppStream::Component &component) { if (ids.contains(component.id())) { return false; } ids.insert(component.id()); return true; }); if (!ids.isEmpty()) { const auto resources = kFilter>(resultsByComponents(components), [](const StreamResult &res) { return !qobject_cast(res.resource)->extendsItself(); }); stream->sendResources(resources, filter.state != AbstractResource::Broken); } else { stream->finish(); } }); }); } } PKResultsStream *PackageKitBackend::findResourceByPackageName(const QUrl &url) { if (url.isLocalFile()) { QMimeDatabase db; const auto mime = db.mimeTypeForUrl(url); if (mime.inherits(QStringLiteral("application/vnd.debian.binary-package")) // || mime.inherits(QStringLiteral("application/x-rpm")) // || mime.inherits(QStringLiteral("application/x-tar")) // || mime.inherits(QStringLiteral("application/x-zstd-compressed-tar")) // || mime.inherits(QStringLiteral("application/x-xz-compressed-tar"))) { return PKResultsStream::create(this, QStringLiteral("PackageKitStream-localpkg"), QVector{new LocalFilePKResource(url, this)}).data(); } } else if (url.scheme() == QLatin1String("appstream")) { const auto appstreamIds = AppStreamUtils::appstreamIds(url); if (appstreamIds.isEmpty()) { Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); } else { return deferredResultStream(u"PackageKitStream-appstream-url"_s, [this, appstreamIds](PKResultsStream *stream) { auto toSend = QSet(); toSend.reserve(appstreamIds.size()); for (const auto &appstreamId : appstreamIds) { const auto components = componentsById(appstreamId); if (components.isEmpty()) { continue; } const auto resources = resultsByComponents(components); for (const auto &resource : resources) { toSend.insert(resource); } } stream->sendResources(QList(toSend.constBegin(), toSend.constEnd())); }); } } return PKResultsStream::create(this, QStringLiteral("PackageKitStream-unknown-url"), QVector{}).data(); } template T PackageKitBackend::resourcesByComponents(const AppStream::ComponentBox &components) const { T ret; ret.reserve(components.size()); QSet done; for (const auto &component : components) { if (component.packageNames().isEmpty() || done.contains(component.id())) { continue; } done += component.id(); auto r = addComponent(component); Q_ASSERT(r); ret << r; } return ret; } QVector PackageKitBackend::resultsByComponents(const AppStream::ComponentBox &components) const { QVector ret; ret.reserve(components.size()); QSet done; for (const auto &component : components) { if (component.packageNames().isEmpty() || component.kind() == AppStream::Component::KindFirmware || done.contains(component.id())) { continue; } done += component.id(); auto r = addComponent(component); Q_ASSERT(r); ret << StreamResult(r, component.sortScore()); } return ret; } bool PackageKitBackend::hasSecurityUpdates() const { return m_hasSecurityUpdates; } int PackageKitBackend::updatesCount() const { auto offline = PackageKit::Daemon::global()->offline(); if (offline->updateTriggered() || offline->upgradeTriggered()) { return 0; } QSet packages; const auto toUpgrade = upgradeablePackages(); for (auto resource : toUpgrade) { packages.insert(resource->packageName()); } return packages.count(); } Transaction *PackageKitBackend::installApplication(AbstractResource *app, const AddonList &addons) { Transaction *transaction = nullptr; if (!addons.addonsToInstall().isEmpty()) { QVector appsToInstall = resourcesByPackageNames>(addons.addonsToInstall()); if (!app->isInstalled()) { appsToInstall << app; } transaction = new PKTransaction(appsToInstall, Transaction::ChangeAddonsRole); } else if (!app->isInstalled()) { transaction = installApplication(app); } if (!addons.addonsToRemove().isEmpty()) { const auto appsToRemove = resourcesByPackageNames>(addons.addonsToRemove()); transaction = new PKTransaction(appsToRemove, Transaction::RemoveRole); } return transaction; } Transaction *PackageKitBackend::installApplication(AbstractResource *app) { return new PKTransaction({app}, Transaction::InstallRole); } Transaction *PackageKitBackend::removeApplication(AbstractResource *app) { Q_ASSERT(!isFetching()); if (!qobject_cast(app)) { Q_EMIT passiveMessage(i18n("Cannot remove '%1'", app->name())); return nullptr; } return new PKTransaction({app}, Transaction::RemoveRole); } QSet PackageKitBackend::upgradeablePackages() const { if (isFetching() || !m_packagesToAdd.isEmpty()) { return {}; } QSet ret; ret.reserve(m_updatesPackageId.size()); for (const QString &pkgid : std::as_const(m_updatesPackageId)) { const QString pkgname = PackageKit::Daemon::packageName(pkgid); const auto pkgs = resourcesByPackageName(pkgname); if (pkgs.isEmpty()) { qWarning() << "PackageKitBackend: Couldn't find resource for" << pkgid; } ret.unite(pkgs); } return kFilter>(ret, [](AbstractResource *resource) { return !static_cast(resource)->extendsItself(); }); } void PackageKitBackend::addPackageToUpdate(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary) { if (info == PackageKit::Transaction::InfoBlocked) { return; } if (info == PackageKit::Transaction::InfoRemoving || info == PackageKit::Transaction::InfoObsoleting) { // Don't try updating packages which need to be removed return; } if (info == PackageKit::Transaction::InfoSecurity) { m_hasSecurityUpdates = true; } m_updatesPackageId += packageId; addPackage(info, packageId, summary, true); } void PackageKitBackend::getUpdatesFinished(PackageKit::Transaction::Exit, uint) { if (!m_updatesPackageId.isEmpty()) { resolvePackages(kTransform(m_updatesPackageId, [](const QString &pkgid) { return PackageKit::Daemon::packageName(pkgid); })); } m_updater->setProgressing(false); includePackagesToAdd(); if (isFetching()) { auto a = new OneTimeAction( [this] { Q_EMIT updatesCountChanged(); }, this); connect(this, &PackageKitBackend::available, a, &OneTimeAction::trigger); } else { Q_EMIT updatesCountChanged(); } if (!m_updater->isDistroUpgrade() && !PackageKit::Daemon::global()->offline()->upgradeTriggered()) { const auto loadDone = [this] { auto nextRelease = AppStreamIntegration::global()->getDistroUpgrade(m_appdata.get()); if (nextRelease) { foundNewMajorVersion(*nextRelease); } }; if (m_appdataLoaded) { loadDone(); } else { connect(m_appdata.get(), &AppStream::Pool::loadFinished, this, loadDone); } } } void PackageKitBackend::foundNewMajorVersion(const AppStream::Release &release) { const QString upgradeVersion = release.version(); const QString newDistroVersionText = AppStreamIntegration::global()->osRelease()->name() + QLatin1Char(' ') + upgradeVersion; QString info; // Message to display when: // - A new major version is available // - An update to the current version is available or pending a reboot info = i18nc("@info:status %1 is a new major version of the user's distro", "%1 is now available.\n" "To be able to upgrade to this new version, first apply all available updates, and then restart the system.", newDistroVersionText); QSharedPointer updateBeforeMajorUpgradeMessage = QSharedPointer::create(InlineMessage::Positive, QStringLiteral("system-software-update"), info); // Message to display when: // - A new major version is available // - No update to the current version are available or pending a reboot DiscoverAction *majorUpgrade = new DiscoverAction(QStringLiteral("system-upgrade-symbolic"), i18nc("@action: button", "Upgrade Now"), this); connect(majorUpgrade, &DiscoverAction::triggered, this, [this, release, upgradeVersion] { if (m_updater->isProgressing()) { return; } m_updatesPackageId.clear(); m_updater->setProgressing(true); m_refresher = PackageKit::Daemon::upgradeSystem(upgradeVersion, PackageKit::Transaction::UpgradeKind::UpgradeKindComplete, PackageKit::Transaction::TransactionFlagSimulate); m_refresher->setHints(globalHints() << QStringLiteral("cache-age=86400" /* 24*60*60 */)); connect(m_refresher, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageToUpdate); connect(m_refresher, &PackageKit::Transaction::percentageChanged, this, &PackageKitBackend::fetchingUpdatesProgressChanged); connect(m_refresher, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(m_refresher, &PackageKit::Transaction::finished, this, [this, release](PackageKit::Transaction::Exit e, uint x) { m_updater->setDistroUpgrade(release); getUpdatesFinished(e, x); }); Q_EMIT inlineMessageChanged({}); ResourcesModel::global()->switchToUpdates(); }); info = i18nc("@info:status %1 is a new major version of the user's distro", "%1 is now available.", newDistroVersionText); QSharedPointer majorUpgradeAvailableMessage = QSharedPointer::create(InlineMessage::Positive, QStringLiteral("system-software-update"), info, majorUpgrade); // Allow upgrade only if up to date on the current release if (!m_updatesPackageId.isEmpty()) { Q_EMIT inlineMessageChanged(updateBeforeMajorUpgradeMessage); return; } // No updates pending or avaiable. We are good to offer the upgrade to the // next major version! Q_EMIT inlineMessageChanged(majorUpgradeAvailableMessage); } // Copy of Transaction::packageName that doesn't create a copy but just pass a reference // It's an optimisation as there's a bunch of allocations that happen from packageName // Having packageName return a QStringRef or a QStringView would fix this issue. // TODO Qt 6: Have packageName and similars return a QStringView static QStringView TransactionPackageName(const QString &packageID) { return QStringView(packageID).left(packageID.indexOf(QLatin1Char(';'))); } bool PackageKitBackend::isPackageNameUpgradeable(const PackageKitResource *resource) const { const QString name = resource->packageName(); for (const QString &pkgid : m_updatesPackageId) { if (TransactionPackageName(pkgid) == name) { return true; } } return false; } QSet PackageKitBackend::upgradeablePackageId(const PackageKitResource *resource) const { QSet ids; const QString name = resource->packageName(); for (const QString &pkgid : m_updatesPackageId) { if (TransactionPackageName(pkgid) == name) { ids.insert(pkgid); } } return ids; } void PackageKitBackend::fetchDetails(const QString &pkgid) { m_details.add(pkgid); } void PackageKitBackend::fetchDetails(const QSet &pkgid) { m_details.add(pkgid); } void PackageKitBackend::performDetailsFetch(const QSet &pkgids) { Q_ASSERT(!pkgids.isEmpty()); const auto ids = pkgids.values(); PackageKit::Transaction *transaction = PackageKit::Daemon::getDetails(ids); connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); } void PackageKitBackend::checkDaemonRunning() { if (!PackageKit::Daemon::isRunning()) { qWarning() << "PackageKit stopped running!"; } else { updateProxy(); } } AbstractBackendUpdater *PackageKitBackend::backendUpdater() const { return m_updater; } QVector PackageKitBackend::extendedBy(const QString &id) const { const auto components = m_appdata->componentsByExtends(id); return resourcesByComponents>(components); } AbstractReviewsBackend *PackageKitBackend::reviewsBackend() const { return m_reviews.data(); } QString PackageKitBackend::displayName() const { return AppStreamIntegration::global()->osRelease()->prettyName(); } int PackageKitBackend::fetchingUpdatesProgress() const { if (!m_refresher) { return 100; } int percentage = m_refresher->percentage(); if (percentage > 100) { return m_refresher->property("lastPercentage").toInt(); } m_refresher->setProperty("lastPercentage", percentage); return percentage; } uint PackageKitBackend::fetchingUpdatesProgressWeight() const { return 50; } InlineMessage *PackageKitBackend::explainDysfunction() const { const auto error = m_appdata->lastError(); if (!error.isEmpty()) { return new InlineMessage(InlineMessage::Error, QStringLiteral("network-disconnect"), error); } if (!PackageKit::Daemon::isRunning()) { return new InlineMessage(InlineMessage::Error, u"run-build-prune-symbolic"_s, i18nc("@info", "The background service (PackageKit) stopped unexpectedly. It may have crashed."), m_reportToDistroAction); } return AbstractResourcesBackend::explainDysfunction(); } void PackageKitBackend::loadAllPackages() { if (m_allPackagesLoaded) { return; } const auto components = m_appdata->components(); for (const auto &component : components) { if (!component.packageNames().isEmpty()) { addComponent(component); } } includePackagesToAdd(); m_allPackagesLoaded = true; } void PackageKitBackend::aboutTo(AboutToAction action) { PackageKit::Offline::Action packageKitAction; switch (action) { case Reboot: packageKitAction = PackageKit::Offline::ActionReboot; break; case PowerOff: packageKitAction = PackageKit::Offline::ActionPowerOff; break; } m_updater->setOfflineUpdateAction(packageKitAction); } #include "PackageKitBackend.moc" #include "moc_PackageKitBackend.cpp"