summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp')
-rw-r--r--archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp430
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();
+}