Repository navigation
feat: add config catalog query and client #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // SPDX-FileCopyrightText: 2026 Uniontech Software Technology Co.,Ltd. | ||
| // | ||
| // SPDX-License-Identifier: LGPL-3.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <QDBusArgument> | ||
| #include <QDBusMetaType> | ||
| #include <QList> | ||
| #include <QMetaType> | ||
| #include <QString> | ||
|
|
||
| class ConfigInfo | ||
| { | ||
| public: | ||
| static void registerMetaType(); | ||
|
|
||
| QString appId; | ||
| QString resourceId; | ||
| QString subpath; | ||
|
|
||
| bool operator==(const ConfigInfo &other) const | ||
| { | ||
| return appId == other.appId && resourceId == other.resourceId && subpath == other.subpath; | ||
| } | ||
| }; | ||
|
|
||
| using ConfigInfoList = QList<ConfigInfo>; | ||
|
|
||
| Q_DECLARE_METATYPE(ConfigInfo) | ||
| Q_DECLARE_METATYPE(ConfigInfoList) | ||
|
|
||
| inline void ConfigInfo::registerMetaType() | ||
| { | ||
| static bool registered = false; | ||
| if (registered) | ||
| return; | ||
|
|
||
| registered = true; | ||
| qRegisterMetaType<ConfigInfo>("ConfigInfo"); | ||
| qDBusRegisterMetaType<ConfigInfo>(); | ||
| qRegisterMetaType<ConfigInfoList>("ConfigInfoList"); | ||
| qDBusRegisterMetaType<ConfigInfoList>(); | ||
| } | ||
|
|
||
| inline QDBusArgument &operator<<(QDBusArgument &argument, const ConfigInfo &info) | ||
| { | ||
| argument.beginStructure(); | ||
| argument << info.appId << info.resourceId << info.subpath; | ||
| argument.endStructure(); | ||
| return argument; | ||
| } | ||
|
|
||
| inline const QDBusArgument &operator>>(const QDBusArgument &argument, ConfigInfo &info) | ||
| { | ||
| argument.beginStructure(); | ||
| argument >> info.appId >> info.resourceId >> info.subpath; | ||
| argument.endStructure(); | ||
| return argument; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,152 @@ | ||||||||||
| // SPDX-FileCopyrightText: 2026 Uniontech Software Technology Co.,Ltd. | ||||||||||
| // | ||||||||||
| // SPDX-License-Identifier: LGPL-3.0-or-later | ||||||||||
|
|
||||||||||
| #include "resourcecatalogclient.h" | ||||||||||
| #include "helper.hpp" | ||||||||||
|
|
||||||||||
| #include "configmanager_internal_interface.h" | ||||||||||
|
|
||||||||||
| #include <QDBusConnection> | ||||||||||
| #include <QDBusPendingReply> | ||||||||||
| #include <QLoggingCategory> | ||||||||||
| #include <QSet> | ||||||||||
|
|
||||||||||
| namespace { | ||||||||||
| const char kServiceName[] = "org.desktopspec.ConfigManager"; | ||||||||||
| const char kObjectPath[] = "/Internal"; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| ResourceCatalogClient &ResourceCatalogClient::instance() | ||||||||||
| { | ||||||||||
| // Keep the QObject alive until process exit. A static value would be | ||||||||||
| // destroyed after QCoreApplication and may access an invalid DBus connection. | ||||||||||
| static ResourceCatalogClient *client = new ResourceCatalogClient(); | ||||||||||
| return *client; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| ResourceCatalogClient::ResourceCatalogClient(QObject *parent) | ||||||||||
| : QObject(parent) | ||||||||||
| , m_interface(new DSGConfigInternal(kServiceName, kObjectPath, | ||||||||||
| QDBusConnection::systemBus(), this)) | ||||||||||
| { | ||||||||||
| ConfigInfo::registerMetaType(); | ||||||||||
|
|
||||||||||
| connect(m_interface, &DSGConfigInternal::configurationsChanged, | ||||||||||
| this, &ResourceCatalogClient::refresh); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| ConfigInfoList ResourceCatalogClient::localConfigurations() const | ||||||||||
| { | ||||||||||
| ConfigInfoList result; | ||||||||||
| for (const QString &app : ::applications()) { | ||||||||||
| const QStringList resources = ::resourcesForApp(app); | ||||||||||
| for (const QString &resource : resources) { | ||||||||||
| result.append({app, resource, QString()}); | ||||||||||
| const QStringList subpaths = ::subpathsForResource(app, resource); | ||||||||||
| for (const QString &subpath : subpaths) | ||||||||||
| result.append({app, resource, subpath}); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| void ResourceCatalogClient::refresh() | ||||||||||
| { | ||||||||||
| m_configurations.clear(); | ||||||||||
| m_configurationsValid = false; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| QStringList ResourceCatalogClient::applications() | ||||||||||
| { | ||||||||||
| QStringList result; | ||||||||||
| result << NoAppId; | ||||||||||
|
|
||||||||||
| QSet<QString> uniqueApps; | ||||||||||
| const auto configs = configurations(); | ||||||||||
| for (const auto &config : configs) { | ||||||||||
| if (!config.appId.isEmpty()) | ||||||||||
| uniqueApps.insert(config.appId); | ||||||||||
| } | ||||||||||
| result.append(uniqueApps.values()); | ||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| ConfigInfoList ResourceCatalogClient::configurations() | ||||||||||
| { | ||||||||||
| if (m_configurationsValid) | ||||||||||
| return m_configurations; | ||||||||||
|
|
||||||||||
| auto reply = m_interface->configurations(); | ||||||||||
| reply.waitForFinished(); | ||||||||||
| if (reply.isError()) { | ||||||||||
| qWarning() << "Failed to query configurations from dde-dconfig-daemon:" | ||||||||||
| << reply.error().message() << ", fallback to local directories."; | ||||||||||
| return localConfigurations(); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (performance): A failed D-Bus query returns Triggers: When the daemon is unreachable and a caller performs multiple catalog queries. Suggested fix: Cache the fallback result and mark it valid, while invalidating it when a later daemon/signal event indicates that a refresh is needed.
Suggested change
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| m_configurations = reply.value(); | ||||||||||
| m_configurationsValid = true; | ||||||||||
| return m_configurations; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| QStringList ResourceCatalogClient::resourcesForApp(const QString &appid) | ||||||||||
| { | ||||||||||
| QStringList result; | ||||||||||
| const auto configs = configurations(); | ||||||||||
| for (const auto &config : configs) { | ||||||||||
| if (config.appId == appid && !result.contains(config.resourceId)) | ||||||||||
| result.append(config.resourceId); | ||||||||||
| } | ||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| QStringList ResourceCatalogClient::resourcesForAllApp() | ||||||||||
| { | ||||||||||
| QStringList result; | ||||||||||
| const auto configs = configurations(); | ||||||||||
| for (const auto &config : configs) { | ||||||||||
| if (config.appId.isEmpty() && !result.contains(config.resourceId)) | ||||||||||
| result.append(config.resourceId); | ||||||||||
| } | ||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| QStringList ResourceCatalogClient::subpathsForResource(const QString &appid, const QString &resourceId) | ||||||||||
| { | ||||||||||
| QStringList result; | ||||||||||
| const auto configs = configurations(); | ||||||||||
| for (const auto &config : configs) { | ||||||||||
| // The root configuration is displayed as the resource item itself. | ||||||||||
| // Only non-empty subpath directories are displayed as child items. | ||||||||||
| if (config.appId == appid && config.resourceId == resourceId | ||||||||||
| && !config.subpath.isEmpty() && !result.contains(config.subpath)) { | ||||||||||
| result.append(config.subpath); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| QStringList ResourceCatalogClient::availableResourcesForApp(const QString &appid) | ||||||||||
| { | ||||||||||
| QStringList result; | ||||||||||
| if (!appid.isEmpty()) | ||||||||||
| result.append(resourcesForApp(appid)); | ||||||||||
| result.append(resourcesForAllApp()); | ||||||||||
|
|
||||||||||
| QSet<QString> unique; | ||||||||||
| for (const QString &resource : result) | ||||||||||
| unique.insert(resource); | ||||||||||
| return unique.values(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| bool ResourceCatalogClient::existAppid(const QString &appid) | ||||||||||
| { | ||||||||||
| return !resourcesForApp(appid).isEmpty(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| bool ResourceCatalogClient::existResource(const QString &appid, const QString &resourceId) | ||||||||||
| { | ||||||||||
| return resourcesForApp(appid).contains(resourceId) | ||||||||||
| || resourcesForAllApp().contains(resourceId); | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // SPDX-FileCopyrightText: 2026 Uniontech Software Technology Co.,Ltd. | ||
| // | ||
| // SPDX-License-Identifier: LGPL-3.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "configinfo.h" | ||
|
|
||
| #include <QObject> | ||
| #include <QStringList> | ||
|
|
||
| class DSGConfigInternal; | ||
|
|
||
| class ResourceCatalogClient : public QObject | ||
| { | ||
| Q_OBJECT | ||
| public: | ||
| static ResourceCatalogClient &instance(); | ||
|
|
||
| QStringList applications(); | ||
| ConfigInfoList configurations(); | ||
| void refresh(); | ||
| QStringList resourcesForApp(const QString &appid); | ||
| QStringList resourcesForAllApp(); | ||
| QStringList subpathsForResource(const QString &appid, const QString &resourceId); | ||
|
|
||
| QStringList availableResourcesForApp(const QString &appid); | ||
| bool existAppid(const QString &appid); | ||
| bool existResource(const QString &appid, const QString &resourceId); | ||
|
|
||
| private: | ||
| explicit ResourceCatalogClient(QObject *parent = nullptr); | ||
| ConfigInfoList localConfigurations() const; | ||
|
|
||
| DSGConfigInternal *m_interface = nullptr; | ||
| ConfigInfoList m_configurations; | ||
| bool m_configurationsValid = false; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| // SPDX-FileCopyrightText: 2026 Uniontech Software Technology Co.,Ltd. | ||
| // | ||
| // SPDX-License-Identifier: LGPL-3.0-or-later | ||
|
|
||
| #include "dconfigcatalog.h" | ||
|
|
||
| #include <DConfigFile> | ||
| #include <QDirIterator> | ||
| #include <QFileInfo> | ||
| #include <QSet> | ||
|
|
||
| namespace { | ||
| QString subpathOf(const QString &baseDir, const QString &dir) | ||
| { | ||
| const QString relativePath = QDir(baseDir).relativeFilePath(dir); | ||
| return relativePath == QLatin1String(".") || relativePath.isEmpty() | ||
| ? QString() : QLatin1Char('/') + relativePath; | ||
| } | ||
|
|
||
| QStringList resourcesForSubpath(const QString &baseDir, const QString &subpath) | ||
| { | ||
| QStringList result; | ||
| QDir dir(baseDir); | ||
| if (!subpath.isEmpty()) | ||
| dir.cd(subpath.mid(1)); | ||
|
|
||
| do { | ||
| const auto entries = dir.entryInfoList({QLatin1String("*.json")}, | ||
| QDir::Files | QDir::Readable); | ||
| for (const QFileInfo &entry : entries) { | ||
| const QString resource = entry.fileName().chopped(QStringLiteral(".json").size()); | ||
| if (!result.contains(resource)) | ||
| result.append(resource); | ||
| } | ||
| } while (dir != QDir(baseDir) && dir.cdUp()); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| void appendConfigurations(ConfigInfoList &result, QSet<QString> &uniqueKeys, | ||
| const QString &appId, const QString &baseDir, | ||
| const QString &localPrefix) | ||
| { | ||
| QStringList dirs; | ||
| dirs.append(baseDir); | ||
|
|
||
| if (appId != NoAppId) { | ||
| QDirIterator dirIterator(baseDir, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable, | ||
| QDirIterator::Subdirectories); | ||
| while (dirIterator.hasNext()) | ||
| dirs.append(dirIterator.next()); | ||
| } | ||
|
|
||
| for (const QString &dir : dirs) { | ||
| const QString subpath = subpathOf(baseDir, dir); | ||
| const QStringList resources = resourcesForSubpath(baseDir, subpath); | ||
| for (const QString &resource : resources) { | ||
| Dtk::Core::DConfigFile file(appId, resource, subpath); | ||
| if (!file.load(localPrefix)) | ||
| continue; | ||
|
|
||
| const QString key = QStringLiteral("%1/%2/%3").arg(appId, resource, subpath); | ||
| if (!uniqueKeys.contains(key)) { | ||
| uniqueKeys.insert(key); | ||
| result.append({appId, resource, subpath}); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| DSGConfigCatalog::DSGConfigCatalog(QObject *parent) | ||
| : QObject(parent) | ||
| { | ||
| ConfigInfo::registerMetaType(); | ||
| } | ||
|
|
||
| void DSGConfigCatalog::setLocalPrefix(const QString &localPrefix) | ||
| { | ||
| if (m_localPrefix == localPrefix) | ||
| return; | ||
|
|
||
| m_localPrefix = localPrefix; | ||
| Q_EMIT configurationsChanged(); | ||
| } | ||
|
|
||
| ConfigInfoList DSGConfigCatalog::configurations() const | ||
| { | ||
| ConfigInfoList result; | ||
| QSet<QString> uniqueKeys; | ||
|
|
||
| for (const QString &dir : Dtk::Core::DConfigMeta::genericMetaDirs(m_localPrefix)) { | ||
| appendConfigurations(result, uniqueKeys, NoAppId, dir, m_localPrefix); | ||
|
|
||
| const auto appDirs = QDir(dir).entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable); | ||
| for (const QString &appDir : appDirs) { | ||
| if (appDir == QLatin1String("overrides")) | ||
| continue; | ||
|
|
||
| appendConfigurations(result, uniqueKeys, appDir, QDir(dir).filePath(appDir), | ||
| m_localPrefix); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // SPDX-FileCopyrightText: 2026 Uniontech Software Technology Co.,Ltd. | ||
| // | ||
| // SPDX-License-Identifier: LGPL-3.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "configinfo.h" | ||
| #include "dconfig_global.h" | ||
|
|
||
| #include <QObject> | ||
|
|
||
| class DSGConfigCatalog : public QObject | ||
| { | ||
| Q_OBJECT | ||
| public: | ||
| explicit DSGConfigCatalog(QObject *parent = nullptr); | ||
|
|
||
| void setLocalPrefix(const QString &localPrefix); | ||
|
|
||
| public Q_SLOTS: | ||
| ConfigInfoList configurations() const; | ||
|
|
||
| Q_SIGNALS: | ||
| void configurationsChanged(); | ||
|
|
||
| private: | ||
| QString m_localPrefix; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (broader_impact): The local fallback and daemon catalog disagree on generic directories containing nested subdirectories: the fallback's
applications()helper treats each nested generic directory as an application and reports its files with that directory name asappId, whileappendConfigurationsdeliberately does not recurse forNoAppId. The same generic nested configuration therefore receives a different identity, or disappears entirely, when the daemon is unavailable.Triggers: When a generic configuration directory contains nested subpath directories and the daemon is unavailable.
Suggested fix: Use the same generic-directory traversal and
NoAppId/subpath interpretation in the fallback as inDSGConfigCatalog, rather than deriving applications from generic directory children.