aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt7
-rw-r--r--autotests/kconfigtest.cpp79
-rw-r--r--autotests/kconfigtest.h1
-rw-r--r--src/core/CMakeLists.txt10
-rw-r--r--src/core/config-kconfig.h.cmake1
-rw-r--r--src/core/kconfig.cpp55
-rw-r--r--src/core/kconfig_p.h2
-rw-r--r--src/core/kconfigbase.h14
-rw-r--r--src/core/kconfigdata.cpp7
-rw-r--r--src/core/kconfigdata.h7
-rw-r--r--src/core/kconfigwatcher.cpp107
-rw-r--r--src/core/kconfigwatcher.h68
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