chore: sync client source after repository split
This commit is contained in:
+41
-41
@@ -1,41 +1,41 @@
|
||||
# 强制所有编译、设计时生成均使用x64,禁止Win32
|
||||
set(CMAKE_GENERATOR_PLATFORM x64 CACHE STRING "强制x64平台,禁用Win32")
|
||||
set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCH x64)
|
||||
# 关闭VS自动Win32设计时预生成
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF)
|
||||
# 清除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")
|
||||
|
||||
project(Updater)
|
||||
set(SRC
|
||||
main.cpp
|
||||
UpdaterLogic.h
|
||||
UpdaterLogic.cpp
|
||||
UpdateTransaction.h
|
||||
UpdateTransaction.cpp
|
||||
)
|
||||
add_executable(Updater ${SRC})
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(Updater PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
endif()
|
||||
|
||||
# 关键:给Updater自身添加OpenSSL头文件目录,解决#include openssl/*找不到
|
||||
target_include_directories(Updater PRIVATE ${OPENSSL_INC})
|
||||
|
||||
# 仅链接Common和Qt,OpenSSL库由Common INTERFACE自动传递
|
||||
target_link_libraries(Updater PRIVATE
|
||||
Common
|
||||
Qt5::Core Qt5::Network Qt5::Gui Qt5::Widgets
|
||||
)
|
||||
|
||||
# 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} $<IF:$<CONFIG:Debug>,--debug,--release> $<TARGET_FILE:Updater>
|
||||
)
|
||||
endif()
|
||||
# 强制所有编译、设计时生成均使用x64,禁止Win32
|
||||
set(CMAKE_GENERATOR_PLATFORM x64 CACHE STRING "强制x64平台,禁用Win32")
|
||||
set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCH x64)
|
||||
# 关闭VS自动Win32设计时预生成
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF)
|
||||
# 清除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")
|
||||
|
||||
project(Updater)
|
||||
set(SRC
|
||||
main.cpp
|
||||
UpdaterLogic.h
|
||||
UpdaterLogic.cpp
|
||||
UpdateTransaction.h
|
||||
UpdateTransaction.cpp
|
||||
)
|
||||
add_executable(Updater ${SRC})
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(Updater PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
endif()
|
||||
|
||||
# 关键:给Updater自身添加OpenSSL头文件目录,解决#include openssl/*找不到
|
||||
target_include_directories(Updater PRIVATE ${OPENSSL_INC})
|
||||
|
||||
# 仅链接Common和Qt,OpenSSL库由Common INTERFACE自动传递
|
||||
target_link_libraries(Updater PRIVATE
|
||||
Common
|
||||
Qt5::Core Qt5::Network Qt5::Gui Qt5::Widgets
|
||||
)
|
||||
|
||||
# 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} $<IF:$<CONFIG:Debug>,--debug,--release> $<TARGET_FILE:Updater>
|
||||
)
|
||||
endif()
|
||||
|
||||
+274
-274
@@ -1,274 +1,274 @@
|
||||
#include "UpdateTransaction.h"
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <QSet>
|
||||
#include <QUuid>
|
||||
|
||||
UpdateTransaction::UpdateTransaction(const QString& installDir, const QString& updateDir, const QString& fromVersion,
|
||||
const QString& toVersion, int manifestId)
|
||||
: m_installDir(QDir::cleanPath(installDir)),
|
||||
m_updateDir(QDir::cleanPath(updateDir.isEmpty() ? QDir(installDir).filePath("update") : updateDir)),
|
||||
m_stateFile(QDir(m_updateDir).filePath("upgrade_state.json")),
|
||||
m_transactionId(QUuid::createUuid().toString(QUuid::WithoutBraces)),
|
||||
m_fromVersion(fromVersion), m_toVersion(toVersion), m_manifestId(manifestId)
|
||||
{
|
||||
m_stagingDir = QDir(m_updateDir).filePath("staging/" + m_transactionId);
|
||||
m_backupDir = QDir(m_updateDir).filePath("backup/" + m_transactionId);
|
||||
}
|
||||
|
||||
bool UpdateTransaction::safeRelativePath(const QString& path)
|
||||
{
|
||||
const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path));
|
||||
return !clean.isEmpty() && !QDir::isAbsolutePath(clean) && clean != ".."
|
||||
&& !clean.startsWith("../") && !clean.contains(":");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::copyOverwrite(const QString& source, const QString& destination) const
|
||||
{
|
||||
if (!QDir().mkpath(QFileInfo(destination).path())) return false;
|
||||
if (QFile::exists(destination) && !QFile::remove(destination)) return false;
|
||||
return QFile::copy(source, destination);
|
||||
}
|
||||
|
||||
bool UpdateTransaction::writeState(const QString& status, const QString& errorCode, const QString& message)
|
||||
{
|
||||
m_state["transaction_id"] = m_transactionId;
|
||||
m_state["from_version"] = m_fromVersion;
|
||||
m_state["to_version"] = m_toVersion;
|
||||
m_state["status"] = status;
|
||||
m_state["manifest_id"] = m_manifestId;
|
||||
m_state["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
m_state["staging_dir"] = m_stagingDir;
|
||||
m_state["backup_dir"] = m_backupDir;
|
||||
m_state["error_code"] = errorCode;
|
||||
m_state["message"] = message;
|
||||
QJsonArray paths;
|
||||
for (const QString& path : m_changedPaths) paths.append(path);
|
||||
m_state["changed_paths"] = paths;
|
||||
QJsonArray obsoletePaths;
|
||||
for (const QString& path : m_obsoletePaths) obsoletePaths.append(path);
|
||||
m_state["obsolete_paths"] = obsoletePaths;
|
||||
|
||||
QDir().mkpath(m_updateDir);
|
||||
QSaveFile file(m_stateFile);
|
||||
if (!file.open(QIODevice::WriteOnly)) return false;
|
||||
const QByteArray payload = QJsonDocument(m_state).toJson(QJsonDocument::Indented);
|
||||
if (file.write(payload) != payload.size()) { file.cancelWriting(); return false; }
|
||||
return file.commit();
|
||||
}
|
||||
|
||||
bool UpdateTransaction::initialize()
|
||||
{
|
||||
QDir(m_stagingDir).removeRecursively();
|
||||
QDir(m_backupDir).removeRecursively();
|
||||
if (!QDir().mkpath(m_stagingDir) || !QDir().mkpath(m_backupDir)) return false;
|
||||
m_state["started_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
return writeState("prepared");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::resumeExisting(QString* errorMessage)
|
||||
{
|
||||
QFile file(m_stateFile);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
if (errorMessage) *errorMessage = "cannot open upgrade_state.json";
|
||||
return false;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
if (!doc.isObject()) {
|
||||
if (errorMessage) *errorMessage = "invalid upgrade_state.json";
|
||||
return false;
|
||||
}
|
||||
m_state = doc.object();
|
||||
const QString expectedToVersion = m_toVersion;
|
||||
const int expectedManifestId = m_manifestId;
|
||||
const QString stateStatus = m_state.value("status").toString();
|
||||
m_transactionId = m_state.value("transaction_id").toString();
|
||||
m_fromVersion = m_state.value("from_version").toString();
|
||||
m_toVersion = m_state.value("to_version").toString();
|
||||
m_manifestId = m_state.value("manifest_id").toInt();
|
||||
if (m_toVersion != expectedToVersion || m_manifestId != expectedManifestId
|
||||
|| (stateStatus != "awaiting_bootstrap" && stateStatus != "post_verify"
|
||||
&& stateStatus != "rollback_required")) {
|
||||
if (errorMessage) *errorMessage = "upgrade state does not match resume request";
|
||||
return false;
|
||||
}
|
||||
m_stagingDir = m_state.value("staging_dir").toString();
|
||||
m_backupDir = m_state.value("backup_dir").toString();
|
||||
m_changedPaths.clear();
|
||||
m_obsoletePaths.clear();
|
||||
for (const QJsonValue& value : m_state.value("changed_paths").toArray()) {
|
||||
const QString path = value.toString();
|
||||
if (!safeRelativePath(path)) {
|
||||
if (errorMessage) *errorMessage = "unsafe path in upgrade state";
|
||||
return false;
|
||||
}
|
||||
m_changedPaths.append(path);
|
||||
}
|
||||
for (const QJsonValue& value : m_state.value("obsolete_paths").toArray()) {
|
||||
const QString path = value.toString();
|
||||
if (!safeRelativePath(path)) {
|
||||
if (errorMessage) *errorMessage = "unsafe obsolete path in upgrade state";
|
||||
return false;
|
||||
}
|
||||
m_obsoletePaths.append(path);
|
||||
}
|
||||
if (m_transactionId.isEmpty() || m_stagingDir.isEmpty() || m_backupDir.isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "incomplete upgrade state";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateTransaction::recordVerifiedFiles(const QStringList& changedPaths,
|
||||
const QStringList& obsoletePaths)
|
||||
{
|
||||
m_changedPaths.clear();
|
||||
m_obsoletePaths.clear();
|
||||
QSet<QString> changedKeys;
|
||||
for (const QString& path : changedPaths) {
|
||||
if (!safeRelativePath(path)) return false;
|
||||
const QString normalized = QDir::fromNativeSeparators(path);
|
||||
changedKeys.insert(normalized.toCaseFolded());
|
||||
m_changedPaths.append(normalized);
|
||||
}
|
||||
for (const QString& path : obsoletePaths) {
|
||||
if (!safeRelativePath(path)) return false;
|
||||
const QString normalized = QDir::fromNativeSeparators(path);
|
||||
if (changedKeys.contains(normalized.toCaseFolded())) return false;
|
||||
m_obsoletePaths.append(normalized);
|
||||
}
|
||||
return writeState("verified");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::backupCurrentFiles()
|
||||
{
|
||||
if (!writeState("waiting_mainapp_exit")) return false;
|
||||
QStringList paths = m_changedPaths;
|
||||
paths.append(m_obsoletePaths);
|
||||
for (const QString& path : paths) {
|
||||
const QString installed = QDir(m_installDir).filePath(path);
|
||||
if (!QFile::exists(installed)) continue;
|
||||
const QString backup = QDir(m_backupDir).filePath(path);
|
||||
if (!copyOverwrite(installed, backup))
|
||||
return markFailed("backup_failed", path);
|
||||
}
|
||||
return writeState("backed_up");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::installStagedFiles(QString* failedPath)
|
||||
{
|
||||
if (!writeState("replacing")) return false;
|
||||
for (const QString& path : m_changedPaths) {
|
||||
const QString source = QDir(m_stagingDir).filePath(path);
|
||||
const QString destination = QDir(m_installDir).filePath(path);
|
||||
if (!copyOverwrite(source, destination)) {
|
||||
if (failedPath) *failedPath = path;
|
||||
writeState("rollback_required", "replace_failed", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return writeState("replaced");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::markAwaitingBootstrap() { return writeState("awaiting_bootstrap"); }
|
||||
|
||||
bool UpdateTransaction::markRollbackRequired(const QString& reason)
|
||||
{ return writeState("rollback_required", "post_install_failed", reason); }
|
||||
|
||||
bool UpdateTransaction::markRolledBack() { return writeState("rolled_back"); }
|
||||
|
||||
bool UpdateTransaction::markPostVerify() { return writeState("post_verify"); }
|
||||
|
||||
bool UpdateTransaction::rollback(QString* failedPath)
|
||||
{
|
||||
writeState("rolling_back");
|
||||
bool ok = true;
|
||||
QStringList paths = m_changedPaths;
|
||||
paths.append(m_obsoletePaths);
|
||||
for (const QString& path : paths) {
|
||||
const QString installed = QDir(m_installDir).filePath(path);
|
||||
const QString backup = QDir(m_backupDir).filePath(path);
|
||||
if (QFile::exists(backup)) {
|
||||
if (!copyOverwrite(backup, installed)) { ok = false; if (failedPath) *failedPath = path; }
|
||||
} else if (QFile::exists(installed) && !QFile::remove(installed)) {
|
||||
ok = false; if (failedPath) *failedPath = path;
|
||||
}
|
||||
}
|
||||
writeState(ok ? "rolled_back" : "failed", ok ? QString() : "rollback_failed",
|
||||
failedPath ? *failedPath : QString());
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool UpdateTransaction::commit()
|
||||
{
|
||||
if (!writeState("committed")) return false;
|
||||
QDir(m_stagingDir).removeRecursively();
|
||||
QDir(m_backupDir).removeRecursively();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateTransaction::markFailed(const QString& errorCode, const QString& message)
|
||||
{ return writeState("failed", errorCode, message); }
|
||||
|
||||
QString UpdateTransaction::transactionId() const { return m_transactionId; }
|
||||
QString UpdateTransaction::fromVersion() const { return m_fromVersion; }
|
||||
QString UpdateTransaction::stagingDir() const { return m_stagingDir; }
|
||||
QString UpdateTransaction::backupDir() const { return m_backupDir; }
|
||||
QStringList UpdateTransaction::obsoletePaths() const { return m_obsoletePaths; }
|
||||
QString UpdateTransaction::healthFile() const { return QDir(m_updateDir).filePath("health_" + m_transactionId + ".ok"); }
|
||||
|
||||
bool UpdateTransaction::recoverInterrupted(const QString& installDir, const QString& updateDir,
|
||||
QString* restoredVersion, QString* errorMessage)
|
||||
{
|
||||
QString stateFile = QDir(updateDir.isEmpty() ? QDir(installDir).filePath("update") : updateDir)
|
||||
.filePath("upgrade_state.json");
|
||||
const QString legacyStateFile = QDir(installDir).filePath("update/upgrade_state.json");
|
||||
if (!QFile::exists(stateFile) && QFile::exists(legacyStateFile))
|
||||
stateFile = legacyStateFile;
|
||||
QFile file(stateFile);
|
||||
if (!file.exists()) return true;
|
||||
if (!file.open(QIODevice::ReadOnly)) { if (errorMessage) *errorMessage = "cannot open upgrade_state.json"; return false; }
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
if (!doc.isObject()) { if (errorMessage) *errorMessage = "invalid upgrade_state.json"; return false; }
|
||||
QJsonObject state = doc.object();
|
||||
const QString status = state.value("status").toString();
|
||||
if (status == "committed") return true;
|
||||
if (status == "rolled_back") {
|
||||
if (restoredVersion) *restoredVersion = state.value("from_version").toString();
|
||||
return true;
|
||||
}
|
||||
const QString backupDir = state.value("backup_dir").toString();
|
||||
QJsonArray paths = state.value("changed_paths").toArray();
|
||||
for (const QJsonValue& value : state.value("obsolete_paths").toArray()) paths.append(value);
|
||||
const QString errorCode = state.value("error_code").toString();
|
||||
const bool mayContainNewFiles = status == "replacing" || status == "replaced"
|
||||
|| status == "post_verify" || status == "rollback_required"
|
||||
|| status == "awaiting_bootstrap" || status == "rolling_back" || errorCode == "rollback_failed";
|
||||
bool ok = true;
|
||||
for (const QJsonValue& value : paths) {
|
||||
const QString path = value.toString();
|
||||
if (!safeRelativePath(path)) { ok = false; continue; }
|
||||
const QString installed = QDir(installDir).filePath(path);
|
||||
const QString backup = QDir(backupDir).filePath(path);
|
||||
if (QFile::exists(backup)) {
|
||||
QDir().mkpath(QFileInfo(installed).path());
|
||||
if (QFile::exists(installed)) QFile::remove(installed);
|
||||
if (!QFile::copy(backup, installed)) ok = false;
|
||||
} else if (mayContainNewFiles) {
|
||||
if (QFile::exists(installed) && !QFile::remove(installed)) ok = false;
|
||||
}
|
||||
}
|
||||
if (restoredVersion) *restoredVersion = state.value("from_version").toString();
|
||||
state["status"] = ok ? "rolled_back" : "failed";
|
||||
QSaveFile output(stateFile);
|
||||
if (output.open(QIODevice::WriteOnly)) { output.write(QJsonDocument(state).toJson(QJsonDocument::Indented)); output.commit(); }
|
||||
if (!ok && errorMessage) *errorMessage = "failed to restore one or more files";
|
||||
return ok;
|
||||
}
|
||||
#include "UpdateTransaction.h"
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <QSet>
|
||||
#include <QUuid>
|
||||
|
||||
UpdateTransaction::UpdateTransaction(const QString& installDir, const QString& updateDir, const QString& fromVersion,
|
||||
const QString& toVersion, int manifestId)
|
||||
: m_installDir(QDir::cleanPath(installDir)),
|
||||
m_updateDir(QDir::cleanPath(updateDir.isEmpty() ? QDir(installDir).filePath("update") : updateDir)),
|
||||
m_stateFile(QDir(m_updateDir).filePath("upgrade_state.json")),
|
||||
m_transactionId(QUuid::createUuid().toString(QUuid::WithoutBraces)),
|
||||
m_fromVersion(fromVersion), m_toVersion(toVersion), m_manifestId(manifestId)
|
||||
{
|
||||
m_stagingDir = QDir(m_updateDir).filePath("staging/" + m_transactionId);
|
||||
m_backupDir = QDir(m_updateDir).filePath("backup/" + m_transactionId);
|
||||
}
|
||||
|
||||
bool UpdateTransaction::safeRelativePath(const QString& path)
|
||||
{
|
||||
const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path));
|
||||
return !clean.isEmpty() && !QDir::isAbsolutePath(clean) && clean != ".."
|
||||
&& !clean.startsWith("../") && !clean.contains(":");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::copyOverwrite(const QString& source, const QString& destination) const
|
||||
{
|
||||
if (!QDir().mkpath(QFileInfo(destination).path())) return false;
|
||||
if (QFile::exists(destination) && !QFile::remove(destination)) return false;
|
||||
return QFile::copy(source, destination);
|
||||
}
|
||||
|
||||
bool UpdateTransaction::writeState(const QString& status, const QString& errorCode, const QString& message)
|
||||
{
|
||||
m_state["transaction_id"] = m_transactionId;
|
||||
m_state["from_version"] = m_fromVersion;
|
||||
m_state["to_version"] = m_toVersion;
|
||||
m_state["status"] = status;
|
||||
m_state["manifest_id"] = m_manifestId;
|
||||
m_state["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
m_state["staging_dir"] = m_stagingDir;
|
||||
m_state["backup_dir"] = m_backupDir;
|
||||
m_state["error_code"] = errorCode;
|
||||
m_state["message"] = message;
|
||||
QJsonArray paths;
|
||||
for (const QString& path : m_changedPaths) paths.append(path);
|
||||
m_state["changed_paths"] = paths;
|
||||
QJsonArray obsoletePaths;
|
||||
for (const QString& path : m_obsoletePaths) obsoletePaths.append(path);
|
||||
m_state["obsolete_paths"] = obsoletePaths;
|
||||
|
||||
QDir().mkpath(m_updateDir);
|
||||
QSaveFile file(m_stateFile);
|
||||
if (!file.open(QIODevice::WriteOnly)) return false;
|
||||
const QByteArray payload = QJsonDocument(m_state).toJson(QJsonDocument::Indented);
|
||||
if (file.write(payload) != payload.size()) { file.cancelWriting(); return false; }
|
||||
return file.commit();
|
||||
}
|
||||
|
||||
bool UpdateTransaction::initialize()
|
||||
{
|
||||
QDir(m_stagingDir).removeRecursively();
|
||||
QDir(m_backupDir).removeRecursively();
|
||||
if (!QDir().mkpath(m_stagingDir) || !QDir().mkpath(m_backupDir)) return false;
|
||||
m_state["started_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
return writeState("prepared");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::resumeExisting(QString* errorMessage)
|
||||
{
|
||||
QFile file(m_stateFile);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
if (errorMessage) *errorMessage = "cannot open upgrade_state.json";
|
||||
return false;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
if (!doc.isObject()) {
|
||||
if (errorMessage) *errorMessage = "invalid upgrade_state.json";
|
||||
return false;
|
||||
}
|
||||
m_state = doc.object();
|
||||
const QString expectedToVersion = m_toVersion;
|
||||
const int expectedManifestId = m_manifestId;
|
||||
const QString stateStatus = m_state.value("status").toString();
|
||||
m_transactionId = m_state.value("transaction_id").toString();
|
||||
m_fromVersion = m_state.value("from_version").toString();
|
||||
m_toVersion = m_state.value("to_version").toString();
|
||||
m_manifestId = m_state.value("manifest_id").toInt();
|
||||
if (m_toVersion != expectedToVersion || m_manifestId != expectedManifestId
|
||||
|| (stateStatus != "awaiting_bootstrap" && stateStatus != "post_verify"
|
||||
&& stateStatus != "rollback_required")) {
|
||||
if (errorMessage) *errorMessage = "upgrade state does not match resume request";
|
||||
return false;
|
||||
}
|
||||
m_stagingDir = m_state.value("staging_dir").toString();
|
||||
m_backupDir = m_state.value("backup_dir").toString();
|
||||
m_changedPaths.clear();
|
||||
m_obsoletePaths.clear();
|
||||
for (const QJsonValue& value : m_state.value("changed_paths").toArray()) {
|
||||
const QString path = value.toString();
|
||||
if (!safeRelativePath(path)) {
|
||||
if (errorMessage) *errorMessage = "unsafe path in upgrade state";
|
||||
return false;
|
||||
}
|
||||
m_changedPaths.append(path);
|
||||
}
|
||||
for (const QJsonValue& value : m_state.value("obsolete_paths").toArray()) {
|
||||
const QString path = value.toString();
|
||||
if (!safeRelativePath(path)) {
|
||||
if (errorMessage) *errorMessage = "unsafe obsolete path in upgrade state";
|
||||
return false;
|
||||
}
|
||||
m_obsoletePaths.append(path);
|
||||
}
|
||||
if (m_transactionId.isEmpty() || m_stagingDir.isEmpty() || m_backupDir.isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "incomplete upgrade state";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateTransaction::recordVerifiedFiles(const QStringList& changedPaths,
|
||||
const QStringList& obsoletePaths)
|
||||
{
|
||||
m_changedPaths.clear();
|
||||
m_obsoletePaths.clear();
|
||||
QSet<QString> changedKeys;
|
||||
for (const QString& path : changedPaths) {
|
||||
if (!safeRelativePath(path)) return false;
|
||||
const QString normalized = QDir::fromNativeSeparators(path);
|
||||
changedKeys.insert(normalized.toCaseFolded());
|
||||
m_changedPaths.append(normalized);
|
||||
}
|
||||
for (const QString& path : obsoletePaths) {
|
||||
if (!safeRelativePath(path)) return false;
|
||||
const QString normalized = QDir::fromNativeSeparators(path);
|
||||
if (changedKeys.contains(normalized.toCaseFolded())) return false;
|
||||
m_obsoletePaths.append(normalized);
|
||||
}
|
||||
return writeState("verified");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::backupCurrentFiles()
|
||||
{
|
||||
if (!writeState("waiting_mainapp_exit")) return false;
|
||||
QStringList paths = m_changedPaths;
|
||||
paths.append(m_obsoletePaths);
|
||||
for (const QString& path : paths) {
|
||||
const QString installed = QDir(m_installDir).filePath(path);
|
||||
if (!QFile::exists(installed)) continue;
|
||||
const QString backup = QDir(m_backupDir).filePath(path);
|
||||
if (!copyOverwrite(installed, backup))
|
||||
return markFailed("backup_failed", path);
|
||||
}
|
||||
return writeState("backed_up");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::installStagedFiles(QString* failedPath)
|
||||
{
|
||||
if (!writeState("replacing")) return false;
|
||||
for (const QString& path : m_changedPaths) {
|
||||
const QString source = QDir(m_stagingDir).filePath(path);
|
||||
const QString destination = QDir(m_installDir).filePath(path);
|
||||
if (!copyOverwrite(source, destination)) {
|
||||
if (failedPath) *failedPath = path;
|
||||
writeState("rollback_required", "replace_failed", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return writeState("replaced");
|
||||
}
|
||||
|
||||
bool UpdateTransaction::markAwaitingBootstrap() { return writeState("awaiting_bootstrap"); }
|
||||
|
||||
bool UpdateTransaction::markRollbackRequired(const QString& reason)
|
||||
{ return writeState("rollback_required", "post_install_failed", reason); }
|
||||
|
||||
bool UpdateTransaction::markRolledBack() { return writeState("rolled_back"); }
|
||||
|
||||
bool UpdateTransaction::markPostVerify() { return writeState("post_verify"); }
|
||||
|
||||
bool UpdateTransaction::rollback(QString* failedPath)
|
||||
{
|
||||
writeState("rolling_back");
|
||||
bool ok = true;
|
||||
QStringList paths = m_changedPaths;
|
||||
paths.append(m_obsoletePaths);
|
||||
for (const QString& path : paths) {
|
||||
const QString installed = QDir(m_installDir).filePath(path);
|
||||
const QString backup = QDir(m_backupDir).filePath(path);
|
||||
if (QFile::exists(backup)) {
|
||||
if (!copyOverwrite(backup, installed)) { ok = false; if (failedPath) *failedPath = path; }
|
||||
} else if (QFile::exists(installed) && !QFile::remove(installed)) {
|
||||
ok = false; if (failedPath) *failedPath = path;
|
||||
}
|
||||
}
|
||||
writeState(ok ? "rolled_back" : "failed", ok ? QString() : "rollback_failed",
|
||||
failedPath ? *failedPath : QString());
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool UpdateTransaction::commit()
|
||||
{
|
||||
if (!writeState("committed")) return false;
|
||||
QDir(m_stagingDir).removeRecursively();
|
||||
QDir(m_backupDir).removeRecursively();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateTransaction::markFailed(const QString& errorCode, const QString& message)
|
||||
{ return writeState("failed", errorCode, message); }
|
||||
|
||||
QString UpdateTransaction::transactionId() const { return m_transactionId; }
|
||||
QString UpdateTransaction::fromVersion() const { return m_fromVersion; }
|
||||
QString UpdateTransaction::stagingDir() const { return m_stagingDir; }
|
||||
QString UpdateTransaction::backupDir() const { return m_backupDir; }
|
||||
QStringList UpdateTransaction::obsoletePaths() const { return m_obsoletePaths; }
|
||||
QString UpdateTransaction::healthFile() const { return QDir(m_updateDir).filePath("health_" + m_transactionId + ".ok"); }
|
||||
|
||||
bool UpdateTransaction::recoverInterrupted(const QString& installDir, const QString& updateDir,
|
||||
QString* restoredVersion, QString* errorMessage)
|
||||
{
|
||||
QString stateFile = QDir(updateDir.isEmpty() ? QDir(installDir).filePath("update") : updateDir)
|
||||
.filePath("upgrade_state.json");
|
||||
const QString legacyStateFile = QDir(installDir).filePath("update/upgrade_state.json");
|
||||
if (!QFile::exists(stateFile) && QFile::exists(legacyStateFile))
|
||||
stateFile = legacyStateFile;
|
||||
QFile file(stateFile);
|
||||
if (!file.exists()) return true;
|
||||
if (!file.open(QIODevice::ReadOnly)) { if (errorMessage) *errorMessage = "cannot open upgrade_state.json"; return false; }
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
if (!doc.isObject()) { if (errorMessage) *errorMessage = "invalid upgrade_state.json"; return false; }
|
||||
QJsonObject state = doc.object();
|
||||
const QString status = state.value("status").toString();
|
||||
if (status == "committed") return true;
|
||||
if (status == "rolled_back") {
|
||||
if (restoredVersion) *restoredVersion = state.value("from_version").toString();
|
||||
return true;
|
||||
}
|
||||
const QString backupDir = state.value("backup_dir").toString();
|
||||
QJsonArray paths = state.value("changed_paths").toArray();
|
||||
for (const QJsonValue& value : state.value("obsolete_paths").toArray()) paths.append(value);
|
||||
const QString errorCode = state.value("error_code").toString();
|
||||
const bool mayContainNewFiles = status == "replacing" || status == "replaced"
|
||||
|| status == "post_verify" || status == "rollback_required"
|
||||
|| status == "awaiting_bootstrap" || status == "rolling_back" || errorCode == "rollback_failed";
|
||||
bool ok = true;
|
||||
for (const QJsonValue& value : paths) {
|
||||
const QString path = value.toString();
|
||||
if (!safeRelativePath(path)) { ok = false; continue; }
|
||||
const QString installed = QDir(installDir).filePath(path);
|
||||
const QString backup = QDir(backupDir).filePath(path);
|
||||
if (QFile::exists(backup)) {
|
||||
QDir().mkpath(QFileInfo(installed).path());
|
||||
if (QFile::exists(installed)) QFile::remove(installed);
|
||||
if (!QFile::copy(backup, installed)) ok = false;
|
||||
} else if (mayContainNewFiles) {
|
||||
if (QFile::exists(installed) && !QFile::remove(installed)) ok = false;
|
||||
}
|
||||
}
|
||||
if (restoredVersion) *restoredVersion = state.value("from_version").toString();
|
||||
state["status"] = ok ? "rolled_back" : "failed";
|
||||
QSaveFile output(stateFile);
|
||||
if (output.open(QIODevice::WriteOnly)) { output.write(QJsonDocument(state).toJson(QJsonDocument::Indented)); output.commit(); }
|
||||
if (!ok && errorMessage) *errorMessage = "failed to restore one or more files";
|
||||
return ok;
|
||||
}
|
||||
|
||||
+54
-54
@@ -1,54 +1,54 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QJsonObject>
|
||||
|
||||
class UpdateTransaction
|
||||
{
|
||||
public:
|
||||
UpdateTransaction(const QString& installDir, const QString& updateDir, const QString& fromVersion,
|
||||
const QString& toVersion, int manifestId);
|
||||
|
||||
bool initialize();
|
||||
bool resumeExisting(QString* errorMessage = nullptr);
|
||||
bool recordVerifiedFiles(const QStringList& changedPaths, const QStringList& obsoletePaths);
|
||||
bool backupCurrentFiles();
|
||||
bool installStagedFiles(QString* failedPath = nullptr);
|
||||
bool markAwaitingBootstrap();
|
||||
bool markRollbackRequired(const QString& reason);
|
||||
bool markRolledBack();
|
||||
bool markPostVerify();
|
||||
bool rollback(QString* failedPath = nullptr);
|
||||
bool commit();
|
||||
bool markFailed(const QString& errorCode, const QString& message);
|
||||
|
||||
QString transactionId() const;
|
||||
QString fromVersion() const;
|
||||
QString stagingDir() const;
|
||||
QString backupDir() const;
|
||||
QStringList obsoletePaths() const;
|
||||
QString healthFile() const;
|
||||
|
||||
static bool recoverInterrupted(const QString& installDir, const QString& updateDir,
|
||||
QString* restoredVersion = nullptr,
|
||||
QString* errorMessage = nullptr);
|
||||
|
||||
private:
|
||||
bool writeState(const QString& status, const QString& errorCode = QString(),
|
||||
const QString& message = QString());
|
||||
bool copyOverwrite(const QString& source, const QString& destination) const;
|
||||
static bool safeRelativePath(const QString& path);
|
||||
|
||||
QString m_installDir;
|
||||
QString m_updateDir;
|
||||
QString m_stateFile;
|
||||
QString m_transactionId;
|
||||
QString m_fromVersion;
|
||||
QString m_toVersion;
|
||||
int m_manifestId = 0;
|
||||
QString m_stagingDir;
|
||||
QString m_backupDir;
|
||||
QStringList m_changedPaths;
|
||||
QStringList m_obsoletePaths;
|
||||
QJsonObject m_state;
|
||||
};
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QJsonObject>
|
||||
|
||||
class UpdateTransaction
|
||||
{
|
||||
public:
|
||||
UpdateTransaction(const QString& installDir, const QString& updateDir, const QString& fromVersion,
|
||||
const QString& toVersion, int manifestId);
|
||||
|
||||
bool initialize();
|
||||
bool resumeExisting(QString* errorMessage = nullptr);
|
||||
bool recordVerifiedFiles(const QStringList& changedPaths, const QStringList& obsoletePaths);
|
||||
bool backupCurrentFiles();
|
||||
bool installStagedFiles(QString* failedPath = nullptr);
|
||||
bool markAwaitingBootstrap();
|
||||
bool markRollbackRequired(const QString& reason);
|
||||
bool markRolledBack();
|
||||
bool markPostVerify();
|
||||
bool rollback(QString* failedPath = nullptr);
|
||||
bool commit();
|
||||
bool markFailed(const QString& errorCode, const QString& message);
|
||||
|
||||
QString transactionId() const;
|
||||
QString fromVersion() const;
|
||||
QString stagingDir() const;
|
||||
QString backupDir() const;
|
||||
QStringList obsoletePaths() const;
|
||||
QString healthFile() const;
|
||||
|
||||
static bool recoverInterrupted(const QString& installDir, const QString& updateDir,
|
||||
QString* restoredVersion = nullptr,
|
||||
QString* errorMessage = nullptr);
|
||||
|
||||
private:
|
||||
bool writeState(const QString& status, const QString& errorCode = QString(),
|
||||
const QString& message = QString());
|
||||
bool copyOverwrite(const QString& source, const QString& destination) const;
|
||||
static bool safeRelativePath(const QString& path);
|
||||
|
||||
QString m_installDir;
|
||||
QString m_updateDir;
|
||||
QString m_stateFile;
|
||||
QString m_transactionId;
|
||||
QString m_fromVersion;
|
||||
QString m_toVersion;
|
||||
int m_manifestId = 0;
|
||||
QString m_stagingDir;
|
||||
QString m_backupDir;
|
||||
QStringList m_changedPaths;
|
||||
QStringList m_obsoletePaths;
|
||||
QJsonObject m_state;
|
||||
};
|
||||
|
||||
+751
-751
File diff suppressed because it is too large
Load Diff
+72
-72
@@ -1,73 +1,73 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include "HttpHelper.h"
|
||||
#include "FileHelper.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QStringList>
|
||||
#include <QNetworkReply>
|
||||
#include <QMap>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
struct FileDownloadItem
|
||||
{
|
||||
QString path;
|
||||
QString url;
|
||||
QString sha256;
|
||||
qint64 size;
|
||||
};
|
||||
|
||||
class UpdaterLogic : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit UpdaterLogic(QObject* parent = nullptr);
|
||||
|
||||
void getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId);
|
||||
bool verifyManifestSignature(const QString& publicKeyPath = "config/manifest_public_key.pem") const;
|
||||
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;
|
||||
|
||||
QList<FileDownloadItem> getFileList() const;
|
||||
QJsonObject getManifest() const { return m_manifest; }
|
||||
QStringList obsoleteFilesComparedTo(const QJsonObject& oldManifest) const;
|
||||
QString getManifestText() const { return m_manifestText; }
|
||||
bool allDownloadSuccess() const { return m_downloadAllOk; }
|
||||
|
||||
signals:
|
||||
void fetchUrlFinished();
|
||||
void downloadProgress(qint64 receivedBytes, qint64 totalBytes,
|
||||
const QString& currentPath, double bytesPerSecond);
|
||||
|
||||
private:
|
||||
bool downloadSingleFile(const QString& url, const QString& savePath, const QString& expectSha256,
|
||||
qint64 expectedSize, const QString& resumePartPath);
|
||||
bool isSafeRelativePath(const QString& path) const;
|
||||
bool isRuntimeProtectedPath(const QString& path) const;
|
||||
QByteArray canonicalManifestBytes(const QJsonObject& manifest) const;
|
||||
bool verifySignature(const QByteArray& payload, const QString& signatureBase64, const QString& publicKeyPath) const;
|
||||
|
||||
HttpHelper m_http;
|
||||
QString m_serverAddr;
|
||||
QJsonObject m_manifest;
|
||||
QString m_manifestText;
|
||||
QList<FileDownloadItem> m_fileItems;
|
||||
bool m_downloadAllOk = false;
|
||||
qint64 m_downloadTotalBytes = 0;
|
||||
qint64 m_downloadCompletedBytes = 0;
|
||||
qint64 m_sessionDownloadedBytes = 0;
|
||||
QString m_currentDownloadPath;
|
||||
QString m_offlineError;
|
||||
QElapsedTimer m_downloadTimer;
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include "HttpHelper.h"
|
||||
#include "FileHelper.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QStringList>
|
||||
#include <QNetworkReply>
|
||||
#include <QMap>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
struct FileDownloadItem
|
||||
{
|
||||
QString path;
|
||||
QString url;
|
||||
QString sha256;
|
||||
qint64 size;
|
||||
};
|
||||
|
||||
class UpdaterLogic : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit UpdaterLogic(QObject* parent = nullptr);
|
||||
|
||||
void getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId);
|
||||
bool verifyManifestSignature(const QString& publicKeyPath = "config/manifest_public_key.pem") const;
|
||||
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;
|
||||
|
||||
QList<FileDownloadItem> getFileList() const;
|
||||
QJsonObject getManifest() const { return m_manifest; }
|
||||
QStringList obsoleteFilesComparedTo(const QJsonObject& oldManifest) const;
|
||||
QString getManifestText() const { return m_manifestText; }
|
||||
bool allDownloadSuccess() const { return m_downloadAllOk; }
|
||||
|
||||
signals:
|
||||
void fetchUrlFinished();
|
||||
void downloadProgress(qint64 receivedBytes, qint64 totalBytes,
|
||||
const QString& currentPath, double bytesPerSecond);
|
||||
|
||||
private:
|
||||
bool downloadSingleFile(const QString& url, const QString& savePath, const QString& expectSha256,
|
||||
qint64 expectedSize, const QString& resumePartPath);
|
||||
bool isSafeRelativePath(const QString& path) const;
|
||||
bool isRuntimeProtectedPath(const QString& path) const;
|
||||
QByteArray canonicalManifestBytes(const QJsonObject& manifest) const;
|
||||
bool verifySignature(const QByteArray& payload, const QString& signatureBase64, const QString& publicKeyPath) const;
|
||||
|
||||
HttpHelper m_http;
|
||||
QString m_serverAddr;
|
||||
QJsonObject m_manifest;
|
||||
QString m_manifestText;
|
||||
QList<FileDownloadItem> m_fileItems;
|
||||
bool m_downloadAllOk = false;
|
||||
qint64 m_downloadTotalBytes = 0;
|
||||
qint64 m_downloadCompletedBytes = 0;
|
||||
qint64 m_sessionDownloadedBytes = 0;
|
||||
QString m_currentDownloadPath;
|
||||
QString m_offlineError;
|
||||
QElapsedTimer m_downloadTimer;
|
||||
};
|
||||
+410
-410
@@ -1,410 +1,410 @@
|
||||
#include <windows.h>
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QProgressDialog>
|
||||
#include <QSaveFile>
|
||||
#include <QStorageInfo>
|
||||
#include <QTextCodec>
|
||||
#include <QThread>
|
||||
#include "UpdaterLogic.h"
|
||||
#include "UpdateTransaction.h"
|
||||
#include "FileHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include "TicketHelper.h"
|
||||
#include "PolicyHelper.h"
|
||||
#include <QVersionNumber>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
SetConsoleOutputCP(65001);
|
||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName("Marsco Updater");
|
||||
|
||||
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();
|
||||
}
|
||||
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 runtimeDir = QApplication::applicationDirPath();
|
||||
|
||||
ConfigHelper& config = ConfigHelper::instance();
|
||||
const QString targetDir = config.installRoot();
|
||||
const QString updateDir = config.updateRoot();
|
||||
QDir().mkpath(updateDir);
|
||||
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(runtimeDir);
|
||||
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";
|
||||
|
||||
if (!resumingFromBootstrap) {
|
||||
QString restoredVersion;
|
||||
QString recoveryError;
|
||||
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &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();
|
||||
|
||||
UpdateTransaction transaction(targetDir, updateDir, 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<double>(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<int>(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<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);
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title, message);
|
||||
return -1;
|
||||
};
|
||||
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(runtimeDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
|
||||
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
|
||||
const QString launchToken = config.getValue("App", "launch_token");
|
||||
const auto launchMainApp = [&](const QString& healthFile = QString()) {
|
||||
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));
|
||||
const bool started = QProcess::startDetached(mainAppPath, args);
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
const auto bootstrapPlanFile = [&]() {
|
||||
return QDir(updateDir).filePath("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(mainExecutable);
|
||||
transaction.markRollbackRequired(reason);
|
||||
progress.setLabelText("正在将回滚工作移交给 Bootstrap...");
|
||||
QApplication::processEvents();
|
||||
if (launchBootstrap("rollback")) {
|
||||
progress.close();
|
||||
return 0;
|
||||
}
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title,
|
||||
reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。");
|
||||
return -1;
|
||||
};
|
||||
|
||||
if (resumingFromBootstrap) {
|
||||
QString resumeError;
|
||||
if (!transaction.resumeExisting(&resumeError)) {
|
||||
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();
|
||||
reportUpdateResult(false);
|
||||
if (stateOk) launchMainApp();
|
||||
progress.close();
|
||||
QMessageBox::warning(nullptr, "更新已回滚",
|
||||
stateOk ? "新版本安装或启动失败,已自动恢复并启动旧版本。"
|
||||
: "旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。");
|
||||
return stateOk ? 0 : -1;
|
||||
}
|
||||
if (bootstrapResult != "success") {
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "自动回滚失败",
|
||||
"Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。");
|
||||
return -1;
|
||||
}
|
||||
} 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, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包...");
|
||||
const QString manifestCacheDir = QDir(updateDir).filePath("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");
|
||||
}
|
||||
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, offlinePackagePath.isEmpty() ? "正在获取安全下载地址..." : "正在准备离线包文件...");
|
||||
if (offlinePackagePath.isEmpty())
|
||||
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
|
||||
if (logic.getFileList().isEmpty())
|
||||
return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list");
|
||||
|
||||
QStorageInfo storage(updateDir);
|
||||
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<qint64>(0, availableBytes))),
|
||||
"disk_space_insufficient");
|
||||
}
|
||||
|
||||
const QString stagingDir = transaction.stagingDir();
|
||||
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))
|
||||
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())));
|
||||
const QString runtimePrefix = config.runtimeRelativePath();
|
||||
const QString bootstrapManifestPath = QDir::fromNativeSeparators(
|
||||
runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable);
|
||||
for (const QString& path : changedPaths) {
|
||||
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
|
||||
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapManifestPath), "bootstrap_self_update_blocked");
|
||||
}
|
||||
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
|
||||
return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed");
|
||||
|
||||
setProgress(66, "正在关闭主程序...");
|
||||
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()));
|
||||
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("启动失败", QString("%1 无法启动。").arg(mainExecutable));
|
||||
|
||||
QElapsedTimer healthTimer;
|
||||
healthTimer.start();
|
||||
while (healthTimer.elapsed() < healthCheckTimeoutMs && !QFile::exists(healthFile)) {
|
||||
QApplication::processEvents();
|
||||
QThread::msleep(100);
|
||||
}
|
||||
if (!QFile::exists(healthFile))
|
||||
return delegateRollback("启动确认失败", QString("新版本在 %1 毫秒内没有完成启动健康确认。").arg(healthCheckTimeoutMs));
|
||||
|
||||
setProgress(99, "正在提交更新事务...");
|
||||
if (!transaction.commit())
|
||||
return delegateRollback("事务提交失败", "新版本已经启动,但无法提交更新事务。");
|
||||
|
||||
QFile::remove(healthFile);
|
||||
QFile::remove(bootstrapPlanFile());
|
||||
reportUpdateResult(true);
|
||||
progress.setValue(100);
|
||||
progress.close();
|
||||
QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion));
|
||||
return 0;
|
||||
}
|
||||
#include <windows.h>
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QProgressDialog>
|
||||
#include <QSaveFile>
|
||||
#include <QStorageInfo>
|
||||
#include <QTextCodec>
|
||||
#include <QThread>
|
||||
#include "UpdaterLogic.h"
|
||||
#include "UpdateTransaction.h"
|
||||
#include "FileHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include "TicketHelper.h"
|
||||
#include "PolicyHelper.h"
|
||||
#include <QVersionNumber>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
SetConsoleOutputCP(65001);
|
||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName("Marsco Updater");
|
||||
|
||||
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();
|
||||
}
|
||||
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 runtimeDir = QApplication::applicationDirPath();
|
||||
|
||||
ConfigHelper& config = ConfigHelper::instance();
|
||||
const QString targetDir = config.installRoot();
|
||||
const QString updateDir = config.updateRoot();
|
||||
QDir().mkpath(updateDir);
|
||||
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(runtimeDir);
|
||||
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";
|
||||
|
||||
if (!resumingFromBootstrap) {
|
||||
QString restoredVersion;
|
||||
QString recoveryError;
|
||||
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &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();
|
||||
|
||||
UpdateTransaction transaction(targetDir, updateDir, 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<double>(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<int>(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<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);
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title, message);
|
||||
return -1;
|
||||
};
|
||||
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(runtimeDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
|
||||
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
|
||||
const QString launchToken = config.getValue("App", "launch_token");
|
||||
const auto launchMainApp = [&](const QString& healthFile = QString()) {
|
||||
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));
|
||||
const bool started = QProcess::startDetached(mainAppPath, args);
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
const auto bootstrapPlanFile = [&]() {
|
||||
return QDir(updateDir).filePath("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(mainExecutable);
|
||||
transaction.markRollbackRequired(reason);
|
||||
progress.setLabelText("正在将回滚工作移交给 Bootstrap...");
|
||||
QApplication::processEvents();
|
||||
if (launchBootstrap("rollback")) {
|
||||
progress.close();
|
||||
return 0;
|
||||
}
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title,
|
||||
reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。");
|
||||
return -1;
|
||||
};
|
||||
|
||||
if (resumingFromBootstrap) {
|
||||
QString resumeError;
|
||||
if (!transaction.resumeExisting(&resumeError)) {
|
||||
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();
|
||||
reportUpdateResult(false);
|
||||
if (stateOk) launchMainApp();
|
||||
progress.close();
|
||||
QMessageBox::warning(nullptr, "更新已回滚",
|
||||
stateOk ? "新版本安装或启动失败,已自动恢复并启动旧版本。"
|
||||
: "旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。");
|
||||
return stateOk ? 0 : -1;
|
||||
}
|
||||
if (bootstrapResult != "success") {
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "自动回滚失败",
|
||||
"Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。");
|
||||
return -1;
|
||||
}
|
||||
} 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, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包...");
|
||||
const QString manifestCacheDir = QDir(updateDir).filePath("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");
|
||||
}
|
||||
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, offlinePackagePath.isEmpty() ? "正在获取安全下载地址..." : "正在准备离线包文件...");
|
||||
if (offlinePackagePath.isEmpty())
|
||||
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
|
||||
if (logic.getFileList().isEmpty())
|
||||
return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list");
|
||||
|
||||
QStorageInfo storage(updateDir);
|
||||
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<qint64>(0, availableBytes))),
|
||||
"disk_space_insufficient");
|
||||
}
|
||||
|
||||
const QString stagingDir = transaction.stagingDir();
|
||||
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))
|
||||
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())));
|
||||
const QString runtimePrefix = config.runtimeRelativePath();
|
||||
const QString bootstrapManifestPath = QDir::fromNativeSeparators(
|
||||
runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable);
|
||||
for (const QString& path : changedPaths) {
|
||||
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
|
||||
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapManifestPath), "bootstrap_self_update_blocked");
|
||||
}
|
||||
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
|
||||
return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed");
|
||||
|
||||
setProgress(66, "正在关闭主程序...");
|
||||
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()));
|
||||
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("启动失败", QString("%1 无法启动。").arg(mainExecutable));
|
||||
|
||||
QElapsedTimer healthTimer;
|
||||
healthTimer.start();
|
||||
while (healthTimer.elapsed() < healthCheckTimeoutMs && !QFile::exists(healthFile)) {
|
||||
QApplication::processEvents();
|
||||
QThread::msleep(100);
|
||||
}
|
||||
if (!QFile::exists(healthFile))
|
||||
return delegateRollback("启动确认失败", QString("新版本在 %1 毫秒内没有完成启动健康确认。").arg(healthCheckTimeoutMs));
|
||||
|
||||
setProgress(99, "正在提交更新事务...");
|
||||
if (!transaction.commit())
|
||||
return delegateRollback("事务提交失败", "新版本已经启动,但无法提交更新事务。");
|
||||
|
||||
QFile::remove(healthFile);
|
||||
QFile::remove(bootstrapPlanFile());
|
||||
reportUpdateResult(true);
|
||||
progress.setValue(100);
|
||||
progress.close();
|
||||
QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion));
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user