chore: sync client source after repository split
This commit is contained in:
+116
-116
@@ -1,117 +1,117 @@
|
||||
#include "UpdateLogic.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QApplication>
|
||||
#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();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
#include "UpdateLogic.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QApplication>
|
||||
#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();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
+282
-282
@@ -1,282 +1,282 @@
|
||||
#include <windows.h>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QProgressDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QTextCodec>
|
||||
#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>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
SetConsoleOutputCP(65001);
|
||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName("Marsco Launcher");
|
||||
|
||||
QProgressDialog progress("正在检查软件更新...", QString(), 0, 0);
|
||||
progress.setWindowTitle("Marsco 软件启动器");
|
||||
progress.setCancelButton(nullptr);
|
||||
progress.setWindowModality(Qt::ApplicationModal);
|
||||
progress.setMinimumDuration(0);
|
||||
progress.setAutoClose(false);
|
||||
progress.show();
|
||||
QApplication::processEvents();
|
||||
|
||||
const QString appDir = QApplication::applicationDirPath();
|
||||
ConfigHelper& config = ConfigHelper::instance();
|
||||
const auto isLicenseError = [](const QString& errorText) {
|
||||
const QString text = errorText.toLower();
|
||||
return text.contains("license")
|
||||
|| errorText.contains("授权")
|
||||
|| errorText.contains("过期")
|
||||
|| errorText.contains("设备数量已达到上限")
|
||||
|| errorText.contains("已绑定其他授权");
|
||||
};
|
||||
const auto clearDeviceCredential = [&]() {
|
||||
QFile::remove(QDir(appDir).filePath("config/client_identity.dat"));
|
||||
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()
|
||||
? QString("请输入%1授权 License:").arg(appName.isEmpty() ? "软件" : appName)
|
||||
: QString("%1\n\n请重新输入%2授权 License:").arg(promptReason, appName.isEmpty() ? "软件" : appName);
|
||||
licenseKey = QInputDialog::getText(
|
||||
nullptr,
|
||||
"输入 License",
|
||||
prompt,
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&accepted
|
||||
).trimmed();
|
||||
if (!accepted) {
|
||||
QMessageBox::information(nullptr, "需要 License", "首次启动需要输入管理员提供的 License。");
|
||||
return QString();
|
||||
}
|
||||
if (licenseKey.isEmpty()) {
|
||||
QMessageBox::warning(nullptr, "License 不能为空", "请粘贴管理员在后台创建的 License。");
|
||||
promptReason.clear();
|
||||
continue;
|
||||
}
|
||||
if (!config.setValue("License", "license_key", licenseKey)) {
|
||||
QMessageBox::critical(nullptr, "保存 License 失败",
|
||||
QString("无法写入配置文件:%1\n%2").arg(config.configPath(), config.lastError()));
|
||||
return QString();
|
||||
}
|
||||
progress.show();
|
||||
progress.setLabelText("正在验证 License...");
|
||||
QApplication::processEvents();
|
||||
return licenseKey;
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
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, "设备身份验证失败", error);
|
||||
return -1;
|
||||
}
|
||||
clearDeviceCredential();
|
||||
licenseKey = promptAndSaveLicense(QString("当前 License 无法使用:%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 QJsonObject response = logic.getCheckResult();
|
||||
const int targetVersionId = response.value("version_id").toInt();
|
||||
const QString appId = logic.getAppId();
|
||||
const QString channel = logic.getChannel();
|
||||
|
||||
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() ? "设备或 License 授权无效。" : message;
|
||||
if (isLicenseError(displayMessage)) {
|
||||
clearDeviceCredential();
|
||||
const QString newLicense = promptAndSaveLicense(QString("当前授权被服务端拒绝:%1").arg(displayMessage));
|
||||
if (!newLicense.isEmpty()) {
|
||||
progress.close();
|
||||
QMessageBox::information(nullptr, "License 已保存", "请重新启动 Launcher 完成设备授权和更新检查。");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
QMessageBox::critical(nullptr, "授权被拒绝", displayMessage);
|
||||
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 QString value = config.getValue("Runtime", key).trimmed();
|
||||
return value.isEmpty() ? fallback : value;
|
||||
};
|
||||
const QString mainExecutable = configuredName("main_executable", "MainApp.exe");
|
||||
const QString updaterExecutable = configuredName("updater_executable", "Updater.exe");
|
||||
const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
|
||||
const auto importOfflinePackage = [&]() {
|
||||
const QString package = QFileDialog::getOpenFileName(nullptr, "选择离线更新包", QString(), "Marsco 离线更新包 (*.upd)");
|
||||
return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)});
|
||||
};
|
||||
const QString deviceId = config.getValue("Update", "device_id");
|
||||
if (QCoreApplication::arguments().contains("--import-offline")) {
|
||||
progress.close();
|
||||
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包。");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto startMainApp = [&]() {
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
|
||||
launchToken, &ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
return false;
|
||||
}
|
||||
const bool started = QProcess::startDetached(mainAppPath,
|
||||
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
|
||||
progress.setLabelText("正在验证本地运行策略...");
|
||||
QApplication::processEvents();
|
||||
|
||||
PolicyHelper policy(appDir);
|
||||
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "无法启动", QString("本地版本策略无效:%1").arg(policy.errorString()));
|
||||
return -1;
|
||||
}
|
||||
if (policy.isExpired())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "无法启动", "本地版本策略已过期,请连接网络或联系管理员。");
|
||||
return -1;
|
||||
}
|
||||
if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate))
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "当前版本不可运行",
|
||||
policy.message().isEmpty() ? QString("当前版本 %1 已被管理员停用。").arg(currentVersion)
|
||||
: policy.message());
|
||||
return -1;
|
||||
}
|
||||
|
||||
LocalStateHelper state(appDir);
|
||||
if (!state.loadState())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "无法启动", QString("无法读取本地状态:%1").arg(state.errorString()));
|
||||
return -1;
|
||||
}
|
||||
if (state.isPolicySeqRolledBack(policy.policySeq()))
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "安全检查失败", "检测到版本策略序列回退,已阻止启动。");
|
||||
return -1;
|
||||
}
|
||||
if (state.isSystemTimeRewound())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "安全检查失败", "检测到系统时间可能被回拨,已阻止启动。");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (networkOk && needUpdate)
|
||||
{
|
||||
progress.close();
|
||||
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
|
||||
const QString dialogTitle = rollbackOperation ? "版本回退"
|
||||
: (policy.forceUpdate() ? "必须更新" : "发现新版本");
|
||||
const QString prompt = policy.message().isEmpty()
|
||||
? QString(rollbackOperation ? "管理员提供了版本 %1 作为回退目标,是否现在降级?"
|
||||
: "发现新版本 %1,是否现在更新?").arg(latestVer)
|
||||
: policy.message() + QString("\n目标版本:%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;
|
||||
}
|
||||
if (!accepted) {
|
||||
if (!startMainApp()) {
|
||||
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
|
||||
if (!QProcess::startDetached(updaterPath, updaterArgs))
|
||||
{
|
||||
QMessageBox::critical(nullptr, "更新器启动失败", QString("无法启动更新器:%1").arg(updaterPath));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!networkOk) {
|
||||
progress.close();
|
||||
if (QMessageBox::question(nullptr, "服务器不可用", "当前无法连接更新服务器。是否导入离线更新包?",
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
|
||||
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包或无法启动更新器。");
|
||||
return 0;
|
||||
}
|
||||
progress.show();
|
||||
}
|
||||
|
||||
if (!networkOk && !policy.isOfflineAllowed())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "网络不可用", "无法连接更新服务器,且当前策略不允许离线启动。");
|
||||
return -1;
|
||||
}
|
||||
|
||||
progress.setLabelText(networkOk ? "当前已是最新版本,正在启动..." : "当前处于离线模式,正在启动...");
|
||||
QApplication::processEvents();
|
||||
if (!startMainApp())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
|
||||
return -1;
|
||||
}
|
||||
progress.close();
|
||||
return 0;
|
||||
}
|
||||
#include <windows.h>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QProgressDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QTextCodec>
|
||||
#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>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
SetConsoleOutputCP(65001);
|
||||
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName("Marsco Launcher");
|
||||
|
||||
QProgressDialog progress("正在检查软件更新...", QString(), 0, 0);
|
||||
progress.setWindowTitle("Marsco 软件启动器");
|
||||
progress.setCancelButton(nullptr);
|
||||
progress.setWindowModality(Qt::ApplicationModal);
|
||||
progress.setMinimumDuration(0);
|
||||
progress.setAutoClose(false);
|
||||
progress.show();
|
||||
QApplication::processEvents();
|
||||
|
||||
const QString appDir = QApplication::applicationDirPath();
|
||||
ConfigHelper& config = ConfigHelper::instance();
|
||||
const auto isLicenseError = [](const QString& errorText) {
|
||||
const QString text = errorText.toLower();
|
||||
return text.contains("license")
|
||||
|| errorText.contains("授权")
|
||||
|| errorText.contains("过期")
|
||||
|| errorText.contains("设备数量已达到上限")
|
||||
|| errorText.contains("已绑定其他授权");
|
||||
};
|
||||
const auto clearDeviceCredential = [&]() {
|
||||
QFile::remove(QDir(appDir).filePath("config/client_identity.dat"));
|
||||
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()
|
||||
? QString("请输入%1授权 License:").arg(appName.isEmpty() ? "软件" : appName)
|
||||
: QString("%1\n\n请重新输入%2授权 License:").arg(promptReason, appName.isEmpty() ? "软件" : appName);
|
||||
licenseKey = QInputDialog::getText(
|
||||
nullptr,
|
||||
"输入 License",
|
||||
prompt,
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&accepted
|
||||
).trimmed();
|
||||
if (!accepted) {
|
||||
QMessageBox::information(nullptr, "需要 License", "首次启动需要输入管理员提供的 License。");
|
||||
return QString();
|
||||
}
|
||||
if (licenseKey.isEmpty()) {
|
||||
QMessageBox::warning(nullptr, "License 不能为空", "请粘贴管理员在后台创建的 License。");
|
||||
promptReason.clear();
|
||||
continue;
|
||||
}
|
||||
if (!config.setValue("License", "license_key", licenseKey)) {
|
||||
QMessageBox::critical(nullptr, "保存 License 失败",
|
||||
QString("无法写入配置文件:%1\n%2").arg(config.configPath(), config.lastError()));
|
||||
return QString();
|
||||
}
|
||||
progress.show();
|
||||
progress.setLabelText("正在验证 License...");
|
||||
QApplication::processEvents();
|
||||
return licenseKey;
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
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, "设备身份验证失败", error);
|
||||
return -1;
|
||||
}
|
||||
clearDeviceCredential();
|
||||
licenseKey = promptAndSaveLicense(QString("当前 License 无法使用:%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 QJsonObject response = logic.getCheckResult();
|
||||
const int targetVersionId = response.value("version_id").toInt();
|
||||
const QString appId = logic.getAppId();
|
||||
const QString channel = logic.getChannel();
|
||||
|
||||
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() ? "设备或 License 授权无效。" : message;
|
||||
if (isLicenseError(displayMessage)) {
|
||||
clearDeviceCredential();
|
||||
const QString newLicense = promptAndSaveLicense(QString("当前授权被服务端拒绝:%1").arg(displayMessage));
|
||||
if (!newLicense.isEmpty()) {
|
||||
progress.close();
|
||||
QMessageBox::information(nullptr, "License 已保存", "请重新启动 Launcher 完成设备授权和更新检查。");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
QMessageBox::critical(nullptr, "授权被拒绝", displayMessage);
|
||||
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 QString value = config.getValue("Runtime", key).trimmed();
|
||||
return value.isEmpty() ? fallback : value;
|
||||
};
|
||||
const QString mainExecutable = configuredName("main_executable", "MainApp.exe");
|
||||
const QString updaterExecutable = configuredName("updater_executable", "Updater.exe");
|
||||
const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
|
||||
const auto importOfflinePackage = [&]() {
|
||||
const QString package = QFileDialog::getOpenFileName(nullptr, "选择离线更新包", QString(), "Marsco 离线更新包 (*.upd)");
|
||||
return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)});
|
||||
};
|
||||
const QString deviceId = config.getValue("Update", "device_id");
|
||||
if (QCoreApplication::arguments().contains("--import-offline")) {
|
||||
progress.close();
|
||||
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包。");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto startMainApp = [&]() {
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
|
||||
launchToken, &ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
return false;
|
||||
}
|
||||
const bool started = QProcess::startDetached(mainAppPath,
|
||||
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
|
||||
progress.setLabelText("正在验证本地运行策略...");
|
||||
QApplication::processEvents();
|
||||
|
||||
PolicyHelper policy(appDir);
|
||||
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "无法启动", QString("本地版本策略无效:%1").arg(policy.errorString()));
|
||||
return -1;
|
||||
}
|
||||
if (policy.isExpired())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "无法启动", "本地版本策略已过期,请连接网络或联系管理员。");
|
||||
return -1;
|
||||
}
|
||||
if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate))
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "当前版本不可运行",
|
||||
policy.message().isEmpty() ? QString("当前版本 %1 已被管理员停用。").arg(currentVersion)
|
||||
: policy.message());
|
||||
return -1;
|
||||
}
|
||||
|
||||
LocalStateHelper state(appDir);
|
||||
if (!state.loadState())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "无法启动", QString("无法读取本地状态:%1").arg(state.errorString()));
|
||||
return -1;
|
||||
}
|
||||
if (state.isPolicySeqRolledBack(policy.policySeq()))
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "安全检查失败", "检测到版本策略序列回退,已阻止启动。");
|
||||
return -1;
|
||||
}
|
||||
if (state.isSystemTimeRewound())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "安全检查失败", "检测到系统时间可能被回拨,已阻止启动。");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (networkOk && needUpdate)
|
||||
{
|
||||
progress.close();
|
||||
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
|
||||
const QString dialogTitle = rollbackOperation ? "版本回退"
|
||||
: (policy.forceUpdate() ? "必须更新" : "发现新版本");
|
||||
const QString prompt = policy.message().isEmpty()
|
||||
? QString(rollbackOperation ? "管理员提供了版本 %1 作为回退目标,是否现在降级?"
|
||||
: "发现新版本 %1,是否现在更新?").arg(latestVer)
|
||||
: policy.message() + QString("\n目标版本:%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;
|
||||
}
|
||||
if (!accepted) {
|
||||
if (!startMainApp()) {
|
||||
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
|
||||
if (!QProcess::startDetached(updaterPath, updaterArgs))
|
||||
{
|
||||
QMessageBox::critical(nullptr, "更新器启动失败", QString("无法启动更新器:%1").arg(updaterPath));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!networkOk) {
|
||||
progress.close();
|
||||
if (QMessageBox::question(nullptr, "服务器不可用", "当前无法连接更新服务器。是否导入离线更新包?",
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) {
|
||||
if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包或无法启动更新器。");
|
||||
return 0;
|
||||
}
|
||||
progress.show();
|
||||
}
|
||||
|
||||
if (!networkOk && !policy.isOfflineAllowed())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "网络不可用", "无法连接更新服务器,且当前策略不允许离线启动。");
|
||||
return -1;
|
||||
}
|
||||
|
||||
progress.setLabelText(networkOk ? "当前已是最新版本,正在启动..." : "当前处于离线模式,正在启动...");
|
||||
QApplication::processEvents();
|
||||
if (!startMainApp())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath));
|
||||
return -1;
|
||||
}
|
||||
progress.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user