feat: 优化客户端跨平台配置和运行态管理
This commit is contained in:
+85
-16
@@ -13,6 +13,7 @@
|
||||
#include <QMessageBox>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QDebug>
|
||||
#include <string>
|
||||
@@ -46,6 +47,8 @@ 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(
|
||||
@@ -173,9 +176,31 @@ bool writeConfigValueToFile(const QString& configPath, const QString& key,
|
||||
|
||||
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;
|
||||
@@ -198,6 +223,8 @@ QString windowsErrorMessage(DWORD 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"),
|
||||
@@ -397,6 +424,12 @@ bool ConfigHelper::writeFileWithElevationIfNeeded(const QString& path, const QBy
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (isUserDataPath(path))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = localError;
|
||||
return false;
|
||||
}
|
||||
if (data.size() > 24 * 1024)
|
||||
{
|
||||
if (errorMessage)
|
||||
@@ -423,6 +456,12 @@ bool ConfigHelper::removeFileWithElevationIfNeeded(const QString& path, QString*
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (isUserDataPath(path))
|
||||
{
|
||||
if (errorMessage)
|
||||
*errorMessage = localError;
|
||||
return false;
|
||||
}
|
||||
return removeFileWithElevation(path, localError, errorMessage);
|
||||
#else
|
||||
if (errorMessage)
|
||||
@@ -458,15 +497,44 @@ QString ConfigHelper::installRoot() const
|
||||
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::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
|
||||
{
|
||||
@@ -572,16 +640,15 @@ bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
||||
|
||||
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"))
|
||||
clientIdentityPath(),
|
||||
policyPath(),
|
||||
localStatePath()
|
||||
};
|
||||
for (const QString& staleFile : staleFiles)
|
||||
{
|
||||
QString removeError;
|
||||
if (!ConfigHelper::removeFileWithElevationIfNeeded(staleFile, &removeError))
|
||||
if (!removeFile(staleFile, &removeError))
|
||||
{
|
||||
m_error = QStringLiteral("Cannot remove stale runtime file after config change: %1. %2")
|
||||
.arg(staleFile, removeError);
|
||||
@@ -608,9 +675,11 @@ bool ConfigHelper::sanitizeConfigFileAfterImport(QSettings& settings)
|
||||
QCryptographicHash::hash(normalizedEmptyConfig, QCryptographicHash::Sha256).toHex());
|
||||
|
||||
QString writeError;
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_configPath, emptyFileBytes, &writeError))
|
||||
if (!writeBytesToFile(m_configPath, emptyFileBytes, &writeError))
|
||||
{
|
||||
m_error = QStringLiteral("Cannot clear app_config.json after registry import: %1").arg(writeError);
|
||||
// 清空 app_config.json 只是为了减少明文配置暴露,不是启动必需步骤。
|
||||
// 如果安装目录或文件只读,不再为了清空源配置弹 UAC;运行配置已经写入 HKCU 注册表。
|
||||
m_error = QStringLiteral("Cannot clear app_config.json after registry import without elevation: %1").arg(writeError);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user