#include "ConfigHelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #ifndef NOMINMAX #define NOMINMAX #endif #include #include #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 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; } 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; }