2026-07-14 01:37:06 +00:00
#include <QApplication>
#include <QCoreApplication>
#include <QDebug>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QInputDialog>
#include <QLineEdit>
#include <QTranslator>
#include "UpdateLogic.h"
2026-07-10 02:45:13 +00:00
#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>
2026-07-14 01:37:06 +00:00
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" ));
2026-07-10 02:45:13 +00:00
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 ();
2026-07-14 01:37:06 +00:00
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 ());
};
2026-07-10 02:45:13 +00:00
QString licenseKey = config . getValue ( "License" , "license_key" ). trimmed ();
auto promptAndSaveLicense = [ & ]( const QString & reason ) -> QString {
QString promptReason = reason ;
while ( true ) {
progress . close ();
2026-07-14 01:37:06 +00:00
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\n Please 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 ,
2026-07-10 02:45:13 +00:00
QLineEdit :: Normal ,
QString (),
& accepted
2026-07-14 01:37:06 +00:00
). 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..." ));
2026-07-10 02:45:13 +00:00
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 )) {
2026-07-14 01:37:06 +00:00
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 ));
2026-07-10 02:45:13 +00:00
if ( licenseKey . isEmpty ()) return 0 ;
}
UpdateLogic logic ;
logic . checkUpdate ();
const bool needUpdate = logic . getNeedUpdate ();
const bool networkOk = logic . isNetworkOk ();
const QString latestVer = logic . getLatestVersion ();
2026-07-14 01:37:06 +00:00
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" );
2026-07-10 02:45:13 +00:00
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 ();
2026-07-14 01:37:06 +00:00
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 );
2026-07-10 02:45:13 +00:00
return - 1 ;
}
2026-07-14 01:37:06 +00:00
const auto configuredName = [ & ]( const QString & key , const QString & fallback ) {
const QString value = config . getValue ( "Runtime" , key ). trimmed ();
return value . isEmpty () ? fallback : value ;
2026-07-10 02:45:13 +00:00
};
const QString mainExecutable = configuredName ( "main_executable" , "MainApp.exe" );
const QString updaterExecutable = configuredName ( "updater_executable" , "Updater.exe" );
2026-07-14 01:37:06 +00:00
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 )});
};
2026-07-10 02:45:13 +00:00
const QString deviceId = config . getValue ( "Update" , "device_id" );
2026-07-14 01:37:06 +00:00
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 ;
}
2026-07-10 02:45:13 +00:00
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 ;
};
2026-07-14 01:37:06 +00:00
progress . setLabelText ( QCoreApplication :: translate ( "Launcher" , "Verifying local runtime policy..." ));
2026-07-10 02:45:13 +00:00
QApplication :: processEvents ();
PolicyHelper policy ( appDir );
if ( ! policy . loadPolicy ( "config/version_policy.dat" ) || ! policy . isValid ())
{
progress . close ();
2026-07-14 01:37:06 +00:00
QMessageBox :: critical ( nullptr ,
QCoreApplication :: translate ( "Launcher" , "Cannot Start" ),
QCoreApplication :: translate ( "Launcher" , "The local version policy is invalid: %1" ). arg ( policy . errorString ()));
return - 1 ;
2026-07-10 02:45:13 +00:00
}
if ( policy . isExpired ())
{
progress . close ();
2026-07-14 01:37:06 +00:00
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 ;
2026-07-10 02:45:13 +00:00
}
if (( ! policy . allowRun () || ! policy . isVersionAllowed ( currentVersion )) && ! ( networkOk && needUpdate ))
{
progress . close ();
2026-07-14 01:37:06 +00:00
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 ());
2026-07-10 02:45:13 +00:00
return - 1 ;
}
LocalStateHelper state ( appDir );
if ( ! state . loadState ())
{
progress . close ();
2026-07-14 01:37:06 +00:00
QMessageBox :: critical ( nullptr ,
QCoreApplication :: translate ( "Launcher" , "Cannot Start" ),
QCoreApplication :: translate ( "Launcher" , "Cannot read the local state: %1" ). arg ( state . errorString ()));
return - 1 ;
2026-07-10 02:45:13 +00:00
}
if ( state . isPolicySeqRolledBack ( policy . policySeq ()))
{
progress . close ();
2026-07-14 01:37:06 +00:00
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 ;
2026-07-10 02:45:13 +00:00
}
if ( state . isSystemTimeRewound ())
{
progress . close ();
2026-07-14 01:37:06 +00:00
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 ;
2026-07-10 02:45:13 +00:00
}
2026-07-14 01:37:06 +00:00
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" , " \n Target version: %1" ). arg ( latestVer );
2026-07-10 02:45:13 +00:00
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 ) {
2026-07-14 01:37:06 +00:00
if ( ! startMainApp ()) {
QMessageBox :: critical ( nullptr ,
QCoreApplication :: translate ( "Launcher" , "Startup Failed" ),
QCoreApplication :: translate ( "Launcher" , "Cannot start the main application: %1" ). arg ( mainAppPath ));
return - 1 ;
}
2026-07-10 02:45:13 +00:00
return 0 ;
}
const QStringList updaterArgs { appId , channel , latestVer , QString :: number ( targetVersionId )};
2026-07-14 01:37:06 +00:00
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 ;
2026-07-10 02:45:13 +00:00
}
2026-07-14 01:37:06 +00:00
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 ;
2026-07-10 02:45:13 +00:00
}
progress . show ();
}
2026-07-14 01:37:06 +00:00
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..." ));
2026-07-10 02:45:13 +00:00
QApplication :: processEvents ();
if ( ! startMainApp ())
{
progress . close ();
2026-07-14 01:37:06 +00:00
QMessageBox :: critical ( nullptr ,
QCoreApplication :: translate ( "Launcher" , "Startup Failed" ),
QCoreApplication :: translate ( "Launcher" , "Cannot start the main application: %1" ). arg ( mainAppPath ));
2026-07-10 02:45:13 +00:00
return - 1 ;
}
progress . close ();
return 0 ;
}