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
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user