diff options
Diffstat (limited to 'archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp')
| -rw-r--r-- | archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp b/archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp new file mode 100644 index 0000000000..2b6ef19015 --- /dev/null +++ b/archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ + +#include "ProjTExternalUpdater.h" +#include <QCoreApplication> +#include <QDateTime> +#include <QDebug> +#include <QDir> +#include <QMessageBox> +#include <QProcess> +#include <QProgressDialog> +#include <QSettings> +#include <QTimer> +#include <algorithm> +#include <memory> + +#include "StringUtils.h" + +#include "BuildConfig.h" + +#include "ui/dialogs/UpdateAvailableDialog.h" + +class ProjTExternalUpdater::Private +{ + public: + QDir appDir; + QDir dataDir; + QTimer updateTimer; + bool allowBeta{}; + bool autoCheck{}; + double updateInterval{}; + QDateTime lastCheck; + std::unique_ptr<QSettings> settings; + + QWidget* parent{}; +}; + +ProjTExternalUpdater::ProjTExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir) + : priv(new ProjTExternalUpdater::Private()) +{ + priv->appDir = QDir(appDir); + priv->dataDir = QDir(dataDir); + auto settings_file = priv->dataDir.absoluteFilePath("projtlauncher_update.cfg"); + priv->settings = std::make_unique<QSettings>(settings_file, QSettings::Format::IniFormat); + priv->allowBeta = priv->settings->value("allow_beta", false).toBool(); + priv->autoCheck = priv->settings->value("auto_check", false).toBool(); + bool interval_ok = false; + // default once per day + priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&interval_ok); + if (!interval_ok) + priv->updateInterval = 86400; + auto last_check = priv->settings->value("last_check"); + if (!last_check.isNull() && last_check.isValid()) + { + priv->lastCheck = QDateTime::fromString(last_check.toString(), Qt::ISODate); + } + priv->parent = parent; + connectTimer(); + resetAutoCheckTimer(); + if (priv->updateInterval == 0) + checkForUpdates(false); +} + +ProjTExternalUpdater::~ProjTExternalUpdater() +{ + if (priv->updateTimer.isActive()) + priv->updateTimer.stop(); + disconnectTimer(); + priv->settings->sync(); + delete priv; +} + +void ProjTExternalUpdater::checkForUpdates() +{ + checkForUpdates(true); +} + +void ProjTExternalUpdater::checkForUpdates(bool triggeredByUser) +{ + QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); + progress.setCancelButton(nullptr); + progress.adjustSize(); + if (triggeredByUser) + progress.show(); + QCoreApplication::processEvents(); + + QProcess proc; + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + exe_name = QString("bin/%1").arg(exe_name); +#endif + + QStringList args = { "--check-only", "--dir", priv->dataDir.absolutePath(), "--debug" }; + if (priv->allowBeta) + args.append("--pre-release"); + + proc.start(priv->appDir.absoluteFilePath(exe_name), args); + auto result_start = proc.waitForStarted(5000); + if (!result_start) + { + auto err = proc.error(); + qDebug() << "Failed to start updater after 5 seconds." + << "reason:" << err << proc.errorString(); + auto msgBox = QMessageBox(QMessageBox::Information, + tr("Update Check Failed"), + tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), + QMessageBox::Ok, + priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + priv->lastCheck = QDateTime::currentDateTime(); + priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); + priv->settings->sync(); + resetAutoCheckTimer(); + return; + } + QCoreApplication::processEvents(); + + auto result_finished = proc.waitForFinished(60000); + if (!result_finished) + { + proc.kill(); + auto err = proc.error(); + auto output = proc.readAll(); + qDebug() << "Updater failed to close after 60 seconds." + << "reason:" << err << proc.errorString(); + auto msgBox = QMessageBox(QMessageBox::Information, + tr("Update Check Failed"), + tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), + QMessageBox::Ok, + priv->parent); + msgBox.setDetailedText(output); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + priv->lastCheck = QDateTime::currentDateTime(); + priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); + priv->settings->sync(); + resetAutoCheckTimer(); + return; + } + + auto exit_code = proc.exitCode(); + + auto std_output = proc.readAllStandardOutput(); + auto std_error = proc.readAllStandardError(); + + progress.hide(); + QCoreApplication::processEvents(); + + switch (exit_code) + { + case 0: + // no update available + if (triggeredByUser) + { + qDebug() << "No update available"; + auto msgBox = QMessageBox(QMessageBox::Information, + tr("No Update Available"), + tr("You are running the latest version."), + QMessageBox::Ok, + priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } + break; + case 1: + // there was an error + { + qDebug() << "Updater subprocess error" << qPrintable(std_error); + auto msgBox = QMessageBox(QMessageBox::Warning, + tr("Update Check Error"), + tr("There was an error running the update check."), + QMessageBox::Ok, + priv->parent); + msgBox.setDetailedText(QString(std_error)); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } + break; + case 100: + case 101: + // update or migration available + { + auto [first_line, remainder1] = StringUtils::splitFirst(std_output, '\n'); + auto [second_line, remainder2] = StringUtils::splitFirst(remainder1, '\n'); + auto [third_line, release_notes] = StringUtils::splitFirst(remainder2, '\n'); + auto version_name = StringUtils::splitFirst(first_line, ": ").second.trimmed(); + auto version_tag = StringUtils::splitFirst(second_line, ": ").second.trimmed(); + auto release_timestamp = + QDateTime::fromString(StringUtils::splitFirst(third_line, ": ").second.trimmed(), Qt::ISODate); + if (exit_code == 100) + qDebug() << "Update available:" << version_name << version_tag << release_timestamp; + else + qDebug() << "Migration available:" << version_name << version_tag << release_timestamp; + qDebug() << "Update release notes:" << release_notes; + + offerUpdate(version_name, version_tag, release_notes, exit_code == 101); + } + break; + default: + // unknown error code + { + qDebug() << "Updater exited with unknown code" << exit_code; + auto msgBox = QMessageBox( + QMessageBox::Information, + tr("Unknown Update Error"), + tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exit_code)), + QMessageBox::Ok, + priv->parent); + auto detail_txt = tr("StdOut: %1\nStdErr: %2").arg(QString(std_output)).arg(QString(std_error)); + msgBox.setDetailedText(detail_txt); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } + } + priv->lastCheck = QDateTime::currentDateTime(); + priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); + priv->settings->sync(); + resetAutoCheckTimer(); +} + +bool ProjTExternalUpdater::getAutomaticallyChecksForUpdates() +{ + return priv->autoCheck; +} + +double ProjTExternalUpdater::getUpdateCheckInterval() +{ + return priv->updateInterval; +} + +bool ProjTExternalUpdater::getBetaAllowed() +{ + return priv->allowBeta; +} + +void ProjTExternalUpdater::setAutomaticallyChecksForUpdates(bool check) +{ + priv->autoCheck = check; + priv->settings->setValue("auto_check", check); + priv->settings->sync(); + resetAutoCheckTimer(); +} + +void ProjTExternalUpdater::setUpdateCheckInterval(double seconds) +{ + priv->updateInterval = seconds; + priv->settings->setValue("update_interval", seconds); + priv->settings->sync(); + resetAutoCheckTimer(); +} + +void ProjTExternalUpdater::setBetaAllowed(bool allowed) +{ + priv->allowBeta = allowed; + priv->settings->setValue("allow_beta", allowed); + priv->settings->sync(); +} + +void ProjTExternalUpdater::resetAutoCheckTimer() +{ + if (priv->autoCheck && priv->updateInterval > 0) + { + qint64 timeoutMs = 0; + auto now = QDateTime::currentDateTime(); + if (priv->lastCheck.isValid()) + { + qint64 diff = priv->lastCheck.secsTo(now); + qint64 secs_left = std::max<qint64>(priv->updateInterval - diff, 0); + timeoutMs = secs_left * 1000; + } + timeoutMs = std::min(timeoutMs, static_cast<qint64>(INT_MAX)); + + qDebug() << "Auto update timer starting," << timeoutMs / 1000 << "seconds left"; + priv->updateTimer.start(static_cast<int>(timeoutMs)); + } + else + { + if (priv->updateTimer.isActive()) + priv->updateTimer.stop(); + } +} + +void ProjTExternalUpdater::connectTimer() +{ + connect(&priv->updateTimer, &QTimer::timeout, this, &ProjTExternalUpdater::autoCheckTimerFired); +} + +void ProjTExternalUpdater::disconnectTimer() +{ + disconnect(&priv->updateTimer, &QTimer::timeout, this, &ProjTExternalUpdater::autoCheckTimerFired); +} + +void ProjTExternalUpdater::autoCheckTimerFired() +{ + qDebug() << "Auto update Timer fired"; + checkForUpdates(false); +} + +void ProjTExternalUpdater::offerUpdate(const QString& version_name, + const QString& version_tag, + const QString& release_notes, + bool isMigration) +{ + priv->settings->beginGroup("skip"); + auto should_skip = priv->settings->value(version_tag, false).toBool(); + priv->settings->endGroup(); + + if (should_skip) + { + auto msgBox = QMessageBox(QMessageBox::Information, + tr("No Update Available"), + tr("There are no new updates available."), + QMessageBox::Ok, + priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + return; + } + + UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), + version_name, + release_notes, + isMigration ? UpdateAvailableDialog::Mode::Migration + : UpdateAvailableDialog::Mode::Update); + + auto result = dlg.exec(); + qDebug() << "offer dlg result" << result; + switch (result) + { + case UpdateAvailableDialog::Install: + { + performUpdate(version_tag); + return; + } + case UpdateAvailableDialog::Skip: + { + priv->settings->beginGroup("skip"); + priv->settings->setValue(version_tag, true); + priv->settings->endGroup(); + priv->settings->sync(); + return; + } + default: return; + } +} + +void ProjTExternalUpdater::performUpdate(const QString& version_tag) +{ + QProcess proc; + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + exe_name = QString("bin/%1").arg(exe_name); +#endif + + QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", version_tag }; + if (priv->allowBeta) + args.append("--pre-release"); + + proc.setProgram(priv->appDir.absoluteFilePath(exe_name)); + proc.setArguments(args); + auto result = proc.startDetached(); + if (!result) + { + qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); + } + QCoreApplication::exit(); +} |
