add file+dir watcher & gatekeep reloads
C++ / fmt (pull_request) Successful in 5s
JS/TS / lint (pull_request) Successful in 21s
JS/TS / fmt (pull_request) Successful in 20s
Python / lint (pull_request) Successful in 32s
Python / fmt (pull_request) Successful in 34s
Python / test (pull_request) Successful in 58s
Python / typecheck (pull_request) Failing after 58s
Rust / fmt (pull_request) Successful in 43s
Rust / build (pull_request) Successful in 1m48s
C++ / build (pull_request) Successful in 2m36s
Rust / clippy (pull_request) Successful in 1m18s
Python / buildcheck (pull_request) Successful in 2m21s
C++ / clang-tidy (pull_request) Successful in 3m54s

This commit is contained in:
2026-08-14 18:40:50 +02:00
parent bda2c3821e
commit 97ecf68b46
2 changed files with 130 additions and 15 deletions
+113 -10
View File
@@ -24,6 +24,12 @@
#include <QDir>
#include <QDebug>
#include <QFutureWatcher>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <qcontainerfwd.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qthreadpool.h>
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
+17 -5
View File
@@ -1,11 +1,14 @@
#pragma once
#include "configobject.hpp"
#include <qqmlregistration.h>
#include <QTimer>
#include <QtConcurrent/QtConcurrent>
#include <QByteArray>
#include <QFileSystemWatcher>
#include <QFuture>
#include <qtmetamacros.h>
#include <QMutex>
#include <QTimer>
#include <qqmlregistration.h>
#include <QtConcurrent/QtConcurrent>
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<void> m_loadFuture;