feat(client): improve SDK packaging and registry config
This commit is contained in:
+15
-12
@@ -1,7 +1,6 @@
|
||||
project(Common LANGUAGES C CXX)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Network Widgets)
|
||||
|
||||
set(SRC
|
||||
project(Common LANGUAGES C CXX)
|
||||
|
||||
set(SRC
|
||||
HttpHelper.h
|
||||
HttpHelper.cpp
|
||||
FileHelper.h
|
||||
@@ -28,11 +27,15 @@ target_include_directories(Common
|
||||
)
|
||||
|
||||
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
|
||||
target_link_directories(Common INTERFACE
|
||||
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
|
||||
$<$<CONFIG:Release>:${OPENSSL_LIB_RELEASE}>
|
||||
)
|
||||
target_link_libraries(Common
|
||||
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
|
||||
INTERFACE libssl.lib libcrypto.lib
|
||||
)
|
||||
target_link_directories(Common INTERFACE
|
||||
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
|
||||
$<$<NOT:$<CONFIG:Debug>>:${OPENSSL_LIB_RELEASE}>
|
||||
)
|
||||
target_link_libraries(Common
|
||||
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
|
||||
INTERFACE libssl.lib libcrypto.lib
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(Common INTERFACE shell32)
|
||||
endif()
|
||||
|
||||
+567
-97
@@ -1,27 +1,410 @@
|
||||
#include "ConfigHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
#include "ConfigHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QMessageBox>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <QDebug>
|
||||
#include <string>
|
||||
#ifdef Q_OS_WIN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const QString kElevatedConfigSetFlag = QStringLiteral("--simcae-config-set");
|
||||
const QString kElevatedFileWriteFlag = QStringLiteral("--simcae-file-write");
|
||||
const QString kElevatedFileRemoveFlag = QStringLiteral("--simcae-file-remove");
|
||||
const QString kConfigPathPrefix = QStringLiteral("--config-path-b64=");
|
||||
const QString kConfigKeyPrefix = QStringLiteral("--config-key-b64=");
|
||||
const QString kConfigValuePrefix = QStringLiteral("--config-value-b64=");
|
||||
const QString kFilePathPrefix = QStringLiteral("--file-path-b64=");
|
||||
const QString kFileDataPrefix = QStringLiteral("--file-data-b64=");
|
||||
const QString kRegistryOrganization = QStringLiteral("Marsco");
|
||||
const QString kRegistryApplication = QStringLiteral("UpdateClientSDK");
|
||||
const QString kRegistryInstallationsGroup = QStringLiteral("installations");
|
||||
const QString kRegistryConfigGroup = QStringLiteral("config");
|
||||
const QString kRegistryMetaGroup = QStringLiteral("_meta");
|
||||
const QString kRegistrySourceHashKey = QStringLiteral("source_sha256");
|
||||
const QString kRegistrySourcePathKey = QStringLiteral("source_path");
|
||||
const QString kRegistryRuntimeRootKey = QStringLiteral("runtime_root");
|
||||
const QString kRegistryImportedAtKey = QStringLiteral("imported_at_utc");
|
||||
|
||||
QString encodeArgument(const QString& value)
|
||||
{
|
||||
return QString::fromLatin1(value.toUtf8().toBase64(
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||
}
|
||||
|
||||
QString encodeBytes(const QByteArray& value)
|
||||
{
|
||||
return QString::fromLatin1(value.toBase64(
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||
}
|
||||
|
||||
QString decodeArgument(const QString& value)
|
||||
{
|
||||
return QString::fromUtf8(QByteArray::fromBase64(value.toLatin1(),
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||
}
|
||||
|
||||
QByteArray decodeBytes(const QString& value)
|
||||
{
|
||||
return QByteArray::fromBase64(value.toLatin1(),
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
}
|
||||
|
||||
QString findArgumentValue(const QStringList& arguments, const QString& prefix)
|
||||
{
|
||||
for (const QString& argument : arguments)
|
||||
if (argument.startsWith(prefix))
|
||||
return argument.mid(prefix.size());
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool writeBytesToFile(const QString& path, const QByteArray& bytes, QString* errorMessage)
|
||||
{
|
||||
QDir dir(QFileInfo(path).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot create directory: %1").arg(dir.path());
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile output(path);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot open file for writing: %1").arg(output.errorString());
|
||||
return false;
|
||||
}
|
||||
if (output.write(bytes) != bytes.size())
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot write full file: %1").arg(output.errorString());
|
||||
output.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
if (!output.commit())
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot commit file: %1").arg(output.errorString());
|
||||
return false;
|
||||
}
|
||||
if (errorMessage)
|
||||
errorMessage->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeFile(const QString& path, QString* errorMessage)
|
||||
{
|
||||
if (!QFile::exists(path))
|
||||
{
|
||||
if (errorMessage)
|
||||
errorMessage->clear();
|
||||
return true;
|
||||
}
|
||||
if (QFile::remove(path))
|
||||
{
|
||||
if (errorMessage)
|
||||
errorMessage->clear();
|
||||
return true;
|
||||
}
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot remove file: %1").arg(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeConfigValueToFile(const QString& configPath, const QString& key,
|
||||
const QString& value, QString* errorMessage)
|
||||
{
|
||||
QFile input(configPath);
|
||||
QJsonObject config;
|
||||
if (input.exists())
|
||||
{
|
||||
if (!input.open(QIODevice::ReadOnly))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot open config for reading: %1").arg(input.errorString());
|
||||
return false;
|
||||
}
|
||||
QJsonParseError parseError;
|
||||
const QByteArray raw = input.readAll();
|
||||
input.close();
|
||||
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
config = document.object();
|
||||
}
|
||||
|
||||
config.insert(key, value);
|
||||
QDir dir(QFileInfo(configPath).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("Cannot create config directory: %1").arg(dir.path());
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
||||
return writeBytesToFile(configPath, payload, errorMessage);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QString windowsErrorMessage(DWORD errorCode)
|
||||
{
|
||||
if (errorCode == ERROR_CANCELLED)
|
||||
return QCoreApplication::translate("ConfigHelper", "The user canceled the administrator permission confirmation.");
|
||||
return QCoreApplication::translate("ConfigHelper", "Windows error %1").arg(errorCode);
|
||||
}
|
||||
|
||||
bool runElevatedSelfCommand(const QStringList& arguments, const QString& targetPath,
|
||||
const QString& originalError, QString* errorMessage)
|
||||
{
|
||||
const QMessageBox::StandardButton choice = QMessageBox::question(
|
||||
nullptr,
|
||||
QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"),
|
||||
QCoreApplication::translate("ConfigHelper", "The current installation directory requires administrator permission to save configuration.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.")
|
||||
.arg(QDir::toNativeSeparators(targetPath), originalError),
|
||||
QMessageBox::Ok | QMessageBox::Cancel,
|
||||
QMessageBox::Ok);
|
||||
if (choice != QMessageBox::Ok)
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QCoreApplication::translate("ConfigHelper", "The user canceled the administrator permission request.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString executable = QCoreApplication::applicationFilePath();
|
||||
if (executable.isEmpty() || !QFileInfo::exists(executable))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QStringLiteral("Cannot locate current executable for elevated config write.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString parameters = arguments.join(QLatin1Char(' '));
|
||||
const QString workingDir = QFileInfo(executable).absolutePath();
|
||||
|
||||
std::wstring verb = L"runas";
|
||||
std::wstring file = executable.toStdWString();
|
||||
std::wstring params = parameters.toStdWString();
|
||||
std::wstring directory = workingDir.toStdWString();
|
||||
|
||||
SHELLEXECUTEINFOW info{};
|
||||
info.cbSize = sizeof(info);
|
||||
info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
info.lpVerb = verb.c_str();
|
||||
info.lpFile = file.c_str();
|
||||
info.lpParameters = params.c_str();
|
||||
info.lpDirectory = directory.c_str();
|
||||
info.nShow = SW_HIDE;
|
||||
|
||||
if (!ShellExecuteExW(&info))
|
||||
{
|
||||
const DWORD err = GetLastError();
|
||||
if (errorMessage)
|
||||
*errorMessage = QStringLiteral("Cannot request administrator permission: %1").arg(windowsErrorMessage(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(info.hProcess, INFINITE);
|
||||
DWORD exitCode = 1;
|
||||
GetExitCodeProcess(info.hProcess, &exitCode);
|
||||
CloseHandle(info.hProcess);
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QStringLiteral("Elevated config write failed with exit code %1.").arg(exitCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (errorMessage)
|
||||
errorMessage->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeConfigValueWithElevation(const QString& configPath, const QString& key,
|
||||
const QString& value, const QString& originalError,
|
||||
QString* errorMessage)
|
||||
{
|
||||
const QStringList arguments{
|
||||
kElevatedConfigSetFlag,
|
||||
kConfigPathPrefix + encodeArgument(configPath),
|
||||
kConfigKeyPrefix + encodeArgument(key),
|
||||
kConfigValuePrefix + encodeArgument(value)
|
||||
};
|
||||
return runElevatedSelfCommand(arguments, configPath, originalError, errorMessage);
|
||||
}
|
||||
|
||||
bool writeBytesWithElevation(const QString& path, const QByteArray& bytes,
|
||||
const QString& originalError, QString* errorMessage)
|
||||
{
|
||||
const QStringList arguments{
|
||||
kElevatedFileWriteFlag,
|
||||
kFilePathPrefix + encodeArgument(path),
|
||||
kFileDataPrefix + encodeBytes(bytes)
|
||||
};
|
||||
return runElevatedSelfCommand(arguments, path, originalError, errorMessage);
|
||||
}
|
||||
|
||||
bool removeFileWithElevation(const QString& path, const QString& originalError, QString* errorMessage)
|
||||
{
|
||||
const QStringList arguments{
|
||||
kElevatedFileRemoveFlag,
|
||||
kFilePathPrefix + encodeArgument(path)
|
||||
};
|
||||
return runElevatedSelfCommand(arguments, path, originalError, errorMessage);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ConfigHelper& ConfigHelper::instance()
|
||||
{
|
||||
static ConfigHelper obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
int ConfigHelper::runElevatedWriteCommandIfRequested()
|
||||
{
|
||||
const QStringList arguments = QCoreApplication::arguments();
|
||||
if (arguments.contains(kElevatedConfigSetFlag))
|
||||
{
|
||||
const QString configPath = decodeArgument(findArgumentValue(arguments, kConfigPathPrefix));
|
||||
const QString key = decodeArgument(findArgumentValue(arguments, kConfigKeyPrefix));
|
||||
const QString value = decodeArgument(findArgumentValue(arguments, kConfigValuePrefix));
|
||||
if (configPath.isEmpty() || key.isEmpty())
|
||||
{
|
||||
qWarning() << "Elevated config write arguments are incomplete.";
|
||||
return 2;
|
||||
}
|
||||
|
||||
QString errorMessage;
|
||||
if (!writeConfigValueToFile(configPath, key, value, &errorMessage))
|
||||
{
|
||||
qWarning() << "Elevated config write failed:" << errorMessage;
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arguments.contains(kElevatedFileWriteFlag))
|
||||
{
|
||||
const QString path = decodeArgument(findArgumentValue(arguments, kFilePathPrefix));
|
||||
const QByteArray bytes = decodeBytes(findArgumentValue(arguments, kFileDataPrefix));
|
||||
if (path.isEmpty())
|
||||
{
|
||||
qWarning() << "Elevated file write arguments are incomplete.";
|
||||
return 2;
|
||||
}
|
||||
|
||||
QString errorMessage;
|
||||
if (!writeBytesToFile(path, bytes, &errorMessage))
|
||||
{
|
||||
qWarning() << "Elevated file write failed:" << errorMessage;
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arguments.contains(kElevatedFileRemoveFlag))
|
||||
{
|
||||
const QString path = decodeArgument(findArgumentValue(arguments, kFilePathPrefix));
|
||||
if (path.isEmpty())
|
||||
{
|
||||
qWarning() << "Elevated file remove arguments are incomplete.";
|
||||
return 2;
|
||||
}
|
||||
|
||||
QString errorMessage;
|
||||
if (!removeFile(path, &errorMessage))
|
||||
{
|
||||
qWarning() << "Elevated file remove failed:" << errorMessage;
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ConfigHelper::writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data,
|
||||
QString* errorMessage)
|
||||
{
|
||||
QString localError;
|
||||
if (writeBytesToFile(path, data, &localError))
|
||||
{
|
||||
if (errorMessage)
|
||||
errorMessage->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (data.size() > 24 * 1024)
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = QString("File is too large for elevated inline write: %1 bytes. Original error: %2")
|
||||
.arg(data.size()).arg(localError);
|
||||
return false;
|
||||
}
|
||||
return writeBytesWithElevation(path, data, localError, errorMessage);
|
||||
#else
|
||||
if (errorMessage)
|
||||
*errorMessage = localError;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ConfigHelper::removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage)
|
||||
{
|
||||
QString localError;
|
||||
if (removeFile(path, &localError))
|
||||
{
|
||||
if (errorMessage)
|
||||
errorMessage->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
return removeFileWithElevation(path, localError, errorMessage);
|
||||
#else
|
||||
if (errorMessage)
|
||||
*errorMessage = localError;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
ConfigHelper& ConfigHelper::instance()
|
||||
{
|
||||
static ConfigHelper obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
ConfigHelper::ConfigHelper()
|
||||
{
|
||||
m_configPath = QApplication::applicationDirPath() + "/config/app_config.json";
|
||||
migrateLegacyIniIfNeeded();
|
||||
qDebug() << "Loading app config path:" << m_configPath;
|
||||
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
||||
}
|
||||
ConfigHelper::ConfigHelper()
|
||||
{
|
||||
m_configPath = QApplication::applicationDirPath() + "/config/app_config.json";
|
||||
m_registryInstallId = QString::fromLatin1(QCryptographicHash::hash(
|
||||
QDir::cleanPath(QApplication::applicationDirPath()).toUtf8(),
|
||||
QCryptographicHash::Sha256).toHex());
|
||||
migrateLegacyIniIfNeeded();
|
||||
syncRegistryFromConfigFileIfChanged();
|
||||
qDebug() << "Loading app config path:" << m_configPath;
|
||||
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
||||
qDebug() << "Registry installation id:" << m_registryInstallId;
|
||||
}
|
||||
|
||||
QString ConfigHelper::configPath() const
|
||||
{
|
||||
@@ -57,80 +440,167 @@ QString ConfigHelper::runtimeRelativePath() const
|
||||
return relative.trimmed();
|
||||
}
|
||||
|
||||
QString ConfigHelper::lastError() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
QFile file(m_configPath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return QString();
|
||||
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||
return QString();
|
||||
|
||||
return document.object().value(key).toVariant().toString();
|
||||
}
|
||||
|
||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
m_error.clear();
|
||||
|
||||
QFile input(m_configPath);
|
||||
QJsonObject config;
|
||||
if (input.exists())
|
||||
{
|
||||
if (!input.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QString("Cannot open config for reading: %1").arg(input.errorString());
|
||||
return false;
|
||||
}
|
||||
QJsonParseError parseError;
|
||||
const QByteArray raw = input.readAll();
|
||||
input.close();
|
||||
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
{
|
||||
m_error = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
config = document.object();
|
||||
}
|
||||
|
||||
config.insert(key, value);
|
||||
QDir dir(QFileInfo(m_configPath).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
m_error = QString("Cannot create config directory: %1").arg(dir.path());
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile output(m_configPath);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
{
|
||||
m_error = QString("Cannot open config for writing: %1").arg(output.errorString());
|
||||
return false;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
||||
if (output.write(payload) != payload.size())
|
||||
{
|
||||
m_error = QString("Cannot write full config file: %1").arg(output.errorString());
|
||||
output.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
if (!output.commit())
|
||||
{
|
||||
m_error = QString("Cannot commit config file: %1").arg(output.errorString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
QString ConfigHelper::lastError() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
void ConfigHelper::enterRegistryGroup(QSettings& settings) const
|
||||
{
|
||||
settings.beginGroup(kRegistryInstallationsGroup);
|
||||
settings.beginGroup(m_registryInstallId);
|
||||
}
|
||||
|
||||
bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
||||
{
|
||||
QFile file(m_configPath);
|
||||
if (!file.exists())
|
||||
return true;
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QStringLiteral("Cannot open config for reading: %1").arg(file.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QByteArray raw = file.readAll();
|
||||
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
{
|
||||
m_error = QStringLiteral("Invalid config JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray normalizedConfig = QJsonDocument(document.object()).toJson(QJsonDocument::Compact);
|
||||
const QString sourceHash = QString::fromLatin1(
|
||||
QCryptographicHash::hash(normalizedConfig, QCryptographicHash::Sha256).toHex());
|
||||
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||
kRegistryOrganization, kRegistryApplication);
|
||||
enterRegistryGroup(settings);
|
||||
settings.beginGroup(kRegistryMetaGroup);
|
||||
const QString previousHash = settings.value(kRegistrySourceHashKey).toString();
|
||||
settings.endGroup();
|
||||
|
||||
if (previousHash == sourceHash)
|
||||
return true;
|
||||
|
||||
const bool configChangedAfterPreviousImport = !previousHash.isEmpty();
|
||||
|
||||
settings.beginGroup(kRegistryConfigGroup);
|
||||
settings.remove(QString());
|
||||
const QJsonObject config = document.object();
|
||||
for (auto it = config.constBegin(); it != config.constEnd(); ++it)
|
||||
settings.setValue(it.key(), it.value().toVariant());
|
||||
settings.endGroup();
|
||||
|
||||
settings.beginGroup(kRegistryMetaGroup);
|
||||
settings.setValue(kRegistrySourceHashKey, sourceHash);
|
||||
settings.setValue(kRegistrySourcePathKey, QDir::toNativeSeparators(QFileInfo(m_configPath).absoluteFilePath()));
|
||||
settings.setValue(kRegistryRuntimeRootKey, QDir::toNativeSeparators(QApplication::applicationDirPath()));
|
||||
settings.setValue(kRegistryImportedAtKey, QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot sync config to registry.");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_error.clear();
|
||||
qDebug() << "Synced app_config.json to registry installation id:" << m_registryInstallId;
|
||||
|
||||
if (configChangedAfterPreviousImport)
|
||||
{
|
||||
const QString configDir = QFileInfo(m_configPath).absolutePath();
|
||||
const QStringList staleFiles{
|
||||
QDir(configDir).filePath(QStringLiteral("client_identity.dat")),
|
||||
QDir(configDir).filePath(QStringLiteral("version_policy.dat"))
|
||||
};
|
||||
for (const QString& staleFile : staleFiles)
|
||||
{
|
||||
QString removeError;
|
||||
if (!ConfigHelper::removeFileWithElevationIfNeeded(staleFile, &removeError))
|
||||
{
|
||||
m_error = QStringLiteral("Cannot remove stale runtime file after config change: %1. %2")
|
||||
.arg(staleFile, removeError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Removed stale client identity and policy after app_config.json changed.";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::readRegistryValue(const QString& key, QString* value) const
|
||||
{
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||
kRegistryOrganization, kRegistryApplication);
|
||||
enterRegistryGroup(settings);
|
||||
settings.beginGroup(kRegistryConfigGroup);
|
||||
if (!settings.contains(key))
|
||||
{
|
||||
settings.endGroup();
|
||||
return false;
|
||||
}
|
||||
if (value)
|
||||
*value = settings.value(key).toString();
|
||||
settings.endGroup();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::writeRegistryValue(const QString& key, const QString& value)
|
||||
{
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||
kRegistryOrganization, kRegistryApplication);
|
||||
enterRegistryGroup(settings);
|
||||
settings.beginGroup(kRegistryConfigGroup);
|
||||
settings.setValue(key, value);
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot write config value to registry: %1").arg(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ConfigHelper::readFileValue(const QString& key) const
|
||||
{
|
||||
QFile file(m_configPath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return QString();
|
||||
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||
return QString();
|
||||
|
||||
return document.object().value(key).toVariant().toString();
|
||||
}
|
||||
|
||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
QString value;
|
||||
if (readRegistryValue(key, &value))
|
||||
return value;
|
||||
return readFileValue(key);
|
||||
}
|
||||
|
||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
m_error.clear();
|
||||
|
||||
return writeRegistryValue(key, value);
|
||||
}
|
||||
|
||||
bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
{
|
||||
|
||||
+30
-17
@@ -1,22 +1,35 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
|
||||
class ConfigHelper
|
||||
{
|
||||
public:
|
||||
static ConfigHelper& instance();
|
||||
QString getValue(const QString& section, const QString& key) const;
|
||||
#pragma once
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
class QSettings;
|
||||
|
||||
class ConfigHelper
|
||||
{
|
||||
public:
|
||||
static ConfigHelper& instance();
|
||||
static int runElevatedWriteCommandIfRequested();
|
||||
static bool writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data,
|
||||
QString* errorMessage = nullptr);
|
||||
static bool removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage = nullptr);
|
||||
QString getValue(const QString& section, const QString& key) const;
|
||||
bool setValue(const QString& section, const QString& key, const QString& value);
|
||||
QString configPath() const;
|
||||
QString installRoot() const;
|
||||
QString runtimeRoot() const;
|
||||
QString updateRoot() const;
|
||||
QString runtimeRelativePath() const;
|
||||
QString lastError() const;
|
||||
|
||||
private:
|
||||
ConfigHelper();
|
||||
bool migrateLegacyIniIfNeeded();
|
||||
QString m_configPath;
|
||||
QString m_error;
|
||||
};
|
||||
QString runtimeRelativePath() const;
|
||||
QString lastError() const;
|
||||
|
||||
private:
|
||||
ConfigHelper();
|
||||
bool migrateLegacyIniIfNeeded();
|
||||
void enterRegistryGroup(QSettings& settings) const;
|
||||
bool syncRegistryFromConfigFileIfChanged();
|
||||
bool readRegistryValue(const QString& key, QString* value) const;
|
||||
bool writeRegistryValue(const QString& key, const QString& value);
|
||||
QString readFileValue(const QString& key) const;
|
||||
QString m_configPath;
|
||||
QString m_registryInstallId;
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSaveFile>
|
||||
#include <QSysInfo>
|
||||
#include <QUuid>
|
||||
#include <QTimer>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSysInfo>
|
||||
#include <QUuid>
|
||||
#include <QTimer>
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
@@ -45,5 +44,5 @@ bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token
|
||||
ConfigHelper& config=ConfigHelper::instance();
|
||||
QString installation=config.getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!config.setValue("Device","installation_id",installation)){m_error=QString("cannot save installation id to %1: %2").arg(config.configPath(),config.lastError());return false;}}
|
||||
QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}};
|
||||
QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error=QString("cannot save device credential to %1: %2").arg(path,out.errorString());return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true;
|
||||
}
|
||||
QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);QString writeError;if(!ConfigHelper::writeFileWithElevationIfNeeded(path,bytes,&writeError)){m_error=QString("cannot save device credential to %1: %2").arg(path,writeError);return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ bool FileHelper::copyFileOverwrite(const QString &src, const QString &dst)
|
||||
{
|
||||
if (QFile::exists(dst))
|
||||
{
|
||||
if (!QFile::remove(dst))
|
||||
{
|
||||
qDebug() << "无法删除旧文件:" << dst;
|
||||
return false;
|
||||
}
|
||||
if (!QFile::remove(dst))
|
||||
{
|
||||
qDebug() << "Cannot remove old file:" << dst;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return QFile::copy(src, dst);
|
||||
}
|
||||
@@ -39,4 +39,4 @@ bool FileHelper::killProcess(const QString &exeName)
|
||||
process.start("taskkill /f /im " + exeName);
|
||||
process.waitForFinished(1000);
|
||||
return !isProcessRunning(exeName);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
class HttpHelper
|
||||
{
|
||||
public:
|
||||
// 每次调用独立创建manager,不做成成员变量
|
||||
// Create an independent manager for each call instead of keeping it as a member.
|
||||
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
};
|
||||
};
|
||||
|
||||
+28
-34
@@ -1,8 +1,7 @@
|
||||
#include "LocalStateHelper.h"
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QDir>
|
||||
#include "LocalStateHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
|
||||
LocalStateHelper::LocalStateHelper(const QString& baseDir)
|
||||
: m_baseDir(baseDir)
|
||||
@@ -13,19 +12,12 @@ LocalStateHelper::LocalStateHelper(const QString& baseDir)
|
||||
bool LocalStateHelper::loadState(const QString& relativePath)
|
||||
{
|
||||
m_filePath = m_baseDir + "/" + relativePath;
|
||||
QFile file(m_filePath);
|
||||
if (!file.exists())
|
||||
{
|
||||
QDir dir(QFileInfo(m_filePath).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
m_error = QString("Cannot create state directory: %1").arg(dir.path());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state = QJsonObject{
|
||||
{"max_policy_seq", 0},
|
||||
{"last_success_run_at", QString()},
|
||||
QFile file(m_filePath);
|
||||
if (!file.exists())
|
||||
{
|
||||
m_state = QJsonObject{
|
||||
{"max_policy_seq", 0},
|
||||
{"last_success_run_at", QString()},
|
||||
{"last_online_verified_at", QString()},
|
||||
{"last_success_version", QString()}
|
||||
};
|
||||
@@ -60,22 +52,24 @@ bool LocalStateHelper::loadState(const QString& relativePath)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalStateHelper::saveState() const
|
||||
{
|
||||
if (!m_loaded)
|
||||
return false;
|
||||
|
||||
QFile file(m_filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(m_state);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
bool LocalStateHelper::saveState() const
|
||||
{
|
||||
if (!m_loaded)
|
||||
{
|
||||
m_error = "State is not loaded";
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(m_state);
|
||||
QString writeError;
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_filePath, doc.toJson(QJsonDocument::Indented), &writeError))
|
||||
{
|
||||
m_error = QString("Cannot save state file: %1").arg(writeError);
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalStateHelper::isLoaded() const
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
private:
|
||||
QString m_baseDir;
|
||||
QString m_filePath;
|
||||
QString m_error;
|
||||
mutable QString m_error;
|
||||
QJsonObject m_state;
|
||||
bool m_loaded = false;
|
||||
};
|
||||
|
||||
+19
-15
@@ -1,11 +1,11 @@
|
||||
#include "PolicyHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <algorithm>
|
||||
#include "PolicyHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <algorithm>
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
@@ -31,13 +31,17 @@ bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& si
|
||||
return isValid();
|
||||
}
|
||||
|
||||
bool PolicyHelper::savePolicy(const QString& relativePath) const
|
||||
{
|
||||
QSaveFile file(m_baseDir + "/" + relativePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) return false;
|
||||
const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented);
|
||||
return file.write(bytes) == bytes.size() && file.commit();
|
||||
}
|
||||
bool PolicyHelper::savePolicy(const QString& relativePath) const
|
||||
{
|
||||
const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented);
|
||||
QString writeError;
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_baseDir + "/" + relativePath, bytes, &writeError)) {
|
||||
m_error = "Cannot save policy file: " + writeError;
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
||||
return false;
|
||||
}
|
||||
const QByteArray raw = file.readAll(); file.close();
|
||||
QFile::remove(consumingPath); // 一次性消费;无论成功失败都不能重放。
|
||||
QFile::remove(consumingPath); // Consume once; it must not be replayed regardless of success or failure.
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
|
||||
Reference in New Issue
Block a user