新增了客户端打包成sdk的功能和服务端docker镜像打包的功能,并修复了一些问题

This commit is contained in:
2026-07-07 09:48:01 +00:00
parent cd3dca042d
commit 9e545cb328
50 changed files with 5251 additions and 875 deletions
+9 -9
View File
@@ -1,9 +1,9 @@
# 强制所有编译、设计时生成均使用x64,禁止Win32
set(CMAKE_GENERATOR_PLATFORM x64 CACHE STRING "强制x64平台,禁用Win32")
# 强制所有编译、设计时生成均使用x64,禁止Win32
set(CMAKE_GENERATOR_PLATFORM x64 CACHE STRING "强制x64平台,禁用Win32")
set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCH x64)
# 关闭VS自动Win32设计时预生成
# 关闭VS自动Win32设计时预生成
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF)
# 清除32位Strawberry Perl路径干扰
# 清除32位Strawberry Perl路径干扰
list(REMOVE_ITEM CMAKE_INCLUDE_PATH "D:/softwaresInstallDir/strawberry-perl-5.22.1.3-32bit/c/include")
list(REMOVE_ITEM CMAKE_LIBRARY_PATH "D:/softwaresInstallDir/strawberry-perl-5.22.1.3-32bit/c/lib")
@@ -21,21 +21,21 @@ if(WIN32)
set_target_properties(Updater PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
# 关键:给Updater自身添加OpenSSL头文件目录,解决#include openssl/*找不到
# 关键:给Updater自身添加OpenSSL头文件目录,解决#include openssl/*找不到
target_include_directories(Updater PRIVATE ${OPENSSL_INC})
# 仅链接Common和QtOpenSSL库由Common INTERFACE自动传递
# 仅链接Common和QtOpenSSL库由Common INTERFACE自动传递
target_link_libraries(Updater PRIVATE
Common
Qt5::Core Qt5::Network Qt5::Gui Qt5::Widgets
)
# Qt部署脚本
# Qt部署脚本
if(WIN32)
get_target_property(QT_WINDEPLOYQT_EXE Qt5::qmake IMPORTED_LOCATION)
get_filename_component(QT_BIN_PATH "${QT_WINDEPLOYQT_EXE}" DIRECTORY)
set(WINDEPLOYQT "${QT_BIN_PATH}/windeployqt.exe")
add_custom_command(TARGET Updater POST_BUILD
COMMAND ${WINDEPLOYQT} --debug $<TARGET_FILE:Updater>
COMMAND ${WINDEPLOYQT} $<IF:$<CONFIG:Debug>,--debug,--release> $<TARGET_FILE:Updater>
)
endif()
endif()
+60
View File
@@ -12,6 +12,7 @@
#include <QCryptographicHash>
#include <QDir>
#include <QJsonDocument>
#include <QSaveFile>
#include <QApplication>
#include <algorithm>
#include "ConfigHelper.h"
@@ -325,6 +326,52 @@ bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString&
return true;
}
bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& stagingDir)
{
m_offlineError.clear();
QFile package(packagePath);
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = "无法打开离线更新包"; return false; }
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = "离线包格式标识无效"; return false; }
const QByteArray lengthBytes = package.read(8);
if (lengthBytes.size() != 8) { m_offlineError = "离线包头不完整"; return false; }
quint64 headerSize = 0;
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = "离线包头长度无效"; return false; }
QJsonParseError error;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = "离线包头 JSON 无效"; return false; }
const QJsonObject wrapper = wrapperDoc.object();
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = "离线包 RSA 签名无效"; return false; }
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = "离线包签名元数据无效"; return false; }
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = "Manifest 摘要与包签名不一致"; return false; }
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = "离线 Manifest 无效"; return false; }
manifest.insert("signature", wrapper.value("manifest_signature").toString());
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = "包信息与 Manifest 身份不一致"; return false; }
if (!verifyManifestSignature()) { m_offlineError = "离线 Manifest RSA 签名无效"; return false; }
const qint64 payloadStart = 16 + qint64(headerSize);
const QJsonArray entries = packageMeta.value("files").toArray();
for (const QJsonValue& value : entries) {
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = "离线包包含不安全路径或越界数据: " + path; return false; }
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
if (stagingDir.isEmpty()) continue;
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = "无法准备离线文件: " + path; return false; }
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = "无法创建暂存文件: " + path; return false; }
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = "离线文件读取失败: " + path; return false; } hash.addData(block); remaining -= block.size(); }
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = "离线文件 Hash 或写入失败: " + path; return false; }
}
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = "离线包文件数量与 Manifest 不一致"; return false; }
return true;
}
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
{
QString url = m_serverAddr + "/api/v1/update/download-url";
@@ -640,6 +687,18 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
return true;
}
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
const QString& version, bool success)
{
QJsonArray files;
for (const FileDownloadItem& item : m_fileItems)
files.append(QJsonObject{{"path", item.path}, {"size", item.size}});
QJsonObject body{{"app_id", appId}, {"channel", channel}, {"version", version},
{"result", success ? "success" : "fail"}, {"files", files}};
m_http.postRequest(m_serverAddr + "/api/v1/update/download-report", body,
[](int code, const QJsonObject&) { qDebug() << "Download result report returned code:" << code; });
}
void UpdaterLogic::reportResult(const QString& deviceId,
const QString& fromVer,
const QString& toVer,
@@ -648,6 +707,7 @@ void UpdaterLogic::reportResult(const QString& deviceId,
QString url = m_serverAddr + "/api/v1/update/report";
QJsonObject body;
body["app_id"] = ConfigHelper::instance().getValue("App", "app_id");
body["device_id"] = deviceId;
body["from_version"] = fromVer;
body["to_version"] = toVer;
+4
View File
@@ -29,9 +29,12 @@ public:
bool validateLocalFiles(const QString& stagingDir, const QString& installedDir = QString()) const;
bool saveManifestCache(const QString& cacheDir) const;
bool loadManifestCache(const QString& cacheDir, const QString& version);
bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString());
QString offlineError() const { return m_offlineError; }
void getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId);
void reportResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success);
void reportDownloadResult(const QString& appId, const QString& channel, const QString& version, bool success);
bool downloadAllFiles(const QString& tempDir, const QString& targetDir);
qint64 estimateAdditionalDiskBytes(const QString& targetDir, const QStringList& obsoletePaths) const;
QString calcLocalFileSha256(const QString& filePath) const;
@@ -65,5 +68,6 @@ private:
qint64 m_downloadCompletedBytes = 0;
qint64 m_sessionDownloadedBytes = 0;
QString m_currentDownloadPath;
QString m_offlineError;
QElapsedTimer m_downloadTimer;
};
+115 -36
View File
@@ -17,6 +17,9 @@
#include "UpdateTransaction.h"
#include "FileHelper.h"
#include "ConfigHelper.h"
#include "TicketHelper.h"
#include "PolicyHelper.h"
#include <QVersionNumber>
int main(int argc, char* argv[])
{
@@ -25,15 +28,30 @@ int main(int argc, char* argv[])
QApplication app(argc, argv);
QApplication::setApplicationName("Marsco Updater");
if (argc < 5) {
QMessageBox::critical(nullptr, "更新器参数错误", "更新器缺少应用、渠道或目标版本参数,请从 Launcher 启动。");
return -1;
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;
}
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;
}
appId = argv[1]; channel = argv[2]; targetVersion = argv[3]; targetVersionId = QString(argv[4]).toInt();
}
const QString appId = argv[1];
const QString channel = argv[2];
const QString targetVersion = argv[3];
const int targetVersionId = QString(argv[4]).toInt();
QString bootstrapResult;
for (int i = 5; i < argc; ++i) {
const QString arg = argv[i];
@@ -44,7 +62,25 @@ int main(int argc, char* argv[])
const QString targetDir = QApplication::applicationDirPath();
ConfigHelper& config = ConfigHelper::instance();
if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) {
QMessageBox::critical(nullptr, "离线包不适用", "更新包的应用或渠道与本机配置不一致。");
return -1;
}
QString fromVersion = config.getValue("App", "current_version");
if (!offlinePackagePath.isEmpty()) {
PolicyHelper offlinePolicy(targetDir);
if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired()
|| !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) {
QMessageBox::critical(nullptr, "离线更新被拒绝", "本地签名策略已过期、禁止离线更新或不允许目标版本。");
return -1;
}
if (QVersionNumber::compare(QVersionNumber::fromString(targetVersion),
QVersionNumber::fromString(fromVersion)) < 0
&& !offlinePolicy.allowRollback()) {
QMessageBox::critical(nullptr, "禁止降级", "当前签名策略不允许安装较低版本的离线包。");
return -1;
}
}
QString deviceId = config.getValue("Update", "device_id");
if (deviceId.isEmpty()) deviceId = "unknown_device";
@@ -75,7 +111,6 @@ int main(int argc, char* argv[])
progress.show();
QApplication::processEvents();
UpdaterLogic logic;
UpdateTransaction transaction(targetDir, fromVersion, targetVersion, targetVersionId);
const auto setProgress = [&](int value, const QString& message) {
progress.setValue(value);
@@ -103,22 +138,46 @@ int main(int argc, char* argv[])
formatBytes(static_cast<qint64>(bytesPerSecond))));
QApplication::processEvents();
});
const auto reportUpdateResult = [&](bool success) {
if (offlinePackagePath.isEmpty())
logic.reportResult(deviceId, fromVersion, targetVersion, success);
};
const auto fail = [&](const QString& title, const QString& message,
const QString& errorCode = QString("update_failed")) {
transaction.markFailed(errorCode, message);
logic.reportResult(deviceId, fromVersion, targetVersion, false);
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, title, message);
return -1;
};
const QString mainAppPath = QDir(targetDir).filePath("MainApp.exe");
const QString updaterPath = QDir(targetDir).filePath("Updater.exe");
const QString bootstrapPath = QDir(targetDir).filePath("Bootstrap.exe");
const auto configuredName = [&](const QString& key, const QString& fallback) {
const QString value = config.getValue("Runtime", key).trimmed();
return value.isEmpty() ? fallback : value;
};
const QString mainExecutable = configuredName("main_executable", "MainApp.exe");
const QString updaterExecutable = configuredName("updater_executable", "Updater.exe");
const QString bootstrapExecutable = configuredName("bootstrap_executable", "Bootstrap.exe");
bool timeoutOk = false;
int healthCheckTimeoutMs = config.getValue("Runtime", "health_check_timeout_ms").toInt(&timeoutOk);
if (!timeoutOk || healthCheckTimeoutMs < 1000) healthCheckTimeoutMs = 15000;
const QString mainAppPath = QDir(targetDir).filePath(mainExecutable);
const QString updaterPath = QDir(targetDir).filePath(updaterExecutable);
const QString bootstrapPath = QDir(targetDir).filePath(bootstrapExecutable);
const QString launchToken = config.getValue("App", "launch_token");
const auto launchMainApp = [&](const QString& healthFile = QString()) {
QStringList args{QString("--launcher-token=%1").arg(launchToken)};
QString ticketPath;
QString ticketError;
const QString launchVersion = config.getValue("App", "current_version");
if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken,
&ticketPath, &ticketError)) {
qDebug() << "Cannot create launch ticket:" << ticketError;
return false;
}
QStringList args{QString("--ticket-file=%1").arg(ticketPath)};
if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile));
return QProcess::startDetached(mainAppPath, args);
const bool started = QProcess::startDetached(mainAppPath, args);
if (!started) QFile::remove(ticketPath);
return started;
};
const auto bootstrapPlanFile = [&]() {
return QDir(targetDir).filePath("update/bootstrap_plan_" + transaction.transactionId() + ".txt");
@@ -132,7 +191,7 @@ 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("MainApp.exe");
FileHelper::killProcess(mainExecutable);
transaction.markRollbackRequired(reason);
progress.setLabelText("正在将回滚工作移交给 Bootstrap...");
QApplication::processEvents();
@@ -140,7 +199,7 @@ int main(int argc, char* argv[])
progress.close();
return 0;
}
logic.reportResult(deviceId, fromVersion, targetVersion, false);
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, title,
reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。");
@@ -150,17 +209,19 @@ int main(int argc, char* argv[])
if (resumingFromBootstrap) {
QString resumeError;
if (!transaction.resumeExisting(&resumeError)) {
logic.reportResult(deviceId, fromVersion, targetVersion, false);
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, "事务续办失败",
QString("无法读取 Bootstrap 更新事务:%1").arg(resumeError));
return -1;
}
fromVersion = transaction.fromVersion();
if (QFile::exists(QDir(transaction.backupDir()).filePath(".offline_mode")))
offlinePackagePath = "__bootstrap_resumed_offline__";
if (bootstrapResult == "rolledback") {
const bool stateOk = config.setValue("App", "current_version", fromVersion);
transaction.markRolledBack();
logic.reportResult(deviceId, fromVersion, targetVersion, false);
reportUpdateResult(false);
if (stateOk) launchMainApp();
progress.close();
QMessageBox::warning(nullptr, "更新已回滚",
@@ -169,7 +230,7 @@ int main(int argc, char* argv[])
return stateOk ? 0 : -1;
}
if (bootstrapResult != "success") {
logic.reportResult(deviceId, fromVersion, targetVersion, false);
reportUpdateResult(false);
progress.close();
QMessageBox::critical(nullptr, "自动回滚失败",
"Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。");
@@ -177,16 +238,25 @@ int main(int argc, char* argv[])
}
} 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, "正在获取并验证版本清单...");
logic.getManifest(appId, channel, targetVersion, targetVersionId);
setProgress(resumingFromBootstrap ? 72 : 10, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包...");
const QString manifestCacheDir = QDir(targetDir).filePath("update/manifest_cache");
if (resumingFromBootstrap) {
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
return delegateRollback("清单缓存失败", "Bootstrap 安装后无法读取签名 Manifest 缓存。");
} else if (offlinePackagePath.isEmpty()) {
logic.getManifest(appId, channel, targetVersion, targetVersionId);
}
if (!logic.verifyManifestSignature()) {
if (resumingFromBootstrap)
return delegateRollback("安全验证失败", "Bootstrap 安装后无法重新验证版本清单签名。");
return fail("安全验证失败", "版本清单签名无效,更新已停止。请联系管理员。", "manifest_signature_invalid");
}
const QString manifestCacheDir = QDir(targetDir).filePath("update/manifest_cache");
QStringList obsoletePaths;
if (!resumingFromBootstrap && fromVersion != targetVersion) {
UpdaterLogic oldManifestLogic;
@@ -206,8 +276,9 @@ int main(int argc, char* argv[])
}
if (!resumingFromBootstrap) {
setProgress(25, "正在获取安全下载地址...");
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
setProgress(25, offlinePackagePath.isEmpty() ? "正在获取安全下载地址..." : "正在准备离线包文件...");
if (offlinePackagePath.isEmpty())
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
if (logic.getFileList().isEmpty())
return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list");
@@ -224,9 +295,17 @@ int main(int argc, char* argv[])
}
const QString stagingDir = transaction.stagingDir();
setProgress(30, QString("正在下载并校验 %1 个版本文件...").arg(logic.getFileList().size()));
if (!logic.downloadAllFiles(stagingDir, targetDir))
return fail("下载失败", "部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。", "download_failed");
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))
@@ -238,15 +317,15 @@ int main(int argc, char* argv[])
while (stagingFiles.hasNext())
changedPaths.append(QDir::fromNativeSeparators(stagingRoot.relativeFilePath(stagingFiles.next())));
for (const QString& path : changedPaths) {
if (path.compare("Bootstrap.exe", Qt::CaseInsensitive) == 0)
return fail("Bootstrap 无法自更新", "本次版本包含新的 Bootstrap.exe。请使用安装包升级 Bootstrap,再重新发布业务版本。", "bootstrap_self_update_blocked");
if (path.compare(bootstrapExecutable, Qt::CaseInsensitive) == 0)
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapExecutable), "bootstrap_self_update_blocked");
}
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed");
setProgress(66, "正在关闭主程序...");
if (!FileHelper::killProcess("MainApp.exe"))
return fail("无法关闭主程序", "MainApp.exe 仍在运行,请手动关闭后重试。", "mainapp_close_failed");
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()));
@@ -300,16 +379,16 @@ int main(int argc, char* argv[])
QFile::remove(healthFile);
setProgress(94, "正在启动新版本并等待健康确认...");
if (!launchMainApp(healthFile))
return delegateRollback("启动失败", "MainApp.exe 无法启动。");
return delegateRollback("启动失败", QString("%1 无法启动。").arg(mainExecutable));
QElapsedTimer healthTimer;
healthTimer.start();
while (healthTimer.elapsed() < 15000 && !QFile::exists(healthFile)) {
while (healthTimer.elapsed() < healthCheckTimeoutMs && !QFile::exists(healthFile)) {
QApplication::processEvents();
QThread::msleep(100);
}
if (!QFile::exists(healthFile))
return delegateRollback("启动确认失败", "新版本在 15 秒内没有完成启动健康确认。");
return delegateRollback("启动确认失败", QString("新版本在 %1 毫秒内没有完成启动健康确认。").arg(healthCheckTimeoutMs));
setProgress(99, "正在提交更新事务...");
if (!transaction.commit())
@@ -317,7 +396,7 @@ int main(int argc, char* argv[])
QFile::remove(healthFile);
QFile::remove(bootstrapPlanFile());
logic.reportResult(deviceId, fromVersion, targetVersion, true);
reportUpdateResult(true);
progress.setValue(100);
progress.close();
QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion));