Files

864 lines
29 KiB
C++
Raw Permalink Normal View History

#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 <QStandardPaths>
#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");
const QString kEmbeddedServerConfigPath = QStringLiteral(":/simcae/server_config.json");
const QString kApiBaseUrlKey = QStringLiteral("api_base_url");
// 需要提权写入时,子进程参数统一用 Base64Url 编码。
// 这样可以避免 Windows 路径、中文、空格或换行在 ShellExecute 参数传递中被截断或误解析。
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);
}
bool isRegistryManagedConfigKey(const QString& key)
{
// 服务端地址是编译期 qrc 配置,不进入注册表。
// 其他运行配置会在 Launcher 首次启动时导入注册表,之后以注册表为准。
return key != kApiBaseUrlKey;
}
bool isPathInsideDirectory(const QString& path, const QString& directory)
{
if (path.isEmpty() || directory.isEmpty())
return false;
QString normalizedPath = QDir::cleanPath(QFileInfo(path).absoluteFilePath());
QString normalizedDirectory = QDir::cleanPath(QFileInfo(directory).absoluteFilePath());
#ifdef Q_OS_WIN
normalizedPath = normalizedPath.toLower();
normalizedDirectory = normalizedDirectory.toLower();
#endif
return normalizedPath == normalizedDirectory
|| normalizedPath.startsWith(normalizedDirectory + QDir::separator());
}
bool isUserDataPath(const QString& path)
{
return isPathInsideDirectory(path, ConfigHelper::instance().dataRoot());
}
QJsonObject registryManagedConfigObject(const QJsonObject& source)
{
QJsonObject result;
for (auto it = source.constBegin(); it != source.constEnd(); ++it)
{
if (isRegistryManagedConfigKey(it.key()))
result.insert(it.key(), it.value());
}
return result;
}
#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)
{
// 安装到 C:\Program Files 等目录时,普通用户不能直接修改配置或运行态文件。
// 这里不让主进程一直以管理员运行,而是在确实需要写入时临时拉起自身完成单次写入。
const QMessageBox::StandardButton choice = QMessageBox::question(
nullptr,
QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"),
QCoreApplication::translate("ConfigHelper", "The current operation needs administrator permission to modify a protected file.\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;
}
QString ConfigHelper::executableNameForCurrentPlatform(const QString& configuredValue,
const QString& fallbackBaseName)
{
QString name = configuredValue.trimmed();
if (name.isEmpty())
name = fallbackBaseName.trimmed();
#ifdef Q_OS_WIN
const QString fileName = QFileInfo(name).fileName();
if (!fileName.endsWith(QStringLiteral(".exe"), Qt::CaseInsensitive)
&& QFileInfo(fileName).suffix().isEmpty()) {
name += QStringLiteral(".exe");
}
#else
if (name.endsWith(QStringLiteral(".exe"), Qt::CaseInsensitive))
name.chop(4);
#endif
return name;
}
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 (isUserDataPath(path))
{
if (errorMessage)
*errorMessage = localError;
return false;
}
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
if (isUserDataPath(path))
{
if (errorMessage)
*errorMessage = localError;
return false;
}
return removeFileWithElevation(path, localError, errorMessage);
#else
if (errorMessage)
*errorMessage = localError;
return false;
#endif
}
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();
removeRegistryValue(kApiBaseUrlKey);
qDebug() << "Loading app config path:" << m_configPath;
qDebug() << "File exists?" << QFile::exists(m_configPath);
qDebug() << "Registry installation id:" << m_registryInstallId;
}
QString ConfigHelper::configPath() const
{
return m_configPath;
}
QString ConfigHelper::installRoot() const
{
QString relativeRoot = getValue("Runtime", "install_root").trimmed();
if (relativeRoot.isEmpty())
relativeRoot = ".";
return QDir::cleanPath(QDir(runtimeRoot()).filePath(relativeRoot));
}
QString ConfigHelper::runtimeRoot() const
{
return QDir::cleanPath(QApplication::applicationDirPath());
}
QString ConfigHelper::dataRoot() const
{
QString base = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
if (base.isEmpty())
base = QDir::homePath();
return QDir::cleanPath(QDir(base).filePath(
QStringLiteral("Marsco/UpdateClientSDK/installations/%1").arg(m_registryInstallId)));
}
QString ConfigHelper::dataConfigDir() const
{
return QDir(dataRoot()).filePath(QStringLiteral("config"));
}
QString ConfigHelper::clientIdentityPath() const
{
return QDir(dataConfigDir()).filePath(QStringLiteral("client_identity.dat"));
}
QString ConfigHelper::policyPath() const
{
return QDir(dataConfigDir()).filePath(QStringLiteral("version_policy.dat"));
}
QString ConfigHelper::localStatePath() const
{
return QDir(dataConfigDir()).filePath(QStringLiteral("local_state.json"));
}
QString ConfigHelper::updateRoot() const
{
return QDir(dataRoot()).filePath("update");
}
QString ConfigHelper::runtimeRelativePath() const
{
QString relative = QDir::fromNativeSeparators(QDir(installRoot()).relativeFilePath(runtimeRoot()));
relative = QDir::cleanPath(relative);
if (relative == ".")
return QString();
while (relative.startsWith("./"))
relative = relative.mid(2);
return relative.trimmed();
}
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 QJsonObject config = registryManagedConfigObject(document.object());
const QByteArray normalizedConfig = QJsonDocument(config).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;
if (config.isEmpty())
{
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 empty app_config.json marker to registry.");
return false;
}
m_error.clear();
qDebug() << "app_config.json is empty; keeping existing registry configuration.";
return true;
}
const bool configChangedAfterPreviousImport = !previousHash.isEmpty();
settings.beginGroup(kRegistryConfigGroup);
settings.remove(QString());
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 QStringList staleFiles{
clientIdentityPath(),
policyPath(),
localStatePath()
};
for (const QString& staleFile : staleFiles)
{
QString removeError;
if (!removeFile(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, policy and local state after app_config.json changed.";
}
if (!config.isEmpty() && !sanitizeConfigFileAfterImport(settings))
{
qWarning() << "Cannot sanitize app_config.json after registry import:" << m_error;
return true;
}
return true;
}
bool ConfigHelper::sanitizeConfigFileAfterImport(QSettings& settings)
{
const QJsonObject emptyConfig;
const QByteArray normalizedEmptyConfig = QJsonDocument(emptyConfig).toJson(QJsonDocument::Compact);
const QByteArray emptyFileBytes = QJsonDocument(emptyConfig).toJson(QJsonDocument::Indented);
const QString sanitizedHash = QString::fromLatin1(
QCryptographicHash::hash(normalizedEmptyConfig, QCryptographicHash::Sha256).toHex());
QString writeError;
if (!writeBytesToFile(m_configPath, emptyFileBytes, &writeError))
{
// 清空 app_config.json 只是为了减少明文配置暴露,不是启动必需步骤。
// 如果安装目录或文件只读,不再为了清空源配置弹 UAC;运行配置已经写入 HKCU 注册表。
m_error = QStringLiteral("Cannot clear app_config.json after registry import without elevation: %1").arg(writeError);
return false;
}
settings.beginGroup(kRegistryMetaGroup);
settings.setValue(kRegistrySourceHashKey, sanitizedHash);
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 update registry source hash after clearing app_config.json.");
return false;
}
m_error.clear();
qDebug() << "Cleared app_config.json after importing configuration to registry.";
return true;
}
bool ConfigHelper::removeRegistryValue(const QString& key) const
{
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
kRegistryOrganization, kRegistryApplication);
enterRegistryGroup(settings);
settings.beginGroup(kRegistryConfigGroup);
settings.remove(key);
settings.endGroup();
settings.sync();
return settings.status() == QSettings::NoError;
}
QString ConfigHelper::readEmbeddedValue(const QString& key) const
{
if (key != kApiBaseUrlKey)
return QString();
QFile file(kEmbeddedServerConfigPath);
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().trimmed();
}
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
{
if (!isRegistryManagedConfigKey(key))
return QString();
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);
const QString embeddedValue = readEmbeddedValue(key);
if (!embeddedValue.isEmpty())
return embeddedValue;
if (!isRegistryManagedConfigKey(key))
return QString();
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()
{
if (QFile::exists(m_configPath))
return true;
const QString legacyPath = QApplication::applicationDirPath() + "/client.ini";
if (!QFile::exists(legacyPath))
return false;
QSettings ini(legacyPath, QSettings::IniFormat);
QJsonObject config;
const auto copyText = [&](const QString& section, const QString& key, const QString& fallback = QString()) {
const QString value = ini.value(section + "/" + key, fallback).toString();
config.insert(key, value);
};
copyText("App", "app_id");
copyText("App", "app_name", "Marsco Demo App");
copyText("App", "channel", "stable");
copyText("App", "current_version", "1.0.0");
copyText("App", "client_protocol", "3");
copyText("App", "launch_token");
copyText("License", "license_key");
copyText("Server", "api_base_url");
copyText("Server", "client_token");
copyText("Update", "request_timeout_ms", "5000");
copyText("Update", "temp_folder", "update_temp");
copyText("Update", "device_id");
copyText("Runtime", "install_root", ".");
copyText("Runtime", "main_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "MainApp"));
copyText("Runtime", "launcher_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Launcher"));
copyText("Runtime", "updater_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Updater"));
copyText("Runtime", "bootstrap_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Bootstrap"));
copyText("Runtime", "health_check_timeout_ms", "15000");
#ifdef Q_OS_WIN
config.insert("platform", "windows");
#elif defined(Q_OS_LINUX)
config.insert("platform", "linux");
#else
config.insert("platform", "unknown");
#endif
config.insert("arch", "x64");
QDir().mkpath(QFileInfo(m_configPath).path());
QSaveFile output(m_configPath);
if (!output.open(QIODevice::WriteOnly))
return false;
output.write(QJsonDocument(config).toJson(QJsonDocument::Indented));
const bool saved = output.commit();
if (saved)
qDebug() << "Migrated legacy client.ini to" << m_configPath;
return saved;
}