feat(client): improve SDK packaging and registry config

This commit is contained in:
2026-07-14 01:37:06 +00:00
parent cb5819ab7c
commit 94b20bf2bb
38 changed files with 3040 additions and 3960 deletions
+290 -210
View File
@@ -1,19 +1,18 @@
#include <windows.h>
#include <QApplication>
#include <QCoreApplication>
#include <QApplication>
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QElapsedTimer>
#include <QFile>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QSaveFile>
#include <QStorageInfo>
#include <QTextCodec>
#include <QThread>
#include "UpdaterLogic.h"
#include <QProcess>
#include <QProgressDialog>
#include <QSaveFile>
#include <QStorageInfo>
#include <QThread>
#include <QTranslator>
#include "UpdaterLogic.h"
#include "UpdateTransaction.h"
#include "FileHelper.h"
#include "ConfigHelper.h"
@@ -21,35 +20,44 @@
#include "PolicyHelper.h"
#include <QVersionNumber>
int main(int argc, char* argv[])
{
SetConsoleOutputCP(65001);
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
QApplication app(argc, argv);
QApplication::setApplicationName("Marsco Updater");
UpdaterLogic logic;
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QApplication::setApplicationName("Marsco Updater");
QTranslator translator;
if (translator.load(":/i18n/update-client_zh_CN.qm"))
app.installTranslator(&translator);
const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested();
if (elevatedWriteExitCode >= 0)
return elevatedWriteExitCode;
UpdaterLogic logic;
QString offlinePackagePath;
QString appId;
QString channel;
QString targetVersion;
int targetVersionId = 0;
if (argc >= 2 && QString(argv[1]).startsWith("--offline-package=")) {
offlinePackagePath = QString(argv[1]).mid(QString("--offline-package=").size());
if (!logic.loadOfflinePackage(offlinePackagePath)) {
QMessageBox::critical(nullptr, "离线包无效", logic.offlineError());
return -1;
}
if (argc >= 2 && QString(argv[1]).startsWith("--offline-package=")) {
offlinePackagePath = QString(argv[1]).mid(QString("--offline-package=").size());
if (!logic.loadOfflinePackage(offlinePackagePath)) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Invalid Offline Package"),
logic.offlineError());
return -1;
}
const QJsonObject packageManifest = logic.getManifest();
appId = packageManifest.value("app_id").toString();
channel = packageManifest.value("channel").toString();
targetVersion = packageManifest.value("version").toString();
targetVersionId = packageManifest.value("manifest_seq").toInt();
} else {
if (argc < 5) {
QMessageBox::critical(nullptr, "更新器参数错误", "更新器缺少在线更新参数或离线更新包。");
return -1;
}
} else {
if (argc < 5) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Updater Argument Error"),
QCoreApplication::translate("Updater", "The updater is missing online update arguments or an offline update package."));
return -1;
}
appId = argv[1]; channel = argv[2]; targetVersion = argv[3]; targetVersionId = QString(argv[4]).toInt();
}
QString bootstrapResult;
@@ -64,48 +72,58 @@ int main(int argc, char* argv[])
ConfigHelper& config = ConfigHelper::instance();
const QString targetDir = config.installRoot();
const QString updateDir = config.updateRoot();
QDir().mkpath(updateDir);
if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) {
QMessageBox::critical(nullptr, "离线包不适用", "更新包的应用或渠道与本机配置不一致。");
return -1;
}
QDir().mkpath(updateDir);
if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Offline Package Not Applicable"),
QCoreApplication::translate("Updater", "The update package application or channel does not match the local configuration."));
return -1;
}
QString fromVersion = config.getValue("App", "current_version");
if (!offlinePackagePath.isEmpty()) {
PolicyHelper offlinePolicy(runtimeDir);
if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired()
|| !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) {
QMessageBox::critical(nullptr, "离线更新被拒绝", "本地签名策略已过期、禁止离线更新或不允许目标版本。");
return -1;
}
if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired()
|| !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Offline Update Rejected"),
QCoreApplication::translate("Updater", "The local signed policy has expired, forbids offline updates, or does not allow the target version."));
return -1;
}
if (QVersionNumber::compare(QVersionNumber::fromString(targetVersion),
QVersionNumber::fromString(fromVersion)) < 0
&& !offlinePolicy.allowRollback()) {
QMessageBox::critical(nullptr, "禁止降级", "当前签名策略不允许安装较低版本的离线包。");
return -1;
}
&& !offlinePolicy.allowRollback()) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Downgrade Forbidden"),
QCoreApplication::translate("Updater", "The current signed policy does not allow installing an offline package with a lower version."));
return -1;
}
}
QString deviceId = config.getValue("Update", "device_id");
if (deviceId.isEmpty()) deviceId = "unknown_device";
if (!resumingFromBootstrap) {
QString restoredVersion;
QString recoveryError;
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) {
QMessageBox::critical(nullptr, "更新恢复失败",
QString("检测到上次更新未完成,但无法恢复旧版本:%1\n请不要继续运行软件,并联系管理员。").arg(recoveryError));
return -1;
}
if (!restoredVersion.isEmpty() && restoredVersion != fromVersion) {
if (!config.setValue("App", "current_version", restoredVersion)) {
QMessageBox::critical(nullptr, "更新恢复失败", "旧文件已经恢复,但无法恢复版本状态,请检查配置目录写入权限。");
return -1;
}
if (!resumingFromBootstrap) {
QString restoredVersion;
QString recoveryError;
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Update Recovery Failed"),
QCoreApplication::translate("Updater", "An unfinished update was detected, but the old version could not be restored: %1\nDo not continue running the software. Please contact the administrator.")
.arg(recoveryError));
return -1;
}
if (!restoredVersion.isEmpty() && restoredVersion != fromVersion) {
if (!config.setValue("App", "current_version", restoredVersion)) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Update Recovery Failed"),
QCoreApplication::translate("Updater", "The old files were restored, but the version state could not be restored. Please check write permissions for the configuration directory."));
return -1;
}
fromVersion = restoredVersion;
}
}
QProgressDialog progress("正在准备更新...", QString(), 0, 100);
progress.setWindowTitle(QString("正在更新到 %1").arg(targetVersion));
QProgressDialog progress(QCoreApplication::translate("Updater", "Preparing update..."), QString(), 0, 100);
progress.setWindowTitle(QCoreApplication::translate("Updater", "Updating to %1").arg(targetVersion));
progress.setCancelButton(nullptr);
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
@@ -131,10 +149,11 @@ int main(int argc, char* argv[])
return QString::number(bytes) + " B";
};
QObject::connect(&logic, &UpdaterLogic::downloadProgress,
[&](qint64 received, qint64 total, const QString& path, double bytesPerSecond) {
const int value = total > 0 ? 30 + static_cast<int>(30 * received / total) : 60;
const QString fileText = path.isEmpty() ? QString("正在准备下载...")
: QString("正在下载:%1").arg(path);
[&](qint64 received, qint64 total, const QString& path, double bytesPerSecond) {
const int value = total > 0 ? 30 + static_cast<int>(30 * received / total) : 60;
const QString fileText = path.isEmpty()
? QCoreApplication::translate("Updater", "Preparing download...")
: QCoreApplication::translate("Updater", "Downloading: %1").arg(path);
progress.setValue(value);
progress.setLabelText(QString("%1\n%2 / %3 · %4/s")
.arg(fileText, formatBytes(received), formatBytes(total),
@@ -193,31 +212,32 @@ int main(int argc, char* argv[])
};
return QFile::exists(bootstrapPath) && QProcess::startDetached(bootstrapPath, args);
};
const auto delegateRollback = [&](const QString& title, const QString& reason) {
FileHelper::killProcess(mainExecutable);
transaction.markRollbackRequired(reason);
progress.setLabelText("正在将回滚工作移交给 Bootstrap...");
QApplication::processEvents();
const auto delegateRollback = [&](const QString& title, const QString& reason) {
FileHelper::killProcess(mainExecutable);
transaction.markRollbackRequired(reason);
progress.setLabelText(QCoreApplication::translate("Updater", "Delegating rollback to Bootstrap..."));
QApplication::processEvents();
if (launchBootstrap("rollback")) {
progress.close();
return 0;
}
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, title,
reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。");
return -1;
};
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, title,
reason + QCoreApplication::translate("Updater", "\n\nCannot start Bootstrap to perform rollback. Do not continue running the software. Please contact the administrator."));
return -1;
};
if (resumingFromBootstrap) {
QString resumeError;
if (!transaction.resumeExisting(&resumeError)) {
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, "事务续办失败",
QString("无法读取 Bootstrap 更新事务:%1").arg(resumeError));
return -1;
}
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Transaction Resume Failed"),
QCoreApplication::translate("Updater", "Cannot read the Bootstrap update transaction: %1").arg(resumeError));
return -1;
}
fromVersion = transaction.fromVersion();
if (QFile::exists(QDir(transaction.backupDir()).filePath(".offline_mode")))
offlinePackagePath = "__bootstrap_resumed_offline__";
@@ -225,40 +245,54 @@ int main(int argc, char* argv[])
const bool stateOk = config.setValue("App", "current_version", fromVersion);
transaction.markRolledBack();
reportUpdateResult(false);
if (stateOk) launchMainApp();
progress.close();
QMessageBox::warning(nullptr, "更新已回滚",
stateOk ? "新版本安装或启动失败,已自动恢复并启动旧版本。"
: "旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。");
return stateOk ? 0 : -1;
}
if (bootstrapResult != "success") {
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, "自动回滚失败",
"Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。");
return -1;
}
} else if (!transaction.initialize()) {
return fail("更新准备失败", "无法创建更新事务目录或保存事务状态。", "transaction_init_failed");
} else if (!offlinePackagePath.isEmpty()) {
QFile marker(QDir(transaction.backupDir()).filePath(".offline_mode"));
if (!marker.open(QIODevice::WriteOnly) || marker.write("offline\n") != 8)
return fail("更新准备失败", "无法保存离线事务标记。", "offline_marker_failed");
}
setProgress(resumingFromBootstrap ? 72 : 10, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包...");
if (stateOk) launchMainApp();
progress.close();
QMessageBox::warning(nullptr,
QCoreApplication::translate("Updater", "Update Rolled Back"),
stateOk
? QCoreApplication::translate("Updater", "The new version failed to install or start. The old version has been restored and started automatically.")
: QCoreApplication::translate("Updater", "The old files were restored, but the old version number could not be written back. Please check configuration directory permissions."));
return stateOk ? 0 : -1;
}
if (bootstrapResult != "success") {
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Updater", "Automatic Rollback Failed"),
QCoreApplication::translate("Updater", "Bootstrap could not fully restore the old version. Do not continue running the software. Please contact the administrator."));
return -1;
}
} else if (!transaction.initialize()) {
return fail(QCoreApplication::translate("Updater", "Update Preparation Failed"),
QCoreApplication::translate("Updater", "Cannot create the update transaction directory or save the transaction state."),
"transaction_init_failed");
} else if (!offlinePackagePath.isEmpty()) {
QFile marker(QDir(transaction.backupDir()).filePath(".offline_mode"));
if (!marker.open(QIODevice::WriteOnly) || marker.write("offline\n") != 8)
return fail(QCoreApplication::translate("Updater", "Update Preparation Failed"),
QCoreApplication::translate("Updater", "Cannot save the offline transaction marker."),
"offline_marker_failed");
}
setProgress(resumingFromBootstrap ? 72 : 10,
offlinePackagePath.isEmpty()
? QCoreApplication::translate("Updater", "Fetching and verifying version manifest...")
: QCoreApplication::translate("Updater", "Verifying offline update package..."));
const QString manifestCacheDir = QDir(updateDir).filePath("manifest_cache");
if (resumingFromBootstrap) {
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
return delegateRollback("清单缓存失败", "Bootstrap 安装后无法读取签名 Manifest 缓存。");
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation."));
} else if (offlinePackagePath.isEmpty()) {
logic.getManifest(appId, channel, targetVersion, targetVersionId);
}
if (!logic.verifyManifestSignature()) {
if (resumingFromBootstrap)
return delegateRollback("安全验证失败", "Bootstrap 安装后无法重新验证版本清单签名。");
return fail("安全验证失败", "版本清单签名无效,更新已停止。请联系管理员。", "manifest_signature_invalid");
if (resumingFromBootstrap)
return delegateRollback(QCoreApplication::translate("Updater", "Security Verification Failed"),
QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation."));
return fail(QCoreApplication::translate("Updater", "Security Verification Failed"),
QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."),
"manifest_signature_invalid");
}
QStringList obsoletePaths;
if (!resumingFromBootstrap && fromVersion != targetVersion) {
@@ -272,47 +306,64 @@ int main(int argc, char* argv[])
qDebug() << "No valid signed previous manifest cache; obsolete deletion skipped for safety";
}
}
if (!logic.saveManifestCache(manifestCacheDir)) {
if (resumingFromBootstrap)
return delegateRollback("清单缓存失败", "无法保存新版本 Manifest 缓存。");
return fail("清单缓存失败", "无法保存新版本 Manifest 缓存,更新已停止。", "manifest_cache_failed");
}
if (!resumingFromBootstrap) {
setProgress(25, offlinePackagePath.isEmpty() ? "正在获取安全下载地址..." : "正在准备离线包文件...");
if (offlinePackagePath.isEmpty())
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
if (logic.getFileList().isEmpty())
return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list");
if (!logic.saveManifestCache(manifestCacheDir)) {
if (resumingFromBootstrap)
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache."));
return fail(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."),
"manifest_cache_failed");
}
if (!resumingFromBootstrap) {
setProgress(25,
offlinePackagePath.isEmpty()
? QCoreApplication::translate("Updater", "Fetching secure download URLs...")
: QCoreApplication::translate("Updater", "Preparing offline package files..."));
if (offlinePackagePath.isEmpty())
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
if (logic.getFileList().isEmpty())
return fail(QCoreApplication::translate("Updater", "No Files to Update"),
QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."),
"empty_file_list");
QStorageInfo storage(updateDir);
storage.refresh();
const qint64 requiredBytes = logic.estimateAdditionalDiskBytes(targetDir, obsoletePaths);
const qint64 availableBytes = storage.bytesAvailable();
if (!storage.isValid() || !storage.isReady() || availableBytes < requiredBytes) {
return fail("磁盘空间不足",
QString("更新至少需要 %1 可用空间,安装盘当前仅剩 %2。\n"
"所需空间已包含下载文件、旧版本备份和安全余量。")
.arg(formatBytes(requiredBytes), formatBytes(qMax<qint64>(0, availableBytes))),
"disk_space_insufficient");
}
const QString stagingDir = transaction.stagingDir();
setProgress(30, QString(offlinePackagePath.isEmpty() ? "正在下载并校验 %1 个版本文件..." : "正在提取并校验 %1 个离线文件...").arg(logic.getFileList().size()));
if (!offlinePackagePath.isEmpty()) {
if (!logic.loadOfflinePackage(offlinePackagePath, stagingDir))
return fail("离线包提取失败", logic.offlineError(), "offline_package_invalid");
} else {
if (!logic.downloadAllFiles(stagingDir, targetDir)) {
logic.reportDownloadResult(appId, channel, targetVersion, false);
return fail("下载失败", "部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。", "download_failed");
}
logic.reportDownloadResult(appId, channel, targetVersion, true);
}
setProgress(58, "正在校验完整版本文件...");
if (!logic.validateLocalFiles(stagingDir, targetDir))
return fail("文件校验失败", "暂存文件与版本清单不一致,更新已停止。", "staging_verify_failed");
const qint64 availableBytes = storage.bytesAvailable();
if (!storage.isValid() || !storage.isReady() || availableBytes < requiredBytes) {
return fail(QCoreApplication::translate("Updater", "Insufficient Disk Space"),
QCoreApplication::translate("Updater",
"The update requires at least %1 of free space, but the installation drive currently has only %2.\n"
"The required space includes downloaded files, old version backups, and a safety margin.")
.arg(formatBytes(requiredBytes), formatBytes(qMax<qint64>(0, availableBytes))),
"disk_space_insufficient");
}
const QString stagingDir = transaction.stagingDir();
setProgress(30, offlinePackagePath.isEmpty()
? QCoreApplication::translate("Updater", "Downloading and verifying %1 version files...").arg(logic.getFileList().size())
: QCoreApplication::translate("Updater", "Extracting and verifying %1 offline files...").arg(logic.getFileList().size()));
if (!offlinePackagePath.isEmpty()) {
if (!logic.loadOfflinePackage(offlinePackagePath, stagingDir))
return fail(QCoreApplication::translate("Updater", "Offline Package Extraction Failed"),
logic.offlineError(),
"offline_package_invalid");
} else {
if (!logic.downloadAllFiles(stagingDir, targetDir)) {
logic.reportDownloadResult(appId, channel, targetVersion, false);
return fail(QCoreApplication::translate("Updater", "Download Failed"),
QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network and try again."),
"download_failed");
}
logic.reportDownloadResult(appId, channel, targetVersion, true);
}
setProgress(58, QCoreApplication::translate("Updater", "Verifying complete version files..."));
if (!logic.validateLocalFiles(stagingDir, targetDir))
return fail(QCoreApplication::translate("Updater", "File Verification Failed"),
QCoreApplication::translate("Updater", "The staged files do not match the version manifest. The update has stopped."),
"staging_verify_failed");
QStringList changedPaths;
QDir stagingRoot(stagingDir);
@@ -322,89 +373,118 @@ int main(int argc, char* argv[])
const QString runtimePrefix = config.runtimeRelativePath();
const QString bootstrapManifestPath = QDir::fromNativeSeparators(
runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable);
for (const QString& path : changedPaths) {
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapManifestPath), "bootstrap_self_update_blocked");
}
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed");
setProgress(66, "正在关闭主程序...");
if (!FileHelper::killProcess(mainExecutable))
return fail("无法关闭主程序", QString("%1 仍在运行,请手动关闭后重试。").arg(mainExecutable), "mainapp_close_failed");
setProgress(72, QString("正在备份 %1 个待变更文件(其中删除 %2 个)...")
.arg(changedPaths.size() + obsoletePaths.size()).arg(obsoletePaths.size()));
if (!transaction.backupCurrentFiles())
return fail("备份失败", "无法备份当前版本文件,尚未安装新版本。请检查磁盘空间和目录权限。", "backup_failed");
const QString planFile = bootstrapPlanFile();
QSaveFile plan(planFile);
if (!plan.open(QIODevice::WriteOnly))
return fail("接管准备失败", "无法创建 Bootstrap 文件计划。", "bootstrap_plan_failed");
for (const QString& path : changedPaths) {
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
return fail(QCoreApplication::translate("Updater", "Bootstrap Cannot Self-Update"),
QCoreApplication::translate("Updater", "This version contains a new %1. Please upgrade this component with the installer, then publish the business version again.")
.arg(bootstrapManifestPath),
"bootstrap_self_update_blocked");
}
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
return fail(QCoreApplication::translate("Updater", "Transaction Recording Failed"),
QCoreApplication::translate("Updater", "Cannot save the list of verified or pending deletion files. The update has stopped."),
"transaction_record_failed");
setProgress(66, QCoreApplication::translate("Updater", "Closing the main application..."));
if (!FileHelper::killProcess(mainExecutable))
return fail(QCoreApplication::translate("Updater", "Cannot Close Main Application"),
QCoreApplication::translate("Updater", "%1 is still running. Please close it manually and try again.").arg(mainExecutable),
"mainapp_close_failed");
setProgress(72, QCoreApplication::translate("Updater", "Backing up %1 files to be changed, including %2 files to be deleted...")
.arg(changedPaths.size() + obsoletePaths.size()).arg(obsoletePaths.size()));
if (!transaction.backupCurrentFiles())
return fail(QCoreApplication::translate("Updater", "Backup Failed"),
QCoreApplication::translate("Updater", "Cannot back up current version files. The new version has not been installed. Please check disk space and directory permissions."),
"backup_failed");
const QString planFile = bootstrapPlanFile();
QSaveFile plan(planFile);
if (!plan.open(QIODevice::WriteOnly))
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
QCoreApplication::translate("Updater", "Cannot create the Bootstrap file plan."),
"bootstrap_plan_failed");
const auto writePlanItem = [&](char operation, const QString& path) {
const QByteArray line = QByteArray(1, operation) + '\t' + path.toUtf8() + '\n';
return plan.write(line) == line.size();
};
for (const QString& path : changedPaths) {
if (!writePlanItem('C', path)) {
plan.cancelWriting();
return fail("接管准备失败", "无法写入 Bootstrap 复制计划。", "bootstrap_plan_failed");
}
}
for (const QString& path : obsoletePaths) {
if (!writePlanItem('D', path)) {
plan.cancelWriting();
return fail("接管准备失败", "无法写入 Bootstrap 删除计划。", "bootstrap_plan_failed");
}
}
if (!plan.commit() || !transaction.markAwaitingBootstrap())
return fail("接管准备失败", "无法提交 Bootstrap 文件计划或事务状态。", "bootstrap_plan_failed");
setProgress(78, "正在将安装工作移交给 Bootstrap...");
if (!launchBootstrap("install"))
return fail("Bootstrap 启动失败", QString("无法启动独立更新接管程序:%1").arg(bootstrapPath), "bootstrap_start_failed");
if (!writePlanItem('C', path)) {
plan.cancelWriting();
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
QCoreApplication::translate("Updater", "Cannot write the Bootstrap copy plan."),
"bootstrap_plan_failed");
}
}
for (const QString& path : obsoletePaths) {
if (!writePlanItem('D', path)) {
plan.cancelWriting();
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
QCoreApplication::translate("Updater", "Cannot write the Bootstrap deletion plan."),
"bootstrap_plan_failed");
}
}
if (!plan.commit() || !transaction.markAwaitingBootstrap())
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
QCoreApplication::translate("Updater", "Cannot commit the Bootstrap file plan or transaction state."),
"bootstrap_plan_failed");
setProgress(78, QCoreApplication::translate("Updater", "Delegating installation to Bootstrap..."));
if (!launchBootstrap("install"))
return fail(QCoreApplication::translate("Updater", "Bootstrap Startup Failed"),
QCoreApplication::translate("Updater", "Cannot start the standalone update handoff program: %1").arg(bootstrapPath),
"bootstrap_start_failed");
progress.close();
return 0;
}
setProgress(82, "正在校验 Bootstrap 安装结果...");
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
return delegateRollback("安装校验失败", "新版本文件安装后校验未通过。");
for (const QString& path : transaction.obsoletePaths()) {
if (QFile::exists(QDir(targetDir).filePath(path)))
return delegateRollback("废弃文件清理失败", QString("废弃文件仍然存在:%1").arg(path));
}
setProgress(89, "正在保存新版本状态...");
if (!config.setValue("App", "current_version", targetVersion)
|| config.getValue("App", "current_version") != targetVersion)
return delegateRollback("状态保存失败", "无法保存当前版本号。");
const QString healthFile = transaction.healthFile();
QFile::remove(healthFile);
setProgress(94, "正在启动新版本并等待健康确认...");
if (!launchMainApp(healthFile))
return delegateRollback("启动失败", QString("%1 无法启动。").arg(mainExecutable));
setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result..."));
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
return delegateRollback(QCoreApplication::translate("Updater", "Installation Verification Failed"),
QCoreApplication::translate("Updater", "New version files failed verification after installation."));
for (const QString& path : transaction.obsoletePaths()) {
if (QFile::exists(QDir(targetDir).filePath(path)))
return delegateRollback(QCoreApplication::translate("Updater", "Obsolete File Cleanup Failed"),
QCoreApplication::translate("Updater", "An obsolete file still exists: %1").arg(path));
}
setProgress(89, QCoreApplication::translate("Updater", "Saving new version state..."));
if (!config.setValue("App", "current_version", targetVersion)
|| config.getValue("App", "current_version") != targetVersion)
return delegateRollback(QCoreApplication::translate("Updater", "State Save Failed"),
QCoreApplication::translate("Updater", "Cannot save the current version number."));
const QString healthFile = transaction.healthFile();
QFile::remove(healthFile);
setProgress(94, QCoreApplication::translate("Updater", "Starting the new version and waiting for health confirmation..."));
if (!launchMainApp(healthFile))
return delegateRollback(QCoreApplication::translate("Updater", "Startup Failed"),
QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable));
QElapsedTimer healthTimer;
healthTimer.start();
while (healthTimer.elapsed() < healthCheckTimeoutMs && !QFile::exists(healthFile)) {
QApplication::processEvents();
QThread::msleep(100);
}
if (!QFile::exists(healthFile))
return delegateRollback("启动确认失败", QString("新版本在 %1 毫秒内没有完成启动健康确认。").arg(healthCheckTimeoutMs));
setProgress(99, "正在提交更新事务...");
if (!transaction.commit())
return delegateRollback("事务提交失败", "新版本已经启动,但无法提交更新事务。");
}
if (!QFile::exists(healthFile))
return delegateRollback(QCoreApplication::translate("Updater", "Startup Confirmation Failed"),
QCoreApplication::translate("Updater", "The new version did not complete startup health confirmation within %1 milliseconds.")
.arg(healthCheckTimeoutMs));
setProgress(99, QCoreApplication::translate("Updater", "Committing update transaction..."));
if (!transaction.commit())
return delegateRollback(QCoreApplication::translate("Updater", "Transaction Commit Failed"),
QCoreApplication::translate("Updater", "The new version has started, but the update transaction could not be committed."));
QFile::remove(healthFile);
QFile::remove(bootstrapPlanFile());
reportUpdateResult(true);
progress.setValue(100);
progress.close();
QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion));
reportUpdateResult(true);
progress.setValue(100);
progress.close();
QMessageBox::information(nullptr,
QCoreApplication::translate("Updater", "Update Complete"),
QCoreApplication::translate("Updater", "The software has been successfully updated to %1 and passed the startup health check.")
.arg(targetVersion));
return 0;
}