完成框架目录和模块重构;新增核心、单例、工具、加密及框架 SDK;统一任务、相机和 RPC 接口,并增加离线检测、相机标定、备份恢复、版本管理、崩溃守护和心跳监控功能。
450 lines
17 KiB
C++
450 lines
17 KiB
C++
#pragma once
|
|
|
|
#include <eui_tools/export.h>
|
|
#include <nlohmannJson.hpp>
|
|
|
|
#include <QCoreApplication>
|
|
#include <QCryptographicHash>
|
|
#include <QDateTime>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QLockFile>
|
|
#include <QString>
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#include <openssl/evp.h>
|
|
#include <openssl/rand.h>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
#if defined(Q_OS_UNIX)
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
// JSON 文件读写封装。默认使用 ordered_json,配置文件可保持原有字段顺序。
|
|
class euiJson : public nlohmann::json {
|
|
public:
|
|
using Base = nlohmann::json;
|
|
using Json = nlohmann::json;
|
|
using OrderedJson = nlohmann::ordered_json;
|
|
|
|
// 直接使用 euiJson 时,它与 nlohmann::json 具有相同的值语义和 API。
|
|
using Base::Base;
|
|
euiJson() = default;
|
|
euiJson(const Base& value) : Base(value) {}
|
|
euiJson(Base&& value) noexcept : Base(std::move(value)) {}
|
|
euiJson& operator=(const Base& value) { Base::operator=(value); return *this; }
|
|
euiJson& operator=(Base&& value) noexcept { Base::operator=(std::move(value)); return *this; }
|
|
|
|
struct BackupInfo {
|
|
std::filesystem::path path;
|
|
QString fileName;
|
|
QDateTime backupTime;
|
|
int index = -1;
|
|
};
|
|
|
|
struct ReadResult {
|
|
bool success = false;
|
|
bool loadedFromBackup = false;
|
|
QString error;
|
|
BackupInfo backup;
|
|
};
|
|
|
|
struct WriteResult {
|
|
bool success = false;
|
|
QString error;
|
|
BackupInfo backup;
|
|
};
|
|
|
|
static constexpr int maxBackupCount = 5;
|
|
static constexpr int lockTimeoutMs = 10000;
|
|
static constexpr int staleLockTimeoutMs = 30000;
|
|
|
|
static EUI_TOOLS_API std::filesystem::path tempPath(const std::filesystem::path& jsonPath);
|
|
|
|
// Backups are named <source-filename>.<serial>, beginning at serial 0.
|
|
static EUI_TOOLS_API std::filesystem::path backupPath(const std::filesystem::path& jsonPath, int serial);
|
|
|
|
static EUI_TOOLS_API BackupInfo latestBackup(const std::filesystem::path& jsonPath);
|
|
|
|
static EUI_TOOLS_API WriteResult backup(const std::filesystem::path& jsonPath);
|
|
|
|
static EUI_TOOLS_API bool copyFile(const std::filesystem::path& source,
|
|
const std::filesystem::path& destination,
|
|
QString* error = nullptr);
|
|
|
|
template <typename JsonType>
|
|
static ReadResult read(const std::filesystem::path& jsonPath,
|
|
JsonType& output,
|
|
bool decryptEnabled = false)
|
|
{
|
|
const std::filesystem::path normalizedPath = normalized(jsonPath);
|
|
const auto pathMutex = mutexFor(normalizedPath);
|
|
std::unique_lock<std::mutex> locker(*pathMutex);
|
|
QString lockError;
|
|
const auto fileLock = acquireFileLock(normalizedPath, &lockError);
|
|
if (!fileLock) {
|
|
return { false, false, lockError, {} };
|
|
}
|
|
|
|
JsonType parsed;
|
|
QString parseError;
|
|
if (parseFile(normalizedPath, parsed, parseError, decryptEnabled)) {
|
|
if (!isBackupFile(normalizedPath) && latestBackup(normalizedPath).index < 0) {
|
|
const auto backupResult = backupLocked(normalizedPath);
|
|
if (!backupResult.success) {
|
|
return { false, false, backupResult.error, {} };
|
|
}
|
|
}
|
|
output = std::move(parsed);
|
|
return { true, false, QString(), {} };
|
|
}
|
|
|
|
ReadResult result;
|
|
result.error = parseError;
|
|
result.backup = latestBackup(normalizedPath);
|
|
return result;
|
|
}
|
|
|
|
// Call after the UI has obtained user confirmation from ReadResult::backup.
|
|
// The validated backup replaces the invalid source file before returning it.
|
|
template <typename JsonType>
|
|
static ReadResult recoverFromLatestBackup(const std::filesystem::path& jsonPath,
|
|
JsonType& output,
|
|
bool decryptEnabled = false)
|
|
{
|
|
const std::filesystem::path normalizedPath = normalized(jsonPath);
|
|
const auto pathMutex = mutexFor(normalizedPath);
|
|
std::unique_lock<std::mutex> locker(*pathMutex);
|
|
QString lockError;
|
|
const auto fileLock = acquireFileLock(normalizedPath, &lockError);
|
|
if (!fileLock) {
|
|
return { false, false, lockError, {} };
|
|
}
|
|
|
|
ReadResult result;
|
|
result.backup = latestBackup(normalizedPath);
|
|
if (result.backup.index < 0) {
|
|
result.error = QStringLiteral("No JSON backup is available");
|
|
return result;
|
|
}
|
|
|
|
JsonType parsed;
|
|
QString parseError;
|
|
if (!parseFile(result.backup.path, parsed, parseError, decryptEnabled)) {
|
|
result.error = QStringLiteral("Latest JSON backup is invalid: %1").arg(parseError);
|
|
return result;
|
|
}
|
|
|
|
const std::filesystem::path temporaryPath = tempPath(normalizedPath);
|
|
std::error_code ec;
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
ec.clear();
|
|
std::filesystem::copy_file(result.backup.path, temporaryPath,
|
|
std::filesystem::copy_options::overwrite_existing, ec);
|
|
if (ec) {
|
|
result.error = QStringLiteral("Failed to stage JSON backup recovery: %1")
|
|
.arg(QString::fromStdString(ec.message()));
|
|
return result;
|
|
}
|
|
|
|
QString syncError;
|
|
if (!syncFile(temporaryPath, &syncError)) {
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
result.error = QStringLiteral("Failed to flush JSON temporary file: %1").arg(syncError);
|
|
return result;
|
|
}
|
|
|
|
QString moveError;
|
|
if (!moveFile(temporaryPath, normalizedPath, &moveError)) {
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
result.error = QStringLiteral("Failed to restore JSON backup: %1").arg(moveError);
|
|
return result;
|
|
}
|
|
|
|
output = std::move(parsed);
|
|
result.success = true;
|
|
result.loadedFromBackup = true;
|
|
return result;
|
|
}
|
|
|
|
// The caller owns the confirmation UI/policy. Return false from confirm to keep the source unchanged.
|
|
template <typename JsonType, typename Confirm>
|
|
static ReadResult readWithBackupRecovery(const std::filesystem::path& jsonPath,
|
|
JsonType& output,
|
|
bool decryptEnabled,
|
|
Confirm&& confirm)
|
|
{
|
|
ReadResult result = read(jsonPath, output, decryptEnabled);
|
|
if (result.success || result.backup.index < 0) {
|
|
return result;
|
|
}
|
|
if (!confirm(result.backup, result.error)) {
|
|
return result;
|
|
}
|
|
return recoverFromLatestBackup(jsonPath, output, decryptEnabled);
|
|
}
|
|
|
|
template <typename JsonType>
|
|
static WriteResult write(const std::filesystem::path& jsonPath,
|
|
const JsonType& value,
|
|
bool encryptEnabled = false)
|
|
{
|
|
const std::filesystem::path normalizedPath = normalized(jsonPath);
|
|
const auto pathMutex = mutexFor(normalizedPath);
|
|
std::unique_lock<std::mutex> locker(*pathMutex);
|
|
|
|
WriteResult result;
|
|
QString lockError;
|
|
const auto fileLock = acquireFileLock(normalizedPath, &lockError);
|
|
if (!fileLock) {
|
|
result.error = lockError;
|
|
return result;
|
|
}
|
|
std::string serialized;
|
|
try {
|
|
serialized = value.dump(4);
|
|
}
|
|
catch (const std::exception& e) {
|
|
result.error = QStringLiteral("JSON 序列化失败:") + QString::fromLocal8Bit(e.what());
|
|
return result;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(normalizedPath.parent_path(), ec);
|
|
if (ec) {
|
|
result.error = QStringLiteral("无法创建 JSON 目录:") + QString::fromStdString(ec.message());
|
|
return result;
|
|
}
|
|
|
|
const std::filesystem::path temporaryPath = tempPath(normalizedPath);
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
ec.clear();
|
|
{
|
|
QByteArray outputData = QByteArray::fromStdString(serialized);
|
|
if (encryptEnabled) {
|
|
QString encryptionError;
|
|
if (!encrypt(outputData, outputData, encryptionError)) {
|
|
result.error = encryptionError;
|
|
return result;
|
|
}
|
|
}
|
|
std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc);
|
|
if (!output.is_open()) {
|
|
result.error = QStringLiteral("无法打开临时 JSON 文件:")
|
|
+ QString::fromStdWString(temporaryPath.wstring());
|
|
return result;
|
|
}
|
|
output.write(outputData.constData(), static_cast<std::streamsize>(outputData.size()));
|
|
output.flush();
|
|
if (!output.good()) {
|
|
output.close();
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
result.error = QStringLiteral("写入临时 JSON 文件失败:")
|
|
+ QString::fromStdWString(temporaryPath.wstring());
|
|
return result;
|
|
}
|
|
}
|
|
|
|
QString syncError;
|
|
if (!syncFile(temporaryPath, &syncError)) {
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
result.error = QStringLiteral("Failed to flush JSON temporary file: %1").arg(syncError);
|
|
return result;
|
|
}
|
|
|
|
const bool sourceExisted = std::filesystem::exists(normalizedPath, ec) && !ec;
|
|
if (sourceExisted) {
|
|
result = backupLocked(normalizedPath);
|
|
if (!result.success) {
|
|
std::filesystem::remove(temporaryPath, ec);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
QString moveError;
|
|
if (!moveFile(temporaryPath, normalizedPath, &moveError)) {
|
|
if (sourceExisted) {
|
|
restoreLatestBackupLocked(normalizedPath);
|
|
}
|
|
result.success = false;
|
|
result.error = QStringLiteral("Failed to replace JSON file: %1").arg(moveError);
|
|
return result;
|
|
}
|
|
|
|
result.success = true;
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
static EUI_TOOLS_API std::filesystem::path normalized(const std::filesystem::path& path);
|
|
|
|
static EUI_TOOLS_API std::filesystem::path executableDirectory();
|
|
|
|
static EUI_TOOLS_API bool isInside(const std::filesystem::path& child, const std::filesystem::path& parent);
|
|
|
|
static EUI_TOOLS_API std::filesystem::path backupDirectory(const std::filesystem::path& normalizedPath);
|
|
|
|
static EUI_TOOLS_API std::shared_ptr<std::mutex> mutexFor(const std::filesystem::path& path);
|
|
|
|
static EUI_TOOLS_API void assignError(QString* destination, const QString& error);
|
|
|
|
static EUI_TOOLS_API bool isBackupFile(const std::filesystem::path& path);
|
|
|
|
static std::unique_ptr<QLockFile> acquireFileLock(const std::filesystem::path& path,
|
|
QString* error = nullptr)
|
|
{
|
|
auto lock = std::make_unique<QLockFile>(QString::fromStdWString(path.wstring()) + QStringLiteral(".lock"));
|
|
lock->setStaleLockTime(staleLockTimeoutMs);
|
|
if (lock->tryLock(lockTimeoutMs)) {
|
|
return lock;
|
|
}
|
|
assignError(error, QStringLiteral("Unable to acquire JSON file lock: %1")
|
|
.arg(QString::fromStdWString(path.wstring())));
|
|
return {};
|
|
}
|
|
|
|
static bool syncFile(const std::filesystem::path& path, QString* error = nullptr)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
const HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (file == INVALID_HANDLE_VALUE) {
|
|
assignError(error, QStringLiteral("CreateFileW failed (Windows error %1)")
|
|
.arg(static_cast<qulonglong>(GetLastError())));
|
|
return false;
|
|
}
|
|
const bool success = FlushFileBuffers(file) != 0;
|
|
const DWORD lastError = success ? ERROR_SUCCESS : GetLastError();
|
|
CloseHandle(file);
|
|
if (!success) {
|
|
assignError(error, QStringLiteral("FlushFileBuffers failed (Windows error %1)")
|
|
.arg(static_cast<qulonglong>(lastError)));
|
|
}
|
|
return success;
|
|
#elif defined(Q_OS_UNIX)
|
|
const int file = ::open(path.c_str(), O_RDWR);
|
|
if (file < 0) {
|
|
assignError(error, QStringLiteral("open failed while syncing JSON file"));
|
|
return false;
|
|
}
|
|
const bool success = ::fsync(file) == 0;
|
|
::close(file);
|
|
if (!success) {
|
|
assignError(error, QStringLiteral("fsync failed while syncing JSON file"));
|
|
}
|
|
return success;
|
|
#else
|
|
Q_UNUSED(path);
|
|
Q_UNUSED(error);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
static bool syncParentDirectory(const std::filesystem::path& path, QString* error = nullptr)
|
|
{
|
|
#if defined(Q_OS_UNIX)
|
|
const int directory = ::open(path.parent_path().c_str(), O_RDONLY);
|
|
if (directory < 0) {
|
|
assignError(error, QStringLiteral("open failed while syncing JSON directory"));
|
|
return false;
|
|
}
|
|
const bool success = ::fsync(directory) == 0;
|
|
::close(directory);
|
|
if (!success) {
|
|
assignError(error, QStringLiteral("fsync failed while syncing JSON directory"));
|
|
}
|
|
return success;
|
|
#else
|
|
Q_UNUSED(path);
|
|
Q_UNUSED(error);
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
// Header-only because write() is a template. This avoids a static-library version dependency.
|
|
static bool moveFile(const std::filesystem::path& source,
|
|
const std::filesystem::path& destination,
|
|
QString* error = nullptr)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
if (MoveFileExW(source.c_str(), destination.c_str(),
|
|
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0) {
|
|
return true;
|
|
}
|
|
assignError(error, QStringLiteral("MoveFileExW failed (Windows error %1)")
|
|
.arg(static_cast<qulonglong>(GetLastError())));
|
|
return false;
|
|
#else
|
|
std::error_code ec;
|
|
std::filesystem::rename(source, destination, ec);
|
|
if (ec) {
|
|
assignError(error, QStringLiteral("rename failed: %1")
|
|
.arg(QString::fromStdString(ec.message())));
|
|
return false;
|
|
}
|
|
return syncParentDirectory(destination, error);
|
|
#endif
|
|
}
|
|
|
|
template <typename JsonType>
|
|
static bool parseFile(const std::filesystem::path& path, JsonType& output, QString& error,
|
|
bool decryptEnabled)
|
|
{
|
|
QFile input(QString::fromStdWString(path.wstring()));
|
|
if (!input.open(QIODevice::ReadOnly)) {
|
|
error = QStringLiteral("无法打开 JSON 文件:") + input.errorString();
|
|
return false;
|
|
}
|
|
const QByteArray encryptedContent = input.readAll();
|
|
if (encryptedContent.isEmpty()) {
|
|
error = QStringLiteral("JSON 文件为空");
|
|
return false;
|
|
}
|
|
QByteArray content;
|
|
if (!decrypt(encryptedContent, content, error, decryptEnabled)) {
|
|
return false;
|
|
}
|
|
try {
|
|
JsonType parsed = JsonType::parse(content.toStdString(), nullptr, false);
|
|
if (parsed.is_discarded()) {
|
|
error = QStringLiteral("JSON 格式无效");
|
|
return false;
|
|
}
|
|
output = std::move(parsed);
|
|
return true;
|
|
}
|
|
catch (const std::exception& e) {
|
|
error = QStringLiteral("JSON 解析失败:") + QString::fromLocal8Bit(e.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static EUI_TOOLS_API WriteResult backupLocked(const std::filesystem::path& normalizedPath);
|
|
|
|
static EUI_TOOLS_API void restoreLatestBackupLocked(const std::filesystem::path& normalizedPath);
|
|
|
|
static EUI_TOOLS_API bool deriveKey(const QByteArray& salt, QByteArray& key, QString& error);
|
|
|
|
static EUI_TOOLS_API bool encrypt(const QByteArray& plainText, QByteArray& encrypted, QString& error);
|
|
|
|
static EUI_TOOLS_API bool decrypt(const QByteArray& encrypted, QByteArray& plainText, QString& error,
|
|
bool enabled);
|
|
|
|
static EUI_TOOLS_API QByteArray encryptionMagic();
|
|
|
|
inline static std::mutex mutexRegistryMutex_;
|
|
inline static std::map<std::string, std::weak_ptr<std::mutex>> mutexRegistry_;
|
|
};
|