Files

427 lines
20 KiB
C++
Raw Permalink Normal View History

#include <QApplication>
#include <QCoreApplication>
#include <QDebug>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QInputDialog>
#include <QLineEdit>
#include <QTranslator>
#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[])
{
QApplication app(argc, argv);
QApplication::setApplicationName("Marsco Launcher");
QTranslator translator;
if (translator.load(":/i18n/update-client_zh_CN.qm"))
app.installTranslator(&translator);
const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested();
if (elevatedWriteExitCode >= 0)
return elevatedWriteExitCode;
QProgressDialog progress(QCoreApplication::translate("Launcher", "Checking for software updates..."), QString(), 0, 0);
progress.setWindowTitle(QCoreApplication::translate("Launcher", "Marsco Launcher"));
progress.setCancelButton(nullptr);
progress.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")
|| 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 QJsonObject response = logic.getCheckResult();
const int targetVersionId = response.value("version_id").toInt();
const QString appId = logic.getAppId();
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 auto configuredName = [&](const QString& key, const QString& fallback) {
return ConfigHelper::executableNameForCurrentPlatform(
config.getValue("Runtime", key), fallback);
};
const QString mainExecutable = configuredName("main_executable", "MainApp");
const QString updaterExecutable = configuredName("updater_executable", "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(
"Launcher",
"Cannot start the main application because the executable file does not exist.\nExecutable: %1\nCheck main_executable and install_root in the generated client configuration.")
.arg(mainAppPath);
return false;
}
QString ticketPath;
QString ticketError;
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
launchToken, &ticketPath, &ticketError)) {
qDebug() << "Cannot create launch ticket:" << ticketError;
mainStartupError = QCoreApplication::translate(
"Launcher",
"Cannot start the main application because the one-time launch ticket could not be created.\nExecutable: %1\nDetails: %2")
.arg(mainAppPath, ticketError);
return false;
}
const bool started = QProcess::startDetached(mainAppPath,
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
if (!started) {
QFile::remove(ticketPath);
mainStartupError = QCoreApplication::translate(
"Launcher",
"Cannot start the main application process.\nExecutable: %1\nTicket file: %2\nCheck file permissions, dependent DLLs/shared libraries, and whether the executable can run independently.")
.arg(mainAppPath, ticketPath);
}
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())
{
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."));
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();
}
}
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;
}
if (!accepted) {
if (!startMainApp()) {
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Startup Failed"),
mainStartupError.isEmpty()
? QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)
: mainStartupError);
return -1;
}
return 0;
}
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
if (!QFileInfo::exists(updaterPath))
{
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
QCoreApplication::translate(
"Launcher",
"Cannot start the updater because the executable file does not exist.\nExecutable: %1\nCheck updater_executable and install_root in the generated client configuration.")
.arg(updaterPath));
return -1;
}
if (!QProcess::startDetached(updaterPath, updaterArgs))
{
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
QCoreApplication::translate(
"Launcher",
"Cannot start the updater process.\nExecutable: %1\nArguments: %2\nCheck file permissions, dependent DLLs/shared libraries, and whether the updater can run independently.")
.arg(updaterPath, updaterArgs.join(QStringLiteral(" "))));
return -1;
}
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();
if (!startMainApp())
{
progress.close();
QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Startup Failed"),
mainStartupError.isEmpty()
? QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)
: mainStartupError);
return -1;
}
progress.close();
return 0;
}