diff options
Diffstat (limited to 'archived/projt-launcher/launcher/updater/projtupdater')
9 files changed, 2638 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.cpp b/archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.cpp new file mode 100644 index 0000000000..84f626ba3c --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.cpp @@ -0,0 +1,1784 @@ +// 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) 2022 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 "ProjTUpdater.h" +#include "BuildConfig.h" +#include "ui/dialogs/ProgressDialog.h" + +#include <cstdlib> +#include <iostream> + +#include <QDebug> + +#include <QAccessible> +#include <QCommandLineParser> +#include <QFileInfo> +#include <QMessageBox> +#include <QNetworkProxy> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QProcess> +#include <QProgressDialog> +#include <memory> + +#include <sys.h> + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> +#include "console/WindowsConsole.hpp" +#endif + +#include <filesystem> +namespace fs = std::filesystem; + +#include "DesktopServices.h" + +#include "updater/projtupdater/UpdaterDialogs.h" + +#include "FileSystem.h" +#include "Json.h" +#include "StringUtils.h" + +#include "net/Download.h" +#include "net/RawHeaderProxy.h" + +#include "MMCZip.h" + +/** output to the log file */ +void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ + static std::mutex loggerMutex; + const std::lock_guard<std::mutex> lock(loggerMutex); // synchronized, QFile logFile is not thread-safe + + QString out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; + + ProjTUpdaterApp* app = static_cast<ProjTUpdaterApp*>(QCoreApplication::instance()); + app->logFile->write(out.toUtf8()); + app->logFile->flush(); + if (app->logToConsole) + { + QTextStream(stderr) << out.toLocal8Bit(); + fflush(stderr); + } +} + +ProjTUpdaterApp::ProjTUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) +{ +#if defined Q_OS_WIN32 + // attach the parent console if stdout not already captured + if (projt::console::AttachWindowsConsole()) + { + consoleAttached = true; + } +#endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); + setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); + setApplicationName(BuildConfig.LAUNCHER_NAME + "Updater"); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); + + // Command line parsing + QCommandLineParser parser; + parser.setApplicationDescription(QObject::tr("An auto-updater for ProjT Launcher")); + + parser.addOptions( + { { { "d", "dir" }, + tr("Use a custom path as application root (use '.' for current directory)."), + tr("directory") }, + { { "V", "projt-version" }, + tr("Use this version as the installed launcher version. (provided because stdout can not be " + "reliably captured on windows)"), + tr("installed launcher version") }, + { { "I", "install-version" }, "Install a specific version.", tr("version name") }, + { { "U", "update-url" }, tr("Update from the specified release feed."), tr("release feed url") }, + { { "c", "check-only" }, + tr("Only check if an update is needed. Exit status 100 if true, 0 if false (or non 0 if " + "there was an error).") }, + { { "p", "pre-release" }, tr("Allow updating to pre-release releases") }, + { { "F", "force" }, tr("Force an update, even if one is not needed.") }, + { { "l", "list" }, tr("List available releases.") }, + { "debug", tr("Log debug to console.") }, + { { "S", "select-ui" }, tr("Select the version to install with a GUI.") }, + { { "D", "allow-downgrade" }, tr("Allow the updater to downgrade to previous versions.") } }); + + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(arguments()); + + logToConsole = parser.isSet("debug"); + + auto updater_executable = QCoreApplication::applicationFilePath(); + +#ifdef Q_OS_MACOS + showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); +#endif + + if (updater_executable.startsWith("/tmp/.mount_")) + { + m_isAppimage = true; + m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (m_appimagePath.isEmpty()) + { + showFatalErrorMessage( + tr("Unsupported Installation"), + tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + } + + m_isFlatpak = DesktopServices::isFlatpak(); + + QString projt_executable = FS::PathCombine(applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + projt_executable.append(".exe"); +#endif + + if (!QFileInfo(projt_executable).isFile()) + { + showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable.")); + } + + m_projtExecutable = projt_executable; + + auto projt_update_url = parser.value("update-url"); + if (projt_update_url.isEmpty()) + projt_update_url = BuildConfig.UPDATER_RELEASES_URL; + + m_releaseFeedUrl = QUrl::fromUserInput(projt_update_url); + + m_checkOnly = parser.isSet("check-only"); + m_forceUpdate = parser.isSet("force"); + m_printOnly = parser.isSet("list"); + auto user_version = parser.value("install-version"); + if (!user_version.isEmpty()) + { + m_userSelectedVersion = Version(user_version); + } + m_selectUI = parser.isSet("select-ui"); + m_allowDowngrade = parser.isSet("allow-downgrade"); + + auto version = parser.value("projt-version"); + if (!version.isEmpty()) + { + if (version.contains('-')) + { + auto index = version.indexOf('-'); + m_prsimVersionChannel = version.mid(index + 1); + version = version.left(index); + } + else + { + m_prsimVersionChannel = "stable"; + } + auto version_parts = version.split('.'); + m_projtVersionMajor = version_parts.takeFirst().toInt(); + m_projtVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_projtVersionPatch = version_parts.takeFirst().toInt(); + else + m_projtVersionPatch = 0; + } + + m_allowPreRelease = parser.isSet("pre-release"); + + QString origCwdPath = QDir::currentPath(); + QString binPath = applicationDirPath(); + + { // find data director + // Root path is used for updates and portable data +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr + m_rootPath = foo.absolutePath(); +#elif defined(Q_OS_WIN32) + m_rootPath = binPath; +#elif defined(Q_OS_MAC) + QDir foo(FS::PathCombine(binPath, "../..")); + m_rootPath = foo.absolutePath(); + // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) + FS::updateTimestamp(m_rootPath); +#endif + } + + QString adjustedBy; + // change folder + QString dirParam = parser.value("dir"); + if (!dirParam.isEmpty()) + { + // the dir param. it makes projt launcher data path point to whatever the user specified + // on command line + adjustedBy = "Command line"; + m_dataPath = dirParam; +#ifndef Q_OS_MACOS + if (QDir(FS::PathCombine(m_rootPath, "UserData")).exists()) + { + m_isPortable = true; + } + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) + { + m_isPortable = true; + } +#endif + } + else if (auto dataDirEnv = QProcessEnvironment::systemEnvironment().value( + QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); + !dataDirEnv.isEmpty()) + { + adjustedBy = "System environment"; + m_dataPath = dataDirEnv; +#ifndef Q_OS_MACOS + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) + { + m_isPortable = true; + } +#endif + } + else + { + QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); + m_dataPath = foo.absolutePath(); + adjustedBy = "Persistent data path"; + +#ifndef Q_OS_MACOS + if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) + { + m_dataPath = portableUserData; + adjustedBy = "Portable user data path"; + m_isPortable = true; + } + else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) + { + m_dataPath = m_rootPath; + adjustedBy = "Portable data path"; + m_isPortable = true; + } +#endif + } + + m_updateLogPath = FS::PathCombine(m_dataPath, "logs", "projt_launcher_update.log"); + + { // setup logging + FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); + static const QString baseLogFile = + BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; + static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); + + if (FS::ensureFolderPathExists("logs")) + { // enough history to track both launches of the updater during a portable install + FS::move(logBase.arg(1), logBase.arg(2)); + FS::move(logBase.arg(0), logBase.arg(1)); + } + + logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0))); + if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + { + showFatalErrorMessage(tr("The launcher data folder is not writable!"), + tr("The updater couldn't create a log file - the data folder is not writable.\n" + "\n" + "Make sure you have write permissions to the data folder.\n" + "(%1)\n" + "\n" + "The updater cannot continue until you fix this problem.") + .arg(m_dataPath)); + return; + } + qInstallMessageHandler(appDebugOutput); + + qSetMessagePattern("%{time process}" + " " + "%{if-debug}D%{endif}" + "%{if-info}I%{endif}" + "%{if-warning}W%{endif}" + "%{if-critical}C%{endif}" + "%{if-fatal}F%{endif}" + " " + "|" + " " + "%{if-category}[%{category}]: %{endif}" + "%{message}"); + + bool foundLoggingRules = false; + + auto logRulesFile = QStringLiteral("qtlogging.ini"); + auto logRulesPath = FS::PathCombine(m_dataPath, logRulesFile); + + qDebug() << "Testing" << logRulesPath << "..."; + foundLoggingRules = QFile::exists(logRulesPath); + + // search the dataPath() + // seach app data standard path + if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) + { + logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); + if (!logRulesPath.isEmpty()) + { + qDebug() << "Found" << logRulesPath << "..."; + foundLoggingRules = true; + } + } + // seach root path + if (!foundLoggingRules) + { + logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); + qDebug() << "Testing" << logRulesPath << "..."; + foundLoggingRules = QFile::exists(logRulesPath); + } + + if (foundLoggingRules) + { + // load and set logging rules + qDebug() << "Loading logging rules from:" << logRulesPath; + QSettings loggingRules(logRulesPath, QSettings::IniFormat); + loggingRules.beginGroup("Rules"); + QStringList rule_names = loggingRules.childKeys(); + QStringList rules; + qDebug() << "Setting log rules:"; + for (auto rule_name : rule_names) + { + auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); + rules.append(rule); + qDebug() << " " << rule; + } + auto rules_str = rules.join("\n"); + QLoggingCategory::setFilterRules(rules_str); + } + + qDebug() << "<> Log initialized."; + } + + { // log debug program info + qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + " Updater, " + + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qDebug() << "Version : " << BuildConfig.printableVersionString(); + qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; + qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qDebug() << "Compiled for : " << BuildConfig.systemID(); + qDebug() << "Compiled by : " << BuildConfig.compilerID(); + qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + if (adjustedBy.size()) + { + qDebug() << "Data dir before adjustment : " << origCwdPath; + qDebug() << "Data dir after adjustment : " << m_dataPath; + qDebug() << "Adjusted by : " << adjustedBy; + } + else + { + qDebug() << "Data dir : " << QDir::currentPath(); + } + qDebug() << "Work dir : " << QDir::currentPath(); + qDebug() << "Binary path : " << binPath; + qDebug() << "Application root path : " << m_rootPath; + qDebug() << "Portable install : " << m_isPortable; + qDebug() << "<> Paths set."; + } + + { // network + m_network = makeShared<QNetworkAccessManager>(new QNetworkAccessManager()); + qDebug() << "Detecting proxy settings..."; + QNetworkProxy proxy = QNetworkProxy::applicationProxy(); + m_network->setProxy(proxy); + } + + auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".projt_launcher_updater_unpack.marker"); + auto marker_file = QFileInfo(marker_file_path); + if (marker_file.exists()) + { + auto target_dir = QString(FS::read(marker_file_path)).trimmed(); + if (target_dir.isEmpty()) + { + qWarning() << "Empty updater marker file contains no install target. making best guess of parent dir"; + target_dir = QDir(m_rootPath).absoluteFilePath(".."); + } + + QMetaObject::invokeMethod( + this, + [this, target_dir]() { moveAndFinishUpdate(target_dir); }, + Qt::QueuedConnection); + } + else + { + QMetaObject::invokeMethod(this, &ProjTUpdaterApp::loadReleaseFeed, Qt::QueuedConnection); + } +} + +ProjTUpdaterApp::~ProjTUpdaterApp() +{ + qDebug() << "updater shutting down"; + // Shut down logger by setting the logger function to nothing + qInstallMessageHandler(nullptr); + +#if defined Q_OS_WIN32 + // Detach from Windows console + if (consoleAttached) + { + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); + } +#endif +} + +void ProjTUpdaterApp::fail(const QString& reason) +{ + qCritical() << qPrintable(reason); + m_status = Failed; + exit(1); +} + +void ProjTUpdaterApp::abort(const QString& reason) +{ + qCritical() << qPrintable(reason); + m_status = Aborted; + exit(2); +} + +void ProjTUpdaterApp::showFatalErrorMessage(const QString& title, const QString& content) +{ + m_status = Failed; + auto msgBox = new QMessageBox(); + msgBox->setWindowTitle(title); + msgBox->setText(content); + msgBox->setStandardButtons(QMessageBox::Ok); + msgBox->setDefaultButton(QMessageBox::Ok); + msgBox->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); + msgBox->setIcon(QMessageBox::Critical); + msgBox->setMinimumWidth(460); + msgBox->adjustSize(); + msgBox->exec(); + exit(1); +} + +void ProjTUpdaterApp::run() +{ + qDebug() << "found" << m_releases.length() << "releases in update feed"; + qDebug() << "loading exe at " << m_projtExecutable; + + if (m_printOnly) + { + printReleases(); + m_status = Succeeded; + return exit(0); + } + + if (!loadProjTVersionFromExe(m_projtExecutable)) + { + m_projtVersion = BuildConfig.printableVersionString(); + m_projtVersionMajor = BuildConfig.VERSION_MAJOR; + m_projtVersionMinor = BuildConfig.VERSION_MINOR; + m_projtVersionPatch = BuildConfig.VERSION_PATCH; + m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; + m_projtGitCommit = BuildConfig.GIT_COMMIT; + } + m_status = Succeeded; + + qDebug() << "Executable reports as:" << m_projtBinaryName << "version:" << m_projtVersion; + qDebug() << "Version major:" << m_projtVersionMajor; + qDebug() << "Version minor:" << m_projtVersionMinor; + qDebug() << "Version minor:" << m_projtVersionPatch; + qDebug() << "Version channel:" << m_prsimVersionChannel; + qDebug() << "Git Commit:" << m_projtGitCommit; + + auto update_candidate = findUpdateCandidate(); + if (update_candidate.release.isValid()) + qDebug() << "Latest release" << update_candidate.release.version; + auto need_update = update_candidate.kind == UpdateKind::Update; + auto has_migration = update_candidate.kind == UpdateKind::Migration; + + if (m_checkOnly) + { + if (need_update || has_migration) + { + QTextStream stdOutStream(stdout); + stdOutStream << "Name: " << update_candidate.release.name << "\n"; + stdOutStream << "Version: " << update_candidate.release.tag_name << "\n"; + stdOutStream << "TimeStamp: " << update_candidate.release.created_at.toString(Qt::ISODate) << "\n"; + stdOutStream << update_candidate.release.body << "\n"; + stdOutStream.flush(); + + return exit(need_update ? 100 : 101); + } + else + { + return exit(0); + } + } + + if (m_isFlatpak) + { + showFatalErrorMessage(tr("Updating flatpack not supported"), + tr("Actions outside of checking if an update is available are not " + "supported when running the flatpak version of ProjT Launcher.")); + return; + } + if (m_isAppimage) + { + bool result = true; + if (need_update) + result = callAppImageUpdate(); + return exit(result ? 0 : 1); + } + + if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) + { + ReleaseInfo update_release = update_candidate.release.isValid() ? update_candidate.release : getLatestRelease(); + if (!m_userSelectedVersion.isEmpty()) + { + bool found = false; + for (auto rls : m_releases) + { + if (rls.version == m_userSelectedVersion) + { + found = true; + update_release = rls; + break; + } + } + if (!found) + { + showFatalErrorMessage("No release for version!", + QString("Can not find a release entry for specified version %1") + .arg(m_userSelectedVersion.toString())); + return; + } + } + else if (m_selectUI) + { + update_release = selectRelease(); + if (!update_release.isValid()) + { + showFatalErrorMessage("No version selected.", "No version was selected."); + return; + } + } + + performUpdate(update_release); + } + + exit(0); +} + +void ProjTUpdaterApp::moveAndFinishUpdate(QDir target) +{ + logUpdate("Finishing update process"); + + logUpdate("Waiting 2 seconds for resources to free"); + this->thread()->sleep(2); + + auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); + QFileInfo manifest(manifest_path); + + auto app_dir = QDir(m_rootPath); + + QStringList file_list; + if (manifest.isFile()) + { + // load manifest from file + logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); + try + { + auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); + auto files = contents.split('\n'); + for (auto file : files) + { + file_list.append(file.trimmed()); + } + } + catch (FS::FileSystemException&) + {} + } + + if (file_list.isEmpty()) + { + logUpdate(tr("Manifest empty, making best guess of the directory contents of %1").arg(m_rootPath)); + auto entries = target.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + for (auto entry : entries) + { + file_list.append(entry.fileName()); + } + } + logUpdate(tr("Installing the following to %1 :\n %2").arg(target.absolutePath()).arg(file_list.join(",\n "))); + + bool error = false; + + QProgressDialog progress(tr("Installing from %1").arg(m_rootPath), "", 0, file_list.length()); + progress.setCancelButton(nullptr); + progress.setMinimumWidth(400); + progress.adjustSize(); + progress.show(); + QCoreApplication::processEvents(); + + logUpdate(tr("Installing from %1").arg(m_rootPath)); + + auto copy = [this, app_dir, target](QString to_install_file) + { + auto rel_path = app_dir.relativeFilePath(to_install_file); + auto install_path = FS::PathCombine(target.absolutePath(), rel_path); + logUpdate(tr("Installing %1 from %2").arg(install_path).arg(to_install_file)); + FS::ensureFilePathExists(install_path); + auto result = FS::copy(to_install_file, install_path).overwrite(true)(); + if (!result) + { + logUpdate(tr("Failed copy %1 to %2").arg(to_install_file).arg(install_path)); + return true; + } + return false; + }; + + int i = 0; + for (auto glob : file_list) + { + QDirIterator iter(m_rootPath, QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + progress.setValue(i); + QCoreApplication::processEvents(); + if (!iter.hasNext() && !glob.isEmpty()) + { + if (auto file_info = QFileInfo(FS::PathCombine(m_rootPath, glob)); file_info.exists()) + { + error |= copy(file_info.absoluteFilePath()); + } + else + { + logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(m_rootPath, glob))); + } + } + else + { + while (iter.hasNext()) + { + error |= copy(iter.next()); + } + } + i++; + } + progress.setValue(i); + QCoreApplication::processEvents(); + + if (error) + { + logUpdate(tr("There were errors installing the update.")); + auto fail_marker = FS::PathCombine(m_dataPath, ".projt_launcher_update.fail"); + FS::copy(m_updateLogPath, fail_marker).overwrite(true)(); + } + else + { + logUpdate(tr("Update succeed.")); + auto success_marker = FS::PathCombine(m_dataPath, ".projt_launcher_update.success"); + FS::copy(m_updateLogPath, success_marker).overwrite(true)(); + } + auto update_lock_path = FS::PathCombine(m_dataPath, ".projt_launcher_update.lock"); + FS::deletePath(update_lock_path); + + QProcess proc; + auto app_exe_name = BuildConfig.LAUNCHER_APP_BINARY_NAME; +#if defined Q_OS_WIN32 + app_exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + app_exe_name.prepend("bin/"); +#endif + + auto app_exe_path = target.absoluteFilePath(app_exe_name); + proc.startDetached(app_exe_path); + + exit(error ? 1 : 0); +} + +void ProjTUpdaterApp::printReleases() +{ + for (auto release : m_releases) + { + std::cout << release.name.toStdString() << " Version: " << release.tag_name.toStdString() << std::endl; + } +} + +QList<ReleaseInfo> ProjTUpdaterApp::nonDraftReleases() +{ + QList<ReleaseInfo> nonDraft; + for (auto rls : m_releases) + { + if (rls.isValid() && !rls.draft) + nonDraft.append(rls); + } + return nonDraft; +} + +QList<ReleaseInfo> ProjTUpdaterApp::newerReleases() +{ + QList<ReleaseInfo> newer; + for (auto rls : nonDraftReleases()) + { + if (rls.version > m_projtVersion) + newer.append(rls); + } + return newer; +} + +ReleaseInfo ProjTUpdaterApp::selectRelease() +{ + QList<ReleaseInfo> releases; + + if (m_allowDowngrade) + { + releases = nonDraftReleases(); + } + else + { + releases = newerReleases(); + } + + if (releases.isEmpty()) + return {}; + + SelectReleaseDialog dlg(Version(m_projtVersion), releases); + auto result = dlg.exec(); + + if (result == QDialog::Rejected) + { + return {}; + } + ReleaseInfo release = dlg.selectedRelease(); + + return release; +} + +QList<ReleaseAsset> ProjTUpdaterApp::validReleaseArtifacts(const ReleaseInfo& release) +{ + QList<ReleaseAsset> valid; + + qDebug() << "Selecting best asset from" << release.tag_name << "for platform" << BuildConfig.BUILD_ARTIFACT + << "portable:" << m_isPortable; + if (BuildConfig.BUILD_ARTIFACT.isEmpty()) + qWarning() << "Build platform is not set!"; + for (auto asset : release.assets) + { + if (asset.name.endsWith(".zsync")) + { + qDebug() << "Rejecting zsync file" << asset.name; + continue; + } + if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) + { + qDebug() << "Rejecting" << asset.name << "because it is an AppImage"; + continue; + } + else if (m_isAppimage && !asset.name.toLower().endsWith("appimage")) + { + qDebug() << "Rejecting" << asset.name << "because it is not an AppImage"; + continue; + } + auto asset_name = asset.name.toLower(); + auto [platform, platform_qt_ver] = StringUtils::splitFirst(BuildConfig.BUILD_ARTIFACT.toLower(), "-qt"); + auto system_is_arm = QSysInfo::buildCpuArchitecture().contains("arm64"); + auto asset_is_arm = asset_name.contains("arm64"); + auto asset_is_archive = asset_name.endsWith(".zip") || asset_name.endsWith(".tar.gz"); + + bool for_platform = !platform.isEmpty() && asset_name.contains(platform); + if (!for_platform) + { + qDebug() << "Rejecting" << asset.name << "because platforms do not match"; + } + bool for_portable = asset_name.contains("portable"); + if (for_platform && asset_name.contains("legacy") && !platform.contains("legacy")) + { + qDebug() << "Rejecting" << asset.name << "because platforms do not match"; + for_platform = false; + } + if (for_platform && ((asset_is_arm && !system_is_arm) || (!asset_is_arm && system_is_arm))) + { + qDebug() << "Rejecting" << asset.name << "because architecture does not match"; + for_platform = false; + } + if (for_platform && platform.contains("windows") && !m_isPortable && asset_is_archive) + { + qDebug() << "Rejecting" << asset.name << "because it is not an installer"; + for_platform = false; + } + + static const QRegularExpression s_qtPattern("-qt(\\d+)"); + auto qt_match = s_qtPattern.match(asset_name); + if (for_platform && qt_match.hasMatch()) + { + if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) + { + qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" + << platform_qt_ver.toInt() << "vs" << qt_match.captured(1).toInt(); + for_platform = false; + } + } + + if (((m_isPortable && for_portable) || (!m_isPortable && !for_portable)) && for_platform) + { + qDebug() << "Accepting" << asset.name; + valid.append(asset); + } + } + return valid; +} + +ReleaseAsset ProjTUpdaterApp::selectAsset(const QList<ReleaseAsset>& assets) +{ + SelectReleaseAssetDialog dlg(assets); + auto result = dlg.exec(); + + if (result == QDialog::Rejected) + { + return {}; + } + + ReleaseAsset asset = dlg.selectedAsset(); + return asset; +} + +void ProjTUpdaterApp::performUpdate(const ReleaseInfo& release) +{ + m_install_release = release; + qDebug() << "Updating to" << release.tag_name; + auto valid_assets = validReleaseArtifacts(release); + qDebug() << "valid release assets:" << valid_assets; + + ReleaseAsset selected_asset; + if (valid_assets.isEmpty()) + { + return showFatalErrorMessage( + tr("No Valid Release Assets"), + tr("Release %1 has no valid assets for this platform: %2") + .arg(release.tag_name) + .arg(tr("%1 portable: %2").arg(BuildConfig.BUILD_ARTIFACT).arg(m_isPortable ? tr("yes") : tr("no")))); + } + else if (valid_assets.length() > 1) + { + selected_asset = selectAsset(valid_assets); + } + else + { + selected_asset = valid_assets.takeFirst(); + } + + if (!selected_asset.isValid()) + { + return showFatalErrorMessage(tr("No version selected."), tr("No version was selected.")); + } + + qDebug() << "will install" << selected_asset; + auto file = downloadAsset(selected_asset); + + if (!file.exists()) + { + return showFatalErrorMessage(tr("Failed to Download"), tr("Failed to download the selected asset.")); + } + + performInstall(file); +} + +QFileInfo ProjTUpdaterApp::downloadAsset(const ReleaseAsset& asset) +{ + auto temp_dir = QDir::tempPath(); + auto file_url = asset.download_url; + auto out_file_path = FS::PathCombine(temp_dir, file_url.fileName()); + + qDebug() << "downloading" << file_url << "to" << out_file_path; + auto download = Net::Download::makeFile(file_url, out_file_path); + download->setNetwork(m_network); + auto progress_dialog = ProgressDialog(); + progress_dialog.adjustSize(); + + progress_dialog.execWithTask(*download); + + qDebug() << "download complete"; + + QFileInfo out_file(out_file_path); + return out_file; +} + +bool ProjTUpdaterApp::callAppImageUpdate() +{ + auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + QProcess proc = QProcess(); + qDebug() << "Calling: AppImageUpdate" << appimage_path; + proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate-x86_64.AppImage")); + proc.setArguments({ appimage_path }); + auto result = proc.startDetached(); + if (!result) + qDebug() << "Failed to start AppImageUpdate reason:" << proc.errorString(); + return result; +} + +void ProjTUpdaterApp::clearUpdateLog() +{ + FS::deletePath(m_updateLogPath); +} + +void ProjTUpdaterApp::logUpdate(const QString& msg) +{ + qDebug() << qUtf8Printable(msg); + FS::append(m_updateLogPath, QStringLiteral("%1\n").arg(msg).toUtf8()); +} + +std::tuple<QDateTime, QString, QString, QString, QString> read_lock_File(const QString& path) +{ + auto contents = QString(FS::read(path)); + auto lines = contents.split('\n'); + + QDateTime timestamp; + QString from, to, target, data_path; + for (auto line : lines) + { + auto index = line.indexOf("="); + if (index < 0) + continue; + auto left = line.left(index); + auto right = line.mid(index + 1); + if (left.toLower() == "timestamp") + { + timestamp = QDateTime::fromString(right, Qt::ISODate); + } + else if (left.toLower() == "from") + { + from = right; + } + else if (left.toLower() == "to") + { + to = right; + } + else if (left.toLower() == "target") + { + target = right; + } + else if (left.toLower() == "data_path") + { + data_path = right; + } + } + return std::make_tuple(timestamp, from, to, target, data_path); +} + +bool write_lock_file(const QString& path, + QDateTime timestamp, + QString from, + QString to, + QString target, + QString data_path) +{ + try + { + FS::write(path, + QStringLiteral("TIMESTAMP=%1\nFROM=%2\nTO=%3\nTARGET=%4\nDATA_PATH=%5\n") + .arg(timestamp.toString(Qt::ISODate)) + .arg(from) + .arg(to) + .arg(target) + .arg(data_path) + .toUtf8()); + } + catch (FS::FileSystemException& err) + { + qWarning() << "Error writing lockfile:" << err.what() << "\n" << err.cause(); + return false; + } + return true; +} + +void ProjTUpdaterApp::performInstall(QFileInfo file) +{ + qDebug() << "starting install"; + auto update_lock_path = FS::PathCombine(m_dataPath, ".projt_launcher_update.lock"); + QFileInfo update_lock(update_lock_path); + if (update_lock.exists()) + { + auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock_path); + auto msg = tr("Update already in progress\n"); + auto infoMsg = tr("This installation has a update lock file present at: %1\n" + "\n" + "Timestamp: %2\n" + "Updating from version %3 to %4\n" + "Target install path: %5\n" + "Data Path: %6" + "\n" + "This likely means that a previous update attempt failed. Please ensure your installation is " + "in working order before " + "proceeding.\n" + "Check the ProjT Launcher updater log at: \n" + "%7\n" + "for details on the last update attempt.\n" + "\n" + "To overwrite this lock and proceed with this update anyway, select \"Ignore\" below.") + .arg(update_lock_path) + .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) + .arg(m_updateLogPath); + QMessageBox msgBox; + msgBox.setText(msg); + msgBox.setInformativeText(infoMsg); + msgBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + switch (msgBox.exec()) + { + case QMessageBox::AcceptRole: break; + case QMessageBox::RejectRole: [[fallthrough]]; + default: return showFatalErrorMessage(tr("Update Aborted"), tr("The update attempt was aborted")); + } + } + clearUpdateLog(); + + auto changelog_path = FS::PathCombine(m_dataPath, ".projt_launcher_update.changelog"); + FS::write(changelog_path, m_install_release.body.toUtf8()); + + logUpdate(tr("Updating from %1 to %2").arg(m_projtVersion).arg(m_install_release.tag_name)); + if (m_isPortable || file.fileName().endsWith(".zip") || file.fileName().endsWith(".tar.gz")) + { + write_lock_file(update_lock_path, + QDateTime::currentDateTime(), + m_projtVersion, + m_install_release.tag_name, + m_rootPath, + m_dataPath); + logUpdate(tr("Updating portable install at %1").arg(m_rootPath)); + unpackAndInstall(file); + } + else + { + logUpdate(tr("Running installer file at %1").arg(file.absoluteFilePath())); + QProcess proc = QProcess(); +#if defined Q_OS_WIN + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#endif + proc.setProgram(file.absoluteFilePath()); + bool result = proc.startDetached(); + logUpdate(tr("Process start result: %1").arg(result ? tr("yes") : tr("no"))); + exit(result ? 0 : 1); + } +} + +void ProjTUpdaterApp::unpackAndInstall(QFileInfo archive) +{ + logUpdate(tr("Backing up install")); + backupAppDir(); + + if (auto loc = unpackArchive(archive)) + { + auto marker_file_path = loc.value().absoluteFilePath(".projt_launcher_updater_unpack.marker"); + FS::write(marker_file_path, m_rootPath.toUtf8()); + + QProcess proc = QProcess(); + + 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.prepend("bin/"); +#endif + + auto new_updater_path = loc.value().absoluteFilePath(exe_name); + logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path)); + if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) + { + logUpdate(tr("Failed to launch '%1' %2").arg(new_updater_path).arg(proc.errorString())); + return exit(10); + } + return exit(); // up to the new updater now + } + return exit(1); // unpack failure +} + +void ProjTUpdaterApp::backupAppDir() +{ + auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); + QFileInfo manifest(manifest_path); + + QStringList file_list; + if (manifest.isFile()) + { + // load manifest from file + + logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); + try + { + auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); + auto files = contents.split('\n'); + for (auto file : files) + { + file_list.append(file.trimmed()); + } + } + catch (FS::FileSystemException&) + {} + } + + if (file_list.isEmpty()) + { + // best guess + if (BuildConfig.BUILD_ARTIFACT.toLower().contains("linux")) + { + file_list.append({ "ProjTLauncher", "bin", "share", "lib" }); + } + else + { // windows by process of elimination + file_list.append({ + "jars", + "projtlauncher.exe", + "projtlauncher_filelink.exe", + "projtlauncher_updater.exe", + "qtlogging.ini", + "imageformats", + "iconengines", + "platforms", + "styles", + "tls", + "qt.conf", + "Qt*.dll", + }); + } + logUpdate("manifest.txt empty or missing. making best guess at files to back up."); + } + logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); + static const QRegularExpression s_replaceRegex("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"); + auto app_dir = QDir(m_rootPath); + auto backup_dir = + FS::PathCombine(app_dir.absolutePath(), + QStringLiteral("backup_") + QString(m_projtVersion).replace(s_replaceRegex, QString("_")) + "-" + + m_projtGitCommit); + FS::ensureFolderPathExists(backup_dir); + auto backup_marker_path = FS::PathCombine(m_dataPath, ".projt_launcher_update_backup_path.txt"); + FS::write(backup_marker_path, backup_dir.toUtf8()); + + QProgressDialog progress(tr("Backing up install at %1").arg(m_rootPath), "", 0, file_list.length()); + progress.setCancelButton(nullptr); + progress.setMinimumWidth(400); + progress.adjustSize(); + progress.show(); + QCoreApplication::processEvents(); + + logUpdate(tr("Backing up install at %1").arg(m_rootPath)); + + auto copy = [this, app_dir, backup_dir](QString to_bak_file) + { + auto rel_path = app_dir.relativeFilePath(to_bak_file); + auto bak_path = FS::PathCombine(backup_dir, rel_path); + logUpdate(tr("Backing up and then removing %1").arg(to_bak_file)); + FS::ensureFilePathExists(bak_path); + auto result = FS::copy(to_bak_file, bak_path).overwrite(true)(); + if (!result) + { + logUpdate(tr("Failed to backup %1 to %2").arg(to_bak_file).arg(bak_path)); + } + else + { + if (!FS::deletePath(to_bak_file)) + logUpdate(tr("Failed to remove %1").arg(to_bak_file)); + } + }; + + int i = 0; + for (auto glob : file_list) + { + QDirIterator iter(app_dir.absolutePath(), + QStringList({ glob }), + QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + progress.setValue(i); + QCoreApplication::processEvents(); + if (!iter.hasNext() && !glob.isEmpty()) + { + if (auto file_info = QFileInfo(FS::PathCombine(app_dir.absolutePath(), glob)); file_info.exists()) + { + copy(file_info.absoluteFilePath()); + } + else + { + logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(app_dir.absolutePath(), glob))); + } + } + else + { + while (iter.hasNext()) + { + copy(iter.next()); + } + } + i++; + } + progress.setValue(i); + QCoreApplication::processEvents(); +} + +std::optional<QDir> ProjTUpdaterApp::unpackArchive(QFileInfo archive) +{ + auto temp_extract_path = FS::PathCombine(m_dataPath, "projt_launcher_update_release"); + FS::ensureFolderPathExists(temp_extract_path); + auto tmp_extract_dir = QDir(temp_extract_path); + + if (archive.fileName().endsWith(".zip")) + { + auto result = MMCZip::extractDir(archive.absoluteFilePath(), tmp_extract_dir.absolutePath()); + if (result) + { + logUpdate(tr("Extracted the following to \"%1\":\n %2") + .arg(tmp_extract_dir.absolutePath()) + .arg(result->join("\n "))); + } + else + { + logUpdate( + tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); + showFatalErrorMessage( + "Failed to extract archive", + tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); + return std::nullopt; + } + } + else if (archive.fileName().endsWith(".tar.gz")) + { + QString cmd = "tar"; + QStringList args = { "-xvf", archive.absoluteFilePath(), "-C", tmp_extract_dir.absolutePath() }; + logUpdate(tr("Running: `%1 %2`").arg(cmd).arg(args.join(" "))); + QProcess proc = QProcess(); + proc.start(cmd, args); + if (!proc.waitForStarted(5000)) + { // wait 5 seconds to start + auto msg = tr("Failed to launch child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); + logUpdate(msg); + showFatalErrorMessage(tr("Failed extract archive"), msg); + return std::nullopt; + } + auto result = proc.waitForFinished(5000); + auto out = proc.readAll(); + logUpdate(out); + if (!result) + { + auto msg = tr("Child process \"%1 %2\" failed.").arg(cmd).arg(args.join(" ")); + logUpdate(msg); + showFatalErrorMessage(tr("Failed to extract archive"), msg); + return std::nullopt; + } + } + else + { + logUpdate(tr("Unknown archive format for %1").arg(archive.absoluteFilePath())); + showFatalErrorMessage("Can not extract", + QStringLiteral("Unknown archive format %1").arg(archive.absoluteFilePath())); + return std::nullopt; + } + + return tmp_extract_dir; +} + +bool ProjTUpdaterApp::loadProjTVersionFromExe(const QString& exe_path) +{ + QProcess proc = QProcess(); + proc.setProcessChannelMode(QProcess::MergedChannels); + proc.setReadChannel(QProcess::StandardOutput); + proc.start(exe_path, { "--version" }); + if (!proc.waitForStarted(5000)) + { + showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launch child process to read version.")); + return false; + } // wait 5 seconds to start + if (!proc.waitForFinished(5000)) + { + showFatalErrorMessage(tr("Failed to Check Version"), tr("Child launcher process failed.")); + return false; + } + auto out = proc.readAllStandardOutput(); + auto lines = out.split('\n'); + lines.removeAll(""); + if (lines.length() < 2) + return false; + else if (lines.length() > 2) + { + auto line1 = lines.takeLast(); + auto line2 = lines.takeLast(); + lines = { line2, line1 }; + } + auto first = lines.takeFirst(); + auto first_parts = first.split(' '); + if (first_parts.length() < 2) + return false; + m_projtBinaryName = first_parts.takeFirst(); + auto version = first_parts.takeFirst().trimmed(); + m_projtVersion = version; + if (version.contains('-')) + { + auto index = version.indexOf('-'); + m_prsimVersionChannel = version.mid(index + 1); + version = version.left(index); + } + else + { + m_prsimVersionChannel = "stable"; + } + auto version_parts = version.split('.'); + if (version_parts.length() < 2) + return false; + m_projtVersionMajor = version_parts.takeFirst().toInt(); + m_projtVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_projtVersionPatch = version_parts.takeFirst().toInt(); + else + m_projtVersionPatch = 0; + m_projtGitCommit = lines.takeFirst().simplified(); + return true; +} + +namespace +{ + QDateTime parseOptionalDateTime(const QJsonObject& object, const QString& key) + { + auto value = Json::ensureString(object, key); + if (value.isEmpty()) + return {}; + auto parsed = QDateTime::fromString(value, Qt::ISODate); + if (!parsed.isValid()) + { + throw Json::JsonException(QString("'%1' is not a ISO formatted date/time value").arg(key)); + } + return parsed; + } + + QUrl parseAssetUrl(const QJsonObject& object) + { + for (const auto& key : + { QStringLiteral("url"), QStringLiteral("download_url"), QStringLiteral("browser_download_url") }) + { + auto value = Json::ensureString(object, key); + if (!value.isEmpty()) + { + return QUrl::fromUserInput(value); + } + } + return {}; + } +} // namespace + +void ProjTUpdaterApp::loadReleaseFeed() +{ + if (!m_releaseFeedUrl.isValid() || m_releaseFeedUrl.isEmpty()) + return fail("release feed url is missing or invalid"); + + qDebug() << "Fetching release feed from" << m_releaseFeedUrl; + downloadReleaseFeed(); +} + +void ProjTUpdaterApp::downloadReleaseFeed() +{ + auto response = std::make_shared<QByteArray>(); + auto download = Net::Download::makeByteArray(m_releaseFeedUrl, response); + download->setNetwork(m_network); + m_current_url = m_releaseFeedUrl.toString(); + + auto feed_headers = new Net::RawHeaderProxy(); + feed_headers->addHeaders({ { "Accept", "application/json" } }); + download->addHeaderProxy(feed_headers); + + connect(download.get(), + &Net::Download::succeeded, + this, + [this, response]() + { + parseReleaseFeed(*response); + run(); + }); + connect(download.get(), &Net::Download::failed, this, &ProjTUpdaterApp::downloadError); + + m_current_task.reset(download); + connect(download.get(), + &Net::Download::finished, + this, + [this]() + { + qDebug() << "Download" << m_current_task->getUid().toString() << "finished"; + m_current_task.reset(); + m_current_url = ""; + }); + + QCoreApplication::processEvents(); + + QMetaObject::invokeMethod(download.get(), &Task::start, Qt::QueuedConnection); +} + +int ProjTUpdaterApp::parseReleaseFeed(const QByteArray& response) +{ + if (response.isEmpty()) + return 0; + + int num_releases = 0; + try + { + auto doc = Json::requireDocument(response, "Release feed"); + QJsonArray release_list; + if (doc.isArray()) + { + release_list = Json::requireArray(doc, "Release feed"); + } + else + { + auto root = Json::requireObject(doc, "Release feed"); + if (root.contains("releases")) + { + release_list = Json::requireArray(root, "releases"); + } + else if (root.contains("versions")) + { + release_list = Json::requireArray(root, "versions"); + } + else + { + throw Json::JsonException("Release feed must be a release array or contain a 'releases' array"); + } + } + + for (auto release_json : release_list) + { + auto release_obj = Json::requireObject(release_json); + + ReleaseInfo release = {}; + release.name = Json::ensureString(release_obj, "name"); + release.tag_name = Json::requireString(release_obj, "tag_name"); + release.created_at = parseOptionalDateTime(release_obj, "created_at"); + release.published_at = parseOptionalDateTime(release_obj, "published_at"); + release.draft = Json::ensureBoolean(release_obj, "draft", false); + release.prerelease = Json::ensureBoolean(release_obj, "prerelease", false); + if (!release.prerelease) + { + auto channel = Json::ensureString(release_obj, "channel").trimmed(); + release.prerelease = !channel.isEmpty() && channel != "stable"; + } + release.body = Json::ensureString(release_obj, "body"); + + auto normalized_tag = release.tag_name; + if (normalized_tag.startsWith(QLatin1Char('v'), Qt::CaseInsensitive)) + { + normalized_tag.remove(0, 1); + } + release.version = Version(normalized_tag); + + auto release_assets_obj = Json::requireArray(release_obj, "assets"); + for (auto asset_json : release_assets_obj) + { + auto asset_obj = Json::requireObject(asset_json); + ReleaseAsset asset = {}; + asset.name = Json::requireString(asset_obj, "name"); + asset.label = Json::ensureString(asset_obj, "label"); + asset.content_type = Json::ensureString(asset_obj, "content_type"); + asset.size = Json::ensureInteger(asset_obj, "size", 0); + asset.created_at = parseOptionalDateTime(asset_obj, "created_at"); + asset.updated_at = parseOptionalDateTime(asset_obj, "updated_at"); + asset.download_url = parseAssetUrl(asset_obj); + if (!asset.download_url.isValid()) + { + throw Json::JsonException(QString("Asset '%1' is missing a valid download url").arg(asset.name)); + } + release.assets.append(asset); + } + + m_releases.append(release); + num_releases++; + } + } + catch (Json::JsonException& e) + { + auto err_msg = QString("Failed to parse release feed: %1\n%2").arg(e.what()).arg(QString::fromUtf8(response)); + fail(err_msg); + } + return num_releases; +} + +ReleaseInfo ProjTUpdaterApp::getLatestRelease() +{ + ReleaseInfo latest; + for (auto release : m_releases) + { + if (release.draft) + continue; + if (release.prerelease && !m_allowPreRelease) + continue; + if (!latest.isValid() || (release.version > latest.version)) + { + latest = release; + } + } + return latest; +} + +namespace +{ + struct LineVersion + { + int x = -1; + int y = -1; + int z = -1; + int t = -1; + bool is_rc = false; + }; + + std::optional<LineVersion> parseLineVersion(QString ver) + { + ver = ver.trimmed(); + if (ver.startsWith(QLatin1Char('v'), Qt::CaseInsensitive)) + { + ver.remove(0, 1); + } + + while (ver.endsWith(QLatin1Char('.'))) + { + ver.chop(1); + } + + const auto dash_parts = ver.split(QLatin1Char('-'), Qt::KeepEmptyParts); + if (dash_parts.size() >= 2) + { + const auto main_part = dash_parts.at(0); + const auto t_part = dash_parts.at(1); + + bool ok_t = false; + bool is_rc = false; + int t = -1; + if (t_part.startsWith("rc", Qt::CaseInsensitive)) + { + const auto rc_part = t_part.mid(2); + t = rc_part.toInt(&ok_t); + is_rc = ok_t; + } + else + { + t = t_part.toInt(&ok_t); + } + if (!ok_t) + return std::nullopt; + + const auto dot_parts = main_part.split(QLatin1Char('.'), Qt::KeepEmptyParts); + if (dot_parts.size() != 3) + return std::nullopt; + + bool ok_x = false; + bool ok_y = false; + bool ok_z = false; + const auto x = dot_parts.at(0).toInt(&ok_x); + const auto y = dot_parts.at(1).toInt(&ok_y); + const auto z = dot_parts.at(2).toInt(&ok_z); + if (!ok_x || !ok_y || !ok_z) + return std::nullopt; + + return LineVersion{ x, y, z, t, is_rc }; + } + + const auto dot_parts = ver.split(QLatin1Char('.'), Qt::KeepEmptyParts); + if (dot_parts.size() != 4) + return std::nullopt; + + bool ok_x = false; + bool ok_y = false; + bool ok_z = false; + bool ok_t = false; + const auto x = dot_parts.at(0).toInt(&ok_x); + const auto y = dot_parts.at(1).toInt(&ok_y); + const auto z = dot_parts.at(2).toInt(&ok_z); + const auto t = dot_parts.at(3).toInt(&ok_t); + if (!ok_x || !ok_y || !ok_z || !ok_t) + return std::nullopt; + + return LineVersion{ x, y, z, t, false }; + } + + int compareLine(const LineVersion& a, const LineVersion& b) + { + if (a.x != b.x) + return a.x < b.x ? -1 : 1; + if (a.y != b.y) + return a.y < b.y ? -1 : 1; + if (a.z != b.z) + return a.z < b.z ? -1 : 1; + return 0; + } + + int compareLinePatch(const LineVersion& a, const LineVersion& b) + { + if (a.is_rc != b.is_rc) + return a.is_rc ? -1 : 1; + if (a.t != b.t) + return a.t < b.t ? -1 : 1; + return 0; + } +} // namespace + +ProjTUpdaterApp::UpdateCandidate ProjTUpdaterApp::findUpdateCandidate() +{ + auto normalizeVersionString = [](QString ver) + { + ver = ver.trimmed(); + if (ver.startsWith(QLatin1Char('v'), Qt::CaseInsensitive)) + { + ver.remove(0, 1); + } + return ver; + }; + + auto appendChannelIfMissing = [](QString ver, const QString& channel) + { + if (!channel.isEmpty() && channel != "stable" && !ver.contains(QString("-%1").arg(channel))) + { + ver += QString("-%1").arg(channel); + } + return ver; + }; + + auto buildVersionFromConfig = [appendChannelIfMissing]() + { + QString ver = BuildConfig.versionString(); + if (BuildConfig.VERSION_CHANNEL != "stable" && BuildConfig.GIT_TAG != ver) + { + ver = appendChannelIfMissing(ver, BuildConfig.VERSION_CHANNEL); + } + return ver; + }; + + QString current_version = m_projtVersion; + if (current_version.isEmpty()) + { + current_version = buildVersionFromConfig(); + } + + // If we parsed a channel from the running binary, ensure it is present. + current_version = appendChannelIfMissing(current_version, m_prsimVersionChannel); + + current_version = normalizeVersionString(current_version); + auto parsed_current = parseLineVersion(current_version); + + UpdateCandidate candidate; + + if (!parsed_current.has_value()) + { + auto latest = getLatestRelease(); + if (latest.isValid() && Version(current_version) < latest.version) + { + candidate.kind = UpdateKind::Update; + candidate.release = latest; + } + return candidate; + } + + const auto current = parsed_current.value(); + + bool has_same_line = false; + bool has_migration = false; + LineVersion best_line = current; + LineVersion best_migration_line; + ReleaseInfo best_same_release; + ReleaseInfo best_migration_release; + + for (const auto& release : m_releases) + { + if (release.draft) + continue; + if (release.prerelease && !m_allowPreRelease) + continue; + + auto parsed_release = parseLineVersion(release.tag_name); + if (!parsed_release.has_value()) + continue; + + auto rel = parsed_release.value(); + auto line_cmp = compareLine(rel, current); + if (line_cmp == 0) + { + if (compareLinePatch(rel, current) > 0 && (!has_same_line || compareLinePatch(rel, best_line) > 0)) + { + best_line = rel; + best_same_release = release; + has_same_line = true; + } + continue; + } + if (line_cmp > 0) + { + if (!has_migration) + { + best_migration_line = rel; + best_migration_release = release; + has_migration = true; + continue; + } + auto best_cmp = compareLine(rel, best_migration_line); + if (best_cmp > 0 || (best_cmp == 0 && compareLinePatch(rel, best_migration_line) > 0)) + { + best_migration_line = rel; + best_migration_release = release; + } + } + } + + if (has_same_line) + { + candidate.kind = UpdateKind::Update; + candidate.release = best_same_release; + return candidate; + } + + if (has_migration) + { + candidate.kind = UpdateKind::Migration; + candidate.release = best_migration_release; + return candidate; + } + + return candidate; +} + +void ProjTUpdaterApp::downloadError(QString reason) +{ + fail(QString("Network request Failed: %1 with reason %2").arg(m_current_url).arg(reason)); +} diff --git a/archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.h b/archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.h new file mode 100644 index 0000000000..6f4fde4dbf --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.h @@ -0,0 +1,199 @@ +// 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. + * + * + * + * ======================================================================== */ + +#pragma once + +#include <QtCore> + +#include <QApplication> +#include <QDataStream> +#include <QDateTime> +#include <QDebug> +#include <QFlag> +#include <QIcon> +#include <QLocalSocket> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QUrl> +#include <memory> +#include <optional> + +#include "QObjectPtr.h" +#include "net/Download.h" + +#define PRISM_EXTERNAL_EXE +#include "FileSystem.h" + +#include "ReleaseInfo.h" + +class ProjTUpdaterApp : public QApplication +{ + // friends for the purpose of limiting access to deprecated stuff + Q_OBJECT + public: + enum Status + { + Starting, + Failed, + Succeeded, + Initialized, + Aborted + }; + ProjTUpdaterApp(int& argc, char** argv); + virtual ~ProjTUpdaterApp(); + void loadReleaseFeed(); + void run(); + Status status() const + { + return m_status; + } + + private: + void fail(const QString& reason); + void abort(const QString& reason); + void showFatalErrorMessage(const QString& title, const QString& content); + + bool loadProjTVersionFromExe(const QString& exe_path); + + void downloadReleaseFeed(); + int parseReleaseFeed(const QByteArray& response); + + enum class UpdateKind + { + None, + Update, + Migration + }; + + struct UpdateCandidate + { + UpdateKind kind = UpdateKind::None; + ReleaseInfo release; + }; + + UpdateCandidate findUpdateCandidate(); + + ReleaseInfo getLatestRelease(); + ReleaseInfo selectRelease(); + QList<ReleaseInfo> newerReleases(); + QList<ReleaseInfo> nonDraftReleases(); + + void printReleases(); + + QList<ReleaseAsset> validReleaseArtifacts(const ReleaseInfo& release); + ReleaseAsset selectAsset(const QList<ReleaseAsset>& assets); + void performUpdate(const ReleaseInfo& release); + void performInstall(QFileInfo file); + void unpackAndInstall(QFileInfo file); + void backupAppDir(); + std::optional<QDir> unpackArchive(QFileInfo file); + + QFileInfo downloadAsset(const ReleaseAsset& asset); + bool callAppImageUpdate(); + + void moveAndFinishUpdate(QDir target); + + public slots: + void downloadError(QString reason); + + private: + const QString& root() + { + return m_rootPath; + } + + bool isPortable() + { + return m_isPortable; + } + + void clearUpdateLog(); + void logUpdate(const QString& msg); + + QString m_rootPath; + QString m_dataPath; + bool m_isPortable = false; + bool m_isAppimage = false; + bool m_isFlatpak = false; + QString m_appimagePath; + QString m_projtExecutable; + QUrl m_releaseFeedUrl; + Version m_userSelectedVersion; + bool m_checkOnly; + bool m_forceUpdate; + bool m_printOnly; + bool m_selectUI; + bool m_allowDowngrade; + bool m_allowPreRelease; + + QString m_updateLogPath; + + QString m_projtBinaryName; + QString m_projtVersion; + int m_projtVersionMajor = -1; + int m_projtVersionMinor = -1; + int m_projtVersionPatch = -1; + QString m_prsimVersionChannel; + QString m_projtGitCommit; + + ReleaseInfo m_install_release; + + Status m_status = Status::Starting; + shared_qobject_ptr<QNetworkAccessManager> m_network; + QString m_current_url; + Task::Ptr m_current_task; + QList<ReleaseInfo> m_releases; + + public: + std::unique_ptr<QFile> logFile; + bool logToConsole = false; + +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + bool consoleAttached = false; +#endif +}; diff --git a/archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.cpp b/archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.cpp new file mode 100644 index 0000000000..8a115972da --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.cpp @@ -0,0 +1,88 @@ +// 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 + * 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 "ReleaseInfo.h" + +QDebug operator<<(QDebug debug, const ReleaseAsset& asset) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "ReleaseAsset( " + "name: " + << asset.name + << ", " + "label: " + << asset.label + << ", " + "content_type: " + << asset.content_type + << ", " + "size: " + << asset.size + << ", " + "created_at: " + << asset.created_at + << ", " + "updated_at: " + << asset.updated_at + << ", " + "download_url: " + << asset.download_url + << " " + ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const ReleaseInfo& release) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "ReleaseInfo( " + "name: " + << release.name + << ", " + "tag_name: " + << release.tag_name + << ", " + "created_at: " + << release.created_at + << ", " + "published_at: " + << release.published_at + << ", " + "prerelease: " + << release.prerelease + << ", " + "draft: " + << release.draft + << ", " + "version: " + << release.version + << ", " + "body: " + << release.body + << ", " + "assets: " + << release.assets + << " " + ")"; + return debug; +} diff --git a/archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.h b/archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.h new file mode 100644 index 0000000000..99da487055 --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.h @@ -0,0 +1,68 @@ +// 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. + * + */ + +#pragma once +#include <QDateTime> +#include <QList> +#include <QString> +#include <QUrl> + +#include <QDebug> + +#include "Version.h" + +struct ReleaseAsset +{ + QString name; + QString label; + QString content_type; + int size = 0; + QDateTime created_at; + QDateTime updated_at; + QUrl download_url; + + bool isValid() const + { + return !name.isEmpty() && download_url.isValid(); + } +}; + +struct ReleaseInfo +{ + QString name; + QString tag_name; + QDateTime created_at; + QDateTime published_at; + bool prerelease = false; + bool draft = false; + QString body; + QList<ReleaseAsset> assets; + Version version; + + bool isValid() const + { + return !tag_name.isEmpty(); + } +}; + +QDebug operator<<(QDebug debug, const ReleaseAsset& releaseAsset); +QDebug operator<<(QDebug debug, const ReleaseInfo& releaseInfo); diff --git a/archived/projt-launcher/launcher/updater/projtupdater/SelectReleaseDialog.ui b/archived/projt-launcher/launcher/updater/projtupdater/SelectReleaseDialog.ui new file mode 100644 index 0000000000..a1aa38371b --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/SelectReleaseDialog.ui @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SelectReleaseDialog</class> + <widget class="QDialog" name="SelectReleaseDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>468</width> + <height>385</height> + </rect> + </property> + <property name="windowTitle"> + <string>Select Release to Install</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0"> + <item> + <widget class="QLabel" name="eplainLabel"> + <property name="text"> + <string>Please select the release you wish to update to.</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="versionsTree"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string notr="true">1</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="changelogTextBrowser"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SelectReleaseDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SelectReleaseDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.cpp b/archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.cpp new file mode 100644 index 0000000000..4534d3963c --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.cpp @@ -0,0 +1,211 @@ +// 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 "UpdaterDialogs.h" + +#include "ui_SelectReleaseDialog.h" + +#include <QPushButton> +#include <QTextBrowser> +#include "Markdown.h" +#include "StringUtils.h" + +SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, + const QList<ReleaseInfo>& releases, + QWidget* parent) + : QDialog(parent), + m_releases(releases), + m_currentVersion(current_version), + ui(new Ui::SelectReleaseDialog) +{ + ui->setupUi(this); + + ui->changelogTextBrowser->setOpenExternalLinks(true); + ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); + ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + ui->versionsTree->setColumnCount(2); + + ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); + ui->versionsTree->header()->setStretchLastSection(false); + + ui->eplainLabel->setText(tr("Select a version to install.\n" + "\n" + "Currently installed version: %1") + .arg(m_currentVersion.toString())); + + loadReleases(); + + connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseDialog::selectionChanged); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); +} + +SelectReleaseDialog::~SelectReleaseDialog() +{ + delete ui; +} + +void SelectReleaseDialog::loadReleases() +{ + for (auto rls : m_releases) + { + appendRelease(rls); + } +} + +void SelectReleaseDialog::appendRelease(ReleaseInfo const& release) +{ + auto rls_item = new QTreeWidgetItem(ui->versionsTree); + rls_item->setText(0, release.tag_name); + rls_item->setExpanded(true); + rls_item->setText(1, release.published_at.toString()); + rls_item->setData(0, Qt::UserRole, QVariant(release.tag_name)); + + ui->versionsTree->addTopLevelItem(rls_item); +} + +ReleaseInfo SelectReleaseDialog::getRelease(QTreeWidgetItem* item) +{ + auto tag_name = item->data(0, Qt::UserRole).toString(); + ReleaseInfo release; + for (auto rls : m_releases) + { + if (rls.tag_name == tag_name) + release = rls; + } + return release; +} + +void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + Q_UNUSED(previous) + ReleaseInfo release = getRelease(current); + QString body = markdownToHTML(release.body.toUtf8()); + m_selectedRelease = release; + + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body)); +} + +SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList<ReleaseAsset>& assets, QWidget* parent) + : QDialog(parent), + m_assets(assets), + ui(new Ui::SelectReleaseDialog) +{ + ui->setupUi(this); + + ui->changelogTextBrowser->setOpenExternalLinks(true); + ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); + ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + ui->versionsTree->setColumnCount(2); + + ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); + ui->versionsTree->header()->setStretchLastSection(false); + + ui->eplainLabel->setText(tr("Select a version to install.")); + + ui->changelogTextBrowser->setHidden(true); + + loadAssets(); + + connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseAssetDialog::selectionChanged); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseAssetDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseAssetDialog::reject); +} + +SelectReleaseAssetDialog::~SelectReleaseAssetDialog() +{ + delete ui; +} + +void SelectReleaseAssetDialog::loadAssets() +{ + for (auto rls : m_assets) + { + appendAsset(rls); + } +} + +void SelectReleaseAssetDialog::appendAsset(ReleaseAsset const& asset) +{ + auto rls_item = new QTreeWidgetItem(ui->versionsTree); + rls_item->setText(0, asset.name); + rls_item->setExpanded(true); + rls_item->setText(1, asset.updated_at.toString()); + rls_item->setData(0, Qt::UserRole, QVariant(asset.download_url.toString())); + + ui->versionsTree->addTopLevelItem(rls_item); +} + +ReleaseAsset SelectReleaseAssetDialog::getAsset(QTreeWidgetItem* item) +{ + auto asset_url = item->data(0, Qt::UserRole).toString(); + ReleaseAsset selected_asset; + for (auto asset : m_assets) + { + if (asset.download_url.toString() == asset_url) + selected_asset = asset; + } + return selected_asset; +} + +void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + Q_UNUSED(previous) + ReleaseAsset asset = getAsset(current); + m_selectedAsset = asset; +} diff --git a/archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.h b/archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.h new file mode 100644 index 0000000000..3c469dc80f --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.h @@ -0,0 +1,109 @@ +// 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. + * + * + * + * ======================================================================== */ + +#pragma once + +#include <QDialog> +#include <QTreeWidgetItem> + +#include "ReleaseInfo.h" +#include "Version.h" + +namespace Ui +{ + class SelectReleaseDialog; +} + +class SelectReleaseDialog : public QDialog +{ + Q_OBJECT + + public: + explicit SelectReleaseDialog(const Version& cur_version, const QList<ReleaseInfo>& releases, QWidget* parent = 0); + ~SelectReleaseDialog(); + + void loadReleases(); + void appendRelease(ReleaseInfo const& release); + ReleaseInfo selectedRelease() + { + return m_selectedRelease; + } + private slots: + ReleaseInfo getRelease(QTreeWidgetItem* item); + void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); + + protected: + QList<ReleaseInfo> m_releases; + ReleaseInfo m_selectedRelease; + Version m_currentVersion; + + Ui::SelectReleaseDialog* ui; +}; + +class SelectReleaseAssetDialog : public QDialog +{ + Q_OBJECT + public: + explicit SelectReleaseAssetDialog(const QList<ReleaseAsset>& assets, QWidget* parent = 0); + ~SelectReleaseAssetDialog(); + + void loadAssets(); + void appendAsset(ReleaseAsset const& asset); + ReleaseAsset selectedAsset() + { + return m_selectedAsset; + } + private slots: + ReleaseAsset getAsset(QTreeWidgetItem* item); + void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); + + protected: + QList<ReleaseAsset> m_assets; + ReleaseAsset m_selectedAsset; + + Ui::SelectReleaseDialog* ui; +}; diff --git a/archived/projt-launcher/launcher/updater/projtupdater/updater.exe.manifest b/archived/projt-launcher/launcher/updater/projtupdater/updater.exe.manifest new file mode 100644 index 0000000000..2bce76b77b --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/updater.exe.manifest @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <application xmlns="urn:schemas-microsoft-com:asm.v3"> + <windowsSettings> + </windowsSettings> + </application> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Windows 10, Windows 11 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <!-- Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <!-- Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <!-- Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> + </compatibility> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> + <security> + <requestedPrivileges> + <requestedExecutionLevel level="asInvoker" uiAccess="false"/> + </requestedPrivileges> + </security> + </trustInfo> +</assembly> diff --git a/archived/projt-launcher/launcher/updater/projtupdater/updater_main.cpp b/archived/projt-launcher/launcher/updater/projtupdater/updater_main.cpp new file mode 100644 index 0000000000..dca529c973 --- /dev/null +++ b/archived/projt-launcher/launcher/updater/projtupdater/updater_main.cpp @@ -0,0 +1,64 @@ +// 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) 2022 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 "ProjTUpdater.h" +int main(int argc, char* argv[]) +{ + ProjTUpdaterApp wUpApp(argc, argv); + + switch (wUpApp.status()) + { + case ProjTUpdaterApp::Starting: + case ProjTUpdaterApp::Initialized: + { + return wUpApp.exec(); + } + case ProjTUpdaterApp::Failed: return 1; + case ProjTUpdaterApp::Succeeded: return 0; + default: return -1; + } +} |
