feat(hardening): 内嵌产品配置 + 系统级状态 + 全量 i18n + 依赖治理 + 资料清理

update-client-hardening 四工作流合入(构建/静态检查已过):

- 配置:不可变产品策略经 UpdateClientResources.cmake 归一化校验后
  内嵌 qrc(URL/相对路径/主程序必须落 install_root/版本/UUID 格式,
  CMake override 写入前复验);可变机器状态迁系统级原生存储;
  运行目录不再落可编辑 app_config.json。
- i18n:生产源全英文(no-Han 检查 41/41),中文进 translations/
  zh_CN 目录;翻译加载收敛 Common/TranslationHelper 唯一入口;
  Bootstrap 补资源文件。
- 依赖:Qt/OpenSSL/架构/Perl 机器路径改 imported targets +
  UpdateClientDependencies.cmake,3rdparty 由父工程契约供给。
- 清理:旧 README/SDK README/3 个重复 PowerShell 打包脚本/PDF 提取
  文本删除,canonical README + 5 份结构化英文文档替代;Launcher
  requireAdministrator manifest;统一 MSVC /utf-8 删运行时编码设置。
This commit is contained in:
Comely
2026-07-10 00:38:23 -07:00
parent b06e003502
commit fe5aa5bbf0
45 changed files with 5033 additions and 4083 deletions
+224 -88
View File
@@ -1,4 +1,3 @@
#include <windows.h>
#include <QApplication>
#include <QDebug>
#include <QMessageBox>
@@ -6,26 +5,28 @@
#include <QProgressDialog>
#include <QInputDialog>
#include <QLineEdit>
#include <QTextCodec>
#include "UpdateLogic.h"
#include "../Common/ConfigHelper.h"
#include "../Common/PolicyHelper.h"
#include "../Common/LocalStateHelper.h"
#include "../Common/TicketHelper.h"
#include "../Common/DeviceIdentityHelper.h"
#include "../Common/IntegrityHelper.h"
#include "../Common/TranslationHelper.h"
#include <QFile>
#include <QFileDialog>
#include <QDir>
int main(int argc, char* argv[])
int main(int argc, char *argv[])
{
SetConsoleOutputCP(65001);
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
QApplication app(argc, argv);
QApplication::setApplicationName("Marsco Launcher");
QProgressDialog progress("正在检查软件更新...", QString(), 0, 0);
progress.setWindowTitle("Marsco 软件启动器");
TranslationHelper translation;
translation.installForSystemLocale();
QProgressDialog progress(QCoreApplication::translate("Launcher", "Checking for software updates..."), QString(), 0, 0);
progress.setWindowTitle(QCoreApplication::translate("Launcher", "Marsco Launcher"));
progress.setCancelButton(nullptr);
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
@@ -34,80 +35,117 @@ int main(int argc, char* argv[])
QApplication::processEvents();
const QString appDir = QApplication::applicationDirPath();
ConfigHelper& config = ConfigHelper::instance();
const auto isLicenseError = [](const QString& errorText) {
ConfigHelper &config = ConfigHelper::instance();
const auto isLicenseError = [&translation](const QString &errorText)
{
const QString text = errorText.toLower();
return text.contains("license")
|| errorText.contains("授权")
|| errorText.contains("过期")
|| errorText.contains("设备数量已达到上限")
|| errorText.contains("已绑定其他授权");
QStringList markers{QStringLiteral("license")};
const char *const markerSources[]{
QT_TRANSLATE_NOOP("LauncherErrorMarker", "authorization"),
QT_TRANSLATE_NOOP("LauncherErrorMarker", "expired"),
QT_TRANSLATE_NOOP("LauncherErrorMarker", "device limit reached"),
QT_TRANSLATE_NOOP("LauncherErrorMarker", "bound to another license")};
for (const char *source : markerSources)
{
markers.append(QCoreApplication::translate("LauncherErrorMarker", source));
markers.append(translation.translate("LauncherErrorMarker", source));
}
for (const QString &marker : markers)
{
if (!marker.isEmpty() && text.contains(marker.toLower()))
return true;
}
return false;
};
const auto clearDeviceCredential = [&]() {
const auto clearDeviceCredential = [&]()
{
QFile::remove(QDir(appDir).filePath("config/client_identity.dat"));
config.setValue("Update", "device_id", QString());
};
QString licenseKey = config.getValue("License", "license_key").trimmed();
auto promptAndSaveLicense = [&](const QString& reason) -> QString {
auto promptAndSaveLicense = [&](const QString &reason) -> QString
{
QString promptReason = reason;
while (true) {
while (true)
{
progress.close();
bool accepted = false;
const QString appName = config.getValue("App", "app_name").trimmed();
const QString displayName = appName.isEmpty()
? QCoreApplication::translate("Launcher", "application")
: appName;
const QString prompt = promptReason.trimmed().isEmpty()
? QString("请输入%1授权 License").arg(appName.isEmpty() ? "软件" : appName)
: QString("%1\n\n请重新输入%2授权 License").arg(promptReason, appName.isEmpty() ? "软件" : appName);
? QCoreApplication::translate("Launcher", "Enter the license for %1:").arg(displayName)
: QCoreApplication::translate("Launcher", "%1\n\nPlease re-enter the license for %2:").arg(promptReason, displayName);
licenseKey = QInputDialog::getText(
nullptr,
"输入 License",
prompt,
QLineEdit::Normal,
QString(),
&accepted
).trimmed();
if (!accepted) {
QMessageBox::information(nullptr, "需要 License", "首次启动需要输入管理员提供的 License。");
nullptr,
QCoreApplication::translate("Launcher", "Enter License"),
prompt,
QLineEdit::Normal,
QString(),
&accepted)
.trimmed();
if (!accepted)
{
QMessageBox::information(
nullptr,
QCoreApplication::translate("Launcher", "License Required"),
QCoreApplication::translate("Launcher", "A license provided by an administrator is required for the first launch."));
return QString();
}
if (licenseKey.isEmpty()) {
QMessageBox::warning(nullptr, "License 不能为空", "请粘贴管理员在后台创建的 License。");
if (licenseKey.isEmpty())
{
QMessageBox::warning(
nullptr,
QCoreApplication::translate("Launcher", "License Cannot Be Empty"),
QCoreApplication::translate("Launcher", "Paste the license created by an administrator."));
promptReason.clear();
continue;
}
if (!config.setValue("License", "license_key", licenseKey)) {
QMessageBox::critical(nullptr, "保存 License 失败",
QString("无法写入配置文件:%1\n%2").arg(config.configPath(), config.lastError()));
if (!config.setValue("License", "license_key", licenseKey))
{
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Failed to Save License"),
QCoreApplication::translate("Launcher", "Cannot save system state at %1:\n%2")
.arg(config.configPath(), config.lastError()));
return QString();
}
progress.show();
progress.setLabelText("正在验证 License...");
progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying license..."));
QApplication::processEvents();
return licenseKey;
}
};
while (true) {
if (licenseKey.isEmpty()) {
while (true)
{
if (licenseKey.isEmpty())
{
licenseKey = promptAndSaveLicense(QString());
if (licenseKey.isEmpty()) return 0;
if (licenseKey.isEmpty())
return 0;
}
DeviceIdentityHelper identity(appDir);
if (identity.ensureIssued(config.getValue("Server", "api_base_url"),
config.getValue("Server", "client_token"),
config.getValue("App", "app_id"),
config.getValue("App", "channel"),
licenseKey)) {
licenseKey))
{
break;
}
const QString error = identity.errorString();
if (!isLicenseError(error)) {
if (!isLicenseError(error))
{
progress.close();
QMessageBox::critical(nullptr, "设备身份验证失败", error);
QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Device Authentication Failed"), error);
return -1;
}
clearDeviceCredential();
licenseKey = promptAndSaveLicense(QString("当前 License 无法使用:%1").arg(error));
if (licenseKey.isEmpty()) return 0;
licenseKey = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current license cannot be used: %1").arg(error));
if (licenseKey.isEmpty())
return 0;
}
UpdateLogic logic;
@@ -121,27 +159,37 @@ int main(int argc, char* argv[])
const QString appId = logic.getAppId();
const QString channel = logic.getChannel();
if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403) {
if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403)
{
progress.close();
const QJsonValue detail = response.value("detail");
const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString();
const QString displayMessage = message.isEmpty() ? "设备或 License 授权无效。" : message;
if (isLicenseError(displayMessage)) {
const QString displayMessage = message.isEmpty()
? QCoreApplication::translate("Launcher", "The device or license authorization is invalid.")
: message;
if (isLicenseError(displayMessage))
{
clearDeviceCredential();
const QString newLicense = promptAndSaveLicense(QString("当前授权被服务端拒绝:%1").arg(displayMessage));
if (!newLicense.isEmpty()) {
const QString newLicense = promptAndSaveLicense(
QCoreApplication::translate("Launcher", "The server rejected the current license: %1").arg(displayMessage));
if (!newLicense.isEmpty())
{
progress.close();
QMessageBox::information(nullptr, "License 已保存", "请重新启动 Launcher 完成设备授权和更新检查。");
QMessageBox::information(
nullptr,
QCoreApplication::translate("Launcher", "License Saved"),
QCoreApplication::translate("Launcher", "Restart Launcher to complete device authorization and check for updates."));
}
return 0;
}
QMessageBox::critical(nullptr, "授权被拒绝", displayMessage);
QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Authorization Denied"), displayMessage);
return -1;
}
const QString launchToken = config.getValue("App", "launch_token");
const QString currentVersion = config.getValue("App", "current_version");
const auto configuredName = [&](const QString& key, const QString& fallback) {
const auto configuredName = [&](const QString &key, const QString &fallback)
{
const QString value = config.getValue("Runtime", key).trimmed();
return value.isEmpty() ? fallback : value;
};
@@ -149,53 +197,99 @@ int main(int argc, char* argv[])
const QString updaterExecutable = configuredName("updater_executable", "Updater.exe");
const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
const auto importOfflinePackage = [&]() {
const QString package = QFileDialog::getOpenFileName(nullptr, "选择离线更新包", QString(), "Marsco 离线更新包 (*.upd)");
IntegrityHelper integrity(config.installRoot());
if (!integrity.verifyInstalledVersion(appId, channel, currentVersion))
{
progress.close();
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Integrity Check Failed"),
QCoreApplication::translate(
"Launcher",
"Application files failed signed manifest verification:\n%1")
.arg(integrity.errorString()));
return -1;
}
const auto importOfflinePackage = [&]()
{
const QString package = QFileDialog::getOpenFileName(
nullptr,
QCoreApplication::translate("Launcher", "Select Offline Update Package"),
QString(),
QCoreApplication::translate("Launcher", "Marsco Offline Update Package (*.upd)"));
return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)});
};
const QString deviceId = config.getValue("Update", "device_id");
if (QCoreApplication::arguments().contains("--import-offline")) {
if (QCoreApplication::arguments().contains("--import-offline"))
{
progress.close();
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包。");
if (!importOfflinePackage())
QMessageBox::information(
nullptr,
QCoreApplication::translate("Launcher", "Offline Update"),
QCoreApplication::translate("Launcher", "No offline update package was selected."));
return 0;
}
const auto startMainApp = [&]() {
const auto startMainApp = [&]()
{
QString ticketPath;
QString ticketError;
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
launchToken, &ticketPath, &ticketError)) {
launchToken, &ticketPath, &ticketError))
{
qDebug() << "Cannot create launch ticket:" << ticketError;
return false;
}
const bool started = QProcess::startDetached(mainAppPath,
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
if (!started) QFile::remove(ticketPath);
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
if (!started)
QFile::remove(ticketPath);
return started;
};
progress.setLabelText("正在验证本地运行策略...");
progress.setLabelText(QCoreApplication::translate("Launcher", "Validating the local run policy..."));
QApplication::processEvents();
PolicyHelper policy(appDir);
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
{
progress.close();
QMessageBox::critical(nullptr, "无法启动", QString("本地版本策略无效:%1").arg(policy.errorString()));
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Unable to Start"),
QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString()));
return -1;
}
if (!policy.isApplicable(appId, channel, currentVersion))
{
progress.close();
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Unable to Start"),
QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString()));
return -1;
}
if (policy.isExpired())
{
progress.close();
QMessageBox::critical(nullptr, "无法启动", "本地版本策略已过期,请连接网络或联系管理员。");
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Unable to Start"),
QCoreApplication::translate("Launcher", "The local version policy has expired. Connect to the network or contact an administrator."));
return -1;
}
if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate))
const bool currentVersionAllowed =
policy.allowRun() && policy.isVersionAllowed(currentVersion);
if (!currentVersionAllowed && !(networkOk && needUpdate))
{
progress.close();
QMessageBox::critical(nullptr, "当前版本不可运行",
policy.message().isEmpty() ? QString("当前版本 %1 已被管理员停用。").arg(currentVersion)
: policy.message());
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Current Version Cannot Run"),
policy.message().isEmpty()
? QCoreApplication::translate("Launcher", "Version %1 has been disabled by an administrator.").arg(currentVersion)
: policy.message());
return -1;
}
@@ -203,19 +297,28 @@ int main(int argc, char* argv[])
if (!state.loadState())
{
progress.close();
QMessageBox::critical(nullptr, "无法启动", QString("无法读取本地状态:%1").arg(state.errorString()));
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Unable to Start"),
QCoreApplication::translate("Launcher", "Cannot read the local state: %1").arg(state.errorString()));
return -1;
}
if (state.isPolicySeqRolledBack(policy.policySeq()))
{
progress.close();
QMessageBox::critical(nullptr, "安全检查失败", "检测到版本策略序列回退,已阻止启动。");
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Security Check Failed"),
QCoreApplication::translate("Launcher", "A version policy sequence rollback was detected, so startup was blocked."));
return -1;
}
if (state.isSystemTimeRewound())
{
progress.close();
QMessageBox::critical(nullptr, "安全检查失败", "检测到系统时间可能被回拨,已阻止启动。");
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Security Check Failed"),
QCoreApplication::translate("Launcher", "A possible system clock rollback was detected, so startup was blocked."));
return -1;
}
@@ -223,22 +326,36 @@ int main(int argc, char* argv[])
{
progress.close();
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
const QString dialogTitle = rollbackOperation ? "版本回退"
: (policy.forceUpdate() ? "必须更新" : "发现新版本");
const bool updateRequired = policy.forceUpdate() || !currentVersionAllowed;
const QString dialogTitle = rollbackOperation
? QCoreApplication::translate("Launcher", "Version Rollback")
: (updateRequired
? QCoreApplication::translate("Launcher", "Update Required")
: QCoreApplication::translate("Launcher", "New Version Available"));
const QString prompt = policy.message().isEmpty()
? QString(rollbackOperation ? "管理员提供了版本 %1 作为回退目标,是否现在降级?"
: "发现新版本 %1,是否现在更新?").arg(latestVer)
: policy.message() + QString("\n目标版本:%1").arg(latestVer);
? (rollbackOperation
? QCoreApplication::translate("Launcher", "An administrator selected version %1 as the rollback target. Downgrade now?")
: QCoreApplication::translate("Launcher", "Version %1 is available. Update now?"))
.arg(latestVer)
: policy.message() + QCoreApplication::translate("Launcher", "\nTarget version: %1").arg(latestVer);
bool accepted = true;
if (policy.forceUpdate()) {
if (updateRequired)
{
QMessageBox::information(nullptr, dialogTitle, prompt);
} else {
accepted = QMessageBox::question(nullptr, dialogTitle, prompt,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes;
}
if (!accepted) {
if (!startMainApp()) {
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
else
{
accepted = QMessageBox::question(nullptr, dialogTitle, prompt,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes;
}
if (!accepted)
{
if (!startMainApp())
{
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Startup Failed"),
QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath));
return -1;
}
return 0;
@@ -246,17 +363,28 @@ int main(int argc, char* argv[])
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
if (!QProcess::startDetached(updaterPath, updaterArgs))
{
QMessageBox::critical(nullptr, "更新器启动失败", QString("无法启动更新器:%1").arg(updaterPath));
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath));
return -1;
}
return 0;
}
if (!networkOk) {
if (!networkOk)
{
progress.close();
if (QMessageBox::question(nullptr, "服务器不可用", "当前无法连接更新服务器。是否导入离线更新包?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包或无法启动更新器。");
if (QMessageBox::question(nullptr,
QCoreApplication::translate("Launcher", "Server Unavailable"),
QCoreApplication::translate("Launcher", "The update server is currently unreachable. Import an offline update package?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)
{
if (!importOfflinePackage())
QMessageBox::information(
nullptr,
QCoreApplication::translate("Launcher", "Offline Update"),
QCoreApplication::translate("Launcher", "No offline update package was selected, or the updater could not be started."));
return 0;
}
progress.show();
@@ -265,16 +393,24 @@ int main(int argc, char* argv[])
if (!networkOk && !policy.isOfflineAllowed())
{
progress.close();
QMessageBox::critical(nullptr, "网络不可用", "无法连接更新服务器,且当前策略不允许离线启动。");
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Network Unavailable"),
QCoreApplication::translate("Launcher", "The update server is unreachable and the current policy does not allow offline startup."));
return -1;
}
progress.setLabelText(networkOk ? "当前已是最新版本,正在启动..." : "当前处于离线模式,正在启动...");
progress.setLabelText(
networkOk ? QCoreApplication::translate("Launcher", "The current version is up to date. Starting...")
: QCoreApplication::translate("Launcher", "Offline mode is active. Starting..."));
QApplication::processEvents();
if (!startMainApp())
{
progress.close();
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
QMessageBox::critical(
nullptr,
QCoreApplication::translate("Launcher", "Startup Failed"),
QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath));
return -1;
}
progress.close();