205 lines
6.7 KiB
C++
205 lines
6.7 KiB
C++
#include <QApplication>
|
|
#include <QCoreApplication>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QMessageBox>
|
|
#include <QProcess>
|
|
#include <QTextStream>
|
|
#include <QThread>
|
|
#include <QTranslator>
|
|
#include <QVector>
|
|
|
|
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<QString>& 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<PlanItem>* items, QVector<QString>* 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<PlanItem> items;
|
|
QVector<QString> 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;
|
|
}
|