461 lines
20 KiB
C++
461 lines
20 KiB
C++
#include <QApplication>
|
|
#include <QDebug>
|
|
#include <QMessageBox>
|
|
#include <QProcess>
|
|
#include <QProgressDialog>
|
|
#include <QInputDialog>
|
|
#include <QLineEdit>
|
|
#include "UpdateLogic.h"
|
|
#include "../Common/ConfigHelper.h"
|
|
#include "../Common/PolicyHelper.h"
|
|
#include "../Common/LocalStateHelper.h"
|
|
#include "../Common/TicketHelper.h"
|
|
#include "../Common/DeviceIdentityHelper.h"
|
|
#include "../Common/IntegrityHelper.h"
|
|
#include "../Common/ManifestBootstrapPolicy.h"
|
|
#include "../Common/TranslationHelper.h"
|
|
#include "../Updater/UpdaterLogic.h"
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QDir>
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
QApplication app(argc, argv);
|
|
QApplication::setApplicationName("Marsco Launcher");
|
|
|
|
TranslationHelper translation;
|
|
translation.installForSystemLocale();
|
|
|
|
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 = [&translation](const QString &errorText)
|
|
{
|
|
const QString text = errorText.toLower();
|
|
QStringList markers{QStringLiteral("license")};
|
|
const char *const markerSources[]{
|
|
QT_TRANSLATE_NOOP("LauncherErrorMarker", "authorization"),
|
|
QT_TRANSLATE_NOOP("LauncherErrorMarker", "expired"),
|
|
QT_TRANSLATE_NOOP("LauncherErrorMarker", "device limit reached"),
|
|
QT_TRANSLATE_NOOP("LauncherErrorMarker", "bound to another license")};
|
|
for (const char *source : markerSources)
|
|
{
|
|
markers.append(QCoreApplication::translate("LauncherErrorMarker", source));
|
|
markers.append(translation.translate("LauncherErrorMarker", source));
|
|
}
|
|
for (const QString &marker : markers)
|
|
{
|
|
if (!marker.isEmpty() && text.contains(marker.toLower()))
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
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 displayName = appName.isEmpty()
|
|
? QCoreApplication::translate("Launcher", "application")
|
|
: appName;
|
|
const QString prompt = promptReason.trimmed().isEmpty()
|
|
? QCoreApplication::translate("Launcher", "Enter the license for %1:").arg(displayName)
|
|
: QCoreApplication::translate("Launcher", "%1\n\nPlease re-enter the license for %2:").arg(promptReason, displayName);
|
|
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 an 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", "Paste the license created by an administrator."));
|
|
promptReason.clear();
|
|
continue;
|
|
}
|
|
if (!config.setValue("License", "license_key", licenseKey))
|
|
{
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Failed to Save License"),
|
|
QCoreApplication::translate("Launcher", "Cannot save system state at %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();
|
|
|
|
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 server rejected the current license: %1").arg(displayMessage));
|
|
if (!newLicense.isEmpty())
|
|
{
|
|
progress.close();
|
|
QMessageBox::information(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "License Saved"),
|
|
QCoreApplication::translate("Launcher", "Restart Launcher to complete device authorization and check for updates."));
|
|
}
|
|
return 0;
|
|
}
|
|
QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Authorization Denied"), 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);
|
|
IntegrityHelper integrity(config.installRoot());
|
|
bool integrityVerified = integrity.verifyInstalledVersion(
|
|
appId, channel, currentVersion);
|
|
QString integrityError = integrity.errorString();
|
|
const UpdateClient::ManifestBootstrapAction bootstrapAction =
|
|
UpdateClient::manifestBootstrapAction(
|
|
integrity.isManifestCacheMissing(), networkOk, needUpdate,
|
|
targetVersionId);
|
|
if (!integrityVerified
|
|
&& bootstrapAction
|
|
== UpdateClient::ManifestBootstrapAction::FetchCurrentManifest)
|
|
{
|
|
UpdaterLogic manifestLogic;
|
|
manifestLogic.getManifest(
|
|
appId, channel, currentVersion, targetVersionId);
|
|
const QString cacheDirectory = QDir(config.updateRoot()).filePath(
|
|
QStringLiteral("manifest_cache"));
|
|
if (manifestLogic.verifyManifestSignature()
|
|
&& manifestLogic.saveManifestCache(cacheDirectory))
|
|
{
|
|
integrityVerified = integrity.verifyInstalledVersion(
|
|
appId, channel, currentVersion);
|
|
integrityError = integrity.errorString();
|
|
}
|
|
else
|
|
{
|
|
integrityError = QCoreApplication::translate(
|
|
"Launcher",
|
|
"The signed manifest for version %1 could not be fetched or cached.")
|
|
.arg(currentVersion);
|
|
}
|
|
}
|
|
else if (!integrityVerified
|
|
&& bootstrapAction
|
|
== UpdateClient::ManifestBootstrapAction::CurrentVersionNotPublished)
|
|
{
|
|
integrityError = QCoreApplication::translate(
|
|
"Launcher",
|
|
"Version %1 is not published on the update server. Publish this exact installed build before distribution so its signed manifest can be verified.")
|
|
.arg(currentVersion);
|
|
}
|
|
if (!integrityVerified)
|
|
{
|
|
progress.close();
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Integrity Check Failed"),
|
|
QCoreApplication::translate(
|
|
"Launcher",
|
|
"Application files failed signed manifest verification:\n%1")
|
|
.arg(integrityError));
|
|
return -1;
|
|
}
|
|
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", "Validating the local run policy..."));
|
|
QApplication::processEvents();
|
|
|
|
PolicyHelper policy(appDir);
|
|
if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid())
|
|
{
|
|
progress.close();
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Unable to Start"),
|
|
QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString()));
|
|
return -1;
|
|
}
|
|
if (!policy.isApplicable(appId, channel, currentVersion))
|
|
{
|
|
progress.close();
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Unable to 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", "Unable to Start"),
|
|
QCoreApplication::translate("Launcher", "The local version policy has expired. Connect to the network or contact an administrator."));
|
|
return -1;
|
|
}
|
|
const bool currentVersionAllowed =
|
|
policy.allowRun() && policy.isVersionAllowed(currentVersion);
|
|
if (!currentVersionAllowed && !(networkOk && needUpdate))
|
|
{
|
|
progress.close();
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Current Version Cannot Run"),
|
|
policy.message().isEmpty()
|
|
? QCoreApplication::translate("Launcher", "Version %1 has been disabled by an administrator.").arg(currentVersion)
|
|
: policy.message());
|
|
return -1;
|
|
}
|
|
|
|
LocalStateHelper state(appDir);
|
|
if (!state.loadState())
|
|
{
|
|
progress.close();
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Unable to 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, so startup was blocked."));
|
|
return -1;
|
|
}
|
|
if (state.isSystemTimeRewound())
|
|
{
|
|
progress.close();
|
|
QMessageBox::critical(
|
|
nullptr,
|
|
QCoreApplication::translate("Launcher", "Security Check Failed"),
|
|
QCoreApplication::translate("Launcher", "A possible system clock rollback was detected, so startup was blocked."));
|
|
return -1;
|
|
}
|
|
|
|
if (networkOk && needUpdate)
|
|
{
|
|
progress.close();
|
|
const bool rollbackOperation = response.value("action").toString() == "rollback_allowed";
|
|
const bool updateRequired = policy.forceUpdate() || !currentVersionAllowed;
|
|
const QString dialogTitle = rollbackOperation
|
|
? QCoreApplication::translate("Launcher", "Version Rollback")
|
|
: (updateRequired
|
|
? QCoreApplication::translate("Launcher", "Update Required")
|
|
: QCoreApplication::translate("Launcher", "New Version Available"));
|
|
const QString prompt = policy.message().isEmpty()
|
|
? (rollbackOperation
|
|
? QCoreApplication::translate("Launcher", "An administrator selected version %1 as the rollback target. Downgrade now?")
|
|
: 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 (updateRequired)
|
|
{
|
|
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)
|
|
{
|
|
progress.close();
|
|
if (QMessageBox::question(nullptr,
|
|
QCoreApplication::translate("Launcher", "Server Unavailable"),
|
|
QCoreApplication::translate("Launcher", "The update server is currently unreachable. 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", "The update server is unreachable and the current policy does not allow offline startup."));
|
|
return -1;
|
|
}
|
|
|
|
progress.setLabelText(
|
|
networkOk ? QCoreApplication::translate("Launcher", "The current version 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;
|
|
}
|