#include #include #include #include #include #include #include #include #include #include "UpdateLogic.h" #include "../Common/ConfigHelper.h" #include "../Common/PolicyHelper.h" #include "../Common/LocalStateHelper.h" #include "../Common/TicketHelper.h" #include "../Common/DeviceIdentityHelper.h" #include #include #include 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(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() ? 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) { 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) { 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, QCoreApplication::translate("Launcher", "Select Offline Update Package"), QString(), QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)")); return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)}); }; 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; } 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(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 && 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"), QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); return -1; } return 0; } const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)}; if (!QProcess::startDetached(updaterPath, updaterArgs)) { QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Updater Startup Failed"), QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath)); 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"), QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); return -1; } progress.close(); return 0; }