feat(client): improve SDK packaging and registry config
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8-bom
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
@@ -22,6 +22,8 @@ Desktop.ini
|
|||||||
*.pdf
|
*.pdf
|
||||||
*.doc
|
*.doc
|
||||||
*.docx
|
*.docx
|
||||||
|
private/
|
||||||
|
pdf_requirements.txt
|
||||||
|
|
||||||
# C/C++ generated artifacts
|
# C/C++ generated artifacts
|
||||||
*.obj
|
*.obj
|
||||||
@@ -49,6 +51,7 @@ CMakeUserPresets.json
|
|||||||
Testing/
|
Testing/
|
||||||
|
|
||||||
# Third-party/business binary drops and generated packages
|
# Third-party/business binary drops and generated packages
|
||||||
|
thirdparty/
|
||||||
App/
|
App/
|
||||||
dist/
|
dist/
|
||||||
*.zip
|
*.zip
|
||||||
|
|||||||
+6
-6
@@ -115,7 +115,7 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|||||||
int argc = 0;
|
int argc = 0;
|
||||||
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||||
if (!argv || argc < 11) {
|
if (!argv || argc < 11) {
|
||||||
MessageBoxW(nullptr, L"Bootstrap 参数不完整。", L"更新接管失败", MB_ICONERROR);
|
MessageBoxW(nullptr, L"Bootstrap arguments are incomplete.", L"Update Handoff Failed", MB_ICONERROR);
|
||||||
if (argv) LocalFree(argv);
|
if (argv) LocalFree(argv);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
@@ -142,12 +142,12 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|||||||
while (std::getline(input, line)) {
|
while (std::getline(input, line)) {
|
||||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
if (line.size() < 3 || line[1] != '\t' || (line[0] != 'C' && line[0] != 'D')) {
|
if (line.size() < 3 || line[1] != '\t' || (line[0] != 'C' && line[0] != 'D')) {
|
||||||
MessageBoxW(nullptr, L"更新计划操作格式无效。", L"更新接管失败", MB_ICONERROR);
|
MessageBoxW(nullptr, L"The update plan operation format is invalid.", L"Update Handoff Failed", MB_ICONERROR);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
const fs::path relative(fromUtf8(line.substr(2)));
|
const fs::path relative(fromUtf8(line.substr(2)));
|
||||||
if (!safeRelative(relative)) {
|
if (!safeRelative(relative)) {
|
||||||
MessageBoxW(nullptr, L"更新计划包含不安全路径。", L"更新接管失败", MB_ICONERROR);
|
MessageBoxW(nullptr, L"The update plan contains an unsafe path.", L"Update Handoff Failed", MB_ICONERROR);
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
items.push_back({static_cast<wchar_t>(line[0]), relative});
|
items.push_back({static_cast<wchar_t>(line[0]), relative});
|
||||||
@@ -182,9 +182,9 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|||||||
|
|
||||||
const std::wstring result = success ? L"success" : (rolledBack ? L"rolledback" : L"rollback-failed");
|
const std::wstring result = success ? L"success" : (rolledBack ? L"rolledback" : L"rollback-failed");
|
||||||
if (!launchUpdater(updater, updateArgs, result)) {
|
if (!launchUpdater(updater, updateArgs, result)) {
|
||||||
std::wstring message = L"无法重新启动 Updater.exe。";
|
std::wstring message = L"Cannot restart Updater.exe.";
|
||||||
if (!failedPath.empty()) message += L"\n失败文件:" + failedPath.wstring();
|
if (!failedPath.empty()) message += L"\nFailed file: " + failedPath.wstring();
|
||||||
MessageBoxW(nullptr, message.c_str(), L"更新接管失败", MB_ICONERROR);
|
MessageBoxW(nullptr, message.c_str(), L"Update Handoff Failed", MB_ICONERROR);
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
return (success || rolledBack) ? 0 : 5;
|
return (success || rolledBack) ? 0 : 5;
|
||||||
|
|||||||
+86
-15
@@ -1,44 +1,109 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(ClientAll LANGUAGES C CXX)
|
project(ClientAll LANGUAGES C CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
|
# Source files should be saved as UTF-8 with BOM; compile source and
|
||||||
|
# execution charsets as UTF-8 instead of setting encodings in code.
|
||||||
add_compile_options(/utf-8)
|
add_compile_options(/utf-8)
|
||||||
endif()
|
endif()
|
||||||
# Qt全局配置
|
|
||||||
|
# Qt global config. Qt is intentionally not hard-coded here; configure it with
|
||||||
|
# CMake-recognized environment variables such as CMAKE_PREFIX_PATH or Qt5_DIR.
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_PREFIX_PATH "C:\\Qt\\5.15.2\\msvc2019_64" ${CMAKE_PREFIX_PATH})
|
|
||||||
find_package(Qt5 REQUIRED COMPONENTS Core Network Gui Widgets)
|
find_package(Qt5 REQUIRED COMPONENTS Core Network Gui Widgets)
|
||||||
# ========== OpenSSL 手动配置(完全抛弃find_package) ==========
|
|
||||||
set(OPENSSL_ROOT_DIR "C:/Program Files/OpenSSL-Win64")
|
get_target_property(QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
|
||||||
set(OPENSSL_INC "${OPENSSL_ROOT_DIR}/include")
|
get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY)
|
||||||
set(OPENSSL_LIB_DEBUG "${OPENSSL_ROOT_DIR}/lib/VC/x64/MDd")
|
find_program(QT_LRELEASE_EXECUTABLE NAMES lrelease lrelease.exe HINTS "${QT_BIN_DIR}" REQUIRED)
|
||||||
set(OPENSSL_LIB_RELEASE "${OPENSSL_ROOT_DIR}/lib/VC/x64/MD")
|
find_program(QT_LUPDATE_EXECUTABLE NAMES lupdate lupdate.exe HINTS "${QT_BIN_DIR}" REQUIRED)
|
||||||
# 全局宏,所有子项目统一启用OpenSSL
|
|
||||||
|
set(TRANSLATION_TS "${CMAKE_SOURCE_DIR}/i18n/update-client_zh_CN.ts")
|
||||||
|
set(TRANSLATION_QM "${CMAKE_SOURCE_DIR}/i18n/update-client_zh_CN.qm")
|
||||||
|
set(TRANSLATION_SCAN_DIRS
|
||||||
|
"${CMAKE_SOURCE_DIR}/Common"
|
||||||
|
"${CMAKE_SOURCE_DIR}/Launcher"
|
||||||
|
"${CMAKE_SOURCE_DIR}/Updater"
|
||||||
|
"${CMAKE_SOURCE_DIR}/MainApp"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${TRANSLATION_QM}"
|
||||||
|
COMMAND "${QT_LRELEASE_EXECUTABLE}" "${TRANSLATION_TS}" -qm "${TRANSLATION_QM}"
|
||||||
|
DEPENDS "${TRANSLATION_TS}"
|
||||||
|
COMMENT "Compile Qt translation: update-client_zh_CN.qm"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(update_client_translations ALL
|
||||||
|
DEPENDS "${TRANSLATION_QM}"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(update_client_lupdate
|
||||||
|
COMMAND "${QT_LUPDATE_EXECUTABLE}"
|
||||||
|
${TRANSLATION_SCAN_DIRS}
|
||||||
|
-extensions cpp,h
|
||||||
|
-no-obsolete
|
||||||
|
-ts "${TRANSLATION_TS}"
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
COMMENT "Update Qt translation source: update-client_zh_CN.ts"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
# Local-only third-party dependencies. The thirdparty directory is ignored by Git.
|
||||||
|
# Override OpenSSL when it is not copied into thirdparty:
|
||||||
|
# -DSIMCAE_OPENSSL_ROOT=<OpenSSL-Win64 root>
|
||||||
|
set(THIRDPARTY_DIR "${CMAKE_SOURCE_DIR}/thirdparty" CACHE PATH "Local third-party dependency root")
|
||||||
|
set(SIMCAE_OPENSSL_ROOT "${THIRDPARTY_DIR}/OpenSSL-Win64" CACHE PATH "OpenSSL Win64 root")
|
||||||
|
|
||||||
|
# OpenSSL manual config. The project expects the Windows OpenSSL layout.
|
||||||
|
if(NOT EXISTS "${SIMCAE_OPENSSL_ROOT}/include/openssl")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"OpenSSL not found: ${SIMCAE_OPENSSL_ROOT}\n"
|
||||||
|
"Copy OpenSSL-Win64 to thirdparty/OpenSSL-Win64, or configure with "
|
||||||
|
"-DSIMCAE_OPENSSL_ROOT=<OpenSSL-Win64 root>."
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
set(OPENSSL_INC "${SIMCAE_OPENSSL_ROOT}/include")
|
||||||
|
set(OPENSSL_LIB_DEBUG "${SIMCAE_OPENSSL_ROOT}/lib/VC/x64/MDd")
|
||||||
|
set(OPENSSL_LIB_RELEASE "${SIMCAE_OPENSSL_ROOT}/lib/VC/x64/MD")
|
||||||
|
foreach(OPENSSL_LIB_DIR IN ITEMS "${OPENSSL_LIB_DEBUG}" "${OPENSSL_LIB_RELEASE}")
|
||||||
|
if(NOT EXISTS "${OPENSSL_LIB_DIR}")
|
||||||
|
message(FATAL_ERROR "OpenSSL library directory not found: ${OPENSSL_LIB_DIR}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
add_compile_definitions(HAVE_OPENSSL=1)
|
add_compile_definitions(HAVE_OPENSSL=1)
|
||||||
# ==========================================================
|
|
||||||
# 统一输出目录
|
# 统一输出目录
|
||||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/lib)
|
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/lib)
|
||||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin)
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin)
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin)
|
||||||
|
|
||||||
# Bootstrap 不依赖 Qt,可在 Updater 退出后替换 Updater 与 Qt 运行库
|
# Bootstrap 不依赖 Qt,可在 Updater 退出后替换 Updater 与 Qt 运行库
|
||||||
add_subdirectory(Bootstrap)
|
add_subdirectory(Bootstrap)
|
||||||
|
|
||||||
# 先编译公共库
|
# 先编译公共库
|
||||||
add_subdirectory(Common)
|
add_subdirectory(Common)
|
||||||
|
|
||||||
# 再编译三个业务程序
|
# 再编译三个业务程序
|
||||||
add_subdirectory(Launcher)
|
add_subdirectory(Launcher)
|
||||||
add_subdirectory(Updater)
|
add_subdirectory(Updater)
|
||||||
add_subdirectory(MainApp)
|
add_subdirectory(MainApp)
|
||||||
|
|
||||||
# 默认启动项目
|
# 默认启动项目
|
||||||
set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Launcher)
|
set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Launcher)
|
||||||
# Copy client.ini and config directory to output bin folder
|
|
||||||
|
# Copy config templates and static resources to output bin folder.
|
||||||
set(APP_CONFIG_SOURCE_FILE "${CMAKE_SOURCE_DIR}/config/app_config.example.json")
|
set(APP_CONFIG_SOURCE_FILE "${CMAKE_SOURCE_DIR}/config/app_config.example.json")
|
||||||
set(CONFIG_SOURCE_DIR "${CMAKE_SOURCE_DIR}/config")
|
set(CONFIG_SOURCE_DIR "${CMAKE_SOURCE_DIR}/config")
|
||||||
set(MANIFEST_PUBLIC_KEY_FILE "${CONFIG_SOURCE_DIR}/manifest_public_key.pem")
|
set(MANIFEST_PUBLIC_KEY_FILE "${CONFIG_SOURCE_DIR}/manifest_public_key.pem")
|
||||||
set(INI_TARGET_FOLDER "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
|
set(INI_TARGET_FOLDER "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
|
||||||
|
|
||||||
# app_config.json 包含运行时版本状态;如果存在旧 client.ini,则交给客户端首次启动迁移
|
# app_config.json 包含运行时版本状态;如果存在旧 client.ini,则交给客户端首次启动迁移
|
||||||
file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}")
|
file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}")
|
||||||
file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}/config")
|
file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}/config")
|
||||||
@@ -46,10 +111,11 @@ if(NOT EXISTS "${INI_TARGET_FOLDER}/config/app_config.json" AND NOT EXISTS "${IN
|
|||||||
configure_file("${APP_CONFIG_SOURCE_FILE}" "${INI_TARGET_FOLDER}/config/app_config.json" COPYONLY)
|
configure_file("${APP_CONFIG_SOURCE_FILE}" "${INI_TARGET_FOLDER}/config/app_config.json" COPYONLY)
|
||||||
endif()
|
endif()
|
||||||
foreach(RUNTIME_RESOURCE local_state.json version_policy.dat)
|
foreach(RUNTIME_RESOURCE local_state.json version_policy.dat)
|
||||||
if(NOT EXISTS "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}")
|
if(EXISTS "${CONFIG_SOURCE_DIR}/${RUNTIME_RESOURCE}" AND NOT EXISTS "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}")
|
||||||
configure_file("${CONFIG_SOURCE_DIR}/${RUNTIME_RESOURCE}" "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}" COPYONLY)
|
configure_file("${CONFIG_SOURCE_DIR}/${RUNTIME_RESOURCE}" "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}" COPYONLY)
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
# 编译阶段同步静态配置资源
|
# 编译阶段同步静态配置资源
|
||||||
add_custom_target(copy_client_resources ALL
|
add_custom_target(copy_client_resources ALL
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${INI_TARGET_FOLDER}
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${INI_TARGET_FOLDER}
|
||||||
@@ -61,10 +127,15 @@ add_custom_target(copy_client_resources ALL
|
|||||||
add_dependencies(Launcher copy_client_resources)
|
add_dependencies(Launcher copy_client_resources)
|
||||||
add_dependencies(Updater copy_client_resources)
|
add_dependencies(Updater copy_client_resources)
|
||||||
add_dependencies(MainApp copy_client_resources)
|
add_dependencies(MainApp copy_client_resources)
|
||||||
|
add_dependencies(Launcher update_client_translations)
|
||||||
|
add_dependencies(Updater update_client_translations)
|
||||||
|
add_dependencies(MainApp update_client_translations)
|
||||||
|
|
||||||
# Install rules for packaging
|
# Install rules for packaging
|
||||||
if(EXISTS ${CONFIG_SOURCE_DIR})
|
if(EXISTS "${APP_CONFIG_SOURCE_FILE}")
|
||||||
install(DIRECTORY ${CONFIG_SOURCE_DIR} DESTINATION bin/config)
|
install(FILES "${APP_CONFIG_SOURCE_FILE}" DESTINATION bin/config RENAME app_config.json)
|
||||||
endif()
|
endif()
|
||||||
if(EXISTS ${MANIFEST_PUBLIC_KEY_FILE})
|
if(EXISTS "${MANIFEST_PUBLIC_KEY_FILE}")
|
||||||
install(FILES ${MANIFEST_PUBLIC_KEY_FILE} DESTINATION bin)
|
install(FILES "${MANIFEST_PUBLIC_KEY_FILE}" DESTINATION bin/config)
|
||||||
|
install(FILES "${MANIFEST_PUBLIC_KEY_FILE}" DESTINATION bin)
|
||||||
endif()
|
endif()
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "windows-x64-base",
|
||||||
|
"displayName": "Windows x64 Base",
|
||||||
|
"description": "Windows x64 build with Visual Studio 2022.",
|
||||||
|
"hidden": true,
|
||||||
|
"generator": "Visual Studio 17 2022",
|
||||||
|
"architecture": {
|
||||||
|
"value": "x64",
|
||||||
|
"strategy": "set"
|
||||||
|
},
|
||||||
|
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||||
|
"installDir": "${sourceDir}/out/install/${presetName}",
|
||||||
|
"condition": {
|
||||||
|
"type": "equals",
|
||||||
|
"lhs": "${hostSystemName}",
|
||||||
|
"rhs": "Windows"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x64-debug",
|
||||||
|
"displayName": "x64 Debug",
|
||||||
|
"description": "Debug build for Windows x64.",
|
||||||
|
"inherits": "windows-x64-base",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_CONFIGURATION_TYPES": "Debug",
|
||||||
|
"CMAKE_INSTALL_CONFIG_NAME": "Debug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x64-release",
|
||||||
|
"displayName": "x64 Release",
|
||||||
|
"description": "Release build for Windows x64.",
|
||||||
|
"inherits": "windows-x64-base",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_CONFIGURATION_TYPES": "Release",
|
||||||
|
"CMAKE_INSTALL_CONFIG_NAME": "Release"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildPresets": [
|
||||||
|
{
|
||||||
|
"name": "x64-debug",
|
||||||
|
"displayName": "x64 Debug",
|
||||||
|
"configurePreset": "x64-debug",
|
||||||
|
"configuration": "Debug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x64-release",
|
||||||
|
"displayName": "x64 Release",
|
||||||
|
"configurePreset": "x64-release",
|
||||||
|
"configuration": "Release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
project(Common LANGUAGES C CXX)
|
project(Common LANGUAGES C CXX)
|
||||||
find_package(Qt5 REQUIRED COMPONENTS Core Network Widgets)
|
|
||||||
|
|
||||||
set(SRC
|
set(SRC
|
||||||
HttpHelper.h
|
HttpHelper.h
|
||||||
@@ -30,9 +29,13 @@ target_include_directories(Common
|
|||||||
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
|
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
|
||||||
target_link_directories(Common INTERFACE
|
target_link_directories(Common INTERFACE
|
||||||
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
|
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
|
||||||
$<$<CONFIG:Release>:${OPENSSL_LIB_RELEASE}>
|
$<$<NOT:$<CONFIG:Debug>>:${OPENSSL_LIB_RELEASE}>
|
||||||
)
|
)
|
||||||
target_link_libraries(Common
|
target_link_libraries(Common
|
||||||
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
|
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
|
||||||
INTERFACE libssl.lib libcrypto.lib
|
INTERFACE libssl.lib libcrypto.lib
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(Common INTERFACE shell32)
|
||||||
|
endif()
|
||||||
|
|||||||
+520
-50
@@ -1,13 +1,282 @@
|
|||||||
#include "ConfigHelper.h"
|
#include "ConfigHelper.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QStringList>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <string>
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const QString kElevatedConfigSetFlag = QStringLiteral("--simcae-config-set");
|
||||||
|
const QString kElevatedFileWriteFlag = QStringLiteral("--simcae-file-write");
|
||||||
|
const QString kElevatedFileRemoveFlag = QStringLiteral("--simcae-file-remove");
|
||||||
|
const QString kConfigPathPrefix = QStringLiteral("--config-path-b64=");
|
||||||
|
const QString kConfigKeyPrefix = QStringLiteral("--config-key-b64=");
|
||||||
|
const QString kConfigValuePrefix = QStringLiteral("--config-value-b64=");
|
||||||
|
const QString kFilePathPrefix = QStringLiteral("--file-path-b64=");
|
||||||
|
const QString kFileDataPrefix = QStringLiteral("--file-data-b64=");
|
||||||
|
const QString kRegistryOrganization = QStringLiteral("Marsco");
|
||||||
|
const QString kRegistryApplication = QStringLiteral("UpdateClientSDK");
|
||||||
|
const QString kRegistryInstallationsGroup = QStringLiteral("installations");
|
||||||
|
const QString kRegistryConfigGroup = QStringLiteral("config");
|
||||||
|
const QString kRegistryMetaGroup = QStringLiteral("_meta");
|
||||||
|
const QString kRegistrySourceHashKey = QStringLiteral("source_sha256");
|
||||||
|
const QString kRegistrySourcePathKey = QStringLiteral("source_path");
|
||||||
|
const QString kRegistryRuntimeRootKey = QStringLiteral("runtime_root");
|
||||||
|
const QString kRegistryImportedAtKey = QStringLiteral("imported_at_utc");
|
||||||
|
|
||||||
|
QString encodeArgument(const QString& value)
|
||||||
|
{
|
||||||
|
return QString::fromLatin1(value.toUtf8().toBase64(
|
||||||
|
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString encodeBytes(const QByteArray& value)
|
||||||
|
{
|
||||||
|
return QString::fromLatin1(value.toBase64(
|
||||||
|
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString decodeArgument(const QString& value)
|
||||||
|
{
|
||||||
|
return QString::fromUtf8(QByteArray::fromBase64(value.toLatin1(),
|
||||||
|
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray decodeBytes(const QString& value)
|
||||||
|
{
|
||||||
|
return QByteArray::fromBase64(value.toLatin1(),
|
||||||
|
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString findArgumentValue(const QStringList& arguments, const QString& prefix)
|
||||||
|
{
|
||||||
|
for (const QString& argument : arguments)
|
||||||
|
if (argument.startsWith(prefix))
|
||||||
|
return argument.mid(prefix.size());
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeBytesToFile(const QString& path, const QByteArray& bytes, QString* errorMessage)
|
||||||
|
{
|
||||||
|
QDir dir(QFileInfo(path).path());
|
||||||
|
if (!dir.exists() && !dir.mkpath("."))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot create directory: %1").arg(dir.path());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSaveFile output(path);
|
||||||
|
if (!output.open(QIODevice::WriteOnly))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot open file for writing: %1").arg(output.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (output.write(bytes) != bytes.size())
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot write full file: %1").arg(output.errorString());
|
||||||
|
output.cancelWriting();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!output.commit())
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot commit file: %1").arg(output.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (errorMessage)
|
||||||
|
errorMessage->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removeFile(const QString& path, QString* errorMessage)
|
||||||
|
{
|
||||||
|
if (!QFile::exists(path))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
errorMessage->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (QFile::remove(path))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
errorMessage->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot remove file: %1").arg(path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeConfigValueToFile(const QString& configPath, const QString& key,
|
||||||
|
const QString& value, QString* errorMessage)
|
||||||
|
{
|
||||||
|
QFile input(configPath);
|
||||||
|
QJsonObject config;
|
||||||
|
if (input.exists())
|
||||||
|
{
|
||||||
|
if (!input.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot open config for reading: %1").arg(input.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QJsonParseError parseError;
|
||||||
|
const QByteArray raw = input.readAll();
|
||||||
|
input.close();
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
config = document.object();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.insert(key, value);
|
||||||
|
QDir dir(QFileInfo(configPath).path());
|
||||||
|
if (!dir.exists() && !dir.mkpath("."))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("Cannot create config directory: %1").arg(dir.path());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
||||||
|
return writeBytesToFile(configPath, payload, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
QString windowsErrorMessage(DWORD errorCode)
|
||||||
|
{
|
||||||
|
if (errorCode == ERROR_CANCELLED)
|
||||||
|
return QCoreApplication::translate("ConfigHelper", "The user canceled the administrator permission confirmation.");
|
||||||
|
return QCoreApplication::translate("ConfigHelper", "Windows error %1").arg(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runElevatedSelfCommand(const QStringList& arguments, const QString& targetPath,
|
||||||
|
const QString& originalError, QString* errorMessage)
|
||||||
|
{
|
||||||
|
const QMessageBox::StandardButton choice = QMessageBox::question(
|
||||||
|
nullptr,
|
||||||
|
QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"),
|
||||||
|
QCoreApplication::translate("ConfigHelper", "The current installation directory requires administrator permission to save configuration.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.")
|
||||||
|
.arg(QDir::toNativeSeparators(targetPath), originalError),
|
||||||
|
QMessageBox::Ok | QMessageBox::Cancel,
|
||||||
|
QMessageBox::Ok);
|
||||||
|
if (choice != QMessageBox::Ok)
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QCoreApplication::translate("ConfigHelper", "The user canceled the administrator permission request.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString executable = QCoreApplication::applicationFilePath();
|
||||||
|
if (executable.isEmpty() || !QFileInfo::exists(executable))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QStringLiteral("Cannot locate current executable for elevated config write.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString parameters = arguments.join(QLatin1Char(' '));
|
||||||
|
const QString workingDir = QFileInfo(executable).absolutePath();
|
||||||
|
|
||||||
|
std::wstring verb = L"runas";
|
||||||
|
std::wstring file = executable.toStdWString();
|
||||||
|
std::wstring params = parameters.toStdWString();
|
||||||
|
std::wstring directory = workingDir.toStdWString();
|
||||||
|
|
||||||
|
SHELLEXECUTEINFOW info{};
|
||||||
|
info.cbSize = sizeof(info);
|
||||||
|
info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||||
|
info.lpVerb = verb.c_str();
|
||||||
|
info.lpFile = file.c_str();
|
||||||
|
info.lpParameters = params.c_str();
|
||||||
|
info.lpDirectory = directory.c_str();
|
||||||
|
info.nShow = SW_HIDE;
|
||||||
|
|
||||||
|
if (!ShellExecuteExW(&info))
|
||||||
|
{
|
||||||
|
const DWORD err = GetLastError();
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QStringLiteral("Cannot request administrator permission: %1").arg(windowsErrorMessage(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitForSingleObject(info.hProcess, INFINITE);
|
||||||
|
DWORD exitCode = 1;
|
||||||
|
GetExitCodeProcess(info.hProcess, &exitCode);
|
||||||
|
CloseHandle(info.hProcess);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QStringLiteral("Elevated config write failed with exit code %1.").arg(exitCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage)
|
||||||
|
errorMessage->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeConfigValueWithElevation(const QString& configPath, const QString& key,
|
||||||
|
const QString& value, const QString& originalError,
|
||||||
|
QString* errorMessage)
|
||||||
|
{
|
||||||
|
const QStringList arguments{
|
||||||
|
kElevatedConfigSetFlag,
|
||||||
|
kConfigPathPrefix + encodeArgument(configPath),
|
||||||
|
kConfigKeyPrefix + encodeArgument(key),
|
||||||
|
kConfigValuePrefix + encodeArgument(value)
|
||||||
|
};
|
||||||
|
return runElevatedSelfCommand(arguments, configPath, originalError, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeBytesWithElevation(const QString& path, const QByteArray& bytes,
|
||||||
|
const QString& originalError, QString* errorMessage)
|
||||||
|
{
|
||||||
|
const QStringList arguments{
|
||||||
|
kElevatedFileWriteFlag,
|
||||||
|
kFilePathPrefix + encodeArgument(path),
|
||||||
|
kFileDataPrefix + encodeBytes(bytes)
|
||||||
|
};
|
||||||
|
return runElevatedSelfCommand(arguments, path, originalError, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removeFileWithElevation(const QString& path, const QString& originalError, QString* errorMessage)
|
||||||
|
{
|
||||||
|
const QStringList arguments{
|
||||||
|
kElevatedFileRemoveFlag,
|
||||||
|
kFilePathPrefix + encodeArgument(path)
|
||||||
|
};
|
||||||
|
return runElevatedSelfCommand(arguments, path, originalError, errorMessage);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
ConfigHelper& ConfigHelper::instance()
|
ConfigHelper& ConfigHelper::instance()
|
||||||
{
|
{
|
||||||
@@ -15,12 +284,126 @@ ConfigHelper& ConfigHelper::instance()
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ConfigHelper::runElevatedWriteCommandIfRequested()
|
||||||
|
{
|
||||||
|
const QStringList arguments = QCoreApplication::arguments();
|
||||||
|
if (arguments.contains(kElevatedConfigSetFlag))
|
||||||
|
{
|
||||||
|
const QString configPath = decodeArgument(findArgumentValue(arguments, kConfigPathPrefix));
|
||||||
|
const QString key = decodeArgument(findArgumentValue(arguments, kConfigKeyPrefix));
|
||||||
|
const QString value = decodeArgument(findArgumentValue(arguments, kConfigValuePrefix));
|
||||||
|
if (configPath.isEmpty() || key.isEmpty())
|
||||||
|
{
|
||||||
|
qWarning() << "Elevated config write arguments are incomplete.";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString errorMessage;
|
||||||
|
if (!writeConfigValueToFile(configPath, key, value, &errorMessage))
|
||||||
|
{
|
||||||
|
qWarning() << "Elevated config write failed:" << errorMessage;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.contains(kElevatedFileWriteFlag))
|
||||||
|
{
|
||||||
|
const QString path = decodeArgument(findArgumentValue(arguments, kFilePathPrefix));
|
||||||
|
const QByteArray bytes = decodeBytes(findArgumentValue(arguments, kFileDataPrefix));
|
||||||
|
if (path.isEmpty())
|
||||||
|
{
|
||||||
|
qWarning() << "Elevated file write arguments are incomplete.";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString errorMessage;
|
||||||
|
if (!writeBytesToFile(path, bytes, &errorMessage))
|
||||||
|
{
|
||||||
|
qWarning() << "Elevated file write failed:" << errorMessage;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.contains(kElevatedFileRemoveFlag))
|
||||||
|
{
|
||||||
|
const QString path = decodeArgument(findArgumentValue(arguments, kFilePathPrefix));
|
||||||
|
if (path.isEmpty())
|
||||||
|
{
|
||||||
|
qWarning() << "Elevated file remove arguments are incomplete.";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString errorMessage;
|
||||||
|
if (!removeFile(path, &errorMessage))
|
||||||
|
{
|
||||||
|
qWarning() << "Elevated file remove failed:" << errorMessage;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigHelper::writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data,
|
||||||
|
QString* errorMessage)
|
||||||
|
{
|
||||||
|
QString localError;
|
||||||
|
if (writeBytesToFile(path, data, &localError))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
errorMessage->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
if (data.size() > 24 * 1024)
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = QString("File is too large for elevated inline write: %1 bytes. Original error: %2")
|
||||||
|
.arg(data.size()).arg(localError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return writeBytesWithElevation(path, data, localError, errorMessage);
|
||||||
|
#else
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = localError;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigHelper::removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage)
|
||||||
|
{
|
||||||
|
QString localError;
|
||||||
|
if (removeFile(path, &localError))
|
||||||
|
{
|
||||||
|
if (errorMessage)
|
||||||
|
errorMessage->clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
return removeFileWithElevation(path, localError, errorMessage);
|
||||||
|
#else
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = localError;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
ConfigHelper::ConfigHelper()
|
ConfigHelper::ConfigHelper()
|
||||||
{
|
{
|
||||||
m_configPath = QApplication::applicationDirPath() + "/config/app_config.json";
|
m_configPath = QApplication::applicationDirPath() + "/config/app_config.json";
|
||||||
|
m_registryInstallId = QString::fromLatin1(QCryptographicHash::hash(
|
||||||
|
QDir::cleanPath(QApplication::applicationDirPath()).toUtf8(),
|
||||||
|
QCryptographicHash::Sha256).toHex());
|
||||||
migrateLegacyIniIfNeeded();
|
migrateLegacyIniIfNeeded();
|
||||||
|
syncRegistryFromConfigFileIfChanged();
|
||||||
qDebug() << "Loading app config path:" << m_configPath;
|
qDebug() << "Loading app config path:" << m_configPath;
|
||||||
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
||||||
|
qDebug() << "Registry installation id:" << m_registryInstallId;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ConfigHelper::configPath() const
|
QString ConfigHelper::configPath() const
|
||||||
@@ -62,9 +445,134 @@ QString ConfigHelper::lastError() const
|
|||||||
return m_error;
|
return m_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
void ConfigHelper::enterRegistryGroup(QSettings& settings) const
|
||||||
|
{
|
||||||
|
settings.beginGroup(kRegistryInstallationsGroup);
|
||||||
|
settings.beginGroup(m_registryInstallId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
||||||
|
{
|
||||||
|
QFile file(m_configPath);
|
||||||
|
if (!file.exists())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
m_error = QStringLiteral("Cannot open config for reading: %1").arg(file.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
const QByteArray raw = file.readAll();
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||||
|
{
|
||||||
|
m_error = QStringLiteral("Invalid config JSON: %1").arg(parseError.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray normalizedConfig = QJsonDocument(document.object()).toJson(QJsonDocument::Compact);
|
||||||
|
const QString sourceHash = QString::fromLatin1(
|
||||||
|
QCryptographicHash::hash(normalizedConfig, QCryptographicHash::Sha256).toHex());
|
||||||
|
|
||||||
|
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||||
|
kRegistryOrganization, kRegistryApplication);
|
||||||
|
enterRegistryGroup(settings);
|
||||||
|
settings.beginGroup(kRegistryMetaGroup);
|
||||||
|
const QString previousHash = settings.value(kRegistrySourceHashKey).toString();
|
||||||
|
settings.endGroup();
|
||||||
|
|
||||||
|
if (previousHash == sourceHash)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const bool configChangedAfterPreviousImport = !previousHash.isEmpty();
|
||||||
|
|
||||||
|
settings.beginGroup(kRegistryConfigGroup);
|
||||||
|
settings.remove(QString());
|
||||||
|
const QJsonObject config = document.object();
|
||||||
|
for (auto it = config.constBegin(); it != config.constEnd(); ++it)
|
||||||
|
settings.setValue(it.key(), it.value().toVariant());
|
||||||
|
settings.endGroup();
|
||||||
|
|
||||||
|
settings.beginGroup(kRegistryMetaGroup);
|
||||||
|
settings.setValue(kRegistrySourceHashKey, sourceHash);
|
||||||
|
settings.setValue(kRegistrySourcePathKey, QDir::toNativeSeparators(QFileInfo(m_configPath).absoluteFilePath()));
|
||||||
|
settings.setValue(kRegistryRuntimeRootKey, QDir::toNativeSeparators(QApplication::applicationDirPath()));
|
||||||
|
settings.setValue(kRegistryImportedAtKey, QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
|
||||||
|
settings.endGroup();
|
||||||
|
settings.sync();
|
||||||
|
|
||||||
|
if (settings.status() != QSettings::NoError)
|
||||||
|
{
|
||||||
|
m_error = QStringLiteral("Cannot sync config to registry.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_error.clear();
|
||||||
|
qDebug() << "Synced app_config.json to registry installation id:" << m_registryInstallId;
|
||||||
|
|
||||||
|
if (configChangedAfterPreviousImport)
|
||||||
|
{
|
||||||
|
const QString configDir = QFileInfo(m_configPath).absolutePath();
|
||||||
|
const QStringList staleFiles{
|
||||||
|
QDir(configDir).filePath(QStringLiteral("client_identity.dat")),
|
||||||
|
QDir(configDir).filePath(QStringLiteral("version_policy.dat"))
|
||||||
|
};
|
||||||
|
for (const QString& staleFile : staleFiles)
|
||||||
|
{
|
||||||
|
QString removeError;
|
||||||
|
if (!ConfigHelper::removeFileWithElevationIfNeeded(staleFile, &removeError))
|
||||||
|
{
|
||||||
|
m_error = QStringLiteral("Cannot remove stale runtime file after config change: %1. %2")
|
||||||
|
.arg(staleFile, removeError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qDebug() << "Removed stale client identity and policy after app_config.json changed.";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigHelper::readRegistryValue(const QString& key, QString* value) const
|
||||||
|
{
|
||||||
|
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||||
|
kRegistryOrganization, kRegistryApplication);
|
||||||
|
enterRegistryGroup(settings);
|
||||||
|
settings.beginGroup(kRegistryConfigGroup);
|
||||||
|
if (!settings.contains(key))
|
||||||
|
{
|
||||||
|
settings.endGroup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value)
|
||||||
|
*value = settings.value(key).toString();
|
||||||
|
settings.endGroup();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigHelper::writeRegistryValue(const QString& key, const QString& value)
|
||||||
|
{
|
||||||
|
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||||
|
kRegistryOrganization, kRegistryApplication);
|
||||||
|
enterRegistryGroup(settings);
|
||||||
|
settings.beginGroup(kRegistryConfigGroup);
|
||||||
|
settings.setValue(key, value);
|
||||||
|
settings.endGroup();
|
||||||
|
settings.sync();
|
||||||
|
|
||||||
|
if (settings.status() != QSettings::NoError)
|
||||||
|
{
|
||||||
|
m_error = QStringLiteral("Cannot write config value to registry: %1").arg(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ConfigHelper::readFileValue(const QString& key) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(section);
|
|
||||||
QFile file(m_configPath);
|
QFile file(m_configPath);
|
||||||
if (!file.open(QIODevice::ReadOnly))
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
return QString();
|
return QString();
|
||||||
@@ -77,59 +585,21 @@ QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
|||||||
return document.object().value(key).toVariant().toString();
|
return document.object().value(key).toVariant().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(section);
|
||||||
|
QString value;
|
||||||
|
if (readRegistryValue(key, &value))
|
||||||
|
return value;
|
||||||
|
return readFileValue(key);
|
||||||
|
}
|
||||||
|
|
||||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||||
{
|
{
|
||||||
Q_UNUSED(section);
|
Q_UNUSED(section);
|
||||||
m_error.clear();
|
m_error.clear();
|
||||||
|
|
||||||
QFile input(m_configPath);
|
return writeRegistryValue(key, value);
|
||||||
QJsonObject config;
|
|
||||||
if (input.exists())
|
|
||||||
{
|
|
||||||
if (!input.open(QIODevice::ReadOnly))
|
|
||||||
{
|
|
||||||
m_error = QString("Cannot open config for reading: %1").arg(input.errorString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
QJsonParseError parseError;
|
|
||||||
const QByteArray raw = input.readAll();
|
|
||||||
input.close();
|
|
||||||
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
|
||||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
|
||||||
{
|
|
||||||
m_error = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
config = document.object();
|
|
||||||
}
|
|
||||||
|
|
||||||
config.insert(key, value);
|
|
||||||
QDir dir(QFileInfo(m_configPath).path());
|
|
||||||
if (!dir.exists() && !dir.mkpath("."))
|
|
||||||
{
|
|
||||||
m_error = QString("Cannot create config directory: %1").arg(dir.path());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSaveFile output(m_configPath);
|
|
||||||
if (!output.open(QIODevice::WriteOnly))
|
|
||||||
{
|
|
||||||
m_error = QString("Cannot open config for writing: %1").arg(output.errorString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
|
||||||
if (output.write(payload) != payload.size())
|
|
||||||
{
|
|
||||||
m_error = QString("Cannot write full config file: %1").arg(output.errorString());
|
|
||||||
output.cancelWriting();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!output.commit())
|
|
||||||
{
|
|
||||||
m_error = QString("Cannot commit config file: %1").arg(output.errorString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigHelper::migrateLegacyIniIfNeeded()
|
bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <QByteArray>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
class QSettings;
|
||||||
|
|
||||||
class ConfigHelper
|
class ConfigHelper
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static ConfigHelper& instance();
|
static ConfigHelper& instance();
|
||||||
|
static int runElevatedWriteCommandIfRequested();
|
||||||
|
static bool writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data,
|
||||||
|
QString* errorMessage = nullptr);
|
||||||
|
static bool removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage = nullptr);
|
||||||
QString getValue(const QString& section, const QString& key) const;
|
QString getValue(const QString& section, const QString& key) const;
|
||||||
bool setValue(const QString& section, const QString& key, const QString& value);
|
bool setValue(const QString& section, const QString& key, const QString& value);
|
||||||
QString configPath() const;
|
QString configPath() const;
|
||||||
@@ -17,6 +24,12 @@ public:
|
|||||||
private:
|
private:
|
||||||
ConfigHelper();
|
ConfigHelper();
|
||||||
bool migrateLegacyIniIfNeeded();
|
bool migrateLegacyIniIfNeeded();
|
||||||
|
void enterRegistryGroup(QSettings& settings) const;
|
||||||
|
bool syncRegistryFromConfigFileIfChanged();
|
||||||
|
bool readRegistryValue(const QString& key, QString* value) const;
|
||||||
|
bool writeRegistryValue(const QString& key, const QString& value);
|
||||||
|
QString readFileValue(const QString& key) const;
|
||||||
QString m_configPath;
|
QString m_configPath;
|
||||||
|
QString m_registryInstallId;
|
||||||
QString m_error;
|
QString m_error;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QSaveFile>
|
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -45,5 +44,5 @@ bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token
|
|||||||
ConfigHelper& config=ConfigHelper::instance();
|
ConfigHelper& config=ConfigHelper::instance();
|
||||||
QString installation=config.getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!config.setValue("Device","installation_id",installation)){m_error=QString("cannot save installation id to %1: %2").arg(config.configPath(),config.lastError());return false;}}
|
QString installation=config.getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!config.setValue("Device","installation_id",installation)){m_error=QString("cannot save installation id to %1: %2").arg(config.configPath(),config.lastError());return false;}}
|
||||||
QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}};
|
QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}};
|
||||||
QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error=QString("cannot save device credential to %1: %2").arg(path,out.errorString());return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true;
|
QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);QString writeError;if(!ConfigHelper::writeFileWithElevationIfNeeded(path,bytes,&writeError)){m_error=QString("cannot save device credential to %1: %2").arg(path,writeError);return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ bool FileHelper::copyFileOverwrite(const QString &src, const QString &dst)
|
|||||||
{
|
{
|
||||||
if (!QFile::remove(dst))
|
if (!QFile::remove(dst))
|
||||||
{
|
{
|
||||||
qDebug() << "无法删除旧文件:" << dst;
|
qDebug() << "Cannot remove old file:" << dst;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
class HttpHelper
|
class HttpHelper
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// 每次调用独立创建manager,不做成成员变量
|
// Create an independent manager for each call instead of keeping it as a member.
|
||||||
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
#include "LocalStateHelper.h"
|
#include "LocalStateHelper.h"
|
||||||
|
#include "ConfigHelper.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QDir>
|
|
||||||
|
|
||||||
LocalStateHelper::LocalStateHelper(const QString& baseDir)
|
LocalStateHelper::LocalStateHelper(const QString& baseDir)
|
||||||
: m_baseDir(baseDir)
|
: m_baseDir(baseDir)
|
||||||
@@ -16,13 +15,6 @@ bool LocalStateHelper::loadState(const QString& relativePath)
|
|||||||
QFile file(m_filePath);
|
QFile file(m_filePath);
|
||||||
if (!file.exists())
|
if (!file.exists())
|
||||||
{
|
{
|
||||||
QDir dir(QFileInfo(m_filePath).path());
|
|
||||||
if (!dir.exists() && !dir.mkpath("."))
|
|
||||||
{
|
|
||||||
m_error = QString("Cannot create state directory: %1").arg(dir.path());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_state = QJsonObject{
|
m_state = QJsonObject{
|
||||||
{"max_policy_seq", 0},
|
{"max_policy_seq", 0},
|
||||||
{"last_success_run_at", QString()},
|
{"last_success_run_at", QString()},
|
||||||
@@ -63,17 +55,19 @@ bool LocalStateHelper::loadState(const QString& relativePath)
|
|||||||
bool LocalStateHelper::saveState() const
|
bool LocalStateHelper::saveState() const
|
||||||
{
|
{
|
||||||
if (!m_loaded)
|
if (!m_loaded)
|
||||||
return false;
|
|
||||||
|
|
||||||
QFile file(m_filePath);
|
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
||||||
{
|
{
|
||||||
|
m_error = "State is not loaded";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonDocument doc(m_state);
|
QJsonDocument doc(m_state);
|
||||||
file.write(doc.toJson(QJsonDocument::Indented));
|
QString writeError;
|
||||||
file.close();
|
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_filePath, doc.toJson(QJsonDocument::Indented), &writeError))
|
||||||
|
{
|
||||||
|
m_error = QString("Cannot save state file: %1").arg(writeError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_error.clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
QString m_baseDir;
|
QString m_baseDir;
|
||||||
QString m_filePath;
|
QString m_filePath;
|
||||||
QString m_error;
|
mutable QString m_error;
|
||||||
QJsonObject m_state;
|
QJsonObject m_state;
|
||||||
bool m_loaded = false;
|
bool m_loaded = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "PolicyHelper.h"
|
#include "PolicyHelper.h"
|
||||||
|
#include "ConfigHelper.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QSaveFile>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#ifdef HAVE_OPENSSL
|
#ifdef HAVE_OPENSSL
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
@@ -33,10 +33,14 @@ bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& si
|
|||||||
|
|
||||||
bool PolicyHelper::savePolicy(const QString& relativePath) const
|
bool PolicyHelper::savePolicy(const QString& relativePath) const
|
||||||
{
|
{
|
||||||
QSaveFile file(m_baseDir + "/" + relativePath);
|
|
||||||
if (!file.open(QIODevice::WriteOnly)) return false;
|
|
||||||
const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented);
|
const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented);
|
||||||
return file.write(bytes) == bytes.size() && file.commit();
|
QString writeError;
|
||||||
|
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_baseDir + "/" + relativePath, bytes, &writeError)) {
|
||||||
|
m_error = "Cannot save policy file: " + writeError;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_error.clear();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const
|
QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const QByteArray raw = file.readAll(); file.close();
|
const QByteArray raw = file.readAll(); file.close();
|
||||||
QFile::remove(consumingPath); // 一次性消费;无论成功失败都不能重放。
|
QFile::remove(consumingPath); // Consume once; it must not be replayed regardless of success or failure.
|
||||||
QJsonParseError parseError;
|
QJsonParseError parseError;
|
||||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
客户端文档入口
|
||||||
|
==============
|
||||||
|
|
||||||
|
本目录用于保存 update-client 客户端子仓库的说明文档。
|
||||||
|
|
||||||
|
建议阅读顺序:
|
||||||
|
|
||||||
|
1. 客户端部署说明.md
|
||||||
|
面向接入和部署,说明 SDK 是什么、怎么放到业务软件目录、app_config.json 怎么填、如何打包。
|
||||||
|
|
||||||
|
2. 第三方依赖说明.md
|
||||||
|
面向编译环境,说明 Qt、OpenSSL、thirdparty/ 和 CMake 环境变量怎么配置。
|
||||||
|
|
||||||
|
3. 本 ReadMe.txt
|
||||||
|
记录客户端配置文件和打包脚本的简要说明。
|
||||||
|
|
||||||
|
|
||||||
|
客户端配置说明
|
||||||
|
==============
|
||||||
|
|
||||||
|
config/app_config.json 是部署配置源文件。Launcher / Updater / MainApp 启动时会把它同步到当前 Windows 用户的注册表,后续运行配置优先从注册表读取。
|
||||||
|
如果 config/app_config.json 内容被修改,下一次启动时会按解析后的 JSON 内容 SHA256 判断变化并重新导入注册表。
|
||||||
|
注册表位置:HKEY_CURRENT_USER\Software\Marsco\UpdateClientSDK\installations\<安装目录SHA256>\config。
|
||||||
|
如果检测到 app_config.json 发生变化,SDK 会删除 config/client_identity.dat 和 config/version_policy.dat,避免继续使用旧授权身份或旧策略;目录不可写时会弹出管理员权限确认框。
|
||||||
|
首次运行时如果该文件不存在且发现旧 client.ini,会自动迁移。
|
||||||
|
|
||||||
|
接入新软件时通常需要修改:
|
||||||
|
|
||||||
|
1. app_id、app_name、channel、current_version。
|
||||||
|
2. api_base_url、client_token、license_key、launch_token。
|
||||||
|
3. main_executable:团队业务主程序文件名。
|
||||||
|
4. launcher_executable、updater_executable、bootstrap_executable。
|
||||||
|
5. health_check_timeout_ms:升级后等待业务程序健康确认的毫秒数,最小 1000。
|
||||||
|
|
||||||
|
完整格式参考 update-client/config/app_config.example.json。
|
||||||
|
运行时生成的 client_identity.dat、local_state.json 等文件不得打入通用 SDK 模板。app_config.json 可以作为部署模板,但不要把某台机器运行后产生的临时状态混进去。
|
||||||
|
|
||||||
|
Windows 发布打包:
|
||||||
|
|
||||||
|
1. 使用 Release 配置编译全部客户端程序。
|
||||||
|
2. 先完成当前版本在线校验,确认 out/bin/update/manifest_cache 中存在对应的签名 Manifest。
|
||||||
|
3. 准备一份实际 app_config.json,确认其中包含正确的 License Key、当前版本和业务程序名。
|
||||||
|
4. 在 PowerShell 执行:
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scripts\package-client.ps1 -ConfigFile .\config\app_config.json
|
||||||
|
5. 输出位于 dist/UpdateClient 和 dist/UpdateClient.zip。
|
||||||
|
|
||||||
|
脚本会拒绝 Debug DLL、PDB、嵌套重复主程序和缺少签名 Manifest 的发布源目录。
|
||||||
+299
@@ -0,0 +1,299 @@
|
|||||||
|
# UpdateClientSDK 客户端部署说明
|
||||||
|
|
||||||
|
本文按“维护者打包 SDK -> 你接入业务软件 -> 联调测试 -> 生成最终客户端包”的顺序说明。你拿到这份文档后,按章节一步一步做即可。
|
||||||
|
|
||||||
|
## 一、这个 SDK 是什么
|
||||||
|
|
||||||
|
UpdateClientSDK 是“独立更新器 SDK / 升级运行时 SDK”。它不是传统的 `include + lib` 形态,而是把自动升级能力做成一组独立程序,让业务软件通过这些程序完成检查更新、下载、安装、回滚和启动保护。
|
||||||
|
|
||||||
|
SDK 核心程序:
|
||||||
|
|
||||||
|
- `Launcher.exe`:用户入口。检查版本、验证授权和策略,决定直接启动业务主程序或进入升级流程。
|
||||||
|
- `Updater.exe`:下载、校验、备份、安装、健康确认、提交或回滚。
|
||||||
|
- `Bootstrap.exe`:处理运行中可能被占用的 EXE/DLL 替换。
|
||||||
|
- `config/app_config.json`:部署配置源文件。启动时会同步到当前 Windows 用户的注册表,运行时优先读注册表。
|
||||||
|
- `config/manifest_public_key.pem`:Manifest 签名公钥,用来验证服务端发布包没有被篡改。
|
||||||
|
|
||||||
|
## 二、维护者:生成 SDK 包
|
||||||
|
|
||||||
|
这一节是 SDK 维护者操作。接入方通常只需要拿到 `UpdateClientSDK.zip`。
|
||||||
|
|
||||||
|
打包前确认:
|
||||||
|
|
||||||
|
1. 已在 Windows 上用 Release 配置编译完成,输出目录里有 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe`。
|
||||||
|
2. `config/manifest_public_key.pem` 和服务端使用的私钥是一对。
|
||||||
|
3. SDK Word 接入说明已经放在 `update-client` 根目录或 `update-client/Docs` 目录下,文件名包含 `SDK`,例如 `SimCAE自动升级SDK接入说明_v0.1.docx`。打包脚本会把它复制到 SDK 根目录,方便接入方一打开压缩包就能看到。
|
||||||
|
4. 如果业务软件本身已经带 Qt DLL,通常不要把 SDK 的 Qt 运行库打进去,避免 Qt 版本混用。
|
||||||
|
|
||||||
|
在 Windows PowerShell 中执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\admin\Desktop\update-client
|
||||||
|
|
||||||
|
.\scripts\package-sdk.ps1 `
|
||||||
|
-SourceDir .\out\bin `
|
||||||
|
-OutputDir .\dist\UpdateClientSDK `
|
||||||
|
-ZipFile .\dist\UpdateClientSDK.zip `
|
||||||
|
-SdkVersion 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
生成结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/
|
||||||
|
UpdateClientSDK/
|
||||||
|
SimCAE自动升级SDK接入说明_v0.1.docx
|
||||||
|
sdk_manifest.json
|
||||||
|
bin/
|
||||||
|
Launcher.exe
|
||||||
|
Updater.exe
|
||||||
|
Bootstrap.exe
|
||||||
|
...
|
||||||
|
config/
|
||||||
|
app_config.json
|
||||||
|
manifest_public_key.pem
|
||||||
|
scripts/
|
||||||
|
install-sdk.ps1
|
||||||
|
package-client.ps1
|
||||||
|
package-sdk.ps1
|
||||||
|
UpdateClientSDK.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
把 `dist/UpdateClientSDK.zip` 发给接入方即可。
|
||||||
|
|
||||||
|
可选参数:
|
||||||
|
|
||||||
|
- `-IncludeDemoMainApp`:把仓库里的 Demo 主程序 `MainApp.exe` 也打进 SDK,方便演示。
|
||||||
|
- `-IncludeQtRuntime`:把 Qt 运行库也打进 SDK。只有业务软件本身不带 Qt 时才建议使用。
|
||||||
|
|
||||||
|
## 三、你:把 SDK 放进业务软件目录
|
||||||
|
|
||||||
|
假设业务软件目录是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D:\SimCAE\
|
||||||
|
bin\
|
||||||
|
SimCAE.exe
|
||||||
|
Qt5Core.dll
|
||||||
|
...
|
||||||
|
Licenses\
|
||||||
|
installerResources\
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐把 SDK 放到 `bin` 目录,和 `SimCAE.exe` 同级;后台发布新版本时仍选择整个 `D:\SimCAE\` 作为发布根目录。
|
||||||
|
|
||||||
|
先解压 SDK:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Expand-Archive D:\交付\UpdateClientSDK.zip -DestinationPath D:\SimCAE_SDK -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
再安装到业务软件的 `bin` 目录:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\SimCAE\bin
|
||||||
|
|
||||||
|
D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
||||||
|
-SdkRoot D:\SimCAE_SDK `
|
||||||
|
-ReleaseDir .
|
||||||
|
```
|
||||||
|
|
||||||
|
安装后目录应类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D:\SimCAE\
|
||||||
|
bin\
|
||||||
|
Launcher.exe
|
||||||
|
Updater.exe
|
||||||
|
Bootstrap.exe
|
||||||
|
SimCAE.exe
|
||||||
|
config\
|
||||||
|
app_config.json
|
||||||
|
manifest_public_key.pem
|
||||||
|
Qt5Core.dll
|
||||||
|
...
|
||||||
|
Licenses\
|
||||||
|
installerResources\
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你需要重新覆盖 `config/app_config.json`,执行安装脚本时加 `-OverwriteConfig`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
||||||
|
-SdkRoot D:\SimCAE_SDK `
|
||||||
|
-ReleaseDir D:\SimCAE\bin `
|
||||||
|
-OverwriteConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应复用 SimCAE 的 `Qt5*.dll`、`platforms/`、`imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则可能出现“无法定位程序输入点”一类错误。
|
||||||
|
|
||||||
|
## 四、你:生成并填写 app_config.json
|
||||||
|
|
||||||
|
最推荐的方式是在服务端管理后台生成客户端配置:
|
||||||
|
|
||||||
|
1. 浏览器打开服务端管理后台,例如 `http://服务器IP:8000/`。
|
||||||
|
2. 登录后台。
|
||||||
|
3. 创建或选择应用,例如 `app_id=simcae`。
|
||||||
|
4. 创建 License。
|
||||||
|
5. 在“客户端配置生成”区域选择应用、渠道、License 和主程序名。
|
||||||
|
6. 点击生成配置。
|
||||||
|
7. 复制生成的 JSON,覆盖 `D:\SimCAE\bin\config\app_config.json`。
|
||||||
|
|
||||||
|
配置同步规则:
|
||||||
|
|
||||||
|
- `app_config.json` 是部署配置源文件,适合交付、复制、人工修改。
|
||||||
|
- Launcher / Updater / MainApp 启动时会计算 `app_config.json` 解析后的 JSON 内容 SHA256;如果 JSON 内容和上次导入时不同,就把文件里的配置重新写入当前 Windows 用户的注册表。
|
||||||
|
- 后续运行时优先从注册表读取配置,不再每次直接读 JSON。
|
||||||
|
- 运行过程中产生的动态值,例如首次输入的 `license_key`、服务端返回的 `device_id`、升级后的 `current_version`,会写入注册表。
|
||||||
|
- 如果你手动修改了 `app_config.json`,下一次启动会重新导入并覆盖注册表里的同名字段。
|
||||||
|
- 如果检测到 `app_config.json` 确实发生变化,SDK 会同时删除 `config/client_identity.dat` 和 `config/version_policy.dat`,避免继续使用旧 License、旧设备身份或旧版本策略;如果安装目录不可写,会弹出管理员权限确认框。
|
||||||
|
- 注册表按安装目录隔离;同一台电脑上多个安装目录不会互相覆盖配置。
|
||||||
|
- 注册表位置为 `HKEY_CURRENT_USER\Software\Marsco\UpdateClientSDK\installations\<安装目录SHA256>\config`。
|
||||||
|
|
||||||
|
关键字段说明:
|
||||||
|
|
||||||
|
- `app_id`:服务端应用 ID,要和管理后台里的应用一致。
|
||||||
|
- `app_name`:应用显示名称。
|
||||||
|
- `channel`:发布渠道,例如 `stable`、`beta`、`dev`。
|
||||||
|
- `current_version`:客户端当前初始版本,必须和后台已发布的版本一致。
|
||||||
|
- `client_protocol`:客户端协议号,当前建议为 `3`。
|
||||||
|
- `launch_token`:本机启动票据 HMAC 密钥。服务端生成配置时会填默认值;正式部署建议按项目统一修改。
|
||||||
|
- `license_key`:管理后台创建 License 后生成的授权码。为空时首次启动 `Launcher.exe` 会弹窗让用户输入并保存到注册表;如果授权错误或过期,也会提示重新输入。
|
||||||
|
- `api_base_url`:服务端 API 地址,例如 `http://192.168.229.128:8000`。
|
||||||
|
- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,必须和服务端一致。
|
||||||
|
- `device_id`:设备 ID。一般可以留空,首次启动时 SDK 会向服务端登记并写入注册表。
|
||||||
|
- `install_root`:被更新的安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`。
|
||||||
|
- `main_executable`:业务主程序相对 `Launcher.exe` 所在目录的路径。SDK 放在 `bin` 且主程序也在 `bin` 时填 `SimCAE.exe`。
|
||||||
|
- `launcher_executable`、`updater_executable`、`bootstrap_executable`:通常不用改。
|
||||||
|
- `platform`、`arch`:当前为 `windows`、`x64`。
|
||||||
|
|
||||||
|
典型配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app_id": "simcae",
|
||||||
|
"app_name": "SimCAE",
|
||||||
|
"channel": "stable",
|
||||||
|
"current_version": "1.0.0",
|
||||||
|
"client_protocol": "3",
|
||||||
|
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
||||||
|
"license_key": "MARSCO-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"api_base_url": "http://192.168.229.128:8000",
|
||||||
|
"client_token": "SimCAEClientToken2026",
|
||||||
|
"request_timeout_ms": "5000",
|
||||||
|
"temp_folder": "update_temp",
|
||||||
|
"device_id": "",
|
||||||
|
"install_root": "..",
|
||||||
|
"main_executable": "SimCAE.exe",
|
||||||
|
"launcher_executable": "Launcher.exe",
|
||||||
|
"updater_executable": "Updater.exe",
|
||||||
|
"bootstrap_executable": "Bootstrap.exe",
|
||||||
|
"health_check_timeout_ms": "15000",
|
||||||
|
"platform": "windows",
|
||||||
|
"arch": "x64"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、你:业务主程序需要配合什么
|
||||||
|
|
||||||
|
当前安全模式下,业务主程序需要配合两件事:
|
||||||
|
|
||||||
|
1. 接收 `--ticket-file=<path>` 参数,验证并消费一次性启动票据。
|
||||||
|
2. 如果收到 `--health-file=<path>` 参数,启动成功后向该路径写入 `ok\n`,让 Updater 确认新版本可用。
|
||||||
|
|
||||||
|
接入位置:
|
||||||
|
|
||||||
|
```text
|
||||||
|
main / WinMain 开头,创建主窗口之前
|
||||||
|
```
|
||||||
|
|
||||||
|
当前仓库里的 `update-client/MainApp/main.cpp` 是接入示例,已经实现:
|
||||||
|
|
||||||
|
- 启动票据校验。
|
||||||
|
- 本地 License/设备身份校验。
|
||||||
|
- 本地策略校验。
|
||||||
|
- Manifest 完整性校验。
|
||||||
|
- 健康标记写入。
|
||||||
|
|
||||||
|
真正接入业务软件时,把这些启动检查逻辑移植到业务主程序。用户入口应改成 `Launcher.exe`,不要让用户直接双击 `SimCAE.exe`。
|
||||||
|
|
||||||
|
## 六、你:准备服务端数据
|
||||||
|
|
||||||
|
首次联调前,服务端至少要准备这些内容:
|
||||||
|
|
||||||
|
1. 创建应用,例如 `simcae`。
|
||||||
|
2. 创建渠道,例如 `stable`。
|
||||||
|
3. 创建 License。
|
||||||
|
4. 发布一个初始版本,例如 `1.0.0`。
|
||||||
|
5. 生成客户端配置,并写入 `bin\config\app_config.json`。客户端下次启动时会自动同步到注册表。
|
||||||
|
|
||||||
|
为什么必须先发布初始版本:Launcher 启动业务主程序前会做 Manifest 完整性校验。这个 Manifest 是服务端发布版本时生成并签名的清单,用来证明当前本地文件属于一个可信版本。如果没有发布过 `current_version` 对应版本,客户端会提示签名 Manifest 缓存缺失。
|
||||||
|
|
||||||
|
后台发布版本时,选择整个安装根目录,例如 `D:\SimCAE\`,不要只选择 `D:\SimCAE\bin`。这样服务端会把 `bin/SimCAE.exe`、`Licenses/`、`installerResources/` 等完整结构写进 Manifest。
|
||||||
|
|
||||||
|
## 七、你:运行和联调
|
||||||
|
|
||||||
|
基础联调步骤:
|
||||||
|
|
||||||
|
1. 确认服务端正在运行。
|
||||||
|
2. 确认 `bin\config\app_config.json` 中 `api_base_url`、`client_token`、`license_key`、`current_version` 正确。
|
||||||
|
3. 双击 `bin\Launcher.exe`。
|
||||||
|
4. 首次启动时如果 `license_key` 为空,按弹窗输入后台创建的 License;SDK 会把它保存到注册表。
|
||||||
|
5. 成功进入业务主程序后,回到后台查看设备、升级日志、下载日志。
|
||||||
|
6. 在后台发布更高版本,例如从 `1.0.0` 发布到 `1.0.1`。
|
||||||
|
7. 再次启动 `Launcher.exe`,验证升级、健康确认和回滚逻辑。
|
||||||
|
|
||||||
|
联调目录建议:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D:\SimCAE_Release\
|
||||||
|
C:\Users\<你的用户名>\Desktop\SimCAE_Release\
|
||||||
|
```
|
||||||
|
|
||||||
|
如果安装在 `C:\Program Files\...` 这类默认不可写目录,普通配置值会写入当前用户注册表,不需要修改 `app_config.json`。但 `client_identity.dat`、`local_state.json`、`version_policy.dat` 等本地状态文件仍位于安装目录下;当这些文件需要写入且目录不可写时,SDK 会弹出 Windows 管理员权限确认框,用户点击“是”后会继续保存。
|
||||||
|
|
||||||
|
注意:这次提权主要覆盖小型配置/状态文件的写入和删除。更新缓存、离线包暂存、升级替换 EXE/DLL 等大文件操作仍建议放在可写目录;如果最终产品必须完整安装到 `C:\Program Files\SimCAE\bin` 并在普通用户下自动升级,后续建议把运行时状态迁移到 `ProgramData` / `AppData`,或让 Updater/Bootstrap 在替换安装目录文件时走管理员权限。
|
||||||
|
|
||||||
|
## 八、维护者:生成某个产品的最终客户端包
|
||||||
|
|
||||||
|
SDK 是给接入方开发使用的。最终给用户安装或分发时,可以从已经联调过的 Release 目录生成最终客户端包。
|
||||||
|
|
||||||
|
在 Windows PowerShell 中执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\admin\Desktop\update-client
|
||||||
|
|
||||||
|
.\scripts\package-client.ps1 `
|
||||||
|
-SourceDir .\out\bin `
|
||||||
|
-ConfigFile .\config\app_config.json `
|
||||||
|
-OutputDir .\dist\UpdateClient `
|
||||||
|
-ZipFile .\dist\UpdateClient.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
`package-client.ps1` 会检查:
|
||||||
|
|
||||||
|
- 配置文件必填字段是否完整。
|
||||||
|
- 主程序、Launcher、Updater、Bootstrap 是否存在。
|
||||||
|
- 是否混入 Debug DLL、PDB、ILK。
|
||||||
|
- 当前版本是否已有签名 Manifest 缓存。
|
||||||
|
- 是否存在重复主程序。
|
||||||
|
|
||||||
|
生成结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/
|
||||||
|
UpdateClient/
|
||||||
|
UpdateClient.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## 九、常见错误
|
||||||
|
|
||||||
|
1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。
|
||||||
|
2. 首次启动保存配置/状态文件失败:如果目录不可写,SDK 会弹出管理员权限确认框;用户取消或当前账号没有管理员权限时仍会失败。
|
||||||
|
3. 首次启动设备登记失败:检查 `api_base_url`、`client_token`、`license_key`、服务端 License 状态。
|
||||||
|
4. 提示 License 错误或过期:在后台确认 License 是否存在、是否被禁用或删除、是否超过最大设备数。
|
||||||
|
5. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配。
|
||||||
|
6. 提示 signed manifest cache missing:先在后台发布一次 `current_version` 对应版本,并让客户端拿到该版本 Manifest。
|
||||||
|
7. 升级后回滚:检查业务程序是否在 `health_check_timeout_ms` 内写入健康标记。
|
||||||
|
8. 发布失败提示主程序不在根目录:服务端 `.env` 中 `RELEASE_MAIN_EXECUTABLE` 默认是 `bin/SimCAE.exe`,发布目录要选择包含 `bin/SimCAE.exe` 的安装根目录。
|
||||||
|
9. 启动时提示 `无法定位程序输入点 ... Qt5*.dll`:通常是 Qt DLL 被不同版本覆盖或混用。恢复业务软件原始 Qt DLL,并重新打包 SDK;SimCAE 场景下不要使用 `-IncludeQtRuntime`。
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# 第三方依赖说明
|
||||||
|
|
||||||
|
`thirdparty/` 是本机依赖目录,已经被 `.gitignore` 忽略,不会提交到 Git。
|
||||||
|
|
||||||
|
当前客户端构建依赖:
|
||||||
|
|
||||||
|
1. Qt 5.15.2 msvc2019_64
|
||||||
|
2. OpenSSL-Win64
|
||||||
|
|
||||||
|
## 1. Qt 配置
|
||||||
|
|
||||||
|
Qt 路径由本机环境变量提供。你需要在 Windows 环境变量里配置 Qt 路径,让 CMake 的 `find_package(Qt5 ...)` 能找到 Qt。
|
||||||
|
|
||||||
|
推荐配置用户环境变量 `CMAKE_PREFIX_PATH`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
[Environment]::SetEnvironmentVariable("CMAKE_PREFIX_PATH", "C:\Qt\5.15.2\msvc2019_64", "User")
|
||||||
|
```
|
||||||
|
|
||||||
|
设置完成后,重新打开 PowerShell 或 Visual Studio。
|
||||||
|
|
||||||
|
如果只想对当前 PowerShell 窗口临时生效:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:CMAKE_PREFIX_PATH="C:\Qt\5.15.2\msvc2019_64"
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以配置更精确的 `Qt5_DIR`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
[Environment]::SetEnvironmentVariable("Qt5_DIR", "C:\Qt\5.15.2\msvc2019_64\lib\cmake\Qt5", "User")
|
||||||
|
```
|
||||||
|
|
||||||
|
`CMAKE_PREFIX_PATH` 和 `Qt5_DIR` 二选一即可,推荐使用 `CMAKE_PREFIX_PATH`。
|
||||||
|
|
||||||
|
一般不需要把 `C:\Qt\5.15.2\msvc2019_64\bin` 加入 `Path`。项目构建后会通过 `windeployqt` 复制运行所需的 Qt DLL。
|
||||||
|
|
||||||
|
## 2. OpenSSL 配置
|
||||||
|
|
||||||
|
OpenSSL 默认放在:
|
||||||
|
|
||||||
|
```text
|
||||||
|
thirdparty/OpenSSL-Win64
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐目录结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
thirdparty/
|
||||||
|
OpenSSL-Win64/
|
||||||
|
include/
|
||||||
|
openssl/
|
||||||
|
lib/
|
||||||
|
VC/
|
||||||
|
x64/
|
||||||
|
MD/
|
||||||
|
MDd/
|
||||||
|
```
|
||||||
|
|
||||||
|
复制命令示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\admin\Desktop\update-client
|
||||||
|
mkdir thirdparty
|
||||||
|
Copy-Item "C:\Program Files\OpenSSL-Win64" ".\thirdparty\OpenSSL-Win64" -Recurse
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 OpenSSL 不放在 `thirdparty/`,可以在配置 CMake 时手动指定:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake -S . -B out\build\x64-Debug `
|
||||||
|
-G "Visual Studio 17 2022" `
|
||||||
|
-A x64 `
|
||||||
|
-DSIMCAE_OPENSSL_ROOT="C:\Program Files\OpenSSL-Win64"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 重新配置和编译
|
||||||
|
|
||||||
|
如果之前配置过 CMake,建议先删除旧缓存:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\admin\Desktop\update-client
|
||||||
|
Remove-Item out\build -Recurse -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
重新配置:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake -S . -B out\build\x64-Debug `
|
||||||
|
-G "Visual Studio 17 2022" `
|
||||||
|
-A x64
|
||||||
|
```
|
||||||
|
|
||||||
|
编译:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --build out\build\x64-Debug --config Debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 提交注意事项
|
||||||
|
|
||||||
|
不要提交下面这些内容:
|
||||||
|
|
||||||
|
```text
|
||||||
|
thirdparty/
|
||||||
|
.vs/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
App/
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.lib
|
||||||
|
*.pdb
|
||||||
|
```
|
||||||
|
|
||||||
|
这些都属于本机依赖、构建产物或打包产物,不应该进 Git。
|
||||||
@@ -8,14 +8,13 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
|||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
# 依赖Qt模块:网络、基础核心、窗口
|
|
||||||
set(CMAKE_PREFIX_PATH "C:\\Qt\\5.15.2\\msvc2019_64" ${CMAKE_PREFIX_PATH})
|
|
||||||
find_package(Qt5 REQUIRED COMPONENTS Core Network Gui Widgets)
|
|
||||||
# 所有源码文件
|
# 所有源码文件
|
||||||
set(SRC_LIST
|
set(SRC_LIST
|
||||||
main.cpp
|
main.cpp
|
||||||
UpdateLogic.h
|
UpdateLogic.h
|
||||||
UpdateLogic.cpp
|
UpdateLogic.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
)
|
)
|
||||||
# 生成可执行程序
|
# 生成可执行程序
|
||||||
add_executable(Launcher ${SRC_LIST})
|
add_executable(Launcher ${SRC_LIST})
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QSaveFile>
|
||||||
#include "PolicyHelper.h"
|
#include "PolicyHelper.h"
|
||||||
#include "LocalStateHelper.h"
|
#include "LocalStateHelper.h"
|
||||||
|
|
||||||
@@ -99,6 +101,65 @@ void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, c
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, const QString& version,
|
||||||
|
int versionId, const QString& cacheDir)
|
||||||
|
{
|
||||||
|
m_error.clear();
|
||||||
|
if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) {
|
||||||
|
m_error = "manifest identity is incomplete";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject response;
|
||||||
|
int statusCode = 0;
|
||||||
|
QJsonObject body;
|
||||||
|
body["app_id"] = appId;
|
||||||
|
body["channel"] = channel;
|
||||||
|
body["version"] = version;
|
||||||
|
body["version_id"] = versionId;
|
||||||
|
m_http.postRequest(m_serverAddr + "/api/v1/update/manifest", body,
|
||||||
|
[&](int code, const QJsonObject& resp) {
|
||||||
|
statusCode = code;
|
||||||
|
response = resp;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (statusCode != 200) {
|
||||||
|
m_error = QString("manifest request failed (HTTP %1)").arg(statusCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject manifest = response.value("manifest").toObject();
|
||||||
|
const QString manifestText = response.value("manifest_text").toString();
|
||||||
|
if (manifest.isEmpty() || manifestText.isEmpty()) {
|
||||||
|
m_error = "manifest response is incomplete";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (manifest.value("app_id").toString() != appId
|
||||||
|
|| manifest.value("channel").toString() != channel
|
||||||
|
|| manifest.value("version").toString() != version) {
|
||||||
|
m_error = "manifest identity does not match current version";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(cacheDir);
|
||||||
|
if (!dir.exists() && !dir.mkpath(".")) {
|
||||||
|
m_error = "cannot create manifest cache directory";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject wrapper;
|
||||||
|
wrapper["manifest"] = manifest;
|
||||||
|
wrapper["manifest_text"] = manifestText;
|
||||||
|
QSaveFile file(dir.filePath("manifest_" + version + ".json"));
|
||||||
|
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented);
|
||||||
|
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||||
|
m_error = "cannot save signed manifest cache";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
qDebug() << "Current version manifest cached to" << file.fileName();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void UpdateLogic::reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success)
|
void UpdateLogic::reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success)
|
||||||
{
|
{
|
||||||
QString url = m_serverAddr + "/api/v1/update/report";
|
QString url = m_serverAddr + "/api/v1/update/report";
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ public:
|
|||||||
void checkUpdate();
|
void checkUpdate();
|
||||||
void getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId);
|
void getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId);
|
||||||
void reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success);
|
void reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success);
|
||||||
|
bool cacheManifest(const QString& appId, const QString& channel, const QString& version,
|
||||||
|
int versionId, const QString& cacheDir);
|
||||||
|
|
||||||
bool getNeedUpdate() const { return m_needUpdate; }
|
bool getNeedUpdate() const { return m_needUpdate; }
|
||||||
QString getLatestVersion() const { return m_latestVer; }
|
QString getLatestVersion() const { return m_latestVer; }
|
||||||
QJsonObject getCheckResult() const { return m_checkResp; }
|
QJsonObject getCheckResult() const { return m_checkResp; }
|
||||||
bool isNetworkOk() const { return m_networkOk; }
|
bool isNetworkOk() const { return m_networkOk; }
|
||||||
int lastStatusCode() const { return m_lastStatusCode; }
|
int lastStatusCode() const { return m_lastStatusCode; }
|
||||||
|
QString errorString() const { return m_error; }
|
||||||
|
|
||||||
QString getAppId() const { return m_appId; }
|
QString getAppId() const { return m_appId; }
|
||||||
QString getChannel() const { return m_channel; }
|
QString getChannel() const { return m_channel; }
|
||||||
@@ -35,4 +38,5 @@ private:
|
|||||||
int m_lastStatusCode = 0;
|
int m_lastStatusCode = 0;
|
||||||
QString m_latestVer;
|
QString m_latestVer;
|
||||||
QJsonObject m_checkResp;
|
QJsonObject m_checkResp;
|
||||||
|
QString m_error;
|
||||||
};
|
};
|
||||||
+117
-49
@@ -1,12 +1,12 @@
|
|||||||
#include <windows.h>
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QProgressDialog>
|
#include <QProgressDialog>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QTextCodec>
|
#include <QTranslator>
|
||||||
#include "UpdateLogic.h"
|
#include "UpdateLogic.h"
|
||||||
#include "../Common/ConfigHelper.h"
|
#include "../Common/ConfigHelper.h"
|
||||||
#include "../Common/PolicyHelper.h"
|
#include "../Common/PolicyHelper.h"
|
||||||
@@ -19,13 +19,18 @@
|
|||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
SetConsoleOutputCP(65001);
|
|
||||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
QApplication::setApplicationName("Marsco Launcher");
|
QApplication::setApplicationName("Marsco Launcher");
|
||||||
|
QTranslator translator;
|
||||||
|
if (translator.load(":/i18n/update-client_zh_CN.qm"))
|
||||||
|
app.installTranslator(&translator);
|
||||||
|
|
||||||
QProgressDialog progress("正在检查软件更新...", QString(), 0, 0);
|
const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested();
|
||||||
progress.setWindowTitle("Marsco 软件启动器");
|
if (elevatedWriteExitCode >= 0)
|
||||||
|
return elevatedWriteExitCode;
|
||||||
|
|
||||||
|
QProgressDialog progress(QCoreApplication::translate("Launcher", "Checking for software updates..."), QString(), 0, 0);
|
||||||
|
progress.setWindowTitle(QCoreApplication::translate("Launcher", "Marsco Launcher"));
|
||||||
progress.setCancelButton(nullptr);
|
progress.setCancelButton(nullptr);
|
||||||
progress.setWindowModality(Qt::ApplicationModal);
|
progress.setWindowModality(Qt::ApplicationModal);
|
||||||
progress.setMinimumDuration(0);
|
progress.setMinimumDuration(0);
|
||||||
@@ -38,13 +43,13 @@ int main(int argc, char* argv[])
|
|||||||
const auto isLicenseError = [](const QString& errorText) {
|
const auto isLicenseError = [](const QString& errorText) {
|
||||||
const QString text = errorText.toLower();
|
const QString text = errorText.toLower();
|
||||||
return text.contains("license")
|
return text.contains("license")
|
||||||
|| errorText.contains("授权")
|
|| text.contains("authorization")
|
||||||
|| errorText.contains("过期")
|
|| text.contains("expired")
|
||||||
|| errorText.contains("设备数量已达到上限")
|
|| text.contains("device limit")
|
||||||
|| errorText.contains("已绑定其他授权");
|
|| text.contains("bound to another license");
|
||||||
};
|
};
|
||||||
const auto clearDeviceCredential = [&]() {
|
const auto clearDeviceCredential = [&]() {
|
||||||
QFile::remove(QDir(appDir).filePath("config/client_identity.dat"));
|
ConfigHelper::removeFileWithElevationIfNeeded(QDir(appDir).filePath("config/client_identity.dat"));
|
||||||
config.setValue("Update", "device_id", QString());
|
config.setValue("Update", "device_id", QString());
|
||||||
};
|
};
|
||||||
QString licenseKey = config.getValue("License", "license_key").trimmed();
|
QString licenseKey = config.getValue("License", "license_key").trimmed();
|
||||||
@@ -55,32 +60,39 @@ int main(int argc, char* argv[])
|
|||||||
bool accepted = false;
|
bool accepted = false;
|
||||||
const QString appName = config.getValue("App", "app_name").trimmed();
|
const QString appName = config.getValue("App", "app_name").trimmed();
|
||||||
const QString prompt = promptReason.trimmed().isEmpty()
|
const QString prompt = promptReason.trimmed().isEmpty()
|
||||||
? QString("请输入%1授权 License:").arg(appName.isEmpty() ? "软件" : appName)
|
? QCoreApplication::translate("Launcher", "Please enter the License for %1:")
|
||||||
: QString("%1\n\n请重新输入%2授权 License:").arg(promptReason, appName.isEmpty() ? "软件" : appName);
|
.arg(appName.isEmpty() ? QCoreApplication::translate("Launcher", "the application") : appName)
|
||||||
|
: QCoreApplication::translate("Launcher", "%1\n\nPlease enter a new License for %2:")
|
||||||
|
.arg(promptReason, appName.isEmpty() ? QCoreApplication::translate("Launcher", "the application") : appName);
|
||||||
licenseKey = QInputDialog::getText(
|
licenseKey = QInputDialog::getText(
|
||||||
nullptr,
|
nullptr,
|
||||||
"输入 License",
|
QCoreApplication::translate("Launcher", "Enter License"),
|
||||||
prompt,
|
prompt,
|
||||||
QLineEdit::Normal,
|
QLineEdit::Normal,
|
||||||
QString(),
|
QString(),
|
||||||
&accepted
|
&accepted
|
||||||
).trimmed();
|
).trimmed();
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
QMessageBox::information(nullptr, "需要 License", "首次启动需要输入管理员提供的 License。");
|
QMessageBox::information(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "License Required"),
|
||||||
|
QCoreApplication::translate("Launcher", "A License provided by the administrator is required for the first launch."));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
if (licenseKey.isEmpty()) {
|
if (licenseKey.isEmpty()) {
|
||||||
QMessageBox::warning(nullptr, "License 不能为空", "请粘贴管理员在后台创建的 License。");
|
QMessageBox::warning(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "License Cannot Be Empty"),
|
||||||
|
QCoreApplication::translate("Launcher", "Please paste the License created in the admin page."));
|
||||||
promptReason.clear();
|
promptReason.clear();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!config.setValue("License", "license_key", licenseKey)) {
|
if (!config.setValue("License", "license_key", licenseKey)) {
|
||||||
QMessageBox::critical(nullptr, "保存 License 失败",
|
QMessageBox::critical(nullptr,
|
||||||
QString("无法写入配置文件:%1\n%2").arg(config.configPath(), config.lastError()));
|
QCoreApplication::translate("Launcher", "Failed to Save License"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot write the configuration file:\n%1\n%2").arg(config.configPath(), config.lastError()));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
progress.show();
|
progress.show();
|
||||||
progress.setLabelText("正在验证 License...");
|
progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying License..."));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
return licenseKey;
|
return licenseKey;
|
||||||
}
|
}
|
||||||
@@ -102,11 +114,13 @@ int main(int argc, char* argv[])
|
|||||||
const QString error = identity.errorString();
|
const QString error = identity.errorString();
|
||||||
if (!isLicenseError(error)) {
|
if (!isLicenseError(error)) {
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "设备身份验证失败", error);
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Device Authentication Failed"),
|
||||||
|
error);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
clearDeviceCredential();
|
clearDeviceCredential();
|
||||||
licenseKey = promptAndSaveLicense(QString("当前 License 无法使用:%1").arg(error));
|
licenseKey = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current License cannot be used: %1").arg(error));
|
||||||
if (licenseKey.isEmpty()) return 0;
|
if (licenseKey.isEmpty()) return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,27 +134,33 @@ int main(int argc, char* argv[])
|
|||||||
const int targetVersionId = response.value("version_id").toInt();
|
const int targetVersionId = response.value("version_id").toInt();
|
||||||
const QString appId = logic.getAppId();
|
const QString appId = logic.getAppId();
|
||||||
const QString channel = logic.getChannel();
|
const QString channel = logic.getChannel();
|
||||||
|
const QString launchToken = config.getValue("App", "launch_token");
|
||||||
|
const QString currentVersion = config.getValue("App", "current_version");
|
||||||
|
|
||||||
if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403) {
|
if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403) {
|
||||||
progress.close();
|
progress.close();
|
||||||
const QJsonValue detail = response.value("detail");
|
const QJsonValue detail = response.value("detail");
|
||||||
const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString();
|
const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString();
|
||||||
const QString displayMessage = message.isEmpty() ? "设备或 License 授权无效。" : message;
|
const QString displayMessage = message.isEmpty()
|
||||||
|
? QCoreApplication::translate("Launcher", "The device or License authorization is invalid.")
|
||||||
|
: message;
|
||||||
if (isLicenseError(displayMessage)) {
|
if (isLicenseError(displayMessage)) {
|
||||||
clearDeviceCredential();
|
clearDeviceCredential();
|
||||||
const QString newLicense = promptAndSaveLicense(QString("当前授权被服务端拒绝:%1").arg(displayMessage));
|
const QString newLicense = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current authorization was rejected by the server: %1").arg(displayMessage));
|
||||||
if (!newLicense.isEmpty()) {
|
if (!newLicense.isEmpty()) {
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::information(nullptr, "License 已保存", "请重新启动 Launcher 完成设备授权和更新检查。");
|
QMessageBox::information(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "License Saved"),
|
||||||
|
QCoreApplication::translate("Launcher", "Please restart Launcher to complete device authorization and update checking."));
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
QMessageBox::critical(nullptr, "授权被拒绝", displayMessage);
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Authorization Rejected"),
|
||||||
|
displayMessage);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString launchToken = config.getValue("App", "launch_token");
|
|
||||||
const QString currentVersion = config.getValue("App", "current_version");
|
|
||||||
const auto configuredName = [&](const QString& key, const QString& fallback) {
|
const auto configuredName = [&](const QString& key, const QString& fallback) {
|
||||||
const QString value = config.getValue("Runtime", key).trimmed();
|
const QString value = config.getValue("Runtime", key).trimmed();
|
||||||
return value.isEmpty() ? fallback : value;
|
return value.isEmpty() ? fallback : value;
|
||||||
@@ -150,13 +170,19 @@ int main(int argc, char* argv[])
|
|||||||
const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
|
const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
|
||||||
const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
|
const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
|
||||||
const auto importOfflinePackage = [&]() {
|
const auto importOfflinePackage = [&]() {
|
||||||
const QString package = QFileDialog::getOpenFileName(nullptr, "选择离线更新包", QString(), "Marsco 离线更新包 (*.upd)");
|
const QString package = QFileDialog::getOpenFileName(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Select Offline Update Package"),
|
||||||
|
QString(),
|
||||||
|
QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)"));
|
||||||
return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)});
|
return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)});
|
||||||
};
|
};
|
||||||
const QString deviceId = config.getValue("Update", "device_id");
|
const QString deviceId = config.getValue("Update", "device_id");
|
||||||
if (QCoreApplication::arguments().contains("--import-offline")) {
|
if (QCoreApplication::arguments().contains("--import-offline")) {
|
||||||
progress.close();
|
progress.close();
|
||||||
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包。");
|
if (!importOfflinePackage())
|
||||||
|
QMessageBox::information(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Offline Update"),
|
||||||
|
QCoreApplication::translate("Launcher", "No offline update package was selected."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,27 +200,32 @@ int main(int argc, char* argv[])
|
|||||||
return started;
|
return started;
|
||||||
};
|
};
|
||||||
|
|
||||||
progress.setLabelText("正在验证本地运行策略...");
|
progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying local runtime policy..."));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
PolicyHelper policy(appDir);
|
PolicyHelper policy(appDir);
|
||||||
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
|
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "无法启动", QString("本地版本策略无效:%1").arg(policy.errorString()));
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot Start"),
|
||||||
|
QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString()));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (policy.isExpired())
|
if (policy.isExpired())
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "无法启动", "本地版本策略已过期,请连接网络或联系管理员。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot Start"),
|
||||||
|
QCoreApplication::translate("Launcher", "The local version policy has expired. Please connect to the network or contact the administrator."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate))
|
if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate))
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "当前版本不可运行",
|
QMessageBox::critical(nullptr,
|
||||||
policy.message().isEmpty() ? QString("当前版本 %1 已被管理员停用。").arg(currentVersion)
|
QCoreApplication::translate("Launcher", "Current Version Cannot Run"),
|
||||||
|
policy.message().isEmpty() ? QCoreApplication::translate("Launcher", "The current version %1 has been disabled by the administrator.").arg(currentVersion)
|
||||||
: policy.message());
|
: policy.message());
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -203,19 +234,25 @@ int main(int argc, char* argv[])
|
|||||||
if (!state.loadState())
|
if (!state.loadState())
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "无法启动", QString("无法读取本地状态:%1").arg(state.errorString()));
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot Start"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot read the local state: %1").arg(state.errorString()));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (state.isPolicySeqRolledBack(policy.policySeq()))
|
if (state.isPolicySeqRolledBack(policy.policySeq()))
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "安全检查失败", "检测到版本策略序列回退,已阻止启动。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Security Check Failed"),
|
||||||
|
QCoreApplication::translate("Launcher", "A version policy sequence rollback was detected. Startup has been blocked."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (state.isSystemTimeRewound())
|
if (state.isSystemTimeRewound())
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "安全检查失败", "检测到系统时间可能被回拨,已阻止启动。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Security Check Failed"),
|
||||||
|
QCoreApplication::translate("Launcher", "A possible system time rollback was detected. Startup has been blocked."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,12 +260,13 @@ int main(int argc, char* argv[])
|
|||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
|
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
|
||||||
const QString dialogTitle = rollbackOperation ? "版本回退"
|
const QString dialogTitle = rollbackOperation ? QCoreApplication::translate("Launcher", "Version Rollback")
|
||||||
: (policy.forceUpdate() ? "必须更新" : "发现新版本");
|
: (policy.forceUpdate() ? QCoreApplication::translate("Launcher", "Update Required") : QCoreApplication::translate("Launcher", "New Version Available"));
|
||||||
const QString prompt = policy.message().isEmpty()
|
const QString prompt = policy.message().isEmpty()
|
||||||
? QString(rollbackOperation ? "管理员提供了版本 %1 作为回退目标,是否现在降级?"
|
? (rollbackOperation
|
||||||
: "发现新版本 %1,是否现在更新?").arg(latestVer)
|
? QCoreApplication::translate("Launcher", "The administrator provided version %1 as the rollback target. Downgrade now?").arg(latestVer)
|
||||||
: policy.message() + QString("\n目标版本:%1").arg(latestVer);
|
: QCoreApplication::translate("Launcher", "Version %1 is available. Update now?").arg(latestVer))
|
||||||
|
: policy.message() + QCoreApplication::translate("Launcher", "\nTarget version: %1").arg(latestVer);
|
||||||
bool accepted = true;
|
bool accepted = true;
|
||||||
if (policy.forceUpdate()) {
|
if (policy.forceUpdate()) {
|
||||||
QMessageBox::information(nullptr, dialogTitle, prompt);
|
QMessageBox::information(nullptr, dialogTitle, prompt);
|
||||||
@@ -238,7 +276,9 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
if (!startMainApp()) {
|
if (!startMainApp()) {
|
||||||
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Startup Failed"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -246,17 +286,39 @@ int main(int argc, char* argv[])
|
|||||||
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
|
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
|
||||||
if (!QProcess::startDetached(updaterPath, updaterArgs))
|
if (!QProcess::startDetached(updaterPath, updaterArgs))
|
||||||
{
|
{
|
||||||
QMessageBox::critical(nullptr, "更新器启动失败", QString("无法启动更新器:%1").arg(updaterPath));
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (networkOk && !needUpdate && targetVersionId > 0 && latestVer == currentVersion)
|
||||||
|
{
|
||||||
|
progress.setLabelText(QCoreApplication::translate("Launcher", "Caching signed version manifest..."));
|
||||||
|
QApplication::processEvents();
|
||||||
|
const QString manifestCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("manifest_cache");
|
||||||
|
if (!logic.cacheManifest(appId, channel, currentVersion, targetVersionId, manifestCacheDir))
|
||||||
|
{
|
||||||
|
progress.close();
|
||||||
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Manifest Cache Failed"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot cache the signed manifest for the current version: %1").arg(logic.errorString()));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!networkOk) {
|
if (!networkOk) {
|
||||||
progress.close();
|
progress.close();
|
||||||
if (QMessageBox::question(nullptr, "服务器不可用", "当前无法连接更新服务器。是否导入离线更新包?",
|
if (QMessageBox::question(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Server Unavailable"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot connect to the update server. Import an offline update package?"),
|
||||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
|
||||||
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包或无法启动更新器。");
|
if (!importOfflinePackage())
|
||||||
|
QMessageBox::information(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Offline Update"),
|
||||||
|
QCoreApplication::translate("Launcher", "No offline update package was selected, or the updater could not be started."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
progress.show();
|
progress.show();
|
||||||
@@ -265,16 +327,22 @@ int main(int argc, char* argv[])
|
|||||||
if (!networkOk && !policy.isOfflineAllowed())
|
if (!networkOk && !policy.isOfflineAllowed())
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "网络不可用", "无法连接更新服务器,且当前策略不允许离线启动。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Network Unavailable"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot connect to the update server, and the current policy does not allow offline startup."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.setLabelText(networkOk ? "当前已是最新版本,正在启动..." : "当前处于离线模式,正在启动...");
|
progress.setLabelText(networkOk
|
||||||
|
? QCoreApplication::translate("Launcher", "The application is up to date. Starting...")
|
||||||
|
: QCoreApplication::translate("Launcher", "Offline mode is active. Starting..."));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
if (!startMainApp())
|
if (!startMainApp())
|
||||||
{
|
{
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Launcher", "Startup Failed"),
|
||||||
|
QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
progress.close();
|
progress.close();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ add_executable(MainApp
|
|||||||
MainWindow.h
|
MainWindow.h
|
||||||
MainWindow.cpp
|
MainWindow.cpp
|
||||||
../Common/ConfigHelper.h
|
../Common/ConfigHelper.h
|
||||||
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
)
|
)
|
||||||
target_include_directories(MainApp
|
target_include_directories(MainApp
|
||||||
PUBLIC ${CMAKE_SOURCE_DIR}/Common
|
PUBLIC ${CMAKE_SOURCE_DIR}/Common
|
||||||
|
|||||||
+9
-6
@@ -1,11 +1,10 @@
|
|||||||
#include <Windows.h>
|
#include <QApplication>
|
||||||
#include <QApplication>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QTextCodec>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
|
#include <QTranslator>
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include "../Common/ConfigHelper.h"
|
#include "../Common/ConfigHelper.h"
|
||||||
#include "../Common/PolicyHelper.h"
|
#include "../Common/PolicyHelper.h"
|
||||||
@@ -16,10 +15,14 @@
|
|||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
SetConsoleOutputCP(936);
|
|
||||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));
|
|
||||||
|
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
|
QTranslator translator;
|
||||||
|
if (translator.load(":/i18n/update-client_zh_CN.qm"))
|
||||||
|
a.installTranslator(&translator);
|
||||||
|
|
||||||
|
const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested();
|
||||||
|
if (elevatedWriteExitCode >= 0)
|
||||||
|
return elevatedWriteExitCode;
|
||||||
|
|
||||||
qDebug() << Qt::endl << "entered main app" << Qt::endl;
|
qDebug() << Qt::endl << "entered main app" << Qt::endl;
|
||||||
|
|
||||||
|
|||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
客户端配置说明
|
|
||||||
==============
|
|
||||||
|
|
||||||
统一从程序目录下的 config/app_config.json 读取配置。
|
|
||||||
首次运行时如果该文件不存在且发现旧 client.ini,会自动迁移。
|
|
||||||
|
|
||||||
接入新软件时通常需要修改:
|
|
||||||
|
|
||||||
1. app_id、app_name、channel、current_version。
|
|
||||||
2. api_base_url、client_token、license_key、launch_token。
|
|
||||||
3. main_executable:团队业务主程序文件名。
|
|
||||||
4. launcher_executable、updater_executable、bootstrap_executable。
|
|
||||||
5. health_check_timeout_ms:升级后等待业务程序健康确认的毫秒数,最小 1000。
|
|
||||||
|
|
||||||
完整格式参考 config/app_config.example.json。
|
|
||||||
运行时生成的 app_config.json、client_identity.dat、local_state.json 等文件不得打入通用 SDK 模板。
|
|
||||||
|
|
||||||
Windows 发布打包:
|
|
||||||
|
|
||||||
1. 使用 Release 配置编译全部客户端程序。
|
|
||||||
2. 先完成当前版本在线校验,确认 out/bin/update/manifest_cache 中存在对应的签名 Manifest。
|
|
||||||
3. 准备一份实际 app_config.json,确认其中包含正确的 License Key、当前版本和业务程序名。
|
|
||||||
4. 在 PowerShell 执行:
|
|
||||||
powershell -ExecutionPolicy Bypass -File .\package-client.ps1 -ConfigFile .\config\app_config.json
|
|
||||||
5. 输出位于 dist/UpdateClient 和 dist/UpdateClient.zip。
|
|
||||||
|
|
||||||
脚本会拒绝 Debug DLL、PDB、嵌套重复主程序和缺少签名 Manifest 的发布源目录。
|
|
||||||
-188
@@ -1,188 +0,0 @@
|
|||||||
# UpdateClientSDK 接入说明
|
|
||||||
|
|
||||||
这个 SDK 是“独立更新器 SDK / 升级运行时 SDK”。它不是传统的 `include + lib` 形态,而是把自动升级能力作为一组独立程序交给业务软件使用。
|
|
||||||
|
|
||||||
SDK 核心程序:
|
|
||||||
|
|
||||||
- `Launcher.exe`:用户入口。检查版本、验证策略,决定启动业务主程序或启动 Updater。
|
|
||||||
- `Updater.exe`:下载、校验、备份、安装、健康确认、提交或回滚。
|
|
||||||
- `Bootstrap.exe`:替换运行中可能被占用的 EXE/DLL。
|
|
||||||
- `config/app_config.json`:接入方配置。
|
|
||||||
- `config/manifest_public_key.pem`:验证服务端签名用的公钥。
|
|
||||||
|
|
||||||
## 接入方需要做什么
|
|
||||||
|
|
||||||
假设接入的软件叫 `YourApp.exe`。
|
|
||||||
|
|
||||||
1. 在服务端管理后台创建应用,例如 `app_id=your_app_id`。
|
|
||||||
2. 创建或确认渠道,例如 `stable`。
|
|
||||||
3. 创建 License,把生成的 `license_key` 填到客户端配置。
|
|
||||||
4. 把业务软件完整安装目录作为发布根目录,例如 `SimCAE\`,其中主程序位于 `bin\SimCAE.exe`。
|
|
||||||
5. 把 SDK 的 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe` 放到 `SimCAE\bin\` 目录,和 `SimCAE.exe` 同级。不要覆盖 SimCAE 自带的 `Qt5*.dll` 和 Qt 插件目录。
|
|
||||||
6. 把 `config/app_config.example.json` 复制成 `SimCAE\bin\config\app_config.json` 并修改字段。
|
|
||||||
7. 用户入口改成 `Launcher.exe`,不要直接双击业务主程序。
|
|
||||||
8. 在管理后台发布新版本时,选择包含业务主程序和 SDK 运行时的干净 Release 根目录。
|
|
||||||
|
|
||||||
## 安装目录写权限要求
|
|
||||||
|
|
||||||
当前 SDK 会在 `Launcher.exe` 所在目录下写入运行时状态文件。SDK 放在 `SimCAE\bin` 时,这些文件实际位于 `SimCAE\bin` 下,例如:
|
|
||||||
|
|
||||||
```text
|
|
||||||
config/app_config.json
|
|
||||||
config/client_identity.dat
|
|
||||||
config/local_state.json
|
|
||||||
config/version_policy.dat
|
|
||||||
update/
|
|
||||||
update_temp/
|
|
||||||
```
|
|
||||||
|
|
||||||
所以联调和普通运行时,`Launcher.exe` 所在目录必须允许当前 Windows 用户写入。不要直接把联调目录放在 `C:\Program Files\...` 后用普通用户启动;该目录默认禁止普通程序写文件,会导致首次启动报错,例如 `cannot save installation id`。
|
|
||||||
|
|
||||||
推荐联调目录:
|
|
||||||
|
|
||||||
```text
|
|
||||||
D:\SimCAE_Release\
|
|
||||||
C:\Users\<你的用户名>\Desktop\SimCAE_Release\
|
|
||||||
```
|
|
||||||
|
|
||||||
如果最终产品必须安装到 `C:\Program Files\SimCAE\bin`,需要额外设计管理员提权、Windows 服务,或把运行时状态迁移到 `ProgramData` / `AppData`。当前交付版本默认按“安装目录可写”的模式工作。
|
|
||||||
|
|
||||||
## SimCAE 目录结构建议
|
|
||||||
|
|
||||||
SimCAE 当前安装目录是根目录下有 `bin/`、`Licenses/`、`installerResources/` 等子目录。SDK 推荐放在 `bin/` 目录,和 `SimCAE.exe` 同级;后台发布时仍选择整个安装根目录:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SimCAE/
|
|
||||||
bin/
|
|
||||||
Launcher.exe
|
|
||||||
Updater.exe
|
|
||||||
Bootstrap.exe
|
|
||||||
SimCAE.exe
|
|
||||||
config/
|
|
||||||
app_config.json
|
|
||||||
manifest_public_key.pem
|
|
||||||
update/
|
|
||||||
manifest_cache/
|
|
||||||
Qt5Core.dll
|
|
||||||
...
|
|
||||||
Licenses/
|
|
||||||
installerResources/
|
|
||||||
maintenancetool.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
这种模式下,后台“发布新版本”时选择整个 `SimCAE/` 目录,服务端会检查 `bin/SimCAE.exe` 是否存在,并把整个安装结构写入 Manifest。客户端配置里 `install_root` 填 `..`,表示被更新的安装根目录是 `bin` 的上一级;升级事务、下载缓存和 Manifest 缓存仍放在 `SimCAE\bin\update`。
|
|
||||||
|
|
||||||
注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应使用和 SimCAE 兼容的 Qt 编译,并复用 SimCAE 的 `Qt5*.dll`、`platforms/`、`imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则会出现“无法定位程序输入点”一类错误。
|
|
||||||
|
|
||||||
## app_config.json 关键字段
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"app_id": "simcae",
|
|
||||||
"app_name": "SimCAE",
|
|
||||||
"channel": "stable",
|
|
||||||
"current_version": "1.0.0",
|
|
||||||
"client_protocol": "3",
|
|
||||||
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
|
||||||
"license_key": "",
|
|
||||||
"api_base_url": "http://YOUR_SERVER_IP:8000",
|
|
||||||
"client_token": "SimCAEClientToken2026",
|
|
||||||
"request_timeout_ms": "5000",
|
|
||||||
"temp_folder": "update_temp",
|
|
||||||
"device_id": "",
|
|
||||||
"install_root": "..",
|
|
||||||
"main_executable": "SimCAE.exe",
|
|
||||||
"launcher_executable": "Launcher.exe",
|
|
||||||
"updater_executable": "Updater.exe",
|
|
||||||
"bootstrap_executable": "Bootstrap.exe",
|
|
||||||
"health_check_timeout_ms": "15000",
|
|
||||||
"platform": "windows",
|
|
||||||
"arch": "x64"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
字段说明:
|
|
||||||
|
|
||||||
- `app_id`:服务端应用 ID,默认填 `simcae`;如果后台创建了别的 App ID,这里同步修改。
|
|
||||||
- `channel`:发布渠道,例如 `stable`、`beta`、`dev`。
|
|
||||||
- `current_version`:当前客户端初始版本。
|
|
||||||
- `client_protocol`:客户端协议号,当前建议为 `3`。
|
|
||||||
- `api_base_url`:服务端 API 地址,例如 `http://192.168.229.128:8000`;服务器 IP 无法提前知道,所以这里需要按现场地址修改。
|
|
||||||
- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,默认交付包已填 `SimCAEClientToken2026`。
|
|
||||||
- `license_key`:管理后台创建 License 后返回的密钥;模板里先留空,创建授权后再填。
|
|
||||||
- `launch_token`:本机启动票据 HMAC 密钥,模板已给默认值,可试跑;正式交付建议改成你自己的 32 字符以上随机字符串。
|
|
||||||
- `install_root`:安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`。
|
|
||||||
- `main_executable`:业务主程序相对 `Launcher.exe` 所在目录的路径,SimCAE 默认填 `SimCAE.exe`。
|
|
||||||
- `health_check_timeout_ms`:升级后等待业务程序写健康标记的时间。
|
|
||||||
|
|
||||||
## 业务主程序需要配合什么
|
|
||||||
|
|
||||||
当前安全模式下,业务主程序需要配合两件事:
|
|
||||||
|
|
||||||
1. 接收 `--ticket-file=<path>` 参数,验证并消费一次性启动票据。
|
|
||||||
2. 如果收到 `--health-file=<path>` 参数,启动成功后向该路径写入 `ok\n`,让 Updater 确认新版本可用。
|
|
||||||
|
|
||||||
当前仓库里的 `client/MainApp/main.cpp` 是接入示例,已经实现了:
|
|
||||||
|
|
||||||
- 启动票据校验。
|
|
||||||
- 本地 License/设备身份校验。
|
|
||||||
- 本地策略校验。
|
|
||||||
- Manifest 完整性校验。
|
|
||||||
- 健康标记写入。
|
|
||||||
|
|
||||||
如果第三方业务程序暂时不想改代码,可以先使用当前 `MainApp.exe` 作为 Demo 验证 SDK 包;真正接入时建议把这些启动检查逻辑移植到业务主程序。
|
|
||||||
|
|
||||||
## 如何生成 SDK 包
|
|
||||||
|
|
||||||
在 Windows PowerShell 中执行:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd client
|
|
||||||
.\package-sdk.ps1 `
|
|
||||||
-SourceDir .\out\bin `
|
|
||||||
-OutputDir .\dist\UpdateClientSDK `
|
|
||||||
-ZipFile .\dist\UpdateClientSDK.zip `
|
|
||||||
-SdkVersion 0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
默认生成的 SDK 不包含 Qt 运行库,避免覆盖业务软件自带的 Qt。只有在接入的软件本身不带 Qt,且你确认要让 SDK 自带一套 Qt 运行库时,才额外添加 `-IncludeQtRuntime`。
|
|
||||||
|
|
||||||
生成结果:
|
|
||||||
|
|
||||||
```text
|
|
||||||
dist/UpdateClientSDK/
|
|
||||||
SimCAE自动升级SDK接入说明_v0.1.docx
|
|
||||||
sdk_manifest.json
|
|
||||||
bin/
|
|
||||||
config/
|
|
||||||
app_config.json
|
|
||||||
manifest_public_key.pem
|
|
||||||
scripts/
|
|
||||||
```
|
|
||||||
|
|
||||||
把 `UpdateClientSDK.zip` 发给接入方即可。
|
|
||||||
|
|
||||||
## 如何生成某个产品的最终客户端包
|
|
||||||
|
|
||||||
SDK 是给开发者接入用的,最终给用户安装/分发时,可以使用:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd client
|
|
||||||
.\package-client.ps1 `
|
|
||||||
-SourceDir .\out\bin `
|
|
||||||
-ConfigFile .\config\app_config.json `
|
|
||||||
-OutputDir .\dist\UpdateClient `
|
|
||||||
-ZipFile .\dist\UpdateClient.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
`package-client.ps1` 会检查配置和必需文件,并生成具体产品的客户端包。
|
|
||||||
|
|
||||||
## 常见错误
|
|
||||||
|
|
||||||
1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。
|
|
||||||
2. 首次启动提示 `cannot save installation id`:当前目录不可写,常见于 `C:\Program Files\...`;请换到可写目录联调,或用管理员权限/提权方案。
|
|
||||||
3. 首次启动设备登记失败:检查 `api_base_url`、`client_token`、`license_key`、服务端 License 状态。
|
|
||||||
4. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配。
|
|
||||||
5. 升级后回滚:检查业务程序是否在 `health_check_timeout_ms` 内写入健康标记。
|
|
||||||
6. 发布失败提示主程序不在根目录:选择发布目录时要选择业务主程序所在目录,而不是上层或下层目录。
|
|
||||||
7. 启动时提示 `无法定位程序输入点 ... Qt5*.dll`:通常是 Qt DLL 被不同版本覆盖或混用。恢复业务软件原始 Qt DLL,并重新打包 SDK;SimCAE 场景下不要使用 `-IncludeQtRuntime`。
|
|
||||||
@@ -3,9 +3,6 @@ set(CMAKE_GENERATOR_PLATFORM x64 CACHE STRING "强制x64平台,禁用Win32")
|
|||||||
set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCH x64)
|
set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCH x64)
|
||||||
# 关闭VS自动Win32设计时预生成
|
# 关闭VS自动Win32设计时预生成
|
||||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF)
|
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)
|
project(Updater)
|
||||||
set(SRC
|
set(SRC
|
||||||
@@ -14,6 +11,7 @@ set(SRC
|
|||||||
UpdaterLogic.cpp
|
UpdaterLogic.cpp
|
||||||
UpdateTransaction.h
|
UpdateTransaction.h
|
||||||
UpdateTransaction.cpp
|
UpdateTransaction.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
)
|
)
|
||||||
add_executable(Updater ${SRC})
|
add_executable(Updater ${SRC})
|
||||||
|
|
||||||
|
|||||||
+19
-18
@@ -3,6 +3,7 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
@@ -341,45 +342,45 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
|
|||||||
{
|
{
|
||||||
m_offlineError.clear();
|
m_offlineError.clear();
|
||||||
QFile package(packagePath);
|
QFile package(packagePath);
|
||||||
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = "无法打开离线更新包"; return false; }
|
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot open the offline update package"); return false; }
|
||||||
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = "离线包格式标识无效"; return false; }
|
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package format identifier is invalid"); return false; }
|
||||||
const QByteArray lengthBytes = package.read(8);
|
const QByteArray lengthBytes = package.read(8);
|
||||||
if (lengthBytes.size() != 8) { m_offlineError = "离线包头不完整"; return false; }
|
if (lengthBytes.size() != 8) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header is incomplete"); return false; }
|
||||||
quint64 headerSize = 0;
|
quint64 headerSize = 0;
|
||||||
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
|
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
|
||||||
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = "离线包头长度无效"; return false; }
|
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header length is invalid"); return false; }
|
||||||
QJsonParseError error;
|
QJsonParseError error;
|
||||||
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
|
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
|
||||||
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = "离线包头 JSON 无效"; return false; }
|
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header JSON is invalid"); return false; }
|
||||||
const QJsonObject wrapper = wrapperDoc.object();
|
const QJsonObject wrapper = wrapperDoc.object();
|
||||||
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
|
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
|
||||||
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
||||||
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
|
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
|
||||||
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = "离线包 RSA 签名无效"; return false; }
|
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package RSA signature is invalid"); return false; }
|
||||||
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
|
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
|
||||||
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = "离线包签名元数据无效"; return false; }
|
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package signature metadata is invalid"); return false; }
|
||||||
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = "Manifest 摘要与包签名不一致"; return false; }
|
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The manifest digest does not match the package signature"); return false; }
|
||||||
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
|
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
|
||||||
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = "离线 Manifest 无效"; return false; }
|
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest is invalid"); return false; }
|
||||||
manifest.insert("signature", wrapper.value("manifest_signature").toString());
|
manifest.insert("signature", wrapper.value("manifest_signature").toString());
|
||||||
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
|
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
|
||||||
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = "包信息与 Manifest 身份不一致"; return false; }
|
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Package information does not match manifest identity"); return false; }
|
||||||
if (!verifyManifestSignature()) { m_offlineError = "离线 Manifest RSA 签名无效"; return false; }
|
if (!verifyManifestSignature()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest RSA signature is invalid"); return false; }
|
||||||
const qint64 payloadStart = 16 + qint64(headerSize);
|
const qint64 payloadStart = 16 + qint64(headerSize);
|
||||||
const QJsonArray entries = packageMeta.value("files").toArray();
|
const QJsonArray entries = packageMeta.value("files").toArray();
|
||||||
for (const QJsonValue& value : entries) {
|
for (const QJsonValue& value : entries) {
|
||||||
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
||||||
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
|
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
|
||||||
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = "离线包包含不安全路径或越界数据: " + path; return false; }
|
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package contains an unsafe path or out-of-range data: %1").arg(path); return false; }
|
||||||
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
|
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
|
||||||
if (stagingDir.isEmpty()) continue;
|
if (stagingDir.isEmpty()) continue;
|
||||||
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = "无法准备离线文件: " + path; return false; }
|
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot prepare offline file: %1").arg(path); return false; }
|
||||||
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = "无法创建暂存文件: " + path; return false; }
|
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot create staged file: %1").arg(path); return false; }
|
||||||
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
|
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
|
||||||
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = "离线文件读取失败: " + path; return false; } hash.addData(block); remaining -= block.size(); }
|
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Failed to read offline file: %1").arg(path); return false; } hash.addData(block); remaining -= block.size(); }
|
||||||
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = "离线文件 Hash 或写入失败: " + path; return false; }
|
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Offline file hash verification or write failed: %1").arg(path); return false; }
|
||||||
}
|
}
|
||||||
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = "离线包文件数量与 Manifest 不一致"; return false; }
|
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package file count does not match the manifest"); return false; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +515,7 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
|||||||
|
|
||||||
if (existingSize > 0 && httpStatus == 200)
|
if (existingSize > 0 && httpStatus == 200)
|
||||||
{
|
{
|
||||||
// 服务端忽略 Range,当前文件是“旧片段 + 完整响应”,必须安全重下。
|
// The server ignored Range; the current file is "old fragment + full response" and must be redownloaded safely.
|
||||||
qDebug() << "Server ignored Range; restart full download:" << savePath;
|
qDebug() << "Server ignored Range; restart full download:" << savePath;
|
||||||
QFile::remove(partPath);
|
QFile::remove(partPath);
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-66
@@ -1,4 +1,3 @@
|
|||||||
#include <windows.h>
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
@@ -11,8 +10,8 @@
|
|||||||
#include <QProgressDialog>
|
#include <QProgressDialog>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QStorageInfo>
|
#include <QStorageInfo>
|
||||||
#include <QTextCodec>
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QTranslator>
|
||||||
#include "UpdaterLogic.h"
|
#include "UpdaterLogic.h"
|
||||||
#include "UpdateTransaction.h"
|
#include "UpdateTransaction.h"
|
||||||
#include "FileHelper.h"
|
#include "FileHelper.h"
|
||||||
@@ -23,10 +22,15 @@
|
|||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
SetConsoleOutputCP(65001);
|
|
||||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
QApplication::setApplicationName("Marsco Updater");
|
QApplication::setApplicationName("Marsco Updater");
|
||||||
|
QTranslator translator;
|
||||||
|
if (translator.load(":/i18n/update-client_zh_CN.qm"))
|
||||||
|
app.installTranslator(&translator);
|
||||||
|
|
||||||
|
const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested();
|
||||||
|
if (elevatedWriteExitCode >= 0)
|
||||||
|
return elevatedWriteExitCode;
|
||||||
|
|
||||||
UpdaterLogic logic;
|
UpdaterLogic logic;
|
||||||
QString offlinePackagePath;
|
QString offlinePackagePath;
|
||||||
@@ -37,7 +41,9 @@ int main(int argc, char* argv[])
|
|||||||
if (argc >= 2 && QString(argv[1]).startsWith("--offline-package=")) {
|
if (argc >= 2 && QString(argv[1]).startsWith("--offline-package=")) {
|
||||||
offlinePackagePath = QString(argv[1]).mid(QString("--offline-package=").size());
|
offlinePackagePath = QString(argv[1]).mid(QString("--offline-package=").size());
|
||||||
if (!logic.loadOfflinePackage(offlinePackagePath)) {
|
if (!logic.loadOfflinePackage(offlinePackagePath)) {
|
||||||
QMessageBox::critical(nullptr, "离线包无效", logic.offlineError());
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Invalid Offline Package"),
|
||||||
|
logic.offlineError());
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const QJsonObject packageManifest = logic.getManifest();
|
const QJsonObject packageManifest = logic.getManifest();
|
||||||
@@ -47,7 +53,9 @@ int main(int argc, char* argv[])
|
|||||||
targetVersionId = packageManifest.value("manifest_seq").toInt();
|
targetVersionId = packageManifest.value("manifest_seq").toInt();
|
||||||
} else {
|
} else {
|
||||||
if (argc < 5) {
|
if (argc < 5) {
|
||||||
QMessageBox::critical(nullptr, "更新器参数错误", "更新器缺少在线更新参数或离线更新包。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Updater Argument Error"),
|
||||||
|
QCoreApplication::translate("Updater", "The updater is missing online update arguments or an offline update package."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
appId = argv[1]; channel = argv[2]; targetVersion = argv[3]; targetVersionId = QString(argv[4]).toInt();
|
appId = argv[1]; channel = argv[2]; targetVersion = argv[3]; targetVersionId = QString(argv[4]).toInt();
|
||||||
@@ -66,7 +74,9 @@ int main(int argc, char* argv[])
|
|||||||
const QString updateDir = config.updateRoot();
|
const QString updateDir = config.updateRoot();
|
||||||
QDir().mkpath(updateDir);
|
QDir().mkpath(updateDir);
|
||||||
if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) {
|
if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) {
|
||||||
QMessageBox::critical(nullptr, "离线包不适用", "更新包的应用或渠道与本机配置不一致。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Offline Package Not Applicable"),
|
||||||
|
QCoreApplication::translate("Updater", "The update package application or channel does not match the local configuration."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
QString fromVersion = config.getValue("App", "current_version");
|
QString fromVersion = config.getValue("App", "current_version");
|
||||||
@@ -74,13 +84,17 @@ int main(int argc, char* argv[])
|
|||||||
PolicyHelper offlinePolicy(runtimeDir);
|
PolicyHelper offlinePolicy(runtimeDir);
|
||||||
if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired()
|
if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired()
|
||||||
|| !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) {
|
|| !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) {
|
||||||
QMessageBox::critical(nullptr, "离线更新被拒绝", "本地签名策略已过期、禁止离线更新或不允许目标版本。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Offline Update Rejected"),
|
||||||
|
QCoreApplication::translate("Updater", "The local signed policy has expired, forbids offline updates, or does not allow the target version."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (QVersionNumber::compare(QVersionNumber::fromString(targetVersion),
|
if (QVersionNumber::compare(QVersionNumber::fromString(targetVersion),
|
||||||
QVersionNumber::fromString(fromVersion)) < 0
|
QVersionNumber::fromString(fromVersion)) < 0
|
||||||
&& !offlinePolicy.allowRollback()) {
|
&& !offlinePolicy.allowRollback()) {
|
||||||
QMessageBox::critical(nullptr, "禁止降级", "当前签名策略不允许安装较低版本的离线包。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Downgrade Forbidden"),
|
||||||
|
QCoreApplication::translate("Updater", "The current signed policy does not allow installing an offline package with a lower version."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,21 +105,25 @@ int main(int argc, char* argv[])
|
|||||||
QString restoredVersion;
|
QString restoredVersion;
|
||||||
QString recoveryError;
|
QString recoveryError;
|
||||||
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) {
|
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) {
|
||||||
QMessageBox::critical(nullptr, "更新恢复失败",
|
QMessageBox::critical(nullptr,
|
||||||
QString("检测到上次更新未完成,但无法恢复旧版本:%1\n请不要继续运行软件,并联系管理员。").arg(recoveryError));
|
QCoreApplication::translate("Updater", "Update Recovery Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "An unfinished update was detected, but the old version could not be restored: %1\nDo not continue running the software. Please contact the administrator.")
|
||||||
|
.arg(recoveryError));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (!restoredVersion.isEmpty() && restoredVersion != fromVersion) {
|
if (!restoredVersion.isEmpty() && restoredVersion != fromVersion) {
|
||||||
if (!config.setValue("App", "current_version", restoredVersion)) {
|
if (!config.setValue("App", "current_version", restoredVersion)) {
|
||||||
QMessageBox::critical(nullptr, "更新恢复失败", "旧文件已经恢复,但无法恢复版本状态,请检查配置目录写入权限。");
|
QMessageBox::critical(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Update Recovery Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "The old files were restored, but the version state could not be restored. Please check write permissions for the configuration directory."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
fromVersion = restoredVersion;
|
fromVersion = restoredVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QProgressDialog progress("正在准备更新...", QString(), 0, 100);
|
QProgressDialog progress(QCoreApplication::translate("Updater", "Preparing update..."), QString(), 0, 100);
|
||||||
progress.setWindowTitle(QString("正在更新到 %1").arg(targetVersion));
|
progress.setWindowTitle(QCoreApplication::translate("Updater", "Updating to %1").arg(targetVersion));
|
||||||
progress.setCancelButton(nullptr);
|
progress.setCancelButton(nullptr);
|
||||||
progress.setWindowModality(Qt::ApplicationModal);
|
progress.setWindowModality(Qt::ApplicationModal);
|
||||||
progress.setMinimumDuration(0);
|
progress.setMinimumDuration(0);
|
||||||
@@ -133,8 +151,9 @@ int main(int argc, char* argv[])
|
|||||||
QObject::connect(&logic, &UpdaterLogic::downloadProgress,
|
QObject::connect(&logic, &UpdaterLogic::downloadProgress,
|
||||||
[&](qint64 received, qint64 total, const QString& path, double bytesPerSecond) {
|
[&](qint64 received, qint64 total, const QString& path, double bytesPerSecond) {
|
||||||
const int value = total > 0 ? 30 + static_cast<int>(30 * received / total) : 60;
|
const int value = total > 0 ? 30 + static_cast<int>(30 * received / total) : 60;
|
||||||
const QString fileText = path.isEmpty() ? QString("正在准备下载...")
|
const QString fileText = path.isEmpty()
|
||||||
: QString("正在下载:%1").arg(path);
|
? QCoreApplication::translate("Updater", "Preparing download...")
|
||||||
|
: QCoreApplication::translate("Updater", "Downloading: %1").arg(path);
|
||||||
progress.setValue(value);
|
progress.setValue(value);
|
||||||
progress.setLabelText(QString("%1\n%2 / %3 · %4/s")
|
progress.setLabelText(QString("%1\n%2 / %3 · %4/s")
|
||||||
.arg(fileText, formatBytes(received), formatBytes(total),
|
.arg(fileText, formatBytes(received), formatBytes(total),
|
||||||
@@ -196,7 +215,7 @@ int main(int argc, char* argv[])
|
|||||||
const auto delegateRollback = [&](const QString& title, const QString& reason) {
|
const auto delegateRollback = [&](const QString& title, const QString& reason) {
|
||||||
FileHelper::killProcess(mainExecutable);
|
FileHelper::killProcess(mainExecutable);
|
||||||
transaction.markRollbackRequired(reason);
|
transaction.markRollbackRequired(reason);
|
||||||
progress.setLabelText("正在将回滚工作移交给 Bootstrap...");
|
progress.setLabelText(QCoreApplication::translate("Updater", "Delegating rollback to Bootstrap..."));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
if (launchBootstrap("rollback")) {
|
if (launchBootstrap("rollback")) {
|
||||||
progress.close();
|
progress.close();
|
||||||
@@ -205,7 +224,7 @@ int main(int argc, char* argv[])
|
|||||||
reportUpdateResult(false);
|
reportUpdateResult(false);
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, title,
|
QMessageBox::critical(nullptr, title,
|
||||||
reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。");
|
reason + QCoreApplication::translate("Updater", "\n\nCannot start Bootstrap to perform rollback. Do not continue running the software. Please contact the administrator."));
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,8 +233,9 @@ int main(int argc, char* argv[])
|
|||||||
if (!transaction.resumeExisting(&resumeError)) {
|
if (!transaction.resumeExisting(&resumeError)) {
|
||||||
reportUpdateResult(false);
|
reportUpdateResult(false);
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "事务续办失败",
|
QMessageBox::critical(nullptr,
|
||||||
QString("无法读取 Bootstrap 更新事务:%1").arg(resumeError));
|
QCoreApplication::translate("Updater", "Transaction Resume Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot read the Bootstrap update transaction: %1").arg(resumeError));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
fromVersion = transaction.fromVersion();
|
fromVersion = transaction.fromVersion();
|
||||||
@@ -227,38 +247,52 @@ int main(int argc, char* argv[])
|
|||||||
reportUpdateResult(false);
|
reportUpdateResult(false);
|
||||||
if (stateOk) launchMainApp();
|
if (stateOk) launchMainApp();
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::warning(nullptr, "更新已回滚",
|
QMessageBox::warning(nullptr,
|
||||||
stateOk ? "新版本安装或启动失败,已自动恢复并启动旧版本。"
|
QCoreApplication::translate("Updater", "Update Rolled Back"),
|
||||||
: "旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。");
|
stateOk
|
||||||
|
? QCoreApplication::translate("Updater", "The new version failed to install or start. The old version has been restored and started automatically.")
|
||||||
|
: QCoreApplication::translate("Updater", "The old files were restored, but the old version number could not be written back. Please check configuration directory permissions."));
|
||||||
return stateOk ? 0 : -1;
|
return stateOk ? 0 : -1;
|
||||||
}
|
}
|
||||||
if (bootstrapResult != "success") {
|
if (bootstrapResult != "success") {
|
||||||
reportUpdateResult(false);
|
reportUpdateResult(false);
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::critical(nullptr, "自动回滚失败",
|
QMessageBox::critical(nullptr,
|
||||||
"Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。");
|
QCoreApplication::translate("Updater", "Automatic Rollback Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Bootstrap could not fully restore the old version. Do not continue running the software. Please contact the administrator."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
} else if (!transaction.initialize()) {
|
} else if (!transaction.initialize()) {
|
||||||
return fail("更新准备失败", "无法创建更新事务目录或保存事务状态。", "transaction_init_failed");
|
return fail(QCoreApplication::translate("Updater", "Update Preparation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot create the update transaction directory or save the transaction state."),
|
||||||
|
"transaction_init_failed");
|
||||||
} else if (!offlinePackagePath.isEmpty()) {
|
} else if (!offlinePackagePath.isEmpty()) {
|
||||||
QFile marker(QDir(transaction.backupDir()).filePath(".offline_mode"));
|
QFile marker(QDir(transaction.backupDir()).filePath(".offline_mode"));
|
||||||
if (!marker.open(QIODevice::WriteOnly) || marker.write("offline\n") != 8)
|
if (!marker.open(QIODevice::WriteOnly) || marker.write("offline\n") != 8)
|
||||||
return fail("更新准备失败", "无法保存离线事务标记。", "offline_marker_failed");
|
return fail(QCoreApplication::translate("Updater", "Update Preparation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot save the offline transaction marker."),
|
||||||
|
"offline_marker_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(resumingFromBootstrap ? 72 : 10, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包...");
|
setProgress(resumingFromBootstrap ? 72 : 10,
|
||||||
|
offlinePackagePath.isEmpty()
|
||||||
|
? QCoreApplication::translate("Updater", "Fetching and verifying version manifest...")
|
||||||
|
: QCoreApplication::translate("Updater", "Verifying offline update package..."));
|
||||||
const QString manifestCacheDir = QDir(updateDir).filePath("manifest_cache");
|
const QString manifestCacheDir = QDir(updateDir).filePath("manifest_cache");
|
||||||
if (resumingFromBootstrap) {
|
if (resumingFromBootstrap) {
|
||||||
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
|
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
|
||||||
return delegateRollback("清单缓存失败", "Bootstrap 安装后无法读取签名 Manifest 缓存。");
|
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation."));
|
||||||
} else if (offlinePackagePath.isEmpty()) {
|
} else if (offlinePackagePath.isEmpty()) {
|
||||||
logic.getManifest(appId, channel, targetVersion, targetVersionId);
|
logic.getManifest(appId, channel, targetVersion, targetVersionId);
|
||||||
}
|
}
|
||||||
if (!logic.verifyManifestSignature()) {
|
if (!logic.verifyManifestSignature()) {
|
||||||
if (resumingFromBootstrap)
|
if (resumingFromBootstrap)
|
||||||
return delegateRollback("安全验证失败", "Bootstrap 安装后无法重新验证版本清单签名。");
|
return delegateRollback(QCoreApplication::translate("Updater", "Security Verification Failed"),
|
||||||
return fail("安全验证失败", "版本清单签名无效,更新已停止。请联系管理员。", "manifest_signature_invalid");
|
QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation."));
|
||||||
|
return fail(QCoreApplication::translate("Updater", "Security Verification Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."),
|
||||||
|
"manifest_signature_invalid");
|
||||||
}
|
}
|
||||||
QStringList obsoletePaths;
|
QStringList obsoletePaths;
|
||||||
if (!resumingFromBootstrap && fromVersion != targetVersion) {
|
if (!resumingFromBootstrap && fromVersion != targetVersion) {
|
||||||
@@ -274,45 +308,62 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
if (!logic.saveManifestCache(manifestCacheDir)) {
|
if (!logic.saveManifestCache(manifestCacheDir)) {
|
||||||
if (resumingFromBootstrap)
|
if (resumingFromBootstrap)
|
||||||
return delegateRollback("清单缓存失败", "无法保存新版本 Manifest 缓存。");
|
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||||
return fail("清单缓存失败", "无法保存新版本 Manifest 缓存,更新已停止。", "manifest_cache_failed");
|
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache."));
|
||||||
|
return fail(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."),
|
||||||
|
"manifest_cache_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resumingFromBootstrap) {
|
if (!resumingFromBootstrap) {
|
||||||
setProgress(25, offlinePackagePath.isEmpty() ? "正在获取安全下载地址..." : "正在准备离线包文件...");
|
setProgress(25,
|
||||||
|
offlinePackagePath.isEmpty()
|
||||||
|
? QCoreApplication::translate("Updater", "Fetching secure download URLs...")
|
||||||
|
: QCoreApplication::translate("Updater", "Preparing offline package files..."));
|
||||||
if (offlinePackagePath.isEmpty())
|
if (offlinePackagePath.isEmpty())
|
||||||
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
|
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
|
||||||
if (logic.getFileList().isEmpty())
|
if (logic.getFileList().isEmpty())
|
||||||
return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list");
|
return fail(QCoreApplication::translate("Updater", "No Files to Update"),
|
||||||
|
QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."),
|
||||||
|
"empty_file_list");
|
||||||
|
|
||||||
QStorageInfo storage(updateDir);
|
QStorageInfo storage(updateDir);
|
||||||
storage.refresh();
|
storage.refresh();
|
||||||
const qint64 requiredBytes = logic.estimateAdditionalDiskBytes(targetDir, obsoletePaths);
|
const qint64 requiredBytes = logic.estimateAdditionalDiskBytes(targetDir, obsoletePaths);
|
||||||
const qint64 availableBytes = storage.bytesAvailable();
|
const qint64 availableBytes = storage.bytesAvailable();
|
||||||
if (!storage.isValid() || !storage.isReady() || availableBytes < requiredBytes) {
|
if (!storage.isValid() || !storage.isReady() || availableBytes < requiredBytes) {
|
||||||
return fail("磁盘空间不足",
|
return fail(QCoreApplication::translate("Updater", "Insufficient Disk Space"),
|
||||||
QString("更新至少需要 %1 可用空间,安装盘当前仅剩 %2。\n"
|
QCoreApplication::translate("Updater",
|
||||||
"所需空间已包含下载文件、旧版本备份和安全余量。")
|
"The update requires at least %1 of free space, but the installation drive currently has only %2.\n"
|
||||||
|
"The required space includes downloaded files, old version backups, and a safety margin.")
|
||||||
.arg(formatBytes(requiredBytes), formatBytes(qMax<qint64>(0, availableBytes))),
|
.arg(formatBytes(requiredBytes), formatBytes(qMax<qint64>(0, availableBytes))),
|
||||||
"disk_space_insufficient");
|
"disk_space_insufficient");
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString stagingDir = transaction.stagingDir();
|
const QString stagingDir = transaction.stagingDir();
|
||||||
setProgress(30, QString(offlinePackagePath.isEmpty() ? "正在下载并校验 %1 个版本文件..." : "正在提取并校验 %1 个离线文件...").arg(logic.getFileList().size()));
|
setProgress(30, offlinePackagePath.isEmpty()
|
||||||
|
? QCoreApplication::translate("Updater", "Downloading and verifying %1 version files...").arg(logic.getFileList().size())
|
||||||
|
: QCoreApplication::translate("Updater", "Extracting and verifying %1 offline files...").arg(logic.getFileList().size()));
|
||||||
if (!offlinePackagePath.isEmpty()) {
|
if (!offlinePackagePath.isEmpty()) {
|
||||||
if (!logic.loadOfflinePackage(offlinePackagePath, stagingDir))
|
if (!logic.loadOfflinePackage(offlinePackagePath, stagingDir))
|
||||||
return fail("离线包提取失败", logic.offlineError(), "offline_package_invalid");
|
return fail(QCoreApplication::translate("Updater", "Offline Package Extraction Failed"),
|
||||||
|
logic.offlineError(),
|
||||||
|
"offline_package_invalid");
|
||||||
} else {
|
} else {
|
||||||
if (!logic.downloadAllFiles(stagingDir, targetDir)) {
|
if (!logic.downloadAllFiles(stagingDir, targetDir)) {
|
||||||
logic.reportDownloadResult(appId, channel, targetVersion, false);
|
logic.reportDownloadResult(appId, channel, targetVersion, false);
|
||||||
return fail("下载失败", "部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。", "download_failed");
|
return fail(QCoreApplication::translate("Updater", "Download Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network and try again."),
|
||||||
|
"download_failed");
|
||||||
}
|
}
|
||||||
logic.reportDownloadResult(appId, channel, targetVersion, true);
|
logic.reportDownloadResult(appId, channel, targetVersion, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(58, "正在校验完整版本文件...");
|
setProgress(58, QCoreApplication::translate("Updater", "Verifying complete version files..."));
|
||||||
if (!logic.validateLocalFiles(stagingDir, targetDir))
|
if (!logic.validateLocalFiles(stagingDir, targetDir))
|
||||||
return fail("文件校验失败", "暂存文件与版本清单不一致,更新已停止。", "staging_verify_failed");
|
return fail(QCoreApplication::translate("Updater", "File Verification Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "The staged files do not match the version manifest. The update has stopped."),
|
||||||
|
"staging_verify_failed");
|
||||||
|
|
||||||
QStringList changedPaths;
|
QStringList changedPaths;
|
||||||
QDir stagingRoot(stagingDir);
|
QDir stagingRoot(stagingDir);
|
||||||
@@ -324,24 +375,35 @@ int main(int argc, char* argv[])
|
|||||||
runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable);
|
runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable);
|
||||||
for (const QString& path : changedPaths) {
|
for (const QString& path : changedPaths) {
|
||||||
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
|
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
|
||||||
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapManifestPath), "bootstrap_self_update_blocked");
|
return fail(QCoreApplication::translate("Updater", "Bootstrap Cannot Self-Update"),
|
||||||
|
QCoreApplication::translate("Updater", "This version contains a new %1. Please upgrade this component with the installer, then publish the business version again.")
|
||||||
|
.arg(bootstrapManifestPath),
|
||||||
|
"bootstrap_self_update_blocked");
|
||||||
}
|
}
|
||||||
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
|
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
|
||||||
return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed");
|
return fail(QCoreApplication::translate("Updater", "Transaction Recording Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot save the list of verified or pending deletion files. The update has stopped."),
|
||||||
|
"transaction_record_failed");
|
||||||
|
|
||||||
setProgress(66, "正在关闭主程序...");
|
setProgress(66, QCoreApplication::translate("Updater", "Closing the main application..."));
|
||||||
if (!FileHelper::killProcess(mainExecutable))
|
if (!FileHelper::killProcess(mainExecutable))
|
||||||
return fail("无法关闭主程序", QString("%1 仍在运行,请手动关闭后重试。").arg(mainExecutable), "mainapp_close_failed");
|
return fail(QCoreApplication::translate("Updater", "Cannot Close Main Application"),
|
||||||
|
QCoreApplication::translate("Updater", "%1 is still running. Please close it manually and try again.").arg(mainExecutable),
|
||||||
|
"mainapp_close_failed");
|
||||||
|
|
||||||
setProgress(72, QString("正在备份 %1 个待变更文件(其中删除 %2 个)...")
|
setProgress(72, QCoreApplication::translate("Updater", "Backing up %1 files to be changed, including %2 files to be deleted...")
|
||||||
.arg(changedPaths.size() + obsoletePaths.size()).arg(obsoletePaths.size()));
|
.arg(changedPaths.size() + obsoletePaths.size()).arg(obsoletePaths.size()));
|
||||||
if (!transaction.backupCurrentFiles())
|
if (!transaction.backupCurrentFiles())
|
||||||
return fail("备份失败", "无法备份当前版本文件,尚未安装新版本。请检查磁盘空间和目录权限。", "backup_failed");
|
return fail(QCoreApplication::translate("Updater", "Backup Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot back up current version files. The new version has not been installed. Please check disk space and directory permissions."),
|
||||||
|
"backup_failed");
|
||||||
|
|
||||||
const QString planFile = bootstrapPlanFile();
|
const QString planFile = bootstrapPlanFile();
|
||||||
QSaveFile plan(planFile);
|
QSaveFile plan(planFile);
|
||||||
if (!plan.open(QIODevice::WriteOnly))
|
if (!plan.open(QIODevice::WriteOnly))
|
||||||
return fail("接管准备失败", "无法创建 Bootstrap 文件计划。", "bootstrap_plan_failed");
|
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot create the Bootstrap file plan."),
|
||||||
|
"bootstrap_plan_failed");
|
||||||
const auto writePlanItem = [&](char operation, const QString& path) {
|
const auto writePlanItem = [&](char operation, const QString& path) {
|
||||||
const QByteArray line = QByteArray(1, operation) + '\t' + path.toUtf8() + '\n';
|
const QByteArray line = QByteArray(1, operation) + '\t' + path.toUtf8() + '\n';
|
||||||
return plan.write(line) == line.size();
|
return plan.write(line) == line.size();
|
||||||
@@ -349,43 +411,55 @@ int main(int argc, char* argv[])
|
|||||||
for (const QString& path : changedPaths) {
|
for (const QString& path : changedPaths) {
|
||||||
if (!writePlanItem('C', path)) {
|
if (!writePlanItem('C', path)) {
|
||||||
plan.cancelWriting();
|
plan.cancelWriting();
|
||||||
return fail("接管准备失败", "无法写入 Bootstrap 复制计划。", "bootstrap_plan_failed");
|
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot write the Bootstrap copy plan."),
|
||||||
|
"bootstrap_plan_failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const QString& path : obsoletePaths) {
|
for (const QString& path : obsoletePaths) {
|
||||||
if (!writePlanItem('D', path)) {
|
if (!writePlanItem('D', path)) {
|
||||||
plan.cancelWriting();
|
plan.cancelWriting();
|
||||||
return fail("接管准备失败", "无法写入 Bootstrap 删除计划。", "bootstrap_plan_failed");
|
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot write the Bootstrap deletion plan."),
|
||||||
|
"bootstrap_plan_failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!plan.commit() || !transaction.markAwaitingBootstrap())
|
if (!plan.commit() || !transaction.markAwaitingBootstrap())
|
||||||
return fail("接管准备失败", "无法提交 Bootstrap 文件计划或事务状态。", "bootstrap_plan_failed");
|
return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot commit the Bootstrap file plan or transaction state."),
|
||||||
|
"bootstrap_plan_failed");
|
||||||
|
|
||||||
setProgress(78, "正在将安装工作移交给 Bootstrap...");
|
setProgress(78, QCoreApplication::translate("Updater", "Delegating installation to Bootstrap..."));
|
||||||
if (!launchBootstrap("install"))
|
if (!launchBootstrap("install"))
|
||||||
return fail("Bootstrap 启动失败", QString("无法启动独立更新接管程序:%1").arg(bootstrapPath), "bootstrap_start_failed");
|
return fail(QCoreApplication::translate("Updater", "Bootstrap Startup Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot start the standalone update handoff program: %1").arg(bootstrapPath),
|
||||||
|
"bootstrap_start_failed");
|
||||||
progress.close();
|
progress.close();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(82, "正在校验 Bootstrap 安装结果...");
|
setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result..."));
|
||||||
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
|
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
|
||||||
return delegateRollback("安装校验失败", "新版本文件安装后校验未通过。");
|
return delegateRollback(QCoreApplication::translate("Updater", "Installation Verification Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "New version files failed verification after installation."));
|
||||||
for (const QString& path : transaction.obsoletePaths()) {
|
for (const QString& path : transaction.obsoletePaths()) {
|
||||||
if (QFile::exists(QDir(targetDir).filePath(path)))
|
if (QFile::exists(QDir(targetDir).filePath(path)))
|
||||||
return delegateRollback("废弃文件清理失败", QString("废弃文件仍然存在:%1").arg(path));
|
return delegateRollback(QCoreApplication::translate("Updater", "Obsolete File Cleanup Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "An obsolete file still exists: %1").arg(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(89, "正在保存新版本状态...");
|
setProgress(89, QCoreApplication::translate("Updater", "Saving new version state..."));
|
||||||
if (!config.setValue("App", "current_version", targetVersion)
|
if (!config.setValue("App", "current_version", targetVersion)
|
||||||
|| config.getValue("App", "current_version") != targetVersion)
|
|| config.getValue("App", "current_version") != targetVersion)
|
||||||
return delegateRollback("状态保存失败", "无法保存当前版本号。");
|
return delegateRollback(QCoreApplication::translate("Updater", "State Save Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "Cannot save the current version number."));
|
||||||
|
|
||||||
const QString healthFile = transaction.healthFile();
|
const QString healthFile = transaction.healthFile();
|
||||||
QFile::remove(healthFile);
|
QFile::remove(healthFile);
|
||||||
setProgress(94, "正在启动新版本并等待健康确认...");
|
setProgress(94, QCoreApplication::translate("Updater", "Starting the new version and waiting for health confirmation..."));
|
||||||
if (!launchMainApp(healthFile))
|
if (!launchMainApp(healthFile))
|
||||||
return delegateRollback("启动失败", QString("%1 无法启动。").arg(mainExecutable));
|
return delegateRollback(QCoreApplication::translate("Updater", "Startup Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable));
|
||||||
|
|
||||||
QElapsedTimer healthTimer;
|
QElapsedTimer healthTimer;
|
||||||
healthTimer.start();
|
healthTimer.start();
|
||||||
@@ -394,17 +468,23 @@ int main(int argc, char* argv[])
|
|||||||
QThread::msleep(100);
|
QThread::msleep(100);
|
||||||
}
|
}
|
||||||
if (!QFile::exists(healthFile))
|
if (!QFile::exists(healthFile))
|
||||||
return delegateRollback("启动确认失败", QString("新版本在 %1 毫秒内没有完成启动健康确认。").arg(healthCheckTimeoutMs));
|
return delegateRollback(QCoreApplication::translate("Updater", "Startup Confirmation Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "The new version did not complete startup health confirmation within %1 milliseconds.")
|
||||||
|
.arg(healthCheckTimeoutMs));
|
||||||
|
|
||||||
setProgress(99, "正在提交更新事务...");
|
setProgress(99, QCoreApplication::translate("Updater", "Committing update transaction..."));
|
||||||
if (!transaction.commit())
|
if (!transaction.commit())
|
||||||
return delegateRollback("事务提交失败", "新版本已经启动,但无法提交更新事务。");
|
return delegateRollback(QCoreApplication::translate("Updater", "Transaction Commit Failed"),
|
||||||
|
QCoreApplication::translate("Updater", "The new version has started, but the update transaction could not be committed."));
|
||||||
|
|
||||||
QFile::remove(healthFile);
|
QFile::remove(healthFile);
|
||||||
QFile::remove(bootstrapPlanFile());
|
QFile::remove(bootstrapPlanFile());
|
||||||
reportUpdateResult(true);
|
reportUpdateResult(true);
|
||||||
progress.setValue(100);
|
progress.setValue(100);
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion));
|
QMessageBox::information(nullptr,
|
||||||
|
QCoreApplication::translate("Updater", "Update Complete"),
|
||||||
|
QCoreApplication::translate("Updater", "The software has been successfully updated to %1 and passed the startup health check.")
|
||||||
|
.arg(targetVersion));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMROESbT36XU4d3YjuwU
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArX1FSi06eP8XhX4B3Oy6
|
||||||
WAC2h5p3btFu/IAeF3bVtHMovIA4ZXXKYsiq5FycDnDzyD86ou3B7PP7uHhVhn2l
|
FzfkTe3FBIg6OjWpT1541LivRTRt9HXZ30nKuIs5itXBzBQPMU8hsfR0MD9nfPG/
|
||||||
Uru7QElYRfEfQAFSU5dErc+SZo+oT170cgq1ePPD/YleKPRqFAL221Tbh5pusHcZ
|
NlajfZzqbyxAJlUMeThJiwtyz98wv3VE1hS2Bwuc3mbmtfU0zl/MoPdWrluL3x3C
|
||||||
Ocujit2qrfg5f4rIEzWu7kBKVSJ6WChkjetEL6OZ43ClkDyUGebUuaQ9dv39YVlv
|
bt0ylL00rLBuvjgG21zDflVqj3w9CwpKHFsNrSYoI6vOFX9YrRZ9tKcPEkSRPqts
|
||||||
Fp2pfARi7/7djMMncLVaFU2AAIuSy3jgQg65DOFRVPSr1P/rRfuqU55ZmqAmmZIs
|
411QkAQLtMiOMIIhNe5aFIL7doCnglx32hJeHNCTUSxjM9VyHUTEj8wKN/6Gy5iP
|
||||||
F/KVpne6zLFBXrxf5rNTBch+nxX+hcE7M+K0PJA5Ie669qRQFRwxoJlGpSOvMefQ
|
YZGRafeiJZ32RRIHyssereAg3Xe/3scd+tbFNNYP9xrRhRgDacmkSCBodnbJd4Qc
|
||||||
TwIDAQAB
|
rQIDAQAB
|
||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/i18n">
|
||||||
|
<file>update-client_zh_CN.qm</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
Binary file not shown.
@@ -0,0 +1,874 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="zh_CN">
|
||||||
|
<context>
|
||||||
|
<name>ConfigHelper</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../Common/ConfigHelper.cpp" line="164" />
|
||||||
|
<source>The user canceled the administrator permission confirmation.</source>
|
||||||
|
<translation>用户取消了管理员权限确认。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Common/ConfigHelper.cpp" line="165" />
|
||||||
|
<source>Windows error %1</source>
|
||||||
|
<translation>Windows 错误 %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Common/ConfigHelper.cpp" line="173" />
|
||||||
|
<source>Administrator Permission Required</source>
|
||||||
|
<translation>需要管理员权限</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Common/ConfigHelper.cpp" line="174" />
|
||||||
|
<source>The current installation directory requires administrator permission to save configuration.
|
||||||
|
|
||||||
|
Target file: %1
|
||||||
|
Reason: %2
|
||||||
|
|
||||||
|
Click OK, then choose Yes in the Windows permission confirmation dialog.</source>
|
||||||
|
<translation>当前安装目录需要管理员权限才能保存配置。
|
||||||
|
|
||||||
|
目标文件:%1
|
||||||
|
原因:%2
|
||||||
|
|
||||||
|
点击“确定”后,请在 Windows 权限确认窗口中选择“是”。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Common/ConfigHelper.cpp" line="181" />
|
||||||
|
<source>The user canceled the administrator permission request.</source>
|
||||||
|
<translation>用户取消了管理员权限请求。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>Launcher</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="27" />
|
||||||
|
<source>Checking for software updates...</source>
|
||||||
|
<translation>正在检查软件更新...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="28" />
|
||||||
|
<source>Marsco Launcher</source>
|
||||||
|
<translation>Marsco 软件启动器</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="58" />
|
||||||
|
<source>Please enter the License for %1:</source>
|
||||||
|
<translation>请输入 %1 的授权 License:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="59" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="61" />
|
||||||
|
<source>the application</source>
|
||||||
|
<translation>软件</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="60" />
|
||||||
|
<source>%1
|
||||||
|
|
||||||
|
Please enter a new License for %2:</source>
|
||||||
|
<translation>%1
|
||||||
|
|
||||||
|
请重新输入 %2 的授权 License:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="64" />
|
||||||
|
<source>Enter License</source>
|
||||||
|
<translation>输入 License</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="72" />
|
||||||
|
<source>License Required</source>
|
||||||
|
<translation>需要 License</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="73" />
|
||||||
|
<source>A License provided by the administrator is required for the first launch.</source>
|
||||||
|
<translation>首次启动需要输入管理员提供的 License。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="78" />
|
||||||
|
<source>License Cannot Be Empty</source>
|
||||||
|
<translation>License 不能为空</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="79" />
|
||||||
|
<source>Please paste the License created in the admin page.</source>
|
||||||
|
<translation>请粘贴管理员在后台创建的 License。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="85" />
|
||||||
|
<source>Failed to Save License</source>
|
||||||
|
<translation>保存 License 失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="86" />
|
||||||
|
<source>Cannot write the configuration file:
|
||||||
|
%1
|
||||||
|
%2</source>
|
||||||
|
<translation>无法写入配置文件:
|
||||||
|
%1
|
||||||
|
%2</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="90" />
|
||||||
|
<source>Verifying License...</source>
|
||||||
|
<translation>正在验证 License...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="113" />
|
||||||
|
<source>Device Authentication Failed</source>
|
||||||
|
<translation>设备身份验证失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="118" />
|
||||||
|
<source>The current License cannot be used: %1</source>
|
||||||
|
<translation>当前 License 无法使用:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="138" />
|
||||||
|
<source>The device or License authorization is invalid.</source>
|
||||||
|
<translation>设备或 License 授权无效。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="142" />
|
||||||
|
<source>The current authorization was rejected by the server: %1</source>
|
||||||
|
<translation>当前授权被服务端拒绝:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="146" />
|
||||||
|
<source>License Saved</source>
|
||||||
|
<translation>License 已保存</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="147" />
|
||||||
|
<source>Please restart Launcher to complete device authorization and update checking.</source>
|
||||||
|
<translation>请重新启动 Launcher 完成设备授权和更新检查。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="152" />
|
||||||
|
<source>Authorization Rejected</source>
|
||||||
|
<translation>授权被拒绝</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="169" />
|
||||||
|
<source>Select Offline Update Package</source>
|
||||||
|
<translation>选择离线更新包</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="171" />
|
||||||
|
<source>Marsco offline update package (*.upd)</source>
|
||||||
|
<translation>Marsco 离线更新包 (*.upd)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="179" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="300" />
|
||||||
|
<source>Offline Update</source>
|
||||||
|
<translation>离线更新</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="180" />
|
||||||
|
<source>No offline update package was selected.</source>
|
||||||
|
<translation>未选择离线更新包。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="198" />
|
||||||
|
<source>Verifying local runtime policy...</source>
|
||||||
|
<translation>正在验证本地运行策略...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="206" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="214" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="233" />
|
||||||
|
<source>Cannot Start</source>
|
||||||
|
<translation>无法启动</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="207" />
|
||||||
|
<source>The local version policy is invalid: %1</source>
|
||||||
|
<translation>本地版本策略无效:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="215" />
|
||||||
|
<source>The local version policy has expired. Please connect to the network or contact the administrator.</source>
|
||||||
|
<translation>本地版本策略已过期,请连接网络或联系管理员。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="222" />
|
||||||
|
<source>Current Version Cannot Run</source>
|
||||||
|
<translation>当前版本不可运行</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="223" />
|
||||||
|
<source>The current version %1 has been disabled by the administrator.</source>
|
||||||
|
<translation>当前版本 %1 已被管理员停用。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="234" />
|
||||||
|
<source>Cannot read the local state: %1</source>
|
||||||
|
<translation>无法读取本地状态:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="241" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="249" />
|
||||||
|
<source>Security Check Failed</source>
|
||||||
|
<translation>安全检查失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="242" />
|
||||||
|
<source>A version policy sequence rollback was detected. Startup has been blocked.</source>
|
||||||
|
<translation>检测到版本策略序列回退,已阻止启动。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="250" />
|
||||||
|
<source>A possible system time rollback was detected. Startup has been blocked.</source>
|
||||||
|
<translation>检测到系统时间可能被回拨,已阻止启动。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="258" />
|
||||||
|
<source>Version Rollback</source>
|
||||||
|
<translation>版本回退</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="259" />
|
||||||
|
<source>Update Required</source>
|
||||||
|
<translation>必须更新</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="259" />
|
||||||
|
<source>New Version Available</source>
|
||||||
|
<translation>发现新版本</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="262" />
|
||||||
|
<source>The administrator provided version %1 as the rollback target. Downgrade now?</source>
|
||||||
|
<translation>管理员提供了版本 %1 作为回退目标,是否现在降级?</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="263" />
|
||||||
|
<source>Version %1 is available. Update now?</source>
|
||||||
|
<translation>发现新版本 %1,是否现在更新?</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="264" />
|
||||||
|
<source>
|
||||||
|
Target version: %1</source>
|
||||||
|
<translation>
|
||||||
|
目标版本:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="275" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="324" />
|
||||||
|
<source>Startup Failed</source>
|
||||||
|
<translation>启动失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="276" />
|
||||||
|
<location filename="../Launcher/main.cpp" line="325" />
|
||||||
|
<source>Cannot start the main application: %1</source>
|
||||||
|
<translation>无法启动主程序:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="285" />
|
||||||
|
<source>Updater Startup Failed</source>
|
||||||
|
<translation>更新器启动失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="286" />
|
||||||
|
<source>Cannot start the updater: %1</source>
|
||||||
|
<translation>无法启动更新器:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="298" />
|
||||||
|
<source>Caching signed version manifest...</source>
|
||||||
|
<translation>正在缓存签名版本清单...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="305" />
|
||||||
|
<source>Manifest Cache Failed</source>
|
||||||
|
<translation>清单缓存失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="306" />
|
||||||
|
<source>Cannot cache the signed manifest for the current version: %1</source>
|
||||||
|
<translation>无法缓存当前版本的签名清单:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="295" />
|
||||||
|
<source>Server Unavailable</source>
|
||||||
|
<translation>服务器不可用</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="296" />
|
||||||
|
<source>Cannot connect to the update server. Import an offline update package?</source>
|
||||||
|
<translation>当前无法连接更新服务器。是否导入离线更新包?</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="301" />
|
||||||
|
<source>No offline update package was selected, or the updater could not be started.</source>
|
||||||
|
<translation>未选择离线更新包或无法启动更新器。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="311" />
|
||||||
|
<source>Network Unavailable</source>
|
||||||
|
<translation>网络不可用</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="312" />
|
||||||
|
<source>Cannot connect to the update server, and the current policy does not allow offline startup.</source>
|
||||||
|
<translation>无法连接更新服务器,且当前策略不允许离线启动。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="317" />
|
||||||
|
<source>The application is up to date. Starting...</source>
|
||||||
|
<translation>当前已是最新版本,正在启动...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Launcher/main.cpp" line="318" />
|
||||||
|
<source>Offline mode is active. Starting...</source>
|
||||||
|
<translation>当前处于离线模式,正在启动...</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>Updater</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="40" />
|
||||||
|
<source>Invalid Offline Package</source>
|
||||||
|
<translation>离线包无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="52" />
|
||||||
|
<source>Updater Argument Error</source>
|
||||||
|
<translation>更新器参数错误</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="53" />
|
||||||
|
<source>The updater is missing online update arguments or an offline update package.</source>
|
||||||
|
<translation>更新器缺少在线更新参数或离线更新包。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="73" />
|
||||||
|
<source>Offline Package Not Applicable</source>
|
||||||
|
<translation>离线包不适用</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="74" />
|
||||||
|
<source>The update package application or channel does not match the local configuration.</source>
|
||||||
|
<translation>更新包的应用或渠道与本机配置不一致。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="83" />
|
||||||
|
<source>Offline Update Rejected</source>
|
||||||
|
<translation>离线更新被拒绝</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="84" />
|
||||||
|
<source>The local signed policy has expired, forbids offline updates, or does not allow the target version.</source>
|
||||||
|
<translation>本地签名策略已过期、禁止离线更新或不允许目标版本。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="91" />
|
||||||
|
<source>Downgrade Forbidden</source>
|
||||||
|
<translation>禁止降级</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="92" />
|
||||||
|
<source>The current signed policy does not allow installing an offline package with a lower version.</source>
|
||||||
|
<translation>当前签名策略不允许安装较低版本的离线包。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="104" />
|
||||||
|
<location filename="../Updater/main.cpp" line="112" />
|
||||||
|
<source>Update Recovery Failed</source>
|
||||||
|
<translation>更新恢复失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="105" />
|
||||||
|
<source>An unfinished update was detected, but the old version could not be restored: %1
|
||||||
|
Do not continue running the software. Please contact the administrator.</source>
|
||||||
|
<translation>检测到上次更新未完成,但无法恢复旧版本:%1
|
||||||
|
请不要继续运行软件,并联系管理员。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="113" />
|
||||||
|
<source>The old files were restored, but the version state could not be restored. Please check write permissions for the configuration directory.</source>
|
||||||
|
<translation>旧文件已经恢复,但无法恢复版本状态,请检查配置目录写入权限。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="120" />
|
||||||
|
<source>Preparing update...</source>
|
||||||
|
<translation>正在准备更新...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="121" />
|
||||||
|
<source>Updating to %1</source>
|
||||||
|
<translation>正在更新到 %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="150" />
|
||||||
|
<source>Preparing download...</source>
|
||||||
|
<translation>正在准备下载...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="151" />
|
||||||
|
<source>Downloading: %1</source>
|
||||||
|
<translation>正在下载:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="213" />
|
||||||
|
<source>Delegating rollback to Bootstrap...</source>
|
||||||
|
<translation>正在将回滚工作移交给 Bootstrap...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="222" />
|
||||||
|
<source>
|
||||||
|
|
||||||
|
Cannot start Bootstrap to perform rollback. Do not continue running the software. Please contact the administrator.</source>
|
||||||
|
<translation>
|
||||||
|
|
||||||
|
无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="232" />
|
||||||
|
<source>Transaction Resume Failed</source>
|
||||||
|
<translation>事务续办失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="233" />
|
||||||
|
<source>Cannot read the Bootstrap update transaction: %1</source>
|
||||||
|
<translation>无法读取 Bootstrap 更新事务:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="246" />
|
||||||
|
<source>Update Rolled Back</source>
|
||||||
|
<translation>更新已回滚</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="248" />
|
||||||
|
<source>The new version failed to install or start. The old version has been restored and started automatically.</source>
|
||||||
|
<translation>新版本安装或启动失败,已自动恢复并启动旧版本。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="249" />
|
||||||
|
<source>The old files were restored, but the old version number could not be written back. Please check configuration directory permissions.</source>
|
||||||
|
<translation>旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="256" />
|
||||||
|
<source>Automatic Rollback Failed</source>
|
||||||
|
<translation>自动回滚失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="257" />
|
||||||
|
<source>Bootstrap could not fully restore the old version. Do not continue running the software. Please contact the administrator.</source>
|
||||||
|
<translation>Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="261" />
|
||||||
|
<location filename="../Updater/main.cpp" line="267" />
|
||||||
|
<source>Update Preparation Failed</source>
|
||||||
|
<translation>更新准备失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="262" />
|
||||||
|
<source>Cannot create the update transaction directory or save the transaction state.</source>
|
||||||
|
<translation>无法创建更新事务目录或保存事务状态。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="268" />
|
||||||
|
<source>Cannot save the offline transaction marker.</source>
|
||||||
|
<translation>无法保存离线事务标记。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="274" />
|
||||||
|
<source>Fetching and verifying version manifest...</source>
|
||||||
|
<translation>正在获取并验证版本清单...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="275" />
|
||||||
|
<source>Verifying offline update package...</source>
|
||||||
|
<translation>正在验证离线更新包...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="279" />
|
||||||
|
<location filename="../Updater/main.cpp" line="306" />
|
||||||
|
<location filename="../Updater/main.cpp" line="308" />
|
||||||
|
<source>Manifest Cache Failed</source>
|
||||||
|
<translation>清单缓存失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="280" />
|
||||||
|
<source>Cannot read the signed manifest cache after Bootstrap installation.</source>
|
||||||
|
<translation>Bootstrap 安装后无法读取签名 Manifest 缓存。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="286" />
|
||||||
|
<location filename="../Updater/main.cpp" line="288" />
|
||||||
|
<source>Security Verification Failed</source>
|
||||||
|
<translation>安全验证失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="287" />
|
||||||
|
<source>Cannot reverify the manifest signature after Bootstrap installation.</source>
|
||||||
|
<translation>Bootstrap 安装后无法重新验证版本清单签名。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="289" />
|
||||||
|
<source>The version manifest signature is invalid. The update has stopped. Please contact the administrator.</source>
|
||||||
|
<translation>版本清单签名无效,更新已停止。请联系管理员。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="307" />
|
||||||
|
<source>Cannot save the new version manifest cache.</source>
|
||||||
|
<translation>无法保存新版本 Manifest 缓存。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="309" />
|
||||||
|
<source>Cannot save the new version manifest cache. The update has stopped.</source>
|
||||||
|
<translation>无法保存新版本 Manifest 缓存,更新已停止。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="316" />
|
||||||
|
<source>Fetching secure download URLs...</source>
|
||||||
|
<translation>正在获取安全下载地址...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="317" />
|
||||||
|
<source>Preparing offline package files...</source>
|
||||||
|
<translation>正在准备离线包文件...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="321" />
|
||||||
|
<source>No Files to Update</source>
|
||||||
|
<translation>没有可更新文件</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="322" />
|
||||||
|
<source>The server did not return any version files. The update has stopped.</source>
|
||||||
|
<translation>服务器没有返回任何版本文件,更新已停止。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="330" />
|
||||||
|
<source>Insufficient Disk Space</source>
|
||||||
|
<translation>磁盘空间不足</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="331" />
|
||||||
|
<source>The update requires at least %1 of free space, but the installation drive currently has only %2.
|
||||||
|
The required space includes downloaded files, old version backups, and a safety margin.</source>
|
||||||
|
<translation>更新至少需要 %1 可用空间,安装盘当前仅剩 %2。
|
||||||
|
所需空间已包含下载文件、旧版本备份和安全余量。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="340" />
|
||||||
|
<source>Downloading and verifying %1 version files...</source>
|
||||||
|
<translation>正在下载并校验 %1 个版本文件...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="341" />
|
||||||
|
<source>Extracting and verifying %1 offline files...</source>
|
||||||
|
<translation>正在提取并校验 %1 个离线文件...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="344" />
|
||||||
|
<source>Offline Package Extraction Failed</source>
|
||||||
|
<translation>离线包提取失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="350" />
|
||||||
|
<source>Download Failed</source>
|
||||||
|
<translation>下载失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="351" />
|
||||||
|
<source>Some files failed to download or failed SHA-256 verification. Please check the network and try again.</source>
|
||||||
|
<translation>部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="357" />
|
||||||
|
<source>Verifying complete version files...</source>
|
||||||
|
<translation>正在校验完整版本文件...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="359" />
|
||||||
|
<source>File Verification Failed</source>
|
||||||
|
<translation>文件校验失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="360" />
|
||||||
|
<source>The staged files do not match the version manifest. The update has stopped.</source>
|
||||||
|
<translation>暂存文件与版本清单不一致,更新已停止。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="373" />
|
||||||
|
<source>Bootstrap Cannot Self-Update</source>
|
||||||
|
<translation>Bootstrap 无法自更新</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="374" />
|
||||||
|
<source>This version contains a new %1. Please upgrade this component with the installer, then publish the business version again.</source>
|
||||||
|
<translation>本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="379" />
|
||||||
|
<source>Transaction Recording Failed</source>
|
||||||
|
<translation>事务记录失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="380" />
|
||||||
|
<source>Cannot save the list of verified or pending deletion files. The update has stopped.</source>
|
||||||
|
<translation>无法保存已校验或待删除文件列表,更新已停止。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="383" />
|
||||||
|
<source>Closing the main application...</source>
|
||||||
|
<translation>正在关闭主程序...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="385" />
|
||||||
|
<source>Cannot Close Main Application</source>
|
||||||
|
<translation>无法关闭主程序</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="386" />
|
||||||
|
<source>%1 is still running. Please close it manually and try again.</source>
|
||||||
|
<translation>%1 仍在运行,请手动关闭后重试。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="389" />
|
||||||
|
<source>Backing up %1 files to be changed, including %2 files to be deleted...</source>
|
||||||
|
<translation>正在备份 %1 个待变更文件(其中删除 %2 个)...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="392" />
|
||||||
|
<source>Backup Failed</source>
|
||||||
|
<translation>备份失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="393" />
|
||||||
|
<source>Cannot back up current version files. The new version has not been installed. Please check disk space and directory permissions.</source>
|
||||||
|
<translation>无法备份当前版本文件,尚未安装新版本。请检查磁盘空间和目录权限。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="399" />
|
||||||
|
<location filename="../Updater/main.cpp" line="409" />
|
||||||
|
<location filename="../Updater/main.cpp" line="417" />
|
||||||
|
<location filename="../Updater/main.cpp" line="423" />
|
||||||
|
<source>Bootstrap Handoff Preparation Failed</source>
|
||||||
|
<translation>接管准备失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="400" />
|
||||||
|
<source>Cannot create the Bootstrap file plan.</source>
|
||||||
|
<translation>无法创建 Bootstrap 文件计划。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="410" />
|
||||||
|
<source>Cannot write the Bootstrap copy plan.</source>
|
||||||
|
<translation>无法写入 Bootstrap 复制计划。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="418" />
|
||||||
|
<source>Cannot write the Bootstrap deletion plan.</source>
|
||||||
|
<translation>无法写入 Bootstrap 删除计划。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="424" />
|
||||||
|
<source>Cannot commit the Bootstrap file plan or transaction state.</source>
|
||||||
|
<translation>无法提交 Bootstrap 文件计划或事务状态。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="427" />
|
||||||
|
<source>Delegating installation to Bootstrap...</source>
|
||||||
|
<translation>正在将安装工作移交给 Bootstrap...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="429" />
|
||||||
|
<source>Bootstrap Startup Failed</source>
|
||||||
|
<translation>Bootstrap 启动失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="430" />
|
||||||
|
<source>Cannot start the standalone update handoff program: %1</source>
|
||||||
|
<translation>无法启动独立更新接管程序:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="436" />
|
||||||
|
<source>Verifying Bootstrap installation result...</source>
|
||||||
|
<translation>正在校验 Bootstrap 安装结果...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="438" />
|
||||||
|
<source>Installation Verification Failed</source>
|
||||||
|
<translation>安装校验失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="439" />
|
||||||
|
<source>New version files failed verification after installation.</source>
|
||||||
|
<translation>新版本文件安装后校验未通过。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="442" />
|
||||||
|
<source>Obsolete File Cleanup Failed</source>
|
||||||
|
<translation>废弃文件清理失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="443" />
|
||||||
|
<source>An obsolete file still exists: %1</source>
|
||||||
|
<translation>废弃文件仍然存在:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="446" />
|
||||||
|
<source>Saving new version state...</source>
|
||||||
|
<translation>正在保存新版本状态...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="449" />
|
||||||
|
<source>State Save Failed</source>
|
||||||
|
<translation>状态保存失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="450" />
|
||||||
|
<source>Cannot save the current version number.</source>
|
||||||
|
<translation>无法保存当前版本号。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="454" />
|
||||||
|
<source>Starting the new version and waiting for health confirmation...</source>
|
||||||
|
<translation>正在启动新版本并等待健康确认...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="456" />
|
||||||
|
<source>Startup Failed</source>
|
||||||
|
<translation>启动失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="457" />
|
||||||
|
<source>%1 cannot be started.</source>
|
||||||
|
<translation>%1 无法启动。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="466" />
|
||||||
|
<source>Startup Confirmation Failed</source>
|
||||||
|
<translation>启动确认失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="467" />
|
||||||
|
<source>The new version did not complete startup health confirmation within %1 milliseconds.</source>
|
||||||
|
<translation>新版本在 %1 毫秒内没有完成启动健康确认。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="470" />
|
||||||
|
<source>Committing update transaction...</source>
|
||||||
|
<translation>正在提交更新事务...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="472" />
|
||||||
|
<source>Transaction Commit Failed</source>
|
||||||
|
<translation>事务提交失败</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="473" />
|
||||||
|
<source>The new version has started, but the update transaction could not be committed.</source>
|
||||||
|
<translation>新版本已经启动,但无法提交更新事务。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="481" />
|
||||||
|
<source>Update Complete</source>
|
||||||
|
<translation>更新完成</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/main.cpp" line="482" />
|
||||||
|
<source>The software has been successfully updated to %1 and passed the startup health check.</source>
|
||||||
|
<translation>软件已成功更新到 %1,并通过启动健康检查。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>UpdaterLogic</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="345" />
|
||||||
|
<source>Cannot open the offline update package</source>
|
||||||
|
<translation>无法打开离线更新包</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="346" />
|
||||||
|
<source>The offline package format identifier is invalid</source>
|
||||||
|
<translation>离线包格式标识无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="348" />
|
||||||
|
<source>The offline package header is incomplete</source>
|
||||||
|
<translation>离线包头不完整</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="351" />
|
||||||
|
<source>The offline package header length is invalid</source>
|
||||||
|
<translation>离线包头长度无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="354" />
|
||||||
|
<source>The offline package header JSON is invalid</source>
|
||||||
|
<translation>离线包头 JSON 无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="359" />
|
||||||
|
<source>The offline package RSA signature is invalid</source>
|
||||||
|
<translation>离线包 RSA 签名无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="361" />
|
||||||
|
<source>The offline package signature metadata is invalid</source>
|
||||||
|
<translation>离线包签名元数据无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="362" />
|
||||||
|
<source>The manifest digest does not match the package signature</source>
|
||||||
|
<translation>Manifest 摘要与包签名不一致</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="364" />
|
||||||
|
<source>The offline manifest is invalid</source>
|
||||||
|
<translation>离线 Manifest 无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="367" />
|
||||||
|
<source>Package information does not match manifest identity</source>
|
||||||
|
<translation>包信息与 Manifest 身份不一致</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="368" />
|
||||||
|
<source>The offline manifest RSA signature is invalid</source>
|
||||||
|
<translation>离线 Manifest RSA 签名无效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="374" />
|
||||||
|
<source>The offline package contains an unsafe path or out-of-range data: %1</source>
|
||||||
|
<translation>离线包包含不安全路径或越界数据:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="377" />
|
||||||
|
<source>Cannot prepare offline file: %1</source>
|
||||||
|
<translation>无法准备离线文件:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="378" />
|
||||||
|
<source>Cannot create staged file: %1</source>
|
||||||
|
<translation>无法创建暂存文件:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="380" />
|
||||||
|
<source>Failed to read offline file: %1</source>
|
||||||
|
<translation>离线文件读取失败:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="381" />
|
||||||
|
<source>Offline file hash verification or write failed: %1</source>
|
||||||
|
<translation>离线文件 Hash 或写入失败:%1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../Updater/UpdaterLogic.cpp" line="383" />
|
||||||
|
<source>The offline package file count does not match the manifest</source>
|
||||||
|
<translation>离线包文件数量与 Manifest 不一致</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
客户端脚本说明
|
||||||
|
==============
|
||||||
|
|
||||||
|
本目录保存 update-client 的辅助脚本。项目根目录只保留源码、CMake 入口、Docs 和配置模板,脚本统一放在这里。
|
||||||
|
|
||||||
|
脚本列表:
|
||||||
|
|
||||||
|
1. package-sdk.ps1
|
||||||
|
生成给其他软件接入用的 UpdateClientSDK 包。
|
||||||
|
|
||||||
|
2. package-client.ps1
|
||||||
|
生成某个具体产品的最终客户端发布包。
|
||||||
|
|
||||||
|
3. install-sdk.ps1
|
||||||
|
将 SDK 运行时复制到业务软件 Release 目录。
|
||||||
|
|
||||||
|
推荐在 update-client 根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\package-sdk.ps1 `
|
||||||
|
-SourceDir .\out\bin `
|
||||||
|
-OutputDir .\dist\UpdateClientSDK `
|
||||||
|
-ZipFile .\dist\UpdateClientSDK.zip `
|
||||||
|
-SdkVersion 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\package-client.ps1 `
|
||||||
|
-SourceDir .\out\bin `
|
||||||
|
-ConfigFile .\config\app_config.json `
|
||||||
|
-OutputDir .\dist\UpdateClient `
|
||||||
|
-ZipFile .\dist\UpdateClient.zip
|
||||||
|
```
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
param(
|
param(
|
||||||
[string]$SourceDir = "$PSScriptRoot/out/bin",
|
[string]$SourceDir = "",
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ConfigFile,
|
[string]$ConfigFile,
|
||||||
[string]$OutputDir = "$PSScriptRoot/dist/UpdateClient",
|
[string]$OutputDir = "",
|
||||||
[string]$ZipFile = "$PSScriptRoot/dist/UpdateClient.zip"
|
[string]$ZipFile = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
if ([string]::IsNullOrWhiteSpace($SourceDir)) {
|
||||||
|
$SourceDir = Join-Path $RepoRoot "out/bin"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($OutputDir)) {
|
||||||
|
$OutputDir = Join-Path $RepoRoot "dist/UpdateClient"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ZipFile)) {
|
||||||
|
$ZipFile = Join-Path $RepoRoot "dist/UpdateClient.zip"
|
||||||
|
}
|
||||||
|
|
||||||
$source = (Resolve-Path $SourceDir).Path
|
$source = (Resolve-Path $SourceDir).Path
|
||||||
$config = (Resolve-Path $ConfigFile).Path
|
$config = (Resolve-Path $ConfigFile).Path
|
||||||
$settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json
|
$settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
param(
|
param(
|
||||||
[string]$SourceDir = "$PSScriptRoot/out/bin",
|
[string]$SourceDir = "",
|
||||||
[string]$OutputDir = "$PSScriptRoot/dist/UpdateClientSDK",
|
[string]$OutputDir = "",
|
||||||
[string]$ZipFile = "$PSScriptRoot/dist/UpdateClientSDK.zip",
|
[string]$ZipFile = "",
|
||||||
[string]$SdkVersion = "0.1.0",
|
[string]$SdkVersion = "0.1.0",
|
||||||
[string]$ExampleConfig = "$PSScriptRoot/config/app_config.example.json",
|
[string]$ExampleConfig = "",
|
||||||
[switch]$IncludeDemoMainApp,
|
[switch]$IncludeDemoMainApp,
|
||||||
[switch]$IncludeQtRuntime
|
[switch]$IncludeQtRuntime
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
if ([string]::IsNullOrWhiteSpace($SourceDir)) {
|
||||||
|
$SourceDir = Join-Path $RepoRoot "out/bin"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($OutputDir)) {
|
||||||
|
$OutputDir = Join-Path $RepoRoot "dist/UpdateClientSDK"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ZipFile)) {
|
||||||
|
$ZipFile = Join-Path $RepoRoot "dist/UpdateClientSDK.zip"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ExampleConfig)) {
|
||||||
|
$ExampleConfig = Join-Path $RepoRoot "config/app_config.example.json"
|
||||||
|
}
|
||||||
|
|
||||||
$source = (Resolve-Path $SourceDir).Path
|
$source = (Resolve-Path $SourceDir).Path
|
||||||
$exampleConfigPath = (Resolve-Path $ExampleConfig).Path
|
$exampleConfigPath = (Resolve-Path $ExampleConfig).Path
|
||||||
|
|
||||||
@@ -24,7 +38,7 @@ foreach ($name in $requiredFiles) {
|
|||||||
$publicKeyCandidates = @(
|
$publicKeyCandidates = @(
|
||||||
(Join-Path $source "config/manifest_public_key.pem"),
|
(Join-Path $source "config/manifest_public_key.pem"),
|
||||||
(Join-Path $source "manifest_public_key.pem"),
|
(Join-Path $source "manifest_public_key.pem"),
|
||||||
(Join-Path $PSScriptRoot "config/manifest_public_key.pem")
|
(Join-Path $RepoRoot "config/manifest_public_key.pem")
|
||||||
)
|
)
|
||||||
$publicKey = $publicKeyCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
$publicKey = $publicKeyCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
if (-not $publicKey) {
|
if (-not $publicKey) {
|
||||||
@@ -86,11 +100,14 @@ Get-ChildItem $source -Force | Where-Object {
|
|||||||
Copy-Item $exampleConfigPath (Join-Path $configDir "app_config.json") -Force
|
Copy-Item $exampleConfigPath (Join-Path $configDir "app_config.json") -Force
|
||||||
Copy-Item $publicKey (Join-Path $configDir "manifest_public_key.pem") -Force
|
Copy-Item $publicKey (Join-Path $configDir "manifest_public_key.pem") -Force
|
||||||
|
|
||||||
$wordGuideSource = Get-ChildItem $PSScriptRoot -File -Filter "*.docx" | Where-Object {
|
$wordGuideSource = @($RepoRoot, (Join-Path $RepoRoot "Docs")) |
|
||||||
$_.Name -like "*SDK*.docx" -and $_.Name -notlike "~$*"
|
Where-Object { Test-Path $_ } |
|
||||||
} | Sort-Object Name | Select-Object -First 1
|
ForEach-Object { Get-ChildItem $_ -File -Filter "*.docx" } |
|
||||||
|
Where-Object { $_.Name -like "*SDK*.docx" -and $_.Name -notlike "~$*" } |
|
||||||
|
Sort-Object Name |
|
||||||
|
Select-Object -First 1
|
||||||
if (-not $wordGuideSource) {
|
if (-not $wordGuideSource) {
|
||||||
throw "SDK integration Word guide is missing. Expected a *SDK*.docx file in the client directory."
|
throw "SDK integration Word guide is missing. Expected a *SDK*.docx file in the repository root or Docs directory."
|
||||||
}
|
}
|
||||||
Copy-Item $wordGuideSource.FullName (Join-Path $OutputDir $wordGuideSource.Name) -Force
|
Copy-Item $wordGuideSource.FullName (Join-Path $OutputDir $wordGuideSource.Name) -Force
|
||||||
|
|
||||||
Reference in New Issue
Block a user