feat: 支持 Linux 客户端打包与跨平台运行
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
project(Bootstrap LANGUAGES CXX)
|
||||
|
||||
add_executable(Bootstrap WIN32 main.cpp)
|
||||
target_compile_features(Bootstrap PRIVATE cxx_std_17)
|
||||
target_compile_definitions(Bootstrap PRIVATE UNICODE _UNICODE)
|
||||
target_link_libraries(Bootstrap PRIVATE shell32)
|
||||
project(Bootstrap LANGUAGES CXX)
|
||||
|
||||
add_executable(Bootstrap
|
||||
main.cpp
|
||||
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||
)
|
||||
target_compile_features(Bootstrap PRIVATE cxx_std_17)
|
||||
target_link_libraries(Bootstrap PRIVATE Qt5::Core Qt5::Widgets)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(Bootstrap PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
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 Bootstrap POST_BUILD
|
||||
COMMAND ${WINDEPLOYQT} $<IF:$<CONFIG:Debug>,--debug,--release> $<TARGET_FILE:Bootstrap>
|
||||
COMMENT "Deploy Qt runtime for Bootstrap"
|
||||
)
|
||||
endif()
|
||||
|
||||
+195
-186
@@ -1,191 +1,200 @@
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <filesystem>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::wstring quote(const std::wstring& value)
|
||||
{
|
||||
std::wstring result = L"\"";
|
||||
unsigned backslashes = 0;
|
||||
for (wchar_t ch : value) {
|
||||
if (ch == L'\\') { ++backslashes; continue; }
|
||||
if (ch == L'\"') {
|
||||
result.append(backslashes * 2 + 1, L'\\');
|
||||
result.push_back(L'\"');
|
||||
backslashes = 0;
|
||||
continue;
|
||||
}
|
||||
result.append(backslashes, L'\\');
|
||||
backslashes = 0;
|
||||
result.push_back(ch);
|
||||
}
|
||||
result.append(backslashes * 2, L'\\');
|
||||
result.push_back(L'\"');
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring fromUtf8(const std::string& value)
|
||||
{
|
||||
if (value.empty()) return {};
|
||||
const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
|
||||
static_cast<int>(value.size()), nullptr, 0);
|
||||
if (size <= 0) return {};
|
||||
std::wstring result(size, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
|
||||
static_cast<int>(value.size()), result.data(), size);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool safeRelative(const fs::path& path)
|
||||
{
|
||||
if (path.empty() || path.is_absolute() || path.has_root_name()) return false;
|
||||
for (const auto& part : path)
|
||||
if (part == L"..") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool copyWithRetry(const fs::path& source, const fs::path& destination)
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::create_directories(destination.parent_path(), ec);
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
ec.clear();
|
||||
fs::copy_file(source, destination, fs::copy_options::overwrite_existing, ec);
|
||||
if (!ec) return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool removeWithRetry(const fs::path& path)
|
||||
{
|
||||
std::error_code ec;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
ec.clear();
|
||||
if (!fs::exists(path, ec)) return !ec;
|
||||
if (fs::remove(path, ec)) return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool rollback(const fs::path& installDir, const fs::path& backupDir,
|
||||
const std::vector<fs::path>& paths)
|
||||
{
|
||||
bool success = true;
|
||||
for (const auto& relative : paths) {
|
||||
const fs::path destination = installDir / relative;
|
||||
const fs::path backup = backupDir / relative;
|
||||
std::error_code ec;
|
||||
if (fs::exists(backup, ec)) {
|
||||
if (!copyWithRetry(backup, destination)) success = false;
|
||||
} else if (fs::exists(destination, ec) && !fs::remove(destination, ec)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool launchUpdater(const std::wstring& updater, const std::vector<std::wstring>& updateArgs,
|
||||
const std::wstring& result)
|
||||
{
|
||||
std::wstring command = quote(updater);
|
||||
for (const auto& arg : updateArgs) command += L" " + quote(arg);
|
||||
command += L" " + quote(L"--bootstrap-resume=" + result);
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
std::vector<wchar_t> mutableCommand(command.begin(), command.end());
|
||||
mutableCommand.push_back(L'\0');
|
||||
const BOOL ok = CreateProcessW(updater.c_str(), mutableCommand.data(), nullptr, nullptr,
|
||||
FALSE, 0, nullptr, fs::path(updater).parent_path().c_str(), &si, &pi);
|
||||
if (ok) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); }
|
||||
return ok == TRUE;
|
||||
}
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
{
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
if (!argv || argc < 11) {
|
||||
MessageBoxW(nullptr, L"Bootstrap arguments are incomplete.", L"Update Handoff Failed", MB_ICONERROR);
|
||||
if (argv) LocalFree(argv);
|
||||
#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)
|
||||
{
|
||||
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 fs::path planFile = argv[1];
|
||||
const fs::path installDir = argv[2];
|
||||
const fs::path stagingDir = argv[3];
|
||||
const fs::path backupDir = argv[4];
|
||||
const std::wstring updater = argv[5];
|
||||
const DWORD updaterPid = static_cast<DWORD>(_wtoi(argv[6]));
|
||||
std::vector<std::wstring> updateArgs{argv[7], argv[8], argv[9], argv[10]};
|
||||
const std::wstring mode = argc >= 12 ? argv[11] : L"install";
|
||||
LocalFree(argv);
|
||||
|
||||
if (HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, updaterPid)) {
|
||||
WaitForSingleObject(process, 30000);
|
||||
CloseHandle(process);
|
||||
}
|
||||
|
||||
struct PlanItem { wchar_t operation; fs::path relative; };
|
||||
std::ifstream input(planFile, std::ios::binary);
|
||||
std::vector<PlanItem> items;
|
||||
std::vector<fs::path> paths;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
if (line.size() < 3 || line[1] != '\t' || (line[0] != 'C' && line[0] != 'D')) {
|
||||
MessageBoxW(nullptr, L"The update plan operation format is invalid.", L"Update Handoff Failed", MB_ICONERROR);
|
||||
return 3;
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
const fs::path relative(fromUtf8(line.substr(2)));
|
||||
if (!safeRelative(relative)) {
|
||||
MessageBoxW(nullptr, L"The update plan contains an unsafe path.", L"Update Handoff Failed", MB_ICONERROR);
|
||||
return 3;
|
||||
}
|
||||
items.push_back({static_cast<wchar_t>(line[0]), relative});
|
||||
paths.push_back(relative);
|
||||
}
|
||||
|
||||
bool success = input.eof();
|
||||
bool rolledBack = false;
|
||||
fs::path failedPath;
|
||||
if (mode == L"rollback") {
|
||||
rolledBack = success && rollback(installDir, backupDir, paths);
|
||||
success = false;
|
||||
} else if (success) {
|
||||
for (const auto& item : items) {
|
||||
const fs::path& relative = item.relative;
|
||||
if (_wcsicmp(relative.filename().c_str(), L"Bootstrap.exe") == 0) {
|
||||
success = false;
|
||||
failedPath = relative;
|
||||
break;
|
||||
}
|
||||
const bool itemOk = item.operation == L'C'
|
||||
? copyWithRetry(stagingDir / relative, installDir / relative)
|
||||
: removeWithRetry(installDir / relative);
|
||||
if (!itemOk) {
|
||||
success = false;
|
||||
failedPath = relative;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) rolledBack = rollback(installDir, backupDir, paths);
|
||||
}
|
||||
|
||||
const std::wstring result = success ? L"success" : (rolledBack ? L"rolledback" : L"rollback-failed");
|
||||
if (!success)
|
||||
rolledBack = rollback(installDir, backupDir, paths);
|
||||
}
|
||||
|
||||
const QString result = success ? QStringLiteral("success")
|
||||
: (rolledBack ? QStringLiteral("rolledback") : QStringLiteral("rollback-failed"));
|
||||
if (!launchUpdater(updater, updateArgs, result)) {
|
||||
std::wstring message = L"Cannot restart Updater.exe.";
|
||||
if (!failedPath.empty()) message += L"\nFailed file: " + failedPath.wstring();
|
||||
MessageBoxW(nullptr, message.c_str(), L"Update Handoff Failed", MB_ICONERROR);
|
||||
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;
|
||||
}
|
||||
return (success || rolledBack) ? 0 : 5;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user