diff --git a/Plugins/ZShell/Config/config.cpp b/Plugins/ZShell/Config/config.cpp index 6f17668..c0fcc9d 100644 --- a/Plugins/ZShell/Config/config.cpp +++ b/Plugins/ZShell/Config/config.cpp @@ -24,6 +24,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include namespace ZShell::config { @@ -51,7 +57,25 @@ Config::Config(QObject* parent) m_saveTimer.setInterval(300); connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync); + m_reloadTimer.setSingleShot(true); + m_reloadTimer.setInterval(50); + connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync); + + connect( + &m_watcher, + &QFileSystemWatcher::directoryChanged, + this, + &Config::onWatcherEvent); + + connect( + &m_watcher, + &QFileSystemWatcher::fileChanged, + this, + &Config::onWatcherEvent); + loadSync(); + updateWatch(); + m_firstLoadDone = true; } @@ -80,33 +104,84 @@ void Config::loadSync() { m_loading = false; } +void Config::updateWatch() { + const QFileInfo info(filePath()); + + const QString dir = info.absolutePath(); + const QString file = info.absoluteFilePath(); + + if (!m_watcher.directories().contains(dir)) m_watcher.addPath(dir); + if (info.exists() && !m_watcher.files().contains(file)) + m_watcher.addPath(file); +} + +void Config::onWatcherEvent(const QString&) { + updateWatch(); + + const QFileInfo info(filePath()); + if (!info.exists()) return; + + QFile file(info.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) return; + + const QByteArray hash = + QCryptographicHash::hash(file.readAll(), QCryptographicHash::Sha256); + + { + QMutexLocker lock(&m_writeMutex); + + if (hash == m_lastWriteHash) return; + } + + m_reloadPending = true; + + if (!m_loading) m_reloadTimer.start(); +} + void Config::loadAsync() { m_loading = true; + auto future = QtConcurrent::run([this]() { QFile f(filePath()); + if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); + if (doc.isObject()) { QMetaObject::invokeMethod( this, [this, doc]() { loadFromJson(QJsonValue(doc.object())); m_loading = false; + + if (m_reloadPending) m_reloadTimer.start(); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod( this, - [this]() { m_loading = false; }, + [this]() { + m_loading = false; + + if (m_reloadPending) m_reloadTimer.start(); + }, Qt::QueuedConnection); } } else { qInfo() << "Config: failed to reload from" << filePath() << "- using in-memory values"; + QMetaObject::invokeMethod( - this, [this]() { m_loading = false; }, Qt::QueuedConnection); + this, + [this]() { + m_loading = false; + + if (m_reloadPending) m_reloadTimer.start(); + }, + Qt::QueuedConnection); } }); + m_loadFuture = future; } @@ -119,6 +194,9 @@ void Config::load() { } void Config::reloadAsync() { + if (m_loading) return; + + m_reloadPending = false; loadAsync(); } @@ -128,10 +206,24 @@ void Config::scheduleSave() { } void Config::flushAsync() { - auto future = QtConcurrent::run([this]() { - const QByteArray data = - QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented); - writeAtomically(data); + const QByteArray data = + QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented); + + const QByteArray hash = + QCryptographicHash::hash(data, QCryptographicHash::Sha256); + + QThreadPool::globalInstance()->start([this, data, hash]() { + const bool success = writeAtomically(data); + + QMetaObject::invokeMethod( + this, + [this, success, hash]() { + if (!success) return; + + QMutexLocker lock(&m_writeMutex); + m_lastWriteHash = hash; + }, + Qt::QueuedConnection); }); } @@ -140,15 +232,26 @@ void Config::saveNow() { flushAsync(); } -void Config::writeAtomically(const QByteArray& data) { +bool Config::writeAtomically(const QByteArray& data) { QSaveFile f(filePath()); + if (!f.open(QIODevice::WriteOnly)) { qWarning() << "Config: failed to open for atomic write:" << f.errorString(); - return; + return false; } - f.write(data); - if (!f.commit()) qWarning() << "Config: commit failed:" << f.errorString(); + + if (f.write(data) != data.size()) { + qWarning() << "Config: failed to write:" << f.errorString(); + return false; + } + + if (!f.commit()) { + qWarning() << "Config: commit failed:" << f.errorString(); + return false; + } + + return true; } }; // namespace ZShell::config diff --git a/Plugins/ZShell/Config/config.hpp b/Plugins/ZShell/Config/config.hpp index 477b741..1a201f3 100644 --- a/Plugins/ZShell/Config/config.hpp +++ b/Plugins/ZShell/Config/config.hpp @@ -1,11 +1,14 @@ #pragma once #include "configobject.hpp" -#include -#include -#include + +#include +#include #include -#include +#include +#include +#include +#include class QQmlEngine; class QJSEngine; @@ -79,14 +82,23 @@ class Config : public ConfigObject { private Q_SLOTS: void scheduleSave(); void flushAsync(); + void onWatcherEvent(const QString& path); private: QString filePath() const; - void writeAtomically(const QByteArray& data); + bool writeAtomically(const QByteArray& data); + void updateWatch(); void loadSync(); void loadAsync(); QTimer m_saveTimer; + QTimer m_reloadTimer; + QFileSystemWatcher m_watcher; + + QMutex m_writeMutex; + QByteArray m_lastWriteHash; + + bool m_reloadPending = false; bool m_loading = false; bool m_firstLoadDone = false; QFuture m_loadFuture;