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 <QDir>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
|
#include <qfilesystemwatcher.h>
|
||||||
|
#include <qthreadpool.h>
|
||||||
|
|
||||||
namespace ZShell::config {
|
namespace ZShell::config {
|
||||||
|
|
||||||
@@ -51,7 +57,25 @@ Config::Config(QObject* parent)
|
|||||||
m_saveTimer.setInterval(300);
|
m_saveTimer.setInterval(300);
|
||||||
connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync);
|
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();
|
loadSync();
|
||||||
|
updateWatch();
|
||||||
|
|
||||||
m_firstLoadDone = true;
|
m_firstLoadDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,33 +104,84 @@ void Config::loadSync() {
|
|||||||
m_loading = false;
|
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() {
|
void Config::loadAsync() {
|
||||||
m_loading = true;
|
m_loading = true;
|
||||||
|
|
||||||
auto future = QtConcurrent::run([this]() {
|
auto future = QtConcurrent::run([this]() {
|
||||||
QFile f(filePath());
|
QFile f(filePath());
|
||||||
|
|
||||||
if (f.open(QIODevice::ReadOnly)) {
|
if (f.open(QIODevice::ReadOnly)) {
|
||||||
const auto doc = QJsonDocument::fromJson(f.readAll());
|
const auto doc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
|
||||||
if (doc.isObject()) {
|
if (doc.isObject()) {
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this, doc]() {
|
[this, doc]() {
|
||||||
loadFromJson(QJsonValue(doc.object()));
|
loadFromJson(QJsonValue(doc.object()));
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
|
|
||||||
|
if (m_reloadPending) m_reloadTimer.start();
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
} else {
|
} else {
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
this,
|
this,
|
||||||
[this]() { m_loading = false; },
|
[this]() {
|
||||||
|
m_loading = false;
|
||||||
|
|
||||||
|
if (m_reloadPending) m_reloadTimer.start();
|
||||||
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qInfo() << "Config: failed to reload from" << filePath()
|
qInfo() << "Config: failed to reload from" << filePath()
|
||||||
<< "- using in-memory values";
|
<< "- using in-memory values";
|
||||||
|
|
||||||
QMetaObject::invokeMethod(
|
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;
|
m_loadFuture = future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +194,9 @@ void Config::load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Config::reloadAsync() {
|
void Config::reloadAsync() {
|
||||||
|
if (m_loading) return;
|
||||||
|
|
||||||
|
m_reloadPending = false;
|
||||||
loadAsync();
|
loadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +206,24 @@ void Config::scheduleSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Config::flushAsync() {
|
void Config::flushAsync() {
|
||||||
auto future = QtConcurrent::run([this]() {
|
const QByteArray data =
|
||||||
const QByteArray data =
|
QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented);
|
||||||
QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented);
|
|
||||||
writeAtomically(data);
|
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();
|
flushAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::writeAtomically(const QByteArray& data) {
|
bool Config::writeAtomically(const QByteArray& data) {
|
||||||
QSaveFile f(filePath());
|
QSaveFile f(filePath());
|
||||||
|
|
||||||
if (!f.open(QIODevice::WriteOnly)) {
|
if (!f.open(QIODevice::WriteOnly)) {
|
||||||
qWarning() << "Config: failed to open for atomic write:"
|
qWarning() << "Config: failed to open for atomic write:"
|
||||||
<< f.errorString();
|
<< 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
|
}; // namespace ZShell::config
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "configobject.hpp"
|
#include "configobject.hpp"
|
||||||
#include <qqmlregistration.h>
|
|
||||||
#include <QTimer>
|
#include <QByteArray>
|
||||||
#include <QtConcurrent/QtConcurrent>
|
#include <QFileSystemWatcher>
|
||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
#include <qtmetamacros.h>
|
#include <QMutex>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <qqmlregistration.h>
|
||||||
|
#include <QtConcurrent/QtConcurrent>
|
||||||
|
|
||||||
class QQmlEngine;
|
class QQmlEngine;
|
||||||
class QJSEngine;
|
class QJSEngine;
|
||||||
@@ -79,14 +82,23 @@ class Config : public ConfigObject {
|
|||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void scheduleSave();
|
void scheduleSave();
|
||||||
void flushAsync();
|
void flushAsync();
|
||||||
|
void onWatcherEvent(const QString& path);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString filePath() const;
|
QString filePath() const;
|
||||||
void writeAtomically(const QByteArray& data);
|
bool writeAtomically(const QByteArray& data);
|
||||||
|
void updateWatch();
|
||||||
void loadSync();
|
void loadSync();
|
||||||
void loadAsync();
|
void loadAsync();
|
||||||
|
|
||||||
QTimer m_saveTimer;
|
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_loading = false;
|
||||||
bool m_firstLoadDone = false;
|
bool m_firstLoadDone = false;
|
||||||
QFuture<void> m_loadFuture;
|
QFuture<void> m_loadFuture;
|
||||||
|
|||||||
Reference in New Issue
Block a user