diff options
| -rw-r--r-- | CMakeLists.txt | 7 | ||||
| -rw-r--r-- | autotests/kconfigtest.cpp | 79 | ||||
| -rw-r--r-- | autotests/kconfigtest.h | 1 | ||||
| -rw-r--r-- | src/core/CMakeLists.txt | 10 | ||||
| -rw-r--r-- | src/core/config-kconfig.h.cmake | 1 | ||||
| -rw-r--r-- | src/core/kconfig.cpp | 55 | ||||
| -rw-r--r-- | src/core/kconfig_p.h | 2 | ||||
| -rw-r--r-- | src/core/kconfigbase.h | 14 | ||||
| -rw-r--r-- | src/core/kconfigdata.cpp | 7 | ||||
| -rw-r--r-- | src/core/kconfigdata.h | 7 | ||||
| -rw-r--r-- | src/core/kconfigwatcher.cpp | 107 | ||||
| -rw-r--r-- | src/core/kconfigwatcher.h | 68 | 
12 files changed, 347 insertions, 11 deletions
| diff --git a/CMakeLists.txt b/CMakeLists.txt index a2c900e9..216b7cae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,13 @@ option(KCONFIG_USE_GUI "Build components using Qt5Gui" ON)  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.cpp b/autotests/kconfigtest.cpp index 35075d1a..01c08770 100644 --- a/autotests/kconfigtest.cpp +++ b/autotests/kconfigtest.cpp @@ -23,6 +23,8 @@  #include "kconfigtest.h"  #include "helper.h" +#include "config-kconfig.h" +  #include <QtTest>  #include <qtemporarydir.h>  #include <QStandardPaths> @@ -30,6 +32,7 @@  #include <ksharedconfig.h>  #include <kconfiggroup.h> +#include <kconfigwatcher.h>  #ifdef Q_OS_UNIX  #include <utime.h> @@ -43,6 +46,8 @@ KCONFIGGROUP_DECLARE_FLAGS_QOBJECT(KConfigTest, Flags)  QTEST_MAIN(KConfigTest) +Q_DECLARE_METATYPE(KConfigGroup) +  static QString homePath()  {  #ifdef Q_OS_WIN @@ -101,6 +106,7 @@ void KConfigTest::initTestCase()  {      // ensure we don't use files in the real config directory      QStandardPaths::setTestModeEnabled(true); +    qRegisterMetaType<KConfigGroup>();      // to make sure all files from a previous failed run are deleted      cleanupTestCase(); @@ -1785,3 +1791,76 @@ void KConfigTest::testThreads()          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<QVariant> a, QList<QVariant> b) { +        return a[0].value<KConfigGroup>().name() <  b[0].value<KConfigGroup>().name(); +    }); + +    QCOMPARE(watcherSpy[0][0].value<KConfigGroup>().name(), "TopLevelGroup"); +    QCOMPARE(watcherSpy[0][1].value<QByteArrayList>(), QByteArrayList({"entryA"})); + +    QCOMPARE(watcherSpy[1][0].value<KConfigGroup>().name(), "aSubGroup"); +    QCOMPARE(watcherSpy[1][0].value<KConfigGroup>().parent().name(), "TopLevelGroup"); +    QCOMPARE(watcherSpy[1][1].value<QByteArrayList>(), 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<KConfigGroup>().name(), "TopLevelGroup"); +    QCOMPARE(watcherSpy[0][1].value<QByteArrayList>(), 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<KConfigGroup>().name(), "aSubGroup"); +    QCOMPARE(watcherSpy[0][1].value<QByteArrayList>(), 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<KConfigGroup>().name(), "TopLevelGroup"); +    QCOMPARE(watcherSpy[0][1].value<QByteArrayList>(), QByteArrayList({"someGlobalEntry"})); + +    //watching another file should have only triggered from the kdeglobals change +    QCOMPARE(otherWatcherSpy.count(), 1); +    QCOMPARE(otherWatcherSpy[0][0].value<KConfigGroup>().name(), "TopLevelGroup"); +    QCOMPARE(otherWatcherSpy[0][1].value<QByteArrayList>(), QByteArrayList({"someGlobalEntry"})); +} diff --git a/autotests/kconfigtest.h b/autotests/kconfigtest.h index 367e85de..708da042 100644 --- a/autotests/kconfigtest.h +++ b/autotests/kconfigtest.h @@ -78,6 +78,7 @@ private Q_SLOTS:      void testKdeGlobals();      void testNewlines();      void testXdgListEntry(); +    void testNotify();      void testThreads(); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f06c803f..28aad4f8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -10,8 +10,11 @@ set(libkconfigcore_SRCS     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_compile_definitions(KF5ConfigCore  target_include_directories(KF5ConfigCore INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF5}/KConfigCore>")  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 @@ ecm_generate_headers(KConfigCore_HEADERS    KCoreConfigSkeleton    KEMailSettings    ConversionCheck +  KConfigWatcher    REQUIRED_HEADERS KConfigCore_HEADERS  ) @@ -68,6 +77,7 @@ if (PythonModuleGeneration_FOUND)        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 index 00000000..a4a7519d --- /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 index e4c9935b..df3ad471 100644 --- 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 <cstdlib>  #include <fcntl.h> @@ -54,6 +56,12 @@ static inline int pclose(FILE *stream)  #include <QBasicMutex>  #include <QMutexLocker> +#if KCONFIG_USE_DBUS +#include <QDBusMessage> +#include <QDBusConnection> +#include <QDBusMetaType> +#endif +  bool KConfigPrivate::mappingsRegistered = false;  Q_GLOBAL_STATIC(QStringList, s_globalFiles) // For caching purposes. @@ -424,6 +432,9 @@ bool KConfig::sync()          return false;      } +    QHash<QString, QByteArrayList> notifyGroupsLocal; +    QHash<QString, QByteArrayList> notifyGroupsGlobal; +      if (d->bDirty && d->mBackend) {          const QByteArray utf8Locale(locale().toUtf8()); @@ -439,16 +450,20 @@ bool KConfig::sync()          // 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 @@ bool KConfig::sync()              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<QString, QByteArrayList> &changes, const QString &path) +{ +#if KCONFIG_USE_DBUS +    qDBusRegisterMetaType<QByteArrayList>(); + +    qDBusRegisterMetaType<QHash<QString, QByteArrayList>>(); + +    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 @@ void KConfig::markAsClean()      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 @@ KEntryMap::EntryOptions convertToOptions(KConfig::WriteConfigFlags flags)      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 index dfb3875a..7433ab2f 100644 --- a/src/core/kconfig_p.h +++ b/src/core/kconfig_p.h @@ -60,6 +60,8 @@ public:      QSet<QByteArray> allSubGroups(const QByteArray &parentGroup) const;      bool hasNonDeletedEntries(const QByteArray &group) const; +    void notifyClients(const QHash<QString, QByteArrayList> &changes, const QString &path); +      static QString expandString(const QString &value);  protected: diff --git a/src/core/kconfigbase.h b/src/core/kconfigbase.h index b7403de1..62a0cc31 100644 --- a/src/core/kconfigbase.h +++ b/src/core/kconfigbase.h @@ -58,13 +58,19 @@ public:           */          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.cpp b/src/core/kconfigdata.cpp index 6ef6af07..d80b7d07 100644 --- a/src/core/kconfigdata.cpp +++ b/src/core/kconfigdata.cpp @@ -134,6 +134,8 @@ bool KEntryMap::setEntry(const QByteArray &group, const QByteArray &key, const Q      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 @@ bool KEntryMap::getEntryOption(const QMap< KEntryKey, KEntry >::ConstIterator &i              return it->bDeleted;          case EntryExpansion:              return it->bExpand; +        case EntryNotify: +            return it->bNotify;          default:              break; // fall through          } @@ -296,6 +300,9 @@ void KEntryMap::setEntryOption(QMap< KEntryKey, KEntry >::Iterator it, KEntryMap          case EntryExpansion:              it->bExpand = bf;              break; +        case EntryNotify: +            it->bNotify = bf; +            break;          default:              break; // fall through          } diff --git a/src/core/kconfigdata.h b/src/core/kconfigdata.h index f84e51e3..2a5c643d 100644 --- a/src/core/kconfigdata.h +++ b/src/core/kconfigdata.h @@ -37,7 +37,7 @@ struct KEntry {      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 @@ struct KEntry {       * 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 @@ public:          EntryExpansion = 16,          EntryRawKey = 32,          EntryLocalizedCountry = 64, +        EntryNotify = 128,          EntryDefault = (SearchDefaults << 16),          EntryLocalized = (SearchLocalized << 16)      }; diff --git a/src/core/kconfigwatcher.cpp b/src/core/kconfigwatcher.cpp new file mode 100644 index 00000000..96120c6a --- /dev/null +++ b/src/core/kconfigwatcher.cpp @@ -0,0 +1,107 @@ +/* + *   Copyright 2018 David Edmundson <davidedmundson@kde.org> + * + *   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 <QDBusConnection> +#include <QDBusMessage> +#include <QDBusMetaType> +#endif + +#include <QDebug> +#include <QThreadStorage> +#include <QHash> + +class KConfigWatcherPrivate { +public: +    KSharedConfig::Ptr m_config; +}; + +KConfigWatcher::Ptr KConfigWatcher::create(const KSharedConfig::Ptr &config) +{ +    static QThreadStorage<QHash<KSharedConfig*, QWeakPointer<KConfigWatcher>>> 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<QByteArrayList>(); +    qDBusRegisterMetaType<QHash<QString, QByteArrayList>>(); + +    d->m_config = config; + +    QStringList watchedPaths; +    watchedPaths <<QStringLiteral("/") + d->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<QString, QByteArrayList>))); +    } +#else +    qWarning() << "Use of KConfigWatcher without DBus support. You will not receive updates" +#endif +} + +void KConfigWatcher::onConfigChangeNotification(const QHash<QString, QByteArrayList> &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()); +    } +} + diff --git a/src/core/kconfigwatcher.h b/src/core/kconfigwatcher.h new file mode 100644 index 00000000..b73d3fc2 --- /dev/null +++ b/src/core/kconfigwatcher.h @@ -0,0 +1,68 @@ +/* + *   Copyright 2018 David Edmundson <davidedmundson@kde.org> + * + *   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 <QObject> + +#include <KSharedConfig> +#include <KConfigGroup> + +#include <kconfigcore_export.h> + +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<KConfigWatcher> 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<QString, QByteArrayList> &changes); + +private: +    KConfigWatcher(const KSharedConfig::Ptr &config); +    Q_DISABLE_COPY(KConfigWatcher) +    KConfigWatcherPrivate *const d; +}; + +#endif | 
