diff --git a/src/bin/patchmanager-daemon/dbus/org.SfietKonstantin.patchmanager.xml b/src/bin/patchmanager-daemon/dbus/org.SfietKonstantin.patchmanager.xml index b024cc97..e2cb22aa 100644 --- a/src/bin/patchmanager-daemon/dbus/org.SfietKonstantin.patchmanager.xml +++ b/src/bin/patchmanager-daemon/dbus/org.SfietKonstantin.patchmanager.xml @@ -1,6 +1,11 @@ + + + + + diff --git a/src/bin/patchmanager-daemon/patchmanager-daemon.pro b/src/bin/patchmanager-daemon/patchmanager-daemon.pro index c1eff9a4..41145493 100644 --- a/src/bin/patchmanager-daemon/patchmanager-daemon.pro +++ b/src/bin/patchmanager-daemon/patchmanager-daemon.pro @@ -26,12 +26,14 @@ DEFINES += BUILD_VERSION=\\\"$$BUILD_VERSION\\\" HEADERS += \ patchmanagerobject.h \ patchmanager_include.h \ + patchmanagerfilter.h \ inotifywatcher.h \ journal.h SOURCES += \ main.cpp \ patchmanagerobject.cpp \ + patchmanagerfilter.cpp \ inotifywatcher.cpp \ journal.cpp diff --git a/src/bin/patchmanager-daemon/patchmanagerfilter.cpp b/src/bin/patchmanager-daemon/patchmanagerfilter.cpp new file mode 100644 index 00000000..5102ebc7 --- /dev/null +++ b/src/bin/patchmanager-daemon/patchmanagerfilter.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2025 Patchmanager for SailfishOS contributors: + * - olf "Olf0" + * - Peter G. "nephros" + * - Vlad G. "b100dian" + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * The names of its contributors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + + +/*! + * The current implementation of the filter is a QCache, whole Object contents + * are not actually used, only the keys are. Once a file path has been + * identified as non-existing, it is added to the cache. + * + * Checking for presence is done using QCache::object() (or + * QCache::operator[]), not QCache::contains() in order to have the cache + * notice "usage" of the cached object. + * + * \sa m_filter + */ + +#include "patchmanagerfilter.h" + +#include +#include +#include +#include + +/* initialize the "static members", i.e. a list of very frequesntly accessed files. */ +/* only use relatively stable sonames here. */ +const QStringList libList = QStringList({ + "/usr/lib64/libpreloadpatchmanager.so", + "/lib/ld-linux-aarch64.so.1", + "/lib/ld-linux-armhf.so.3", + "/lib64/libc.so.6", + "/lib64/libdl.so.2", + "/lib64/librt.so.1", + "/lib64/libpthread.so.0", + "/lib64/libgcc_s.so.1", + "/usr/lib64/libtls-padding.so", + "/usr/lib64/libsystemd.so.0", + "/usr/lib64/libcap.so.2", + "/usr/lib64/libmount.so.1", + "/usr/lib64/libblkid.so.1", + "/usr/lib64/libgpg-error.so.0" +}); + +const QStringList etcList = QStringList({ + "/etc/passwd", + "/etc/group", + "/etc/shadow", + "/etc/localtime", + "/etc/ld.so.preload", + "/etc/ld.so.cache", + "/usr/share/locale/locale.alias" +}); + +PatchManagerFilter::PatchManagerFilter(QObject *parent, int maxCost) + : QObject(parent) + , QCache(maxCost) +{ +} + +void PatchManagerFilter::setup() +{ + qDebug() << Q_FUNC_INFO; + // set up cache + setMaxCost(HOTCACHE_COST_MAX); + + // use a cost of 1 here so they have less chance to be evicted + foreach(const QString &entry, etcList) { + if (QFileInfo::exists(entry)) { + insert(entry, 1, HOTCACHE_COST_STRONG); + } + } + // they may be wrong, so use a higher cost than default + foreach(const QString &entry, libList) { + QString libentry(entry); + if (Q_PROCESSOR_WORDSIZE == 4) { // 32 bit + libentry.replace("lib64", "lib"); + } + + if (QFileInfo::exists(libentry)) { + QFileInfo fi(libentry); + insert(fi.canonicalFilePath(), 1, HOTCACHE_COST_WEAK); + } + } +} + +// override QCache::insert(). +bool PatchManagerFilter::insert(const QString &key, quint8 value, int cost) +{ + quint8* data; + // In Qt 5.6 (up to and including 5.12), QCache::object() returns 0 for "not found", + // we cannot accept a zero value here. + if (value == 0) { + qCritical() << "PatchManagerFilter::insert: Inserting zero will lead to wrong results!" + << "Forcing value to 1!"; + data = new quint8(1); + } else { + data = new quint8(value); + } + return QCache::insert(key, data, cost); +} + +// override QCache::contains() +bool PatchManagerFilter::contains(const QString &key) const +{ + if (!m_active) + return false; + + // we do not use QCache::contains here, because ::object() will make the cache notice usage of the object + bool ret = (QCache::object(key) != 0); // NB: returns 0 in Qt < 5.13, nullptr in later versions + + if(ret) { m_hits+=1; } else { m_misses+=1; } + + return ret; +}; + + +QString PatchManagerFilter::stats(bool verbose) const +{ + qDebug() << Q_FUNC_INFO; + QStringList stats; + stats << QStringLiteral("Filter Stats:") + << QStringLiteral("===========================") + << QStringLiteral(" Hotcache entries:: ..............%1").arg(size()) + << QStringLiteral(" Hotcache cost: ..................%1/%2").arg(totalCost()).arg(maxCost()); + if (verbose) { + unsigned int sum = m_hits + m_misses; + if (sum > 0) { + QString ratio; + float ratf = (static_cast(m_hits) / sum); + ratio.setNum(ratf, 'f', 2); + stats << QStringLiteral(" Hotcache hit/miss: ..............%1/%2 (%3%)").arg(m_hits).arg(m_misses).arg(ratio); + } + + stats << QStringLiteral("===========================") + << QStringLiteral(" Hotcache entries:"); + if (count() > HOTCACHE_LOG_MAX) { + stats << QStringLiteral("showing %1/%2").arg(HOTCACHE_LOG_MAX).arg(count()); + auto beg = keys().begin(); auto end = beg + HOTCACHE_LOG_MAX; + for (auto it = beg; it != end; ++it) { + stats << *it; + } + } else { + stats << keys(); + } + } + stats << QStringLiteral("==========================="); + + return stats.join("\n"); +} diff --git a/src/bin/patchmanager-daemon/patchmanagerfilter.h b/src/bin/patchmanager-daemon/patchmanagerfilter.h new file mode 100644 index 00000000..db560db8 --- /dev/null +++ b/src/bin/patchmanager-daemon/patchmanagerfilter.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2025 Patchmanager for SailfishOS contributors: + * - olf "Olf0" + * - Peter G. "nephros" + * - Vlad G. "b100dian" + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * The names of its contributors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#ifndef PATCHMANAGERFILTER_H +#define PATCHMANAGERFILTER_H + +#include +#include + +/* choosing the right cost obviously is critical and difficult ;) + we want lookup times to be faster than the cost of a QFileInfo::exists() + --> smaller is better. + + We also want it to not hold "stale" entries, i.e. files once added and + never accessed again. + --> smaller is also better + + But we also want it to hold the most commonly accessed files, and not rotate + the entries all the time. ideally after some time, it stays somewhat + stable. + --> too small is bad + + Some observations: + + df -i / on SFOS 5.0 gives about 100k used inodes. + On a system with about 100 patched files, running find /usr -exec head -n 1 {} >/dev/null \; + the cost() seems to not go over 3600 or so when maxCost is 5000. + +*/ + +static const int HOTCACHE_COST_MAX = 2500; +static const int HOTCACHE_COST_STRONG = 1; +static const int HOTCACHE_COST_DEFAULT = 2; +static const int HOTCACHE_COST_WEAK = 3; + +// output will be a dbus message. Don't make it too long. +static const int HOTCACHE_LOG_MAX = 4096; + +// As we do not care about the actual cached object, try to use a small one. +// quint8 should be one byte or so +class PatchManagerFilter : public QObject, public QCache +{ + Q_OBJECT + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(unsigned int hits READ hits) + Q_PROPERTY(unsigned int misses READ misses) +public: + PatchManagerFilter(QObject *parent = nullptr, int maxCost = HOTCACHE_COST_MAX); + //~PatchManagerFilter(); + + void setup(); + + // override QCache::insert() + bool insert(const QString &key, quint8 value = 1, int cost = HOTCACHE_COST_DEFAULT); + + // override QCache::contains() + bool contains(const QString &key) const; + + void setActive(bool active) { + if (m_active != active) { + m_active = active; + emit activeChanged(active); + } + }; + bool active() const { return m_active; }; + + unsigned int hits() const { return m_hits; }; + unsigned int misses() const { return m_misses; }; + + //QList> stats() const; + QString stats(bool verbose) const; + +signals: + void activeChanged(bool); + +private: + bool m_active; + + // need to be mutable so we can count from const method. + mutable unsigned int m_hits = 0; + mutable unsigned int m_misses = 0; +}; + +#endif // PATCHMANAGERFILTER_H diff --git a/src/bin/patchmanager-daemon/patchmanagerobject.cpp b/src/bin/patchmanager-daemon/patchmanagerobject.cpp index fa046250..4607a503 100644 --- a/src/bin/patchmanager-daemon/patchmanagerobject.cpp +++ b/src/bin/patchmanager-daemon/patchmanagerobject.cpp @@ -37,6 +37,7 @@ #include "patchmanagerobject.h" #include "patchmanager_adaptor.h" +#include "patchmanagerfilter.h" #include #include @@ -810,6 +811,9 @@ void PatchManagerObject::initialize() qWarning() << Q_FUNC_INFO << "Failed to find ld.so.preload!"; } + // prepare the hotcache + setupFilter(); + qDebug() << Q_FUNC_INFO << PM_APPLY; QFileInfo pa(PM_APPLY); if (pa.exists()) { @@ -1190,6 +1194,17 @@ void PatchManagerObject::process() } +/*! Retrieves some statistics via D-Bus. */ +QString PatchManagerObject::statistics(bool verbose=false) +{ + DBUS_GUARD(QString()) + qDebug() << Q_FUNC_INFO; + setDelayedReply(true); + QMetaObject::invokeMethod(this, NAME(doStatistics), Qt::QueuedConnection, + Q_ARG(QVariantMap, QVariantMap({{QStringLiteral("verbose"), verbose}})), + Q_ARG(QDBusMessage, message())); + return QString(); +} /*! Retrieves a list of Patches via D-Bus. */ QVariantList PatchManagerObject::listPatches() @@ -1834,6 +1849,7 @@ void PatchManagerObject::onTimerAction() { qDebug() << Q_FUNC_INFO; checkForUpdates(); + statistics(false); } void PatchManagerObject::startReadingLocalServer() @@ -1860,22 +1876,48 @@ void PatchManagerObject::startReadingLocalServer() return; } const QByteArray request = clientConnection->readAll(); - QByteArray payload; const QString fakePath = QStringLiteral("%1%2").arg(s_patchmanagerCacheRoot, QString::fromLatin1(request)); - if (!m_failed && QFileInfo::exists(fakePath)) { - payload = fakePath.toLatin1(); + bool passAsIs = true; + if ( + (!m_failed) // return unaltered for failed + && (!m_filter.active() || !m_filter.contains(request)) // filter inactive or not in the list of unpatched files + && (Q_UNLIKELY(QFileInfo::exists(fakePath))) // file is patched + ) + { + passAsIs = false; + } else { + // failed state or file is unpatched + passAsIs = true; + } + /* write the result back to the library as soon as possible */ + if (passAsIs) { + clientConnection->write(request); + } else { + clientConnection->write(fakePath.toLatin1()); + } + clientConnection->flush(); +// clientConnection->waitForBytesWritten(); + + /* print debug and manage the cache after writing the data: + * if the file didn't exist, we add it to the cache. + * otherwise, we so nothing, but check that it wasn't wrongly in the + * cache, which shouldn't happen. + * Note that we don't actually store anything in the cache, we're only + * interested in the key and the cost management. + */ + if (passAsIs) { // file didn't exist if (qEnvironmentVariableIsSet("PM_DEBUG_SOCKET")) { - qDebug() << Q_FUNC_INFO << "Requested:" << request << "Sending:" << payload; + qDebug() << Q_FUNC_INFO << "Requested:" << request << "was sent unaltered."; } + m_filter.insert(request); } else { - payload = request; if (qEnvironmentVariableIsSet("PM_DEBUG_SOCKET")) { - qDebug() << Q_FUNC_INFO << "Requested:" << request << "is sent unaltered."; + qDebug() << Q_FUNC_INFO << "Requested:" << request << "Sent:" << fakePath; + } + if (m_filter.remove(request)) { + qWarning() << Q_FUNC_INFO << "Hot cache: contained a patched file:" << request << "/" << fakePath; } } - clientConnection->write(payload); - clientConnection->flush(); -// clientConnection->waitForBytesWritten(); }, Qt::DirectConnection); } @@ -2137,6 +2179,38 @@ void PatchManagerObject::doRefreshPatchList() } } +void PatchManagerObject::doStatistics(const QVariantMap ¶ms, const QDBusMessage &message) +{ + qDebug() << Q_FUNC_INFO; + + bool verbose = params.value(QStringLiteral("verbose")).toBool(); + + QStringList result; + result << QStringLiteral("Patchmanager version: %1").arg(getPatchmanagerVersion()) + << QStringLiteral("Applied Patches: %1").arg(m_appliedPatches.count()) + //<< QStringLiteral("Patched files: %1").arg(m_patchedFiles.count()); + << QStringLiteral("Patched files: %1").arg(m_fileToPatch.values().count()); + + if (m_originalWatcher) + result << QStringLiteral("Watched files: %1").arg(m_originalWatcher->files().count()); + + if (m_filter.active()) { + result << m_filter.stats(verbose); + } else { + result << QStringLiteral("Advanced filtering is not active."); + } + + qInfo() << "=================================="; + qInfo() << "======== STATISTICS BEGIN ========"; + qInfo() << "=================================="; + qInfo() << qPrintable(result.join("\n")); + qInfo() << "=================================="; + qInfo() << "======== STATISTICS END =========="; + qInfo() << "=================================="; + + sendMessageReply(message, result.join("\n")); +} + void PatchManagerObject::doListPatches(const QDBusMessage &message) { qDebug() << Q_FUNC_INFO; @@ -2969,3 +3043,19 @@ QString PatchManagerObject::pathToMangledPath(const QString &path, const QString qDebug() << Q_FUNC_INFO << "Path after mangle" << newpath; return newpath; } + +/*! Set up the filter parameters and fill it with some initial contents. + * + * \sa PatchManagerFilter::setup() +*/ +void PatchManagerObject::setupFilter() +{ + if (!getSettings(QStringLiteral("enableFSFilter"), false).toBool()) { + m_filter.setActive(false); + return; + } else { + m_filter.setup(); + m_filter.setActive(true); + } +} + diff --git a/src/bin/patchmanager-daemon/patchmanagerobject.h b/src/bin/patchmanager-daemon/patchmanagerobject.h index 4f85b50b..2c57e49a 100644 --- a/src/bin/patchmanager-daemon/patchmanagerobject.h +++ b/src/bin/patchmanager-daemon/patchmanagerobject.h @@ -51,6 +51,8 @@ #include +#include "patchmanagerfilter.h" + #ifndef SERVER_URL #define SERVER_URL "https://coderus.openrepos.net" #endif @@ -73,6 +75,7 @@ class QSettings; class QNetworkAccessManager; class PatchManagerAdaptor; class QLocalServer; + class PatchManagerObject : public QObject, public QDBusContext { Q_OBJECT @@ -94,6 +97,7 @@ public slots: QVariantList listPatches(); QVariantMap listVersions(); + QString statistics(bool verbose); bool isPatchApplied(const QString &patch); QVariantMap applyPatch(const QString &patch); QVariantMap unapplyPatch(const QString &patch); @@ -158,6 +162,7 @@ private slots: void doRefreshPatchList(); void doListPatches(const QDBusMessage &message); + void doStatistics(const QVariantMap ¶ms, const QDBusMessage &message); bool doPatch(const QString &patchName, bool apply, QString *patchLog = nullptr); void doPatch(const QVariantMap ¶ms, const QDBusMessage &message, bool apply); @@ -267,6 +272,9 @@ private slots: QTimer *m_sessionBusConnector = nullptr; QDBusConnection m_sbus; + + PatchManagerFilter m_filter; + void setupFilter(); }; #endif // PATCHMANAGEROBJECT_H diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml index 158b16ef..7eec3697 100644 --- a/src/qml/SettingsPage.qml +++ b/src/qml/SettingsPage.qml @@ -249,6 +249,18 @@ Page { text: PatchManager.mangleCandidates.join("\n") enabled: fixBitSwitch.checked } + + TextSwitch { + id: filterSwitch + text: qsTranslate("", "Use advanced file existence filtering") + description: qsTranslate("", "Cache file existence checks.") + + " " + qsTranslate("", "Depending on many factors like memory, type of device, and others, this may have a positive or detrimental effect on overall performance.") + + "\n" +qsTranslate("", "If unsure, say N.") + checked: PatchManager.enableFSFilter + onClicked: PatchManager.enableFSFilter = !PatchManager.enableFSFilter + automaticCheck: false + } + } } } diff --git a/src/qml/patchmanager.cpp b/src/qml/patchmanager.cpp index f82c00a8..305de732 100644 --- a/src/qml/patchmanager.cpp +++ b/src/qml/patchmanager.cpp @@ -313,6 +313,22 @@ void PatchManager::setBitnessMangle(bool bitnessMangle) } } +/*! \property PatchManager::enableFSFilter +*/ +/*! \qmlproperty bool PatchManager::enableFSFilter +*/ +bool PatchManager::enableFSFilter() const +{ + return getSettingsSync(QStringLiteral("enableFSFilter"), false).toBool(); +} + +void PatchManager::setEnableFSFilter(bool enableFSFilter) +{ + if (putSettingsSync(QStringLiteral("enableFSFilter"), enableFSFilter)) { + emit enableFSFilterChanged(enableFSFilter); + } +} + PatchManagerModel *PatchManager::installedModel() { return m_installedModel; diff --git a/src/qml/patchmanager.h b/src/qml/patchmanager.h index 3527df74..0fed819d 100644 --- a/src/qml/patchmanager.h +++ b/src/qml/patchmanager.h @@ -86,6 +86,7 @@ class PatchManager: public QObject Q_PROPERTY(bool applyOnBoot READ applyOnBoot WRITE setApplyOnBoot NOTIFY applyOnBootChanged) Q_PROPERTY(bool notifyOnSuccess READ notifyOnSuccess WRITE setNotifyOnSuccess NOTIFY notifyOnSuccessChanged) Q_PROPERTY(bool bitnessMangle READ bitnessMangle WRITE setBitnessMangle NOTIFY bitnessMangleChanged) + Q_PROPERTY(bool enableFSFilter READ enableFSFilter WRITE setEnableFSFilter NOTIFY enableFSFilterChanged) Q_PROPERTY(QStringList mangleCandidates READ mangleCandidates NOTIFY mangleCandidatesChanged) Q_PROPERTY(PatchManagerModel *installedModel READ installedModel CONSTANT) Q_PROPERTY(QVariantMap updates READ getUpdates NOTIFY updatesChanged) @@ -113,6 +114,8 @@ class PatchManager: public QObject void setNotifyOnSuccess(bool notifyOnSuccess); bool bitnessMangle() const; void setBitnessMangle(bool bitnessMangle); + bool enableFSFilter() const; + void setEnableFSFilter (bool enableFSFilter); QStringList mangleCandidates() const; PatchManagerModel *installedModel(); QString trCategory(const QString &category) const; @@ -194,6 +197,7 @@ public slots: void applyOnBootChanged(bool applyOnBoot); void notifyOnSuccessChanged(bool notifyOnSuccess); void bitnessMangleChanged(bool bitnessMangle); + void enableFSFilterChanged(bool enableFSFilter); void mangleCandidatesChanged(const QStringList &mangleCandidates); void updatesChanged(); void toggleServicesChanged(bool toggleServices);