summaryrefslogtreecommitdiff
path: root/meshmc/launcher/updater/UpdateChecker.cpp
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/updater/UpdateChecker.cpp
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/updater/UpdateChecker.cpp')
-rw-r--r--meshmc/launcher/updater/UpdateChecker.cpp304
1 files changed, 304 insertions, 0 deletions
diff --git a/meshmc/launcher/updater/UpdateChecker.cpp b/meshmc/launcher/updater/UpdateChecker.cpp
new file mode 100644
index 0000000000..b97de4ac08
--- /dev/null
+++ b/meshmc/launcher/updater/UpdateChecker.cpp
@@ -0,0 +1,304 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "UpdateChecker.h"
+
+#include <QDebug>
+#include <QDir>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QXmlStreamReader>
+
+#include "BuildConfig.h"
+#include "FileSystem.h"
+#include "net/Download.h"
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+bool UpdateChecker::isPortableMode()
+{
+ // portable.txt lives next to the application binary.
+ return QFile::exists(FS::PathCombine(QCoreApplication::applicationDirPath(),
+ "portable.txt"));
+}
+
+bool UpdateChecker::isAppImage()
+{
+ return !qEnvironmentVariable("APPIMAGE").isEmpty();
+}
+
+QString UpdateChecker::currentVersion()
+{
+ return QString("%1.%2.%3")
+ .arg(BuildConfig.VERSION_MAJOR)
+ .arg(BuildConfig.VERSION_MINOR)
+ .arg(BuildConfig.VERSION_HOTFIX);
+}
+
+QString UpdateChecker::normalizeVersion(const QString& v)
+{
+ QString out = v.trimmed();
+ if (out.startsWith('v', Qt::CaseInsensitive))
+ out.remove(0, 1);
+ return out;
+}
+
+int UpdateChecker::compareVersions(const QString& v1, const QString& v2)
+{
+ const QStringList parts1 = v1.split('.');
+ const QStringList parts2 = v2.split('.');
+ const int len = std::max(parts1.size(), parts2.size());
+ for (int i = 0; i < len; ++i) {
+ const int a = (i < parts1.size()) ? parts1.at(i).toInt() : 0;
+ const int b = (i < parts2.size()) ? parts2.at(i).toInt() : 0;
+ if (a != b)
+ return a - b;
+ }
+ return 0;
+}
+
+// ---------------------------------------------------------------------------
+// Public
+// ---------------------------------------------------------------------------
+
+UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam,
+ QObject* parent)
+ : QObject(parent), m_network(nam)
+{
+}
+
+bool UpdateChecker::isUpdaterSupported()
+{
+ if (!BuildConfig.UPDATER_ENABLED)
+ return false;
+
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+ // On Linux/BSD: disable unless this is a portable install and not an
+ // AppImage.
+ if (isAppImage())
+ return false;
+ if (!isPortableMode())
+ return false;
+#endif
+
+ return true;
+}
+
+void UpdateChecker::checkForUpdate(bool notifyNoUpdate)
+{
+ if (!isUpdaterSupported()) {
+ qDebug() << "UpdateChecker: updater not supported on this "
+ "platform/mode. Skipping.";
+ return;
+ }
+
+ if (m_checking) {
+ qDebug() << "UpdateChecker: check already in progress, ignoring.";
+ return;
+ }
+
+ qDebug() << "UpdateChecker: starting dual-source update check.";
+ m_checking = true;
+ m_feedData.clear();
+ m_githubData.clear();
+
+ m_checkJob.reset(new NetJob("Update Check", m_network));
+ m_checkJob->addNetAction(Net::Download::makeByteArray(
+ QUrl(BuildConfig.UPDATER_FEED_URL), &m_feedData));
+ m_checkJob->addNetAction(Net::Download::makeByteArray(
+ QUrl(BuildConfig.UPDATER_GITHUB_API_URL), &m_githubData));
+
+ connect(m_checkJob.get(), &NetJob::succeeded,
+ [this, notifyNoUpdate]() { onDownloadsFinished(notifyNoUpdate); });
+ connect(m_checkJob.get(), &NetJob::failed, this,
+ &UpdateChecker::onDownloadsFailed);
+
+ m_checkJob->start();
+}
+
+// ---------------------------------------------------------------------------
+// Private slots
+// ---------------------------------------------------------------------------
+
+void UpdateChecker::onDownloadsFinished(bool notifyNoUpdate)
+{
+ m_checking = false;
+ m_checkJob.reset();
+
+ // ---- Parse the RSS feed -----------------------------------------------
+ // We look for the first <item> whose <projt:channel> == "stable".
+ // From that item we read <projt:version> and the <projt:asset> whose
+ // name attribute contains BuildConfig.BUILD_ARTIFACT.
+
+ QString feedVersion;
+ QString downloadUrl;
+ QString releaseNotes;
+
+ {
+ QXmlStreamReader xml(m_feedData);
+ m_feedData.clear();
+
+ bool insideItem = false;
+ bool isStable = false;
+ QString itemVersion;
+ QString itemUrl;
+ QString itemNotes;
+
+ // We iterate forward and take the FIRST stable item we encounter
+ // (the feed lists newest first).
+ while (!xml.atEnd() && !xml.hasError()) {
+ xml.readNext();
+
+ if (xml.isStartElement()) {
+ const QStringView name = xml.name();
+
+ if (name == u"item") {
+ insideItem = true;
+ isStable = false;
+ itemVersion.clear();
+ itemUrl.clear();
+ itemNotes.clear();
+ } else if (insideItem) {
+ if (xml.namespaceUri() ==
+ u"https://projecttick.org/ns/projt-launcher/feed") {
+ if (name == u"version") {
+ itemVersion = xml.readElementText().trimmed();
+ } else if (name == u"channel") {
+ isStable =
+ (xml.readElementText().trimmed() == "stable");
+ } else if (name == u"asset") {
+ const QString assetName =
+ xml.attributes().value("name").toString();
+ const QString assetUrl =
+ xml.attributes().value("url").toString();
+ if (!BuildConfig.BUILD_ARTIFACT.isEmpty() &&
+ assetName.contains(BuildConfig.BUILD_ARTIFACT,
+ Qt::CaseInsensitive)) {
+ itemUrl = assetUrl;
+ }
+ }
+ } else if (name == u"description" &&
+ xml.namespaceUri().isEmpty()) {
+ itemNotes =
+ xml.readElementText(
+ QXmlStreamReader::IncludeChildElements)
+ .trimmed();
+ }
+ }
+ } else if (xml.isEndElement() && xml.name() == u"item" &&
+ insideItem) {
+ insideItem = false;
+ if (isStable && !itemVersion.isEmpty()) {
+ // First stable item wins.
+ feedVersion = normalizeVersion(itemVersion);
+ downloadUrl = itemUrl;
+ releaseNotes = itemNotes;
+ break;
+ }
+ }
+ }
+
+ if (xml.hasError()) {
+ emit checkFailed(
+ tr("Failed to parse update feed: %1").arg(xml.errorString()));
+ return;
+ }
+ }
+
+ if (feedVersion.isEmpty()) {
+ emit checkFailed(
+ tr("No stable release entry found in the update feed."));
+ return;
+ }
+
+ if (downloadUrl.isEmpty()) {
+ qWarning() << "UpdateChecker: feed has version" << feedVersion
+ << "but no asset matching BUILD_ARTIFACT '"
+ << BuildConfig.BUILD_ARTIFACT << "'";
+ // We can still report an update even without a direct URL —
+ // the UpdateDialog will inform the user.
+ }
+
+ // ---- Parse the GitHub releases JSON -----------------------------------
+ // Expect the GitHub REST API format: { "tag_name": "vX.Y.Z", ... }
+
+ QString githubVersion;
+ {
+ QJsonParseError jsonError;
+ const QJsonDocument doc =
+ QJsonDocument::fromJson(m_githubData, &jsonError);
+ m_githubData.clear();
+
+ if (jsonError.error != QJsonParseError::NoError || !doc.isObject()) {
+ emit checkFailed(tr("Failed to parse GitHub releases response: %1")
+ .arg(jsonError.errorString()));
+ return;
+ }
+
+ const QString tag = doc.object().value("tag_name").toString().trimmed();
+ if (tag.isEmpty()) {
+ emit checkFailed(
+ tr("GitHub releases response contained no tag_name field."));
+ return;
+ }
+ githubVersion = normalizeVersion(tag);
+ }
+
+ qDebug() << "UpdateChecker: feed version =" << feedVersion
+ << "| github version =" << githubVersion
+ << "| current =" << currentVersion();
+
+ // ---- Cross-check both sources -----------------------------------------
+ if (feedVersion != githubVersion) {
+ qDebug() << "UpdateChecker: feed and GitHub disagree on version — no "
+ "update reported.";
+ if (notifyNoUpdate)
+ emit noUpdateFound();
+ return;
+ }
+
+ // ---- Compare against the running version ------------------------------
+ if (compareVersions(feedVersion, currentVersion()) <= 0) {
+ qDebug() << "UpdateChecker: already up to date.";
+ if (notifyNoUpdate)
+ emit noUpdateFound();
+ return;
+ }
+
+ qDebug() << "UpdateChecker: update available:" << feedVersion;
+ UpdateAvailableStatus status;
+ status.version = feedVersion;
+ status.downloadUrl = downloadUrl;
+ status.releaseNotes = releaseNotes;
+ emit updateAvailable(status);
+}
+
+void UpdateChecker::onDownloadsFailed(QString reason)
+{
+ m_checking = false;
+ m_checkJob.reset();
+ m_feedData.clear();
+ m_githubData.clear();
+ qCritical() << "UpdateChecker: download failed:" << reason;
+ emit checkFailed(reason);
+}