#include #include #include #include #include #include #include #include #include #include #include struct PlanItem { QChar operation; QString relativePath; }; static void showError(const QString& message) { QMessageBox::critical(nullptr, QApplication::translate("Bootstrap", "Update Handoff Failed"), message); } static QString cleanRelativePath(const QString& value) { QString path = QDir::fromNativeSeparators(value.trimmed()); if (path.isEmpty()) return {}; path = QDir::cleanPath(path); if (path == "." || path == ".." || path.startsWith("../") || path.contains("/../") || QDir::isAbsolutePath(path) || path.contains(':')) { return {}; } return path; } static QString joinPath(const QString& root, const QString& relativePath) { return QDir(root).filePath(relativePath); } static bool removeWithRetry(const QString& path) { // Windows 上主程序退出后,DLL/EXE 句柄可能还会短时间被系统占用。 // Bootstrap 用短重试等待文件释放,而不是一次失败就判定升级失败。 for (int i = 0; i < 100; ++i) { if (!QFileInfo::exists(path)) return true; if (QFile::remove(path)) return true; QThread::msleep(100); } return !QFileInfo::exists(path); } static bool copyWithRetry(const QString& source, const QString& destination) { QDir().mkpath(QFileInfo(destination).absolutePath()); for (int i = 0; i < 100; ++i) { QFile::remove(destination); if (QFile::copy(source, destination)) return true; QThread::msleep(100); } return false; } static bool rollback(const QString& installDir, const QString& backupDir, const QVector& paths) { bool success = true; for (const QString& relativePath : paths) { const QString destination = joinPath(installDir, relativePath); const QString backup = joinPath(backupDir, relativePath); if (QFileInfo::exists(backup)) { if (!copyWithRetry(backup, destination)) success = false; } else if (QFileInfo::exists(destination) && !removeWithRetry(destination)) { success = false; } } return success; } static bool launchUpdater(const QString& updater, const QStringList& updateArgs, const QString& result) { QStringList args = updateArgs; args.append(QStringLiteral("--bootstrap-resume=%1").arg(result)); return QProcess::startDetached(updater, args, QFileInfo(updater).absolutePath()); } static bool readPlan(const QString& planFile, QVector* items, QVector* paths) { QFile file(planFile); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QTextStream input(&file); input.setCodec("UTF-8"); while (!input.atEnd()) { const QString line = input.readLine(); if (line.size() < 3 || line.at(1) != QLatin1Char('\t') || (line.at(0) != QLatin1Char('C') && line.at(0) != QLatin1Char('D'))) { return false; } const QString relativePath = cleanRelativePath(line.mid(2)); if (relativePath.isEmpty()) return false; items->append({line.at(0), relativePath}); paths->append(relativePath); } return true; } static bool isBootstrapSelfPath(const QString& relativePath) { const QString fileName = QFileInfo(relativePath).fileName(); return fileName.compare(QStringLiteral("Bootstrap.exe"), Qt::CaseInsensitive) == 0 || fileName.compare(QStringLiteral("Bootstrap"), Qt::CaseInsensitive) == 0; } int main(int argc, char* argv[]) { QApplication app(argc, argv); QTranslator translator; if (translator.load(QStringLiteral(":/i18n/update-client_zh_CN.qm"))) app.installTranslator(&translator); const QStringList arguments = QCoreApplication::arguments(); if (arguments.size() < 11) { showError(QApplication::translate("Bootstrap", "Bootstrap arguments are incomplete.")); return 2; } const QString planFile = arguments.at(1); const QString installDir = arguments.at(2); const QString stagingDir = arguments.at(3); const QString backupDir = arguments.at(4); const QString updater = arguments.at(5); const QString updaterPid = arguments.at(6); Q_UNUSED(updaterPid); const QStringList updateArgs{arguments.at(7), arguments.at(8), arguments.at(9), arguments.at(10)}; const QString mode = arguments.size() >= 12 ? arguments.at(11) : QStringLiteral("install"); QThread::msleep(500); QVector items; QVector paths; if (!readPlan(planFile, &items, &paths)) { showError(QApplication::translate("Bootstrap", "The update plan is missing or invalid.")); return 3; } bool success = true; bool rolledBack = false; QString failedPath; if (mode == QStringLiteral("rollback")) { rolledBack = rollback(installDir, backupDir, paths); success = false; } else { // Bootstrap 是替换文件的接力进程:Updater 先退出,Bootstrap 再覆盖安装目录。 // 它不会更新自身,避免正在运行的 Bootstrap 被覆盖导致升级中断。 for (const PlanItem& item : items) { const QString& relativePath = item.relativePath; if (isBootstrapSelfPath(relativePath)) { success = false; failedPath = relativePath; break; } const bool itemOk = item.operation == QLatin1Char('C') ? copyWithRetry(joinPath(stagingDir, relativePath), joinPath(installDir, relativePath)) : removeWithRetry(joinPath(installDir, relativePath)); if (!itemOk) { success = false; failedPath = relativePath; break; } } if (!success) rolledBack = rollback(installDir, backupDir, paths); } const QString result = success ? QStringLiteral("success") : (rolledBack ? QStringLiteral("rolledback") : QStringLiteral("rollback-failed")); if (!launchUpdater(updater, updateArgs, result)) { QString message = QApplication::translate("Bootstrap", "Cannot restart Updater."); if (!failedPath.isEmpty()) { message += QApplication::translate("Bootstrap", "\nFailed file: %1") .arg(failedPath); } showError(message); return 4; } return (success || rolledBack) ? 0 : 5; }