738 lines
25 KiB
C++
738 lines
25 KiB
C++
#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;
|
|
}
|
|
|
|
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 (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()
|
|
{
|
|
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
|
|
{
|
|
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::updateRoot() const
|
|
{
|
|
return QDir(runtimeRoot()).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 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 QJsonObject config = document.object();
|
|
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 QString configDir = QFileInfo(m_configPath).absolutePath();
|
|
const QStringList staleFiles{
|
|
QDir(configDir).filePath(QStringLiteral("client_identity.dat")),
|
|
QDir(configDir).filePath(QStringLiteral("version_policy.dat")),
|
|
QDir(configDir).filePath(QStringLiteral("local_state.json"))
|
|
};
|
|
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, 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 (!ConfigHelper::writeFileWithElevationIfNeeded(m_configPath, emptyFileBytes, &writeError))
|
|
{
|
|
m_error = QStringLiteral("Cannot clear app_config.json after registry import: %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::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()
|
|
{
|
|
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;
|
|
}
|