新增了客户端打包成sdk的功能和服务端docker镜像打包的功能,并修复了一些问题
This commit is contained in:
+115
-36
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user