Files
RCiRoot e3f7733316 V20260829-2.6.1
完成框架目录和模块重构;新增核心、单例、工具、加密及框架 SDK;统一任务、相机和 RPC 接口,并增加离线检测、相机标定、备份恢复、版本管理、崩溃守护和心跳监控功能。
2026-09-20 16:01:09 +08:00

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_;
};