#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "UpdaterLogic.h" #include "UpdateTransaction.h" #include "FileHelper.h" #include "ConfigHelper.h" int main(int argc, char* argv[]) { SetConsoleOutputCP(65001); QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); QApplication app(argc, argv); QApplication::setApplicationName("Marsco Updater"); if (argc < 5) { QMessageBox::critical(nullptr, "更新器参数错误", "更新器缺少应用、渠道或目标版本参数,请从 Launcher 启动。"); return -1; } 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]; if (arg.startsWith("--bootstrap-resume=")) bootstrapResult = arg.mid(QString("--bootstrap-resume=").size()); } const bool resumingFromBootstrap = !bootstrapResult.isEmpty(); const QString targetDir = QApplication::applicationDirPath(); ConfigHelper& config = ConfigHelper::instance(); QString fromVersion = config.getValue("App", "current_version"); QString deviceId = config.getValue("Update", "device_id"); if (deviceId.isEmpty()) deviceId = "unknown_device"; if (!resumingFromBootstrap) { QString restoredVersion; QString recoveryError; if (!UpdateTransaction::recoverInterrupted(targetDir, &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; } fromVersion = restoredVersion; } } QProgressDialog progress("正在准备更新...", QString(), 0, 100); progress.setWindowTitle(QString("正在更新到 %1").arg(targetVersion)); progress.setCancelButton(nullptr); progress.setWindowModality(Qt::ApplicationModal); progress.setMinimumDuration(0); progress.setAutoClose(false); progress.setValue(2); progress.show(); QApplication::processEvents(); UpdaterLogic logic; UpdateTransaction transaction(targetDir, fromVersion, targetVersion, targetVersionId); const auto setProgress = [&](int value, const QString& message) { progress.setValue(value); progress.setLabelText(message); QApplication::processEvents(); }; const auto formatBytes = [](qint64 bytes) { const double value = static_cast(bytes); if (bytes >= 1024LL * 1024 * 1024) return QString::number(value / (1024.0 * 1024 * 1024), 'f', 2) + " GB"; if (bytes >= 1024LL * 1024) return QString::number(value / (1024.0 * 1024), 'f', 2) + " MB"; if (bytes >= 1024) return QString::number(value / 1024.0, 'f', 1) + " KB"; 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(30 * received / total) : 60; const QString fileText = path.isEmpty() ? QString("正在准备下载...") : QString("正在下载:%1").arg(path); progress.setValue(value); progress.setLabelText(QString("%1\n%2 / %3 · %4/s") .arg(fileText, formatBytes(received), formatBytes(total), formatBytes(static_cast(bytesPerSecond)))); QApplication::processEvents(); }); 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); 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 QString launchToken = config.getValue("App", "launch_token"); const auto launchMainApp = [&](const QString& healthFile = QString()) { QStringList args{QString("--launcher-token=%1").arg(launchToken)}; if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile)); return QProcess::startDetached(mainAppPath, args); }; const auto bootstrapPlanFile = [&]() { return QDir(targetDir).filePath("update/bootstrap_plan_" + transaction.transactionId() + ".txt"); }; const auto launchBootstrap = [&](const QString& mode) { const QStringList args{ bootstrapPlanFile(), targetDir, transaction.stagingDir(), transaction.backupDir(), updaterPath, QString::number(QCoreApplication::applicationPid()), appId, channel, targetVersion, QString::number(targetVersionId), mode }; return QFile::exists(bootstrapPath) && QProcess::startDetached(bootstrapPath, args); }; const auto delegateRollback = [&](const QString& title, const QString& reason) { FileHelper::killProcess("MainApp.exe"); transaction.markRollbackRequired(reason); progress.setLabelText("正在将回滚工作移交给 Bootstrap..."); QApplication::processEvents(); if (launchBootstrap("rollback")) { progress.close(); return 0; } logic.reportResult(deviceId, fromVersion, targetVersion, false); progress.close(); QMessageBox::critical(nullptr, title, reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。"); return -1; }; if (resumingFromBootstrap) { QString resumeError; if (!transaction.resumeExisting(&resumeError)) { logic.reportResult(deviceId, fromVersion, targetVersion, false); progress.close(); QMessageBox::critical(nullptr, "事务续办失败", QString("无法读取 Bootstrap 更新事务:%1").arg(resumeError)); return -1; } fromVersion = transaction.fromVersion(); if (bootstrapResult == "rolledback") { const bool stateOk = config.setValue("App", "current_version", fromVersion); transaction.markRolledBack(); logic.reportResult(deviceId, fromVersion, targetVersion, false); if (stateOk) launchMainApp(); progress.close(); QMessageBox::warning(nullptr, "更新已回滚", stateOk ? "新版本安装或启动失败,已自动恢复并启动旧版本。" : "旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。"); return stateOk ? 0 : -1; } if (bootstrapResult != "success") { logic.reportResult(deviceId, fromVersion, targetVersion, false); progress.close(); QMessageBox::critical(nullptr, "自动回滚失败", "Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。"); return -1; } } else if (!transaction.initialize()) { return fail("更新准备失败", "无法创建更新事务目录或保存事务状态。", "transaction_init_failed"); } setProgress(resumingFromBootstrap ? 72 : 10, "正在获取并验证版本清单..."); 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; if (oldManifestLogic.loadManifestCache(manifestCacheDir, fromVersion) && oldManifestLogic.getManifest().value("version").toString() == fromVersion && oldManifestLogic.verifyManifestSignature()) { obsoletePaths = logic.obsoleteFilesComparedTo(oldManifestLogic.getManifest()); qDebug() << "Obsolete files from signed previous manifest:" << obsoletePaths; } else { 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, "正在获取安全下载地址..."); logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId); if (logic.getFileList().isEmpty()) return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list"); QStorageInfo storage(targetDir); 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(0, availableBytes))), "disk_space_insufficient"); } 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(58, "正在校验完整版本文件..."); if (!logic.validateLocalFiles(stagingDir, targetDir)) return fail("文件校验失败", "暂存文件与版本清单不一致,更新已停止。", "staging_verify_failed"); QStringList changedPaths; QDir stagingRoot(stagingDir); QDirIterator stagingFiles(stagingDir, QDir::Files, QDirIterator::Subdirectories); 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 (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths)) return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed"); setProgress(66, "正在关闭主程序..."); if (!FileHelper::killProcess("MainApp.exe")) return fail("无法关闭主程序", "MainApp.exe 仍在运行,请手动关闭后重试。", "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"); 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"); 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("启动失败", "MainApp.exe 无法启动。"); QElapsedTimer healthTimer; healthTimer.start(); while (healthTimer.elapsed() < 15000 && !QFile::exists(healthFile)) { QApplication::processEvents(); QThread::msleep(100); } if (!QFile::exists(healthFile)) return delegateRollback("启动确认失败", "新版本在 15 秒内没有完成启动健康确认。"); setProgress(99, "正在提交更新事务..."); if (!transaction.commit()) return delegateRollback("事务提交失败", "新版本已经启动,但无法提交更新事务。"); QFile::remove(healthFile); QFile::remove(bootstrapPlanFile()); logic.reportResult(deviceId, fromVersion, targetVersion, true); progress.setValue(100); progress.close(); QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion)); return 0; }