feat(client): 迁移Hub更新客户端实现

This commit is contained in:
2026-09-08 17:08:58 +08:00
parent 0c500024e6
commit 8dfef45bc4
34 changed files with 2032 additions and 2903 deletions
+258 -214
View File
@@ -1,243 +1,287 @@
#include "UpdateLogic.h"
#include "ConfigHelper.h"
#include <QDebug>
#include <QJsonArray>
#include <QApplication>
#include <QDir>
#include <QSaveFile>
#include "PolicyHelper.h"
#include "LocalStateHelper.h"
UpdateLogic::UpdateLogic(QObject* parent)
: QObject(parent)
{
ConfigHelper& cfg = ConfigHelper::instance();
m_serverAddr = cfg.getValue("Server", "api_base_url");
m_appId = cfg.getValue("App", "app_id");
m_curVer = cfg.getValue("App", "current_version");
m_channel = cfg.getValue("App", "channel");
// Debug print config
qDebug() << "Read server addr:" << m_serverAddr;
qDebug() << "Read app id:" << m_appId;
}
void UpdateLogic::checkUpdate()
{
if (m_serverAddr.isEmpty() || m_appId.isEmpty() || m_curVer.isEmpty() || m_channel.isEmpty())
{
qDebug() << "Config incomplete, abort update check";
m_needUpdate = false;
return;
}
QString url = m_serverAddr + "/api/v1/update/check";
QJsonObject body;
body["app_id"] = m_appId;
body["current_version"] = m_curVer;
body["channel"] = m_channel;
const int configuredProtocol = ConfigHelper::instance().getValue("App", "client_protocol").toInt();
body["client_protocol"] = qMax(3, configuredProtocol);
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
{
qDebug() << "Check update HTTP code:" << code;
m_lastStatusCode = code;
m_checkResp = resp;
m_networkOk = (code == 200);
if (code == 200)
{
const QString appDir = QApplication::applicationDirPath();
PolicyHelper onlinePolicy(appDir);
LocalStateHelper state(appDir);
const bool stateLoaded = state.loadState();
const bool policyValid = onlinePolicy.loadPolicyObject(
resp.value("policy").toObject(), resp.value("policy_text").toString());
if (!policyValid || !stateLoaded
|| state.isPolicySeqRolledBack(onlinePolicy.policySeq())
|| !onlinePolicy.savePolicy())
{
qDebug() << "Online policy rejected:" << onlinePolicy.errorString();
m_networkOk = false;
m_needUpdate = false;
return;
}
state.updateOnlineVerified(onlinePolicy.policySeq());
if (!state.saveState())
{
qDebug() << "Cannot persist online policy state";
m_networkOk = false;
m_needUpdate = false;
return;
}
m_needUpdate = resp["need_update"].toBool();
m_latestVer = resp["latest_version"].toString();
qDebug() << "Need update:" << m_needUpdate;
qDebug() << "Latest version:" << m_latestVer;
qDebug() << "Policy seq:" << onlinePolicy.policySeq();
}
else
{
m_needUpdate = false;
qDebug() << "Check update api failed";
}
});
}
void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId)
{
QString url = m_serverAddr + "/api/v1/update/download-url";
QJsonObject body;
body["app_id"] = appId;
body["channel"] = channel;
body["version"] = ver;
body["version_id"] = verId;
QJsonArray files;
body["files"] = files;
m_http.postRequest(url, body, [](int code, const QJsonObject& resp)
{
qDebug() << "\n========== File Download Url ==========";
qDebug() << resp["files"].toArray();
});
}
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 = QCoreApplication::translate("UpdateLogic",
"Current version manifest identity is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4.")
.arg(appId, channel, version, QString::number(versionId));
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;
});
#include "UpdateLogic.h"
if (statusCode != 200) {
const QJsonValue detail = response.value(QStringLiteral("detail"));
const QString message = detail.isObject()
? detail.toObject().value(QStringLiteral("msg")).toString()
: detail.toString();
m_error = QCoreApplication::translate("UpdateLogic",
"Cannot download signed manifest for the current local version. Stage: cache current version manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
.arg(QString::number(statusCode), appId, channel, version, QString::number(versionId),
message.isEmpty() ? QString() : QCoreApplication::translate("UpdateLogic", "\nServer message: %1").arg(message));
return false;
}
const QJsonObject manifest = response.value("manifest").toObject();
const QString manifestText = response.value("manifest_text").toString();
if (manifest.isEmpty() || manifestText.isEmpty()) {
m_error = QCoreApplication::translate("UpdateLogic",
"The signed manifest response for the current local version is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4.")
.arg(appId, channel, version, QString::number(versionId));
return false;
}
if (manifest.value("app_id").toString() != appId
|| manifest.value("channel").toString() != channel
|| manifest.value("version").toString() != version) {
m_error = QCoreApplication::translate("UpdateLogic",
"The signed manifest identity does not match the current local version. Stage: cache current version manifest. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6.")
.arg(appId, channel, version,
manifest.value("app_id").toString(),
manifest.value("channel").toString(),
manifest.value("version").toString());
return false;
}
QDir dir(cacheDir);
if (!dir.exists() && !dir.mkpath(".")) {
m_error = QCoreApplication::translate("UpdateLogic",
"Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1.")
.arg(cacheDir);
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 = QCoreApplication::translate("UpdateLogic",
"Cannot save signed manifest cache. Stage: cache current version manifest. File: %1. Error: %2.")
.arg(file.fileName(), file.errorString());
return false;
}
qDebug() << "Current version manifest cached to" << file.fileName();
return true;
#include "ConfigHelper.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QSaveFile>
#include <QUrl>
#include <QUrlQuery>
namespace {
QString trimBaseUrl(QString value)
{
value = value.trimmed();
while (value.endsWith(QLatin1Char('/')))
value.chop(1);
return value;
}
bool UpdateLogic::refreshGitTagsFile(const QString& outputPath)
void addQueryValue(QUrlQuery& query, const QString& key, const QString& value)
{
const QString trimmed = value.trimmed();
if (!trimmed.isEmpty())
query.addQueryItem(key, trimmed);
}
QString serverMessage(const QJsonObject& response)
{
const QString msg = response.value(QStringLiteral("msg")).toString();
if (!msg.isEmpty())
return msg;
const QJsonValue detail = response.value(QStringLiteral("detail"));
if (detail.isObject()) {
const QJsonObject object = detail.toObject();
const QString detailMsg = object.value(QStringLiteral("msg")).toString();
if (!detailMsg.isEmpty())
return detailMsg;
const QString error = object.value(QStringLiteral("error")).toString();
if (!error.isEmpty())
return error;
}
return detail.toString();
}
QJsonObject responseDataObject(const QJsonObject& response)
{
return response.value(QStringLiteral("data")).isObject()
? response.value(QStringLiteral("data")).toObject()
: response;
}
QString configValue(const QString& key, const QString& fallback = QString())
{
const QString value = ConfigHelper::instance().getValue(QString(), key).trimmed();
return value.isEmpty() ? fallback : value;
}
} // namespace
UpdateLogic::UpdateLogic(QObject* parent)
: QObject(parent)
{
ConfigHelper& cfg = ConfigHelper::instance();
m_serverAddr = trimBaseUrl(cfg.getValue("Server", "api_base_url"));
m_appId = cfg.getValue("App", "product_code").trimmed();
if (m_appId.isEmpty())
m_appId = cfg.getValue("App", "app_id").trimmed();
m_curVer = cfg.getValue("App", "current_version").trimmed();
m_channel = cfg.getValue("App", "channel").trimmed();
if (m_channel.isEmpty())
m_channel = QStringLiteral("stable");
qDebug() << "Read server addr:" << m_serverAddr;
qDebug() << "Read product code:" << m_appId;
}
void UpdateLogic::checkUpdate()
{
m_error.clear();
if (m_serverAddr.isEmpty()) {
m_error = QCoreApplication::translate("UpdateLogic", "Server address is empty.");
m_needUpdate = false;
m_networkOk = false;
m_lastStatusCode = 0;
m_latestVer.clear();
m_checkResp = QJsonObject();
if (m_serverAddr.isEmpty() || m_appId.isEmpty() || m_curVer.isEmpty())
{
m_error = QCoreApplication::translate(
"UpdateLogic",
"Update configuration is incomplete. Server, product_code and current_version are required.");
qDebug() << "Config incomplete, abort update check:" << m_error;
return;
}
if (configValue(QStringLiteral("client_token")).isEmpty())
{
m_lastStatusCode = 401;
m_error = QCoreApplication::translate(
"UpdateLogic",
"Update configuration is incomplete. client_token is required.");
m_checkResp.insert(QStringLiteral("msg"), m_error);
qDebug() << "Client token missing, abort update check:" << m_error;
return;
}
QUrl url(m_serverAddr + QStringLiteral("/api/v1/client/update/authorized-check"));
QUrlQuery query;
addQueryValue(query, QStringLiteral("productCode"), m_appId);
addQueryValue(query, QStringLiteral("currentVersion"), m_curVer);
addQueryValue(query, QStringLiteral("clientVersion"), configValue(QStringLiteral("client_protocol"), QStringLiteral("3")));
addQueryValue(query, QStringLiteral("channel"), m_channel);
addQueryValue(query, QStringLiteral("os"), configValue(QStringLiteral("platform")));
addQueryValue(query, QStringLiteral("architecture"), configValue(QStringLiteral("arch")));
addQueryValue(query, QStringLiteral("abi"), configValue(QStringLiteral("abi")));
url.setQuery(query);
m_http.getRequest(url.toString(QUrl::FullyEncoded),
[this](int code, const QJsonObject& resp)
{
qDebug() << "Check update HTTP code:" << code;
m_lastStatusCode = code;
m_checkResp = resp;
m_networkOk = (code == 200);
if (code != 200)
{
m_needUpdate = false;
m_error = serverMessage(resp);
qDebug() << "Check update api failed:" << m_error;
return;
}
const QJsonArray releases = resp.value(QStringLiteral("data")).toArray();
QJsonObject selected;
for (const QJsonValue& value : releases) {
const QJsonObject release = value.toObject();
const bool hasPackages = !release.value(QStringLiteral("packages")).toArray().isEmpty();
const bool hasManifest = release.value(QStringLiteral("manifest")).isObject()
|| !release.value(QStringLiteral("manifestUri")).toString().isEmpty();
if (hasPackages && hasManifest) {
selected = release;
break;
}
}
if (selected.isEmpty()) {
m_needUpdate = false;
if (!releases.isEmpty())
m_latestVer = releases.first().toObject().value(QStringLiteral("version")).toString();
qDebug() << "No entitled downloadable update for current client.";
return;
}
m_checkResp = selected;
m_checkResp.insert(QStringLiteral("need_update"), true);
m_checkResp.insert(QStringLiteral("latest_version"), selected.value(QStringLiteral("version")).toString());
m_checkResp.insert(QStringLiteral("release_id"), selected.value(QStringLiteral("id")).toString());
m_needUpdate = true;
m_latestVer = selected.value(QStringLiteral("version")).toString();
qDebug() << "Need update:" << m_needUpdate;
qDebug() << "Latest version:" << m_latestVer;
qDebug() << "Release id:" << selected.value(QStringLiteral("id")).toString();
});
}
void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId)
{
Q_UNUSED(appId);
Q_UNUSED(channel);
Q_UNUSED(ver);
Q_UNUSED(verId);
qDebug() << "SimCAE Hub manifest already contains authorized package download URLs.";
}
bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, const QString& version,
int versionId, const QString& cacheDir)
{
Q_UNUSED(versionId);
m_error.clear();
if (appId.isEmpty() || channel.isEmpty() || version.isEmpty()) {
m_error = QCoreApplication::translate(
"UpdateLogic",
"Current version manifest identity is incomplete. Stage: cache current version manifest. Product: %1, channel: %2, version: %3.")
.arg(appId, channel, version);
return false;
}
QUrl url(m_serverAddr + QStringLiteral("/api/v1/client/update/manifest"));
QUrlQuery query;
addQueryValue(query, QStringLiteral("productCode"), appId);
addQueryValue(query, QStringLiteral("version"), version);
addQueryValue(query, QStringLiteral("clientVersion"), configValue(QStringLiteral("client_protocol"), QStringLiteral("3")));
addQueryValue(query, QStringLiteral("channel"), channel);
addQueryValue(query, QStringLiteral("os"), configValue(QStringLiteral("platform")));
addQueryValue(query, QStringLiteral("architecture"), configValue(QStringLiteral("arch")));
addQueryValue(query, QStringLiteral("abi"), configValue(QStringLiteral("abi")));
url.setQuery(query);
QJsonObject response;
int statusCode = 0;
QJsonObject body;
body["app_id"] = m_appId;
body["channel"] = m_channel;
m_http.postRequest(m_serverAddr + "/api/v1/git/tags", body,
m_http.getRequest(url.toString(QUrl::FullyEncoded),
[&](int code, const QJsonObject& resp) {
statusCode = code;
response = resp;
});
if (statusCode != 200) {
const QJsonValue detail = response.value("detail");
const QString message = detail.isObject()
? detail.toObject().value("msg").toString()
: detail.toString();
m_error = message.isEmpty()
? QCoreApplication::translate("UpdateLogic", "Git tags request failed (HTTP %1).").arg(statusCode)
: message;
const QString message = serverMessage(response);
m_error = QCoreApplication::translate(
"UpdateLogic",
"Cannot download manifest for the current local version. Stage: cache current version manifest. HTTP status: %1. Product: %2, channel: %3, version: %4.%5")
.arg(QString::number(statusCode), appId, channel, version,
message.isEmpty() ? QString() : QCoreApplication::translate("UpdateLogic", "\nServer message: %1").arg(message));
return false;
}
const QString tagsText = response.value("tags_text").toString();
if (tagsText.isEmpty()) {
m_error = QCoreApplication::translate("UpdateLogic", "Git tags response is empty.");
QJsonObject wrapper = responseDataObject(response);
const QJsonObject manifest = wrapper.value(QStringLiteral("manifest")).toObject();
QString manifestText = wrapper.value(QStringLiteral("manifestText")).toString();
if (manifestText.isEmpty())
manifestText = wrapper.value(QStringLiteral("manifest_text")).toString();
if (manifest.isEmpty() || manifestText.isEmpty()) {
m_error = QCoreApplication::translate(
"UpdateLogic",
"The manifest response for the current local version is incomplete. Stage: cache current version manifest. Product: %1, channel: %2, version: %3.")
.arg(appId, channel, version);
return false;
}
const QString manifestProduct = manifest.value(QStringLiteral("productCode")).toString(
manifest.value(QStringLiteral("app_id")).toString());
if (manifestProduct != appId
|| manifest.value(QStringLiteral("channel")).toString() != channel
|| manifest.value(QStringLiteral("version")).toString() != version) {
m_error = QCoreApplication::translate(
"UpdateLogic",
"The manifest identity does not match the current local version. Stage: cache current version manifest. Expected product/channel/version: %1 / %2 / %3. Manifest product/channel/version: %4 / %5 / %6.")
.arg(appId, channel, version,
manifestProduct,
manifest.value(QStringLiteral("channel")).toString(),
manifest.value(QStringLiteral("version")).toString());
return false;
}
QString writeError;
if (!ConfigHelper::writeFileWithElevationIfNeeded(outputPath, tagsText.toUtf8(), &writeError)) {
m_error = QCoreApplication::translate("UpdateLogic", "Cannot write Git tags file: %1").arg(writeError);
QDir dir(cacheDir);
if (!dir.exists() && !dir.mkpath(".")) {
m_error = QCoreApplication::translate(
"UpdateLogic",
"Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1.")
.arg(cacheDir);
return false;
}
qDebug() << "Git tags file refreshed:" << outputPath;
wrapper.insert(QStringLiteral("manifest"), manifest);
wrapper.insert(QStringLiteral("manifestText"), manifestText);
wrapper.insert(QStringLiteral("manifest_text"), manifestText);
QSaveFile file(dir.filePath(QStringLiteral("manifest_") + version + QStringLiteral(".json")));
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented);
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
m_error = QCoreApplication::translate(
"UpdateLogic",
"Cannot save manifest cache. Stage: cache current version manifest. File: %1. Error: %2.")
.arg(file.fileName(), file.errorString());
return false;
}
qDebug() << "Current version manifest cached to" << file.fileName();
return true;
}
bool UpdateLogic::refreshGitTagsFile(const QString& outputPath)
{
Q_UNUSED(outputPath);
m_error.clear();
qDebug() << "Git tag export is not part of the current SimCAE Hub admin pages; skipped.";
return true;
}
void UpdateLogic::reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success)
{
QString url = m_serverAddr + "/api/v1/update/report";
QJsonObject body;
body["app_id"] = m_appId;
body["device_id"] = deviceId;
body["from_version"] = fromVer;
body["to_version"] = toVer;
body["result"] = success ? "success" : "fail";
m_http.postRequest(url, body, [](int code, const QJsonObject& resp)
{
qDebug() << "\n========== Update Report Result ==========";
qDebug() << resp;
});
}
Q_UNUSED(deviceId);
Q_UNUSED(fromVer);
Q_UNUSED(toVer);
Q_UNUSED(success);
qDebug() << "Update result report is not part of the current SimCAE Hub API; skipped.";
}
+3 -2
View File
@@ -24,8 +24,9 @@ public:
int lastStatusCode() const { return m_lastStatusCode; }
QString errorString() const { return m_error; }
QString getAppId() const { return m_appId; }
QString getChannel() const { return m_channel; }
QString getAppId() const { return m_appId; }
QString getProductCode() const { return m_appId; }
QString getChannel() const { return m_channel; }
private:
HttpHelper m_http;
+125 -315
View File
@@ -1,28 +1,75 @@
#include <QApplication>
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QInputDialog>
#include <QLineEdit>
#include <QTranslator>
#include <QUuid>
#include "UpdateLogic.h"
#include "../Common/ConfigHelper.h"
#include "../Common/PolicyHelper.h"
#include "../Common/LocalStateHelper.h"
#include "../Common/TicketHelper.h"
#include "../Common/DeviceIdentityHelper.h"
#include <QFile>
#include <QFileDialog>
#include <QDir>
#include "../Common/ConfigHelper.h"
#include "../Common/TicketHelper.h"
namespace {
QString configValue(const QString& key, const QString& fallback = QString())
{
const QString value = ConfigHelper::instance().getValue(QString(), key).trimmed();
return value.isEmpty() ? fallback : value;
}
QString productCode()
{
return configValue(QStringLiteral("product_code"),
configValue(QStringLiteral("app_id")));
}
QString ensureDeviceId()
{
ConfigHelper& config = ConfigHelper::instance();
QString deviceId = config.getValue(QStringLiteral("Update"), QStringLiteral("device_id")).trimmed();
if (!deviceId.isEmpty())
return deviceId;
deviceId = config.getValue(QStringLiteral("Device"), QStringLiteral("installation_id")).trimmed();
if (deviceId.isEmpty())
deviceId = QUuid::createUuid().toString(QUuid::WithoutBraces);
config.setValue(QStringLiteral("Device"), QStringLiteral("installation_id"), deviceId);
config.setValue(QStringLiteral("Update"), QStringLiteral("device_id"), deviceId);
return deviceId;
}
QString authFailureMessage(const QJsonObject& response, const QString& fallback)
{
const QString msg = response.value(QStringLiteral("msg")).toString();
if (!msg.isEmpty())
return msg;
const QJsonValue detail = response.value(QStringLiteral("detail"));
if (detail.isObject()) {
const QJsonObject object = detail.toObject();
const QString detailMsg = object.value(QStringLiteral("msg")).toString();
if (!detailMsg.isEmpty())
return detailMsg;
const QString error = object.value(QStringLiteral("error")).toString();
if (!error.isEmpty())
return error;
}
const QString detailText = detail.toString();
return detailText.isEmpty() ? fallback : detailText;
}
} // namespace
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QApplication::setApplicationName("Marsco Launcher");
QApplication::setApplicationName("SimCAE Launcher");
QTranslator translator;
if (translator.load(":/i18n/update-client_zh_CN.qm"))
if (translator.load(QStringLiteral(":/i18n/update-client_zh_CN.qm")))
app.installTranslator(&translator);
const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested();
@@ -30,188 +77,45 @@ int main(int argc, char* argv[])
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.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
progress.setAutoClose(false);
progress.show();
QApplication::processEvents();
const QString appDir = QApplication::applicationDirPath();
progress.setWindowTitle(QCoreApplication::translate("Launcher", "SimCAE Launcher"));
progress.setCancelButton(nullptr);
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
progress.setAutoClose(false);
progress.show();
QApplication::processEvents();
ConfigHelper& config = ConfigHelper::instance();
const auto isLicenseError = [](const QString& errorText) {
const QString text = errorText.toLower();
return text.contains("license")
|| text.contains("authorization")
|| text.contains("expired")
|| text.contains("device limit")
|| text.contains("bound to another license");
};
const auto clearDeviceCredential = [&]() {
ConfigHelper::removeFileWithElevationIfNeeded(ConfigHelper::instance().clientIdentityPath());
config.setValue("Update", "device_id", QString());
};
QString licenseKey = config.getValue("License", "license_key").trimmed();
auto promptAndSaveLicense = [&](const QString& reason) -> QString {
QString promptReason = reason;
while (true) {
progress.close();
bool accepted = false;
const QString appName = config.getValue("App", "app_name").trimmed();
const QString prompt = promptReason.trimmed().isEmpty()
? QCoreApplication::translate("Launcher", "Please enter the License for %1:")
.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(
nullptr,
QCoreApplication::translate("Launcher", "Enter License"),
prompt,
QLineEdit::Normal,
QString(),
&accepted
).trimmed();
if (!accepted) {
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();
}
if (licenseKey.isEmpty()) {
QMessageBox::warning(nullptr,
QCoreApplication::translate("Launcher", "License Cannot Be Empty"),
QCoreApplication::translate("Launcher", "Please paste the License created in the admin page."));
promptReason.clear();
continue;
}
if (!config.setValue("License", "license_key", licenseKey)) {
QMessageBox::critical(nullptr,
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();
}
progress.show();
progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying License..."));
QApplication::processEvents();
return licenseKey;
}
};
while (true) {
// License 首次为空、过期或已达设备上限时,在 Launcher 内引导用户重新输入。
// 成功后服务端会签发 client_identity.dat,并把真实 device_id 写入运行配置。
if (licenseKey.isEmpty()) {
licenseKey = promptAndSaveLicense(QString());
if (licenseKey.isEmpty()) return 0;
}
DeviceIdentityHelper identity(appDir);
if (identity.ensureIssued(config.getValue("Server", "api_base_url"),
config.getValue("Server", "client_token"),
config.getValue("App", "app_id"),
config.getValue("App", "channel"),
licenseKey)) {
break;
}
const QString error = identity.errorString();
if (!isLicenseError(error)) {
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Device Authentication Failed"),
error);
return -1;
}
clearDeviceCredential();
licenseKey = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current License cannot be used: %1").arg(error));
if (licenseKey.isEmpty()) return 0;
}
UpdateLogic logic;
logic.checkUpdate();
const bool needUpdate = logic.getNeedUpdate();
const bool networkOk = logic.isNetworkOk();
const QString latestVer = logic.getLatestVersion();
const QString appDir = QApplication::applicationDirPath();
progress.setLabelText(QCoreApplication::translate("Launcher", "Checking authorized updates..."));
QApplication::processEvents();
UpdateLogic logic;
logic.checkUpdate();
const bool needUpdate = logic.getNeedUpdate();
const bool networkOk = logic.isNetworkOk();
const QString latestVer = logic.getLatestVersion();
const QJsonObject response = logic.getCheckResult();
const int targetVersionId = response.value("version_id").toInt();
const QString appId = logic.getAppId();
const QString releaseId = response.value(QStringLiteral("release_id")).toString(
response.value(QStringLiteral("id")).toString());
const QString appId = logic.getProductCode();
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) {
progress.close();
const QJsonValue detail = response.value("detail");
const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString();
const QString displayMessage = message.isEmpty()
? QCoreApplication::translate("Launcher", "The device or License authorization is invalid.")
: message;
if (isLicenseError(displayMessage)) {
clearDeviceCredential();
const QString newLicense = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current authorization was rejected by the server: %1").arg(displayMessage));
if (!newLicense.isEmpty()) {
progress.close();
QMessageBox::information(nullptr,
QCoreApplication::translate("Launcher", "License Saved"),
QCoreApplication::translate("Launcher", "Please restart Launcher to complete device authorization and update checking."));
}
return 0;
}
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Authorization Rejected"),
displayMessage);
return -1;
}
const QString launchToken = config.getValue(QStringLiteral("App"), QStringLiteral("launch_token"));
const QString currentVersion = config.getValue(QStringLiteral("App"), QStringLiteral("current_version"));
const QString deviceId = ensureDeviceId();
const auto configuredName = [&](const QString& key, const QString& fallback) {
return ConfigHelper::executableNameForCurrentPlatform(
config.getValue("Runtime", key), fallback);
config.getValue(QStringLiteral("Runtime"), key), fallback);
};
const QString mainExecutable = configuredName("main_executable", "MainApp");
const QString updaterExecutable = configuredName("updater_executable", "Updater");
const QString mainExecutable = configuredName(QStringLiteral("main_executable"), QStringLiteral("MainApp"));
const QString updaterExecutable = configuredName(QStringLiteral("updater_executable"), QStringLiteral("Updater"));
const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
const auto importOfflinePackage = [&]() {
const QString package = QFileDialog::getOpenFileName(nullptr,
QCoreApplication::translate("Launcher", "Select Offline Update Package"),
QString(),
QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)"));
if (package.isEmpty())
return false;
if (!QFileInfo::exists(updaterPath)) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
QCoreApplication::translate(
"Launcher",
"Cannot import the offline update package because the updater executable does not exist.\nUpdater: %1\nOffline package: %2")
.arg(updaterPath, package));
return false;
}
if (!QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)})) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
QCoreApplication::translate(
"Launcher",
"Cannot start the updater for offline package import.\nUpdater: %1\nOffline package: %2\nCheck file permissions and dependent DLLs/shared libraries.")
.arg(updaterPath, package));
return false;
}
return true;
};
const QString deviceId = config.getValue("Update", "device_id");
if (QCoreApplication::arguments().contains("--import-offline")) {
progress.close();
if (!importOfflinePackage())
QMessageBox::information(nullptr,
QCoreApplication::translate("Launcher", "Offline Update"),
QCoreApplication::translate("Launcher", "No offline update package was selected."));
return 0;
}
QString mainStartupError;
const auto startMainApp = [&]() {
// 只允许 Launcher 启动业务主程序:先生成一次性 ticket,再通过 --ticket-file 传给主程序。
// 业务主程序必须消费并校验 ticket,避免用户直接双击 SimCAE/MainApp 绕过更新和授权检查。
mainStartupError.clear();
if (!QFileInfo::exists(mainAppPath)) {
mainStartupError = QCoreApplication::translate(
@@ -220,9 +124,10 @@ int main(int argc, char* argv[])
.arg(mainAppPath);
return false;
}
QString ticketPath;
QString ticketError;
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
if (!TicketHelper::createTicket(appId, deviceId, currentVersion,
launchToken, &ticketPath, &ticketError)) {
qDebug() << "Cannot create launch ticket:" << ticketError;
mainStartupError = QCoreApplication::translate(
@@ -231,8 +136,9 @@ int main(int argc, char* argv[])
.arg(mainAppPath, ticketError);
return false;
}
const bool started = QProcess::startDetached(mainAppPath,
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
QStringList{QStringLiteral("--ticket-file=%1").arg(ticketPath)});
if (!started) {
QFile::remove(ticketPath);
mainStartupError = QCoreApplication::translate(
@@ -242,97 +148,33 @@ int main(int argc, char* argv[])
}
return started;
};
progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying local runtime policy..."));
QApplication::processEvents();
PolicyHelper policy(appDir);
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
{
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Cannot Start"),
QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString()));
return -1;
}
if (policy.isExpired())
{
progress.close();
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;
}
if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate))
{
progress.close();
QMessageBox::critical(nullptr,
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());
return -1;
}
LocalStateHelper state(appDir);
if (!state.loadState())
{
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Cannot Start"),
QCoreApplication::translate("Launcher", "Cannot read the local state: %1").arg(state.errorString()));
return -1;
}
if (state.isPolicySeqRolledBack(policy.policySeq()))
{
progress.close();
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;
}
if (state.isSystemTimeRewound())
{
if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403) {
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Security Check Failed"),
QCoreApplication::translate("Launcher", "A possible system time rollback was detected. Startup has been blocked."));
QCoreApplication::translate("Launcher", "Update Client Rejected"),
authFailureMessage(response,
QCoreApplication::translate("Launcher", "The update client token is missing or invalid. Please download a valid package from the customer portal.")));
return -1;
}
if (networkOk && policy.gitTagsEnabled())
{
progress.setLabelText(QCoreApplication::translate("Launcher", "Generating Git tag list..."));
QApplication::processEvents();
const QString tagsPath = QDir(appDir).filePath("tags.txt");
if (!logic.refreshGitTagsFile(tagsPath))
{
progress.close();
QMessageBox::warning(nullptr,
QCoreApplication::translate("Launcher", "Git Tag List Failed"),
QCoreApplication::translate("Launcher", "Cannot generate tags.txt, but startup will continue:\n%1").arg(logic.errorString()));
progress.show();
QApplication::processEvents();
}
} else if (!networkOk) {
progress.close();
QMessageBox::warning(nullptr,
QCoreApplication::translate("Launcher", "Update Server Unavailable"),
QCoreApplication::translate("Launcher", "Cannot connect to the update server. The installed application will be started without downloading an update."));
progress.show();
}
if (networkOk && needUpdate)
{
progress.close();
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
const QString dialogTitle = rollbackOperation ? QCoreApplication::translate("Launcher", "Version Rollback")
: (policy.forceUpdate() ? QCoreApplication::translate("Launcher", "Update Required") : QCoreApplication::translate("Launcher", "New Version Available"));
const QString prompt = policy.message().isEmpty()
? (rollbackOperation
? QCoreApplication::translate("Launcher", "The administrator provided version %1 as the rollback target. Downgrade now?").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;
if (policy.forceUpdate()) {
QMessageBox::information(nullptr, dialogTitle, prompt);
} else {
accepted = QMessageBox::question(nullptr, dialogTitle, prompt,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes;
}
const QString prompt = QCoreApplication::translate(
"Launcher",
"Version %1 is available. Update now?").arg(latestVer);
const bool accepted = QMessageBox::question(nullptr,
QCoreApplication::translate("Launcher", "New Version Available"),
prompt,
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) == QMessageBox::Yes;
if (!accepted) {
if (!startMainApp()) {
QMessageBox::critical(nullptr,
@@ -344,7 +186,14 @@ int main(int argc, char* argv[])
}
return 0;
}
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
const QStringList updaterArgs{
appId,
channel,
latestVer,
QStringLiteral("0"),
QStringLiteral("--release-id=%1").arg(releaseId)
};
if (!QFileInfo::exists(updaterPath))
{
QMessageBox::critical(nullptr,
@@ -368,49 +217,10 @@ int main(int argc, char* argv[])
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) {
progress.close();
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) {
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;
}
progress.show();
}
if (!networkOk && !policy.isOfflineAllowed())
{
progress.close();
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;
}
progress.setLabelText(networkOk
? QCoreApplication::translate("Launcher", "The application is up to date. Starting...")
: QCoreApplication::translate("Launcher", "Offline mode is active. Starting..."));
QApplication::processEvents();
: QCoreApplication::translate("Launcher", "Offline startup. Starting..."));
QApplication::processEvents();
if (!startMainApp())
{
progress.close();
@@ -421,6 +231,6 @@ int main(int argc, char* argv[])
: mainStartupError);
return -1;
}
progress.close();
return 0;
}
progress.close();
return 0;
}