diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,13 @@ if(KCONFIG_USE_GUI) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Gui) endif() + +option(KCONFIG_USE_DBUS "Build components using Qt5DBus" ON) +if(KCONFIG_USE_DBUS) + find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED DBus) +endif() + + include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) diff --git a/autotests/kconfigtest.h b/autotests/kconfigtest.h --- a/autotests/kconfigtest.h +++ b/autotests/kconfigtest.h @@ -78,6 +78,7 @@ void testKdeGlobals(); void testNewlines(); void testXdgListEntry(); + void testNotify(); void testThreads(); diff --git a/autotests/kconfigtest.cpp b/autotests/kconfigtest.cpp --- a/autotests/kconfigtest.cpp +++ b/autotests/kconfigtest.cpp @@ -23,13 +23,16 @@ #include "kconfigtest.h" #include "helper.h" +#include "config-kconfig.h" + #include #include #include #include #include #include +#include #ifdef Q_OS_UNIX #include @@ -43,6 +46,8 @@ QTEST_MAIN(KConfigTest) +Q_DECLARE_METATYPE(KConfigGroup) + static QString homePath() { #ifdef Q_OS_WIN @@ -101,6 +106,7 @@ { // ensure we don't use files in the real config directory QStandardPaths::setTestModeEnabled(true); + qRegisterMetaType(); // to make sure all files from a previous failed run are deleted cleanupTestCase(); @@ -1785,3 +1791,76 @@ f.waitForFinished(); } } + +void KConfigTest::testNotify() +{ +#if !KCONFIG_USE_DBUS + QSKIP("KConfig notification requires DBus") +#endif + + KConfig config(TEST_SUBDIR "kconfigtest"); + auto myConfigGroup = KConfigGroup(&config, "TopLevelGroup"); + + //mimics a config in another process, which is watching for events + auto remoteConfig = KSharedConfig::openConfig(TEST_SUBDIR "kconfigtest"); + KConfigWatcher::Ptr watcher = KConfigWatcher::create(remoteConfig); + + //some random config that shouldn't be changing when kconfigtest changes, only on kdeglobals + auto otherRemoteConfig = KSharedConfig::openConfig(TEST_SUBDIR "kconfigtest2"); + KConfigWatcher::Ptr otherWatcher = KConfigWatcher::create(otherRemoteConfig); + + QSignalSpy watcherSpy(watcher.data(), &KConfigWatcher::configChanged); + QSignalSpy otherWatcherSpy(otherWatcher.data(), &KConfigWatcher::configChanged); + + //write entries in a group and subgroup + myConfigGroup.writeEntry("entryA", "foo", KConfig::Persistent | KConfig::Notify); + auto subGroup = myConfigGroup.group("aSubGroup"); + subGroup.writeEntry("entry1", "foo", KConfig::Persistent | KConfig::Notify); + subGroup.writeEntry("entry2", "foo", KConfig::Persistent | KConfig::Notify); + config.sync(); + watcherSpy.wait(); + QCOMPARE(watcherSpy.count(), 2); + + std::sort(watcherSpy.begin(), watcherSpy.end(), [] (QList a, QList b) { + return a[0].value().name() < b[0].value().name(); + }); + + QCOMPARE(watcherSpy[0][0].value().name(), "TopLevelGroup"); + QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entryA"})); + + QCOMPARE(watcherSpy[1][0].value().name(), "aSubGroup"); + QCOMPARE(watcherSpy[1][0].value().parent().name(), "TopLevelGroup"); + QCOMPARE(watcherSpy[1][1].value(), QByteArrayList({"entry1", "entry2"})); + + //delete an entry + watcherSpy.clear(); + myConfigGroup.deleteEntry("entryA", KConfig::Persistent | KConfig::Notify); + config.sync(); + watcherSpy.wait(); + QCOMPARE(watcherSpy.count(), 1); + QCOMPARE(watcherSpy[0][0].value().name(), "TopLevelGroup"); + QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entryA"})); + + //deleting a group, should notify that every entry in that group has changed + watcherSpy.clear(); + myConfigGroup.deleteGroup("aSubGroup", KConfig::Persistent | KConfig::Notify); + config.sync(); + watcherSpy.wait(); + QCOMPARE(watcherSpy.count(), 1); + QCOMPARE(watcherSpy[0][0].value().name(), "aSubGroup"); + QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entry1", "entry2"})); + + //global write still triggers our notification + watcherSpy.clear(); + myConfigGroup.writeEntry("someGlobalEntry", "foo", KConfig::Persistent | KConfig::Notify | KConfig::Global); + config.sync(); + watcherSpy.wait(); + QCOMPARE(watcherSpy.count(), 1); + QCOMPARE(watcherSpy[0][0].value().name(), "TopLevelGroup"); + QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"someGlobalEntry"})); + + //watching another file should have only triggered from the kdeglobals change + QCOMPARE(otherWatcherSpy.count(), 1); + QCOMPARE(otherWatcherSpy[0][0].value().name(), "TopLevelGroup"); + QCOMPARE(otherWatcherSpy[0][1].value(), QByteArrayList({"someGlobalEntry"})); +} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -10,8 +10,11 @@ kcoreconfigskeleton.cpp kauthorized.cpp kemailsettings.cpp + kconfigwatcher.cpp ) +configure_file(config-kconfig.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kconfig.h ) + add_library(KF5ConfigCore ${libkconfigcore_SRCS}) generate_export_header(KF5ConfigCore BASE_NAME KConfigCore) add_library(KF5::ConfigCore ALIAS KF5ConfigCore) @@ -24,6 +27,11 @@ target_include_directories(KF5ConfigCore INTERFACE "$") target_link_libraries(KF5ConfigCore PUBLIC Qt5::Core) + +if(KCONFIG_USE_DBUS) + target_link_libraries(KF5ConfigCore PRIVATE Qt5::DBus) +endif() + if(WIN32) target_link_libraries(KF5ConfigCore PRIVATE ${KDEWIN_LIBRARIES}) endif() @@ -44,6 +52,7 @@ KCoreConfigSkeleton KEMailSettings ConversionCheck + KConfigWatcher REQUIRED_HEADERS KConfigCore_HEADERS ) @@ -68,6 +77,7 @@ kcoreconfigskeleton.h kemailsettings.h conversioncheck.h + kconfigwatcher.h ) endif() diff --git a/src/core/config-kconfig.h.cmake b/src/core/config-kconfig.h.cmake new file mode 100644 --- /dev/null +++ b/src/core/config-kconfig.h.cmake @@ -0,0 +1 @@ +#cmakedefine01 KCONFIG_USE_DBUS diff --git a/src/core/kconfig.cpp b/src/core/kconfig.cpp --- a/src/core/kconfig.cpp +++ b/src/core/kconfig.cpp @@ -23,6 +23,8 @@ #include "kconfig.h" #include "kconfig_p.h" +#include "config-kconfig.h" + #include #include @@ -54,6 +56,12 @@ #include #include +#if KCONFIG_USE_DBUS +#include +#include +#include +#endif + bool KConfigPrivate::mappingsRegistered = false; Q_GLOBAL_STATIC(QStringList, s_globalFiles) // For caching purposes. @@ -424,6 +432,9 @@ return false; } + QHash notifyGroupsLocal; + QHash notifyGroupsGlobal; + if (d->bDirty && d->mBackend) { const QByteArray utf8Locale(locale().toUtf8()); @@ -439,16 +450,20 @@ // Rewrite global/local config only if there is a dirty entry in it. bool writeGlobals = false; bool writeLocals = false; - Q_FOREACH (const KEntry &e, d->entryMap) { + + for (auto it = d->entryMap.constBegin(); it != d->entryMap.constEnd(); ++it) { + auto e = it.value(); if (e.bDirty) { if (e.bGlobal) { writeGlobals = true; + if (e.bNotify) { + notifyGroupsGlobal[QString::fromUtf8(it.key().mGroup)] << it.key().mKey; + } } else { writeLocals = true; - } - - if (writeGlobals && writeLocals) { - break; + if (e.bNotify) { + notifyGroupsLocal[QString::fromUtf8(it.key().mGroup)] << it.key().mKey; + } } } } @@ -485,9 +500,35 @@ d->mBackend->unlock(); } } + + if (!notifyGroupsLocal.isEmpty()) { + d->notifyClients(notifyGroupsLocal, QStringLiteral("/") + name()); + } + if (!notifyGroupsGlobal.isEmpty()) { + d->notifyClients(notifyGroupsGlobal, QStringLiteral("/kdeglobals")); + } + return !d->bDirty; } +void KConfigPrivate::notifyClients(const QHash &changes, const QString &path) +{ +#if KCONFIG_USE_DBUS + qDBusRegisterMetaType(); + + qDBusRegisterMetaType>(); + + QDBusMessage message = QDBusMessage::createSignal(path, + QStringLiteral("org.kde.kconfig.notify"), + QStringLiteral("ConfigChanged")); + message.setArguments({QVariant::fromValue(changes)}); + QDBusConnection::sessionBus().send(message); +#else + Q_UNUSED(changes) + Q_UNUSED(path) +#endif +} + void KConfig::markAsClean() { Q_D(KConfig); @@ -497,6 +538,7 @@ const KEntryMapIterator theEnd = d->entryMap.end(); for (KEntryMapIterator it = d->entryMap.begin(); it != theEnd; ++it) { it->bDirty = false; + it->bNotify = false; } } @@ -874,6 +916,9 @@ if (flags & KConfig::Localized) { options |= KEntryMap::EntryLocalized; } + if (flags & KConfig::Notify) { + options |= KEntryMap::EntryNotify; + } return options; } diff --git a/src/core/kconfig_p.h b/src/core/kconfig_p.h --- a/src/core/kconfig_p.h +++ b/src/core/kconfig_p.h @@ -60,6 +60,8 @@ QSet allSubGroups(const QByteArray &parentGroup) const; bool hasNonDeletedEntries(const QByteArray &group) const; + void notifyClients(const QHash &changes, const QString &path); + static QString expandString(const QString &value); protected: diff --git a/src/core/kconfigbase.h b/src/core/kconfigbase.h --- a/src/core/kconfigbase.h +++ b/src/core/kconfigbase.h @@ -57,14 +57,20 @@ * application specific config file. */ Localized = 0x04, + /**< + * Notify remote KConfigWatchers of changes (requires DBus support) + * Implied persistent + * @since 5.51 + */ + Notify = 0x08 | Persistent, /**< * Add the locale tag to the key when writing it. */ Normal = Persistent - /**< - * Save the entry to the application specific config file without - * a locale tag. This is the default. - */ + /**< + * Save the entry to the application specific config file without + * a locale tag. This is the default. + */ }; Q_DECLARE_FLAGS(WriteConfigFlags, WriteConfigFlag) diff --git a/src/core/kconfigdata.h b/src/core/kconfigdata.h --- a/src/core/kconfigdata.h +++ b/src/core/kconfigdata.h @@ -37,7 +37,7 @@ KEntry() : mValue(), bDirty(false), bGlobal(false), bImmutable(false), bDeleted(false), bExpand(false), bReverted(false), - bLocalizedCountry(false) {} + bLocalizedCountry(false), bNotify(false) {} /** @internal */ QByteArray mValue; /** @@ -69,11 +69,13 @@ * if @c true the value references language and country, e.g. "de_DE". **/ bool bLocalizedCountry: 1; + + bool bNotify: 1; }; // These operators are used to check whether an entry which is about // to be written equals the previous value. As such, this intentionally -// omits the dirty flag from the comparison. +// omits the dirty/notify flag from the comparison. inline bool operator ==(const KEntry &k1, const KEntry &k2) { return k1.bGlobal == k2.bGlobal && k1.bImmutable == k2.bImmutable @@ -172,6 +174,7 @@ EntryExpansion = 16, EntryRawKey = 32, EntryLocalizedCountry = 64, + EntryNotify = 128, EntryDefault = (SearchDefaults << 16), EntryLocalized = (SearchLocalized << 16) }; diff --git a/src/core/kconfigdata.cpp b/src/core/kconfigdata.cpp --- a/src/core/kconfigdata.cpp +++ b/src/core/kconfigdata.cpp @@ -134,6 +134,8 @@ e.mValue = value; e.bDirty = e.bDirty || (options & EntryDirty); + e.bNotify = e.bNotify || (options & EntryNotify); + e.bGlobal = (options & EntryGlobal); //we can't use || here, because changes to entries in //kdeglobals would be written to kdeglobals instead //of the local config file, regardless of the globals flag @@ -269,6 +271,8 @@ return it->bDeleted; case EntryExpansion: return it->bExpand; + case EntryNotify: + return it->bNotify; default: break; // fall through } @@ -296,6 +300,9 @@ case EntryExpansion: it->bExpand = bf; break; + case EntryNotify: + it->bNotify = bf; + break; default: break; // fall through } diff --git a/src/core/kconfigwatcher.h b/src/core/kconfigwatcher.h new file mode 100644 --- /dev/null +++ b/src/core/kconfigwatcher.h @@ -0,0 +1,68 @@ +/* + * Copyright 2018 David Edmundson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KCONFIGWATCHER_H +#define KCONFIGWATCHER_H + +#include + +#include +#include + +#include + +class KConfigWatcherPrivate; + +/* + * Notifies when another client has updated this config file with the Notify flag set. + * @since 5.51 + */ +class KCONFIGCORE_EXPORT KConfigWatcher: public QObject +{ + Q_OBJECT +public: + typedef QSharedPointer Ptr; + + /* + * Instantiate a ConfigWatcher for a given config + * + * @note any additional config sources should be set before this point. + */ + static Ptr create(const KSharedConfig::Ptr &config); + +Q_SIGNALS: + /** + * Emitted when a config group has changed + * The config will be reloaded before this signal is emitted + * + * @arg group the config group that has changed + * @arg names a list of entries that have changed within that group + */ + void configChanged(const KConfigGroup &group, const QByteArrayList &names); + +private Q_SLOTS: + void onConfigChangeNotification(const QHash &changes); + +private: + KConfigWatcher(const KSharedConfig::Ptr &config); + Q_DISABLE_COPY(KConfigWatcher) + KConfigWatcherPrivate *const d; +}; + +#endif diff --git a/src/core/kconfigwatcher.cpp b/src/core/kconfigwatcher.cpp new file mode 100644 --- /dev/null +++ b/src/core/kconfigwatcher.cpp @@ -0,0 +1,107 @@ +/* + * Copyright 2018 David Edmundson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kconfigwatcher.h" + +#include "config-kconfig.h" + +#if KCONFIG_USE_DBUS +#include +#include +#include +#endif + +#include +#include +#include + +class KConfigWatcherPrivate { +public: + KSharedConfig::Ptr m_config; +}; + +KConfigWatcher::Ptr KConfigWatcher::create(const KSharedConfig::Ptr &config) +{ + static QThreadStorage>> watcherList; + + auto c = config.data(); + KConfigWatcher::Ptr watcher; + + if (!watcherList.localData().contains(c)) { + watcher = KConfigWatcher::Ptr(new KConfigWatcher(config)); + + watcherList.localData().insert(c, watcher.toWeakRef()); + + QObject::connect(watcher.data(), &QObject::destroyed, [c]() { + watcherList.localData().remove(c); + }); + } + return watcherList.localData().value(c).toStrongRef(); +} + +KConfigWatcher::KConfigWatcher(const KSharedConfig::Ptr &config): + QObject (nullptr), + d(new KConfigWatcherPrivate) +{ + Q_ASSERT(config); +#if KCONFIG_USE_DBUS + + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + + d->m_config = config; + + QStringList watchedPaths; + watchedPaths <m_config->name(); + for (const QString file: d->m_config->additionalConfigSources()) { + watchedPaths << QStringLiteral("/") + file; + } + if (d->m_config->openFlags() & KConfig::IncludeGlobals) { + watchedPaths << QStringLiteral("/kdeglobals"); + } + + for(const QString &path: qAsConst(watchedPaths)) { + QDBusConnection::sessionBus().connect(QString(), + path, + QStringLiteral("org.kde.kconfig.notify"), + QStringLiteral("ConfigChanged"), + this, + SLOT(onConfigChangeNotification(QHash))); + } +#else + qWarning() << "Use of KConfigWatcher without DBus support. You will not receive updates" +#endif +} + +void KConfigWatcher::onConfigChangeNotification(const QHash &changes) +{ + //should we ever need it we can determine the file changed with QDbusContext::message().path(), but it doesn't seem too useful + + d->m_config->reparseConfiguration(); + + for(auto it = changes.constBegin(); it != changes.constEnd(); it++) { + KConfigGroup group = d->m_config->group(QString());//top level group + const auto parts = it.key().split(QLatin1Char('\x1d')); //magic char, see KConfig + for(const QString &groupName: parts) { + group = group.group(groupName); + } + emit configChanged(group, it.value()); + } +} +