feat: 优化客户端配置和跨平台支持
This commit is contained in:
@@ -3,6 +3,7 @@ project(Bootstrap LANGUAGES CXX)
|
|||||||
add_executable(Bootstrap
|
add_executable(Bootstrap
|
||||||
main.cpp
|
main.cpp
|
||||||
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
|
${CMAKE_SOURCE_DIR}/config/server_config.qrc
|
||||||
)
|
)
|
||||||
target_compile_features(Bootstrap PRIVATE cxx_std_17)
|
target_compile_features(Bootstrap PRIVATE cxx_std_17)
|
||||||
target_link_libraries(Bootstrap PRIVATE Qt5::Core Qt5::Widgets)
|
target_link_libraries(Bootstrap PRIVATE Qt5::Core Qt5::Widgets)
|
||||||
|
|||||||
+59
-2
@@ -43,6 +43,8 @@ const QString kRegistrySourceHashKey = QStringLiteral("source_sha256");
|
|||||||
const QString kRegistrySourcePathKey = QStringLiteral("source_path");
|
const QString kRegistrySourcePathKey = QStringLiteral("source_path");
|
||||||
const QString kRegistryRuntimeRootKey = QStringLiteral("runtime_root");
|
const QString kRegistryRuntimeRootKey = QStringLiteral("runtime_root");
|
||||||
const QString kRegistryImportedAtKey = QStringLiteral("imported_at_utc");
|
const QString kRegistryImportedAtKey = QStringLiteral("imported_at_utc");
|
||||||
|
const QString kEmbeddedServerConfigPath = QStringLiteral(":/simcae/server_config.json");
|
||||||
|
const QString kApiBaseUrlKey = QStringLiteral("api_base_url");
|
||||||
|
|
||||||
QString encodeArgument(const QString& value)
|
QString encodeArgument(const QString& value)
|
||||||
{
|
{
|
||||||
@@ -169,6 +171,22 @@ bool writeConfigValueToFile(const QString& configPath, const QString& key,
|
|||||||
return writeBytesToFile(configPath, payload, errorMessage);
|
return writeBytesToFile(configPath, payload, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isRegistryManagedConfigKey(const QString& key)
|
||||||
|
{
|
||||||
|
return key != kApiBaseUrlKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject registryManagedConfigObject(const QJsonObject& source)
|
||||||
|
{
|
||||||
|
QJsonObject result;
|
||||||
|
for (auto it = source.constBegin(); it != source.constEnd(); ++it)
|
||||||
|
{
|
||||||
|
if (isRegistryManagedConfigKey(it.key()))
|
||||||
|
result.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
QString windowsErrorMessage(DWORD errorCode)
|
QString windowsErrorMessage(DWORD errorCode)
|
||||||
{
|
{
|
||||||
@@ -421,6 +439,7 @@ ConfigHelper::ConfigHelper()
|
|||||||
QCryptographicHash::Sha256).toHex());
|
QCryptographicHash::Sha256).toHex());
|
||||||
migrateLegacyIniIfNeeded();
|
migrateLegacyIniIfNeeded();
|
||||||
syncRegistryFromConfigFileIfChanged();
|
syncRegistryFromConfigFileIfChanged();
|
||||||
|
removeRegistryValue(kApiBaseUrlKey);
|
||||||
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;
|
qDebug() << "Registry installation id:" << m_registryInstallId;
|
||||||
@@ -492,7 +511,8 @@ bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QByteArray normalizedConfig = QJsonDocument(document.object()).toJson(QJsonDocument::Compact);
|
const QJsonObject config = registryManagedConfigObject(document.object());
|
||||||
|
const QByteArray normalizedConfig = QJsonDocument(config).toJson(QJsonDocument::Compact);
|
||||||
const QString sourceHash = QString::fromLatin1(
|
const QString sourceHash = QString::fromLatin1(
|
||||||
QCryptographicHash::hash(normalizedConfig, QCryptographicHash::Sha256).toHex());
|
QCryptographicHash::hash(normalizedConfig, QCryptographicHash::Sha256).toHex());
|
||||||
|
|
||||||
@@ -506,7 +526,6 @@ bool ConfigHelper::syncRegistryFromConfigFileIfChanged()
|
|||||||
if (previousHash == sourceHash)
|
if (previousHash == sourceHash)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
const QJsonObject config = document.object();
|
|
||||||
if (config.isEmpty())
|
if (config.isEmpty())
|
||||||
{
|
{
|
||||||
settings.beginGroup(kRegistryMetaGroup);
|
settings.beginGroup(kRegistryMetaGroup);
|
||||||
@@ -614,6 +633,35 @@ bool ConfigHelper::sanitizeConfigFileAfterImport(QSettings& settings)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ConfigHelper::removeRegistryValue(const QString& key) const
|
||||||
|
{
|
||||||
|
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||||
|
kRegistryOrganization, kRegistryApplication);
|
||||||
|
enterRegistryGroup(settings);
|
||||||
|
settings.beginGroup(kRegistryConfigGroup);
|
||||||
|
settings.remove(key);
|
||||||
|
settings.endGroup();
|
||||||
|
settings.sync();
|
||||||
|
return settings.status() == QSettings::NoError;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ConfigHelper::readEmbeddedValue(const QString& key) const
|
||||||
|
{
|
||||||
|
if (key != kApiBaseUrlKey)
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
QFile file(kEmbeddedServerConfigPath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
|
||||||
|
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
return document.object().value(key).toVariant().toString().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
bool ConfigHelper::readRegistryValue(const QString& key, QString* value) const
|
bool ConfigHelper::readRegistryValue(const QString& key, QString* value) const
|
||||||
{
|
{
|
||||||
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
QSettings settings(QSettings::NativeFormat, QSettings::UserScope,
|
||||||
@@ -653,6 +701,9 @@ bool ConfigHelper::writeRegistryValue(const QString& key, const QString& value)
|
|||||||
|
|
||||||
QString ConfigHelper::readFileValue(const QString& key) const
|
QString ConfigHelper::readFileValue(const QString& key) const
|
||||||
{
|
{
|
||||||
|
if (!isRegistryManagedConfigKey(key))
|
||||||
|
return QString();
|
||||||
|
|
||||||
QFile file(m_configPath);
|
QFile file(m_configPath);
|
||||||
if (!file.open(QIODevice::ReadOnly))
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
return QString();
|
return QString();
|
||||||
@@ -668,6 +719,12 @@ QString ConfigHelper::readFileValue(const QString& key) const
|
|||||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(section);
|
Q_UNUSED(section);
|
||||||
|
const QString embeddedValue = readEmbeddedValue(key);
|
||||||
|
if (!embeddedValue.isEmpty())
|
||||||
|
return embeddedValue;
|
||||||
|
if (!isRegistryManagedConfigKey(key))
|
||||||
|
return QString();
|
||||||
|
|
||||||
QString value;
|
QString value;
|
||||||
if (readRegistryValue(key, &value))
|
if (readRegistryValue(key, &value))
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ private:
|
|||||||
void enterRegistryGroup(QSettings& settings) const;
|
void enterRegistryGroup(QSettings& settings) const;
|
||||||
bool syncRegistryFromConfigFileIfChanged();
|
bool syncRegistryFromConfigFileIfChanged();
|
||||||
bool sanitizeConfigFileAfterImport(QSettings& settings);
|
bool sanitizeConfigFileAfterImport(QSettings& settings);
|
||||||
|
bool removeRegistryValue(const QString& key) const;
|
||||||
|
QString readEmbeddedValue(const QString& key) const;
|
||||||
bool readRegistryValue(const QString& key, QString* value) const;
|
bool readRegistryValue(const QString& key, QString* value) const;
|
||||||
bool writeRegistryValue(const QString& key, const QString& value);
|
bool writeRegistryValue(const QString& key, const QString& value);
|
||||||
QString readFileValue(const QString& key) const;
|
QString readFileValue(const QString& key) const;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token
|
|||||||
const QString trimmedBase=base.trimmed();
|
const QString trimmedBase=base.trimmed();
|
||||||
if(appId.trimmed().isEmpty()){m_error="app_id is empty in config/app_config.json";return false;}
|
if(appId.trimmed().isEmpty()){m_error="app_id is empty in config/app_config.json";return false;}
|
||||||
if(channel.trimmed().isEmpty()){m_error="channel is empty in config/app_config.json";return false;}
|
if(channel.trimmed().isEmpty()){m_error="channel is empty in config/app_config.json";return false;}
|
||||||
if(trimmedBase.isEmpty()||trimmedBase.contains("YOUR_SERVER_IP",Qt::CaseInsensitive)){m_error="api_base_url is not configured. Set it to the update server address, for example http://192.168.229.128:8000";return false;}
|
if(trimmedBase.isEmpty()||trimmedBase.contains("YOUR_SERVER_IP",Qt::CaseInsensitive)){m_error="api_base_url is not configured. Set config/server_config.json before building the client, for example http://192.168.229.128:8000";return false;}
|
||||||
if(token.trimmed().isEmpty()){m_error="client_token is empty in config/app_config.json";return false;}
|
if(token.trimmed().isEmpty()){m_error="client_token is empty in config/app_config.json";return false;}
|
||||||
if(licenseKey.trimmed().isEmpty()){m_error="license_key is empty. Create a License in the admin page and paste the generated key into config/app_config.json";return false;}
|
if(licenseKey.trimmed().isEmpty()){m_error="license_key is empty. Create a License in the admin page and paste the generated key into config/app_config.json";return false;}
|
||||||
ConfigHelper& config=ConfigHelper::instance();
|
ConfigHelper& config=ConfigHelper::instance();
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ bash ./scripts/package-sdk.sh --source-dir ./out/linux/bin --output-dir ./dist/U
|
|||||||
==============
|
==============
|
||||||
|
|
||||||
config/app_config.json 是部署配置源文件。Launcher / Updater / MainApp 启动时会把它同步到当前用户的 QSettings 配置区;Windows 下对应注册表,Linux 下对应用户配置文件。后续运行配置优先从 QSettings 读取。
|
config/app_config.json 是部署配置源文件。Launcher / Updater / MainApp 启动时会把它同步到当前用户的 QSettings 配置区;Windows 下对应注册表,Linux 下对应用户配置文件。后续运行配置优先从 QSettings 读取。
|
||||||
|
config/server_config.json 是编译期服务端地址配置源文件。它通过 config/server_config.qrc 编进 Launcher / Updater / MainApp,不写入 app_config.json,也不写入注册表。修改 api_base_url 后必须重新编译客户端程序才会生效。
|
||||||
如果 config/app_config.json 内容被修改,下一次启动时会按解析后的 JSON 内容 SHA256 判断变化并重新导入注册表。
|
如果 config/app_config.json 内容被修改,下一次启动时会按解析后的 JSON 内容 SHA256 判断变化并重新导入注册表。
|
||||||
非空 app_config.json 成功导入注册表后会自动清空为 {},文件保留不删除,方便下次直接粘贴管理后台生成的新配置。
|
非空 app_config.json 成功导入注册表后会自动清空为 {},文件保留不删除,方便下次直接粘贴管理后台生成的新配置。
|
||||||
Windows 注册表位置:HKEY_CURRENT_USER\Software\Marsco\UpdateClientSDK\installations\<安装目录SHA256>\config。
|
Windows 注册表位置:HKEY_CURRENT_USER\Software\Marsco\UpdateClientSDK\installations\<安装目录SHA256>\config。
|
||||||
@@ -55,10 +56,11 @@ Linux 配置位置由 Qt QSettings 决定,通常在当前用户 home 目录的
|
|||||||
接入新软件时通常需要修改:
|
接入新软件时通常需要修改:
|
||||||
|
|
||||||
1. app_id、app_name、channel、current_version。
|
1. app_id、app_name、channel、current_version。
|
||||||
2. api_base_url、client_token、license_key、launch_token。
|
2. client_token、license_key、launch_token。
|
||||||
3. main_executable:团队业务主程序文件名。
|
3. config/server_config.json 里的 api_base_url。
|
||||||
4. launcher_executable、updater_executable、bootstrap_executable。
|
4. main_executable:团队业务主程序文件名。
|
||||||
5. health_check_timeout_ms:升级后等待业务程序健康确认的毫秒数,最小 1000。
|
5. launcher_executable、updater_executable、bootstrap_executable。
|
||||||
|
6. health_check_timeout_ms:升级后等待业务程序健康确认的毫秒数,最小 1000。
|
||||||
|
|
||||||
Windows 完整格式参考 update-client/config/app_config.example.json;Linux 完整格式参考 update-client/config/app_config.linux.example.json。
|
Windows 完整格式参考 update-client/config/app_config.example.json;Linux 完整格式参考 update-client/config/app_config.linux.example.json。
|
||||||
运行时生成的 client_identity.dat、local_state.json 等文件不得打入通用 SDK 模板。app_config.json 可以作为部署模板,但不要把某台机器运行后产生的临时状态混进去。
|
运行时生成的 client_identity.dat、local_state.json 等文件不得打入通用 SDK 模板。app_config.json 可以作为部署模板,但不要把某台机器运行后产生的临时状态混进去。
|
||||||
@@ -67,7 +69,7 @@ Windows 发布打包:
|
|||||||
|
|
||||||
1. 使用 Release 配置编译全部客户端程序。
|
1. 使用 Release 配置编译全部客户端程序。
|
||||||
2. 先完成当前版本在线校验,确认 out/bin/update/manifest_cache 中存在对应的签名 Manifest。
|
2. 先完成当前版本在线校验,确认 out/bin/update/manifest_cache 中存在对应的签名 Manifest。
|
||||||
3. 准备一份实际 app_config.json,确认其中包含正确的 License Key、当前版本和业务程序名。
|
3. 准备一份实际 app_config.json,确认其中包含正确的 License Key、当前版本和业务程序名;确认客户端程序已用正确的 config/server_config.json 编译。
|
||||||
4. 在 PowerShell 执行:
|
4. 在 PowerShell 执行:
|
||||||
powershell -ExecutionPolicy Bypass -File .\scripts\package-client.ps1 -ConfigFile .\config\app_config.json
|
powershell -ExecutionPolicy Bypass -File .\scripts\package-client.ps1 -ConfigFile .\config\app_config.json
|
||||||
5. 输出位于 dist/UpdateClient 和 dist/UpdateClient.zip。
|
5. 输出位于 dist/UpdateClient 和 dist/UpdateClient.zip。
|
||||||
|
|||||||
+17
-7
@@ -8,7 +8,7 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 重新生成给别人用的 SDK 包 | 二、维护者:生成 SDK 包 |
|
| 重新生成给别人用的 SDK 包 | 二、维护者:生成 SDK 包 |
|
||||||
| 把 SDK 放到 SimCAE 或其他业务软件目录 | 三、你:把 SDK 放进业务软件目录 |
|
| 把 SDK 放到 SimCAE 或其他业务软件目录 | 三、你:把 SDK 放进业务软件目录 |
|
||||||
| 从后台生成 `app_config.json` | 四、你:生成并填写 app_config.json |
|
| 从后台生成 `app_config.json` 和 qrc 服务端配置 | 四、你:生成并填写客户端配置 |
|
||||||
| 给业务主程序接入启动保护代码 | 五、你:业务主程序接入要求 |
|
| 给业务主程序接入启动保护代码 | 五、你:业务主程序接入要求 |
|
||||||
| 验证升级、回滚、健康检查 | 六、你:联调测试 |
|
| 验证升级、回滚、健康检查 | 六、你:联调测试 |
|
||||||
| 生成最终交付给用户的客户端包 | 七、维护者:生成最终客户端包 |
|
| 生成最终交付给用户的客户端包 | 七、维护者:生成最终客户端包 |
|
||||||
@@ -23,6 +23,7 @@ SDK 核心程序:
|
|||||||
- `Updater.exe` / `Updater`:下载、校验、备份、安装、健康确认、提交或回滚。
|
- `Updater.exe` / `Updater`:下载、校验、备份、安装、健康确认、提交或回滚。
|
||||||
- `Bootstrap.exe` / `Bootstrap`:处理运行中可能被占用的 EXE/DLL 或 Linux 可执行文件替换。
|
- `Bootstrap.exe` / `Bootstrap`:处理运行中可能被占用的 EXE/DLL 或 Linux 可执行文件替换。
|
||||||
- `config/app_config.json`:部署配置源文件。启动时会同步到当前用户的 QSettings 配置区;Windows 下对应注册表,Linux 下对应用户配置文件。
|
- `config/app_config.json`:部署配置源文件。启动时会同步到当前用户的 QSettings 配置区;Windows 下对应注册表,Linux 下对应用户配置文件。
|
||||||
|
- `config/server_config.json`:编译期服务端地址配置源文件,通过 `config/server_config.qrc` 编进 Launcher / Updater / MainApp,不写入 `app_config.json` 或注册表。
|
||||||
- `config/manifest_public_key.pem`:Manifest 签名公钥,用来验证服务端发布包没有被篡改。
|
- `config/manifest_public_key.pem`:Manifest 签名公钥,用来验证服务端发布包没有被篡改。
|
||||||
|
|
||||||
## 二、维护者:生成 SDK 包
|
## 二、维护者:生成 SDK 包
|
||||||
@@ -178,7 +179,7 @@ D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
|||||||
|
|
||||||
注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应复用 SimCAE 的 `Qt5*.dll`、`platforms/`、`imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则可能出现“无法定位程序输入点”一类错误。
|
注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应复用 SimCAE 的 `Qt5*.dll`、`platforms/`、`imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则可能出现“无法定位程序输入点”一类错误。
|
||||||
|
|
||||||
## 四、你:生成并填写 app_config.json
|
## 四、你:生成并填写客户端配置
|
||||||
|
|
||||||
最推荐的方式是在服务端管理后台生成客户端配置:
|
最推荐的方式是在服务端管理后台生成客户端配置:
|
||||||
|
|
||||||
@@ -188,11 +189,13 @@ D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
|||||||
4. 创建 License。
|
4. 创建 License。
|
||||||
5. 在“客户端配置生成”区域选择应用、渠道、License 和主程序名。
|
5. 在“客户端配置生成”区域选择应用、渠道、License 和主程序名。
|
||||||
6. 点击生成配置。
|
6. 点击生成配置。
|
||||||
7. 复制生成的 JSON,覆盖 `D:\SimCAE\bin\config\app_config.json`。
|
7. 复制“客户端 app_config.json”,覆盖 `D:\SimCAE\bin\config\app_config.json`。
|
||||||
|
8. 复制“qrc 服务端配置 server_config.json”,覆盖 `update-client\config\server_config.json`,然后重新编译 Launcher / Updater / Bootstrap。这个文件会被 `config/server_config.qrc` 编进程序,不会放进用户机器的 `app_config.json` 或注册表。
|
||||||
|
|
||||||
配置同步规则:
|
配置同步规则:
|
||||||
|
|
||||||
- `app_config.json` 是部署配置源文件,适合交付、复制、人工修改。
|
- `app_config.json` 是部署配置源文件,适合交付、复制、人工修改。
|
||||||
|
- `api_base_url` 不再属于 `app_config.json` 字段。它只存在于 `config/server_config.json`,并通过 qrc 编进程序。
|
||||||
- Launcher / Updater / MainApp 启动时会计算 `app_config.json` 解析后的 JSON 内容 SHA256;如果 JSON 内容和上次导入时不同,就把文件里的配置重新写入当前 Windows 用户的注册表。
|
- Launcher / Updater / MainApp 启动时会计算 `app_config.json` 解析后的 JSON 内容 SHA256;如果 JSON 内容和上次导入时不同,就把文件里的配置重新写入当前 Windows 用户的注册表。
|
||||||
- 后续运行时优先从注册表读取配置,不再每次直接读 JSON。
|
- 后续运行时优先从注册表读取配置,不再每次直接读 JSON。
|
||||||
- 运行过程中产生的动态值,例如首次输入的 `license_key`、服务端返回的 `device_id`、升级后的 `current_version`,会写入注册表。
|
- 运行过程中产生的动态值,例如首次输入的 `license_key`、服务端返回的 `device_id`、升级后的 `current_version`,会写入注册表。
|
||||||
@@ -213,7 +216,6 @@ D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
|||||||
- `client_protocol`:客户端协议号,当前建议为 `3`。
|
- `client_protocol`:客户端协议号,当前建议为 `3`。
|
||||||
- `launch_token`:本机启动票据 HMAC 密钥。服务端生成配置时会填默认值;正式部署建议按项目统一修改。
|
- `launch_token`:本机启动票据 HMAC 密钥。服务端生成配置时会填默认值;正式部署建议按项目统一修改。
|
||||||
- `license_key`:管理后台创建 License 后生成的授权码。为空时首次启动 `Launcher.exe` 会弹窗让用户输入并保存到注册表;如果授权错误或过期,也会提示重新输入。
|
- `license_key`:管理后台创建 License 后生成的授权码。为空时首次启动 `Launcher.exe` 会弹窗让用户输入并保存到注册表;如果授权错误或过期,也会提示重新输入。
|
||||||
- `api_base_url`:服务端 API 地址,例如 `http://192.168.229.128:8000`。
|
|
||||||
- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,必须和服务端一致。
|
- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,必须和服务端一致。
|
||||||
- `device_id`:设备 ID。一般可以留空,首次启动时 SDK 会向服务端登记并写入注册表。
|
- `device_id`:设备 ID。一般可以留空,首次启动时 SDK 会向服务端登记并写入注册表。
|
||||||
- `install_root`:被更新的安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`。
|
- `install_root`:被更新的安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`。
|
||||||
@@ -232,7 +234,6 @@ D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
|||||||
"client_protocol": "3",
|
"client_protocol": "3",
|
||||||
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
||||||
"license_key": "MARSCO-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
"license_key": "MARSCO-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
"api_base_url": "http://192.168.229.128:8000",
|
|
||||||
"client_token": "SimCAEClientToken2026",
|
"client_token": "SimCAEClientToken2026",
|
||||||
"request_timeout_ms": "5000",
|
"request_timeout_ms": "5000",
|
||||||
"temp_folder": "update_temp",
|
"temp_folder": "update_temp",
|
||||||
@@ -248,6 +249,14 @@ D:\SimCAE_SDK\scripts\install-sdk.ps1 `
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
对应的 `config/server_config.json` 示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"api_base_url": "http://192.168.229.128:8000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 五、你:业务主程序需要配合什么
|
## 五、你:业务主程序需要配合什么
|
||||||
|
|
||||||
当前安全模式下,业务主程序需要配合两件事:
|
当前安全模式下,业务主程序需要配合两件事:
|
||||||
@@ -309,6 +318,7 @@ TicketHelper::consumeAndVerify(
|
|||||||
3. 创建 License。
|
3. 创建 License。
|
||||||
4. 发布一个初始版本,例如 `1.0.0`。
|
4. 发布一个初始版本,例如 `1.0.0`。
|
||||||
5. 生成客户端配置,并写入 `bin\config\app_config.json`。客户端下次启动时会自动同步到注册表。
|
5. 生成客户端配置,并写入 `bin\config\app_config.json`。客户端下次启动时会自动同步到注册表。
|
||||||
|
6. 生成 qrc 服务端配置,并写入源码目录 `config\server_config.json` 后重新编译 SDK 程序。
|
||||||
|
|
||||||
为什么必须先发布初始版本:Launcher 启动业务主程序前会做 Manifest 完整性校验。这个 Manifest 是服务端发布版本时生成并签名的清单,用来证明当前本地文件属于一个可信版本。如果没有发布过 `current_version` 对应版本,客户端会提示签名 Manifest 缓存缺失。
|
为什么必须先发布初始版本:Launcher 启动业务主程序前会做 Manifest 完整性校验。这个 Manifest 是服务端发布版本时生成并签名的清单,用来证明当前本地文件属于一个可信版本。如果没有发布过 `current_version` 对应版本,客户端会提示签名 Manifest 缓存缺失。
|
||||||
|
|
||||||
@@ -319,7 +329,7 @@ TicketHelper::consumeAndVerify(
|
|||||||
基础联调步骤:
|
基础联调步骤:
|
||||||
|
|
||||||
1. 确认服务端正在运行。
|
1. 确认服务端正在运行。
|
||||||
2. 确认 `bin\config\app_config.json` 中 `api_base_url`、`client_token`、`license_key`、`current_version` 正确。
|
2. 确认 `bin\config\app_config.json` 中 `client_token`、`license_key`、`current_version` 正确,并确认 `Launcher.exe` 已用正确的 `config/server_config.json` 重新编译。
|
||||||
3. 双击 `bin\Launcher.exe`。
|
3. 双击 `bin\Launcher.exe`。
|
||||||
4. 首次启动时如果 `license_key` 为空,按弹窗输入后台创建的 License;SDK 会把它保存到注册表。
|
4. 首次启动时如果 `license_key` 为空,按弹窗输入后台创建的 License;SDK 会把它保存到注册表。
|
||||||
5. 成功进入业务主程序后,回到后台查看设备、升级日志、下载日志。
|
5. 成功进入业务主程序后,回到后台查看设备、升级日志、下载日志。
|
||||||
@@ -398,7 +408,7 @@ Linux 下如果程序安装在 `/opt`、`/usr/local` 等普通用户不可写目
|
|||||||
|
|
||||||
1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。
|
1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。
|
||||||
2. 首次启动保存配置/状态文件失败:如果目录不可写,SDK 会弹出管理员权限确认框;用户取消或当前账号没有管理员权限时仍会失败。
|
2. 首次启动保存配置/状态文件失败:如果目录不可写,SDK 会弹出管理员权限确认框;用户取消或当前账号没有管理员权限时仍会失败。
|
||||||
3. 首次启动设备登记失败:检查 `api_base_url`、`client_token`、`license_key`、服务端 License 状态。
|
3. 首次启动设备登记失败:检查编译进 qrc 的 `config/server_config.json`、`client_token`、`license_key`、服务端 License 状态。
|
||||||
4. 提示 License 错误或过期:在后台确认 License 是否存在、是否被禁用或删除、是否超过最大设备数。
|
4. 提示 License 错误或过期:在后台确认 License 是否存在、是否被禁用或删除、是否超过最大设备数。
|
||||||
5. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配。
|
5. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配。
|
||||||
6. 提示 signed manifest cache missing:先在后台发布一次 `current_version` 对应版本,并让客户端拿到该版本 Manifest。
|
6. 提示 signed manifest cache missing:先在后台发布一次 `current_version` 对应版本,并让客户端拿到该版本 Manifest。
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ set(CMAKE_AUTORCC ON)
|
|||||||
# 所有源码文件
|
# 所有源码文件
|
||||||
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
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
)
|
${CMAKE_SOURCE_DIR}/config/server_config.qrc
|
||||||
|
)
|
||||||
# 生成可执行程序
|
# 生成可执行程序
|
||||||
add_executable(Launcher ${SRC_LIST})
|
add_executable(Launcher ${SRC_LIST})
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ project(MainApp)
|
|||||||
add_executable(MainApp
|
add_executable(MainApp
|
||||||
main.cpp
|
main.cpp
|
||||||
MainWindow.h
|
MainWindow.h
|
||||||
MainWindow.cpp
|
MainWindow.cpp
|
||||||
../Common/ConfigHelper.h
|
../Common/ConfigHelper.h
|
||||||
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
)
|
${CMAKE_SOURCE_DIR}/config/server_config.qrc
|
||||||
|
)
|
||||||
target_include_directories(MainApp
|
target_include_directories(MainApp
|
||||||
PUBLIC ${CMAKE_SOURCE_DIR}/Common
|
PUBLIC ${CMAKE_SOURCE_DIR}/Common
|
||||||
PRIVATE ${OPENSSL_INC}
|
PRIVATE ${OPENSSL_INC}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ set(SRC
|
|||||||
main.cpp
|
main.cpp
|
||||||
UpdaterLogic.h
|
UpdaterLogic.h
|
||||||
UpdaterLogic.cpp
|
UpdaterLogic.cpp
|
||||||
UpdateTransaction.h
|
UpdateTransaction.h
|
||||||
UpdateTransaction.cpp
|
UpdateTransaction.cpp
|
||||||
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
${CMAKE_SOURCE_DIR}/i18n/update-client.qrc
|
||||||
)
|
${CMAKE_SOURCE_DIR}/config/server_config.qrc
|
||||||
|
)
|
||||||
add_executable(Updater ${SRC})
|
add_executable(Updater ${SRC})
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"current_version": "1.0.0",
|
"current_version": "1.0.0",
|
||||||
"client_protocol": "3",
|
"client_protocol": "3",
|
||||||
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
||||||
"license_key": "",
|
"license_key": "",
|
||||||
"api_base_url": "http://YOUR_SERVER_IP:8000",
|
|
||||||
"client_token": "SimCAEClientToken2026",
|
"client_token": "SimCAEClientToken2026",
|
||||||
"request_timeout_ms": "5000",
|
"request_timeout_ms": "5000",
|
||||||
"temp_folder": "update_temp",
|
"temp_folder": "update_temp",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"client_protocol": "3",
|
"client_protocol": "3",
|
||||||
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
"launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes",
|
||||||
"license_key": "",
|
"license_key": "",
|
||||||
"api_base_url": "http://YOUR_SERVER_IP:8000",
|
|
||||||
"client_token": "SimCAEClientToken2026",
|
"client_token": "SimCAEClientToken2026",
|
||||||
"request_timeout_ms": "5000",
|
"request_timeout_ms": "5000",
|
||||||
"temp_folder": "update_temp",
|
"temp_folder": "update_temp",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"api_base_url": "http://YOUR_SERVER_IP:8000"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/simcae">
|
||||||
|
<file>server_config.json</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
@@ -48,11 +48,11 @@ function Join-RelativePath([string]$Base, [string]$Child) {
|
|||||||
return "$baseNorm/$childNorm"
|
return "$baseNorm/$childNorm"
|
||||||
}
|
}
|
||||||
|
|
||||||
$requiredFields = @(
|
$requiredFields = @(
|
||||||
"app_id", "channel", "api_base_url", "current_version",
|
"app_id", "channel", "current_version",
|
||||||
"client_token", "launch_token", "license_key",
|
"client_token", "launch_token", "license_key",
|
||||||
"main_executable", "launcher_executable", "updater_executable", "bootstrap_executable"
|
"main_executable", "launcher_executable", "updater_executable", "bootstrap_executable"
|
||||||
)
|
)
|
||||||
foreach ($field in $requiredFields) {
|
foreach ($field in $requiredFields) {
|
||||||
if (-not $settings.$field) {
|
if (-not $settings.$field) {
|
||||||
throw "Config file is missing required field: $field"
|
throw "Config file is missing required field: $field"
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ CONFIG_FILE="$(realpath "$CONFIG_FILE")"
|
|||||||
OUTPUT_DIR="$(realpath -m "$OUTPUT_DIR")"
|
OUTPUT_DIR="$(realpath -m "$OUTPUT_DIR")"
|
||||||
ARCHIVE_FILE="$(realpath -m "$ARCHIVE_FILE")"
|
ARCHIVE_FILE="$(realpath -m "$ARCHIVE_FILE")"
|
||||||
|
|
||||||
for field in app_id channel api_base_url current_version client_token launch_token license_key main_executable launcher_executable updater_executable bootstrap_executable; do
|
for field in app_id channel current_version client_token launch_token license_key main_executable launcher_executable updater_executable bootstrap_executable; do
|
||||||
if [[ -z "$(json_value "$field")" ]]; then
|
if [[ -z "$(json_value "$field")" ]]; then
|
||||||
echo "Config file is missing required field: $field" >&2
|
echo "Config file is missing required field: $field" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ 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
|
||||||
|
Copy-Item (Join-Path $RepoRoot "config/server_config.json") (Join-Path $configDir "server_config.json") -Force
|
||||||
|
Copy-Item (Join-Path $RepoRoot "config/server_config.qrc") (Join-Path $configDir "server_config.qrc") -Force
|
||||||
|
|
||||||
$commonSourceDir = Join-Path $RepoRoot "Common"
|
$commonSourceDir = Join-Path $RepoRoot "Common"
|
||||||
$commonSourceFiles = @(
|
$commonSourceFiles = @(
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ shopt -u dotglob nullglob
|
|||||||
|
|
||||||
cp "$EXAMPLE_CONFIG" "$OUTPUT_DIR/config/app_config.json"
|
cp "$EXAMPLE_CONFIG" "$OUTPUT_DIR/config/app_config.json"
|
||||||
cp "$PUBLIC_KEY" "$OUTPUT_DIR/config/manifest_public_key.pem"
|
cp "$PUBLIC_KEY" "$OUTPUT_DIR/config/manifest_public_key.pem"
|
||||||
|
cp "$REPO_ROOT/config/server_config.json" "$OUTPUT_DIR/config/server_config.json"
|
||||||
|
cp "$REPO_ROOT/config/server_config.qrc" "$OUTPUT_DIR/config/server_config.qrc"
|
||||||
|
|
||||||
for common_file in ConfigHelper.h ConfigHelper.cpp TicketHelper.h TicketHelper.cpp; do
|
for common_file in ConfigHelper.h ConfigHelper.cpp TicketHelper.h TicketHelper.cpp; do
|
||||||
common_path="$REPO_ROOT/Common/$common_file"
|
common_path="$REPO_ROOT/Common/$common_file"
|
||||||
|
|||||||
Reference in New Issue
Block a user