feat(updater): tie in updater part 1

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers 2023-06-25 21:36:20 -07:00
parent 90da57a806
commit d8e0b14dc4
No known key found for this signature in database
GPG Key ID: E10E321EB160949B
8 changed files with 390 additions and 16 deletions

View File

@ -132,6 +132,8 @@
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
#include "updater/MacSparkleUpdater.h" #include "updater/MacSparkleUpdater.h"
#else
#include "updater/PrismExternalUpdater.h"
#endif #endif
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
@ -271,11 +273,16 @@ void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr
Application::Application(int& argc, char** argv) : QApplication(argc, argv) Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ {
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
// attach the parent console // attach the parent console if stdout not already captured
auto stdout_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE));
if (stdout_type == FILE_TYPE_CHAR || stdout_type == FILE_TYPE_UNKNOWN) {
if (AttachConsole(ATTACH_PARENT_PROCESS)) { if (AttachConsole(ATTACH_PARENT_PROCESS)) {
BindCrtHandlesToStdHandles(true, true, true); BindCrtHandlesToStdHandles(true, true, true);
consoleAttached = true; consoleAttached = true;
} }
} else if (stdout_type == FILE_TYPE_DISK || stdout_type == FILE_TYPE_PIPE ) {
BindCrtHandlesToStdHandles(true, true, true);
}
#endif #endif
setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationName(BuildConfig.LAUNCHER_NAME);
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
@ -823,6 +830,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qDebug() << "Initializing updater"; qDebug() << "Initializing updater";
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
m_updater.reset(new MacSparkleUpdater()); m_updater.reset(new MacSparkleUpdater());
#else
m_updater.reset(new PrismExternalUpdater(m_rootPath, dataPath));
#endif #endif
qDebug() << "<> Updater started."; qDebug() << "<> Updater started.";
} }

View File

@ -179,6 +179,11 @@ set(MAC_UPDATE_SOURCES
updater/MacSparkleUpdater.mm updater/MacSparkleUpdater.mm
) )
set(PRISM_UPDATE_SOURCES
updater/PrismExternalUpdater.h
updater/PrismExternalUpdater.cpp
)
# Backend for the news bar... there's usually no news. # Backend for the news bar... there's usually no news.
set(NEWS_SOURCES set(NEWS_SOURCES
# News System # News System
@ -728,6 +733,8 @@ set(LOGIC_SOURCES
if(APPLE) if(APPLE)
set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES})
else()
set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PRISM_UPDATE_SOURCES})
endif() endif()
SET(LAUNCHER_SOURCES SET(LAUNCHER_SOURCES

View File

@ -35,6 +35,7 @@
*/ */
#include "StringUtils.h" #include "StringUtils.h"
#include <qpair.h>
#include <QRegularExpression> #include <QRegularExpression>
#include <QUuid> #include <QUuid>
@ -149,7 +150,7 @@ QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_
} }
if ((url_compact.length() >= max_len) && hard_limit) { if ((url_compact.length() >= max_len) && hard_limit) {
// still too long, truncate normaly // still too long, truncate normally
url_compact = QString(str_url); url_compact = QString(str_url);
auto to_remove = url_compact.length() - max_len + 3; auto to_remove = url_compact.length() - max_len + 3;
url_compact.remove(url_compact.length() - to_remove - 1, to_remove); url_compact.remove(url_compact.length() - to_remove - 1, to_remove);
@ -182,3 +183,28 @@ QString StringUtils::getRandomAlphaNumeric()
{ {
return QUuid::createUuid().toString(QUuid::Id128); return QUuid::createUuid().toString(QUuid::Id128);
} }
QPair<QString, QString> splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs = Qt::CaseSensitive) {
QString left, right;
auto index = s.indexOf(sep, 0, cs);
left = s.mid(0, index);
right = s.mid(index + 1);
return qMakePair(left, right);
}
QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive) {
QString left, right;
auto index = s.indexOf(sep, 0, cs);
left = s.mid(0, index);
right = s.mid(index + 1);
return qMakePair(left, right);
}
QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re) {
QString left, right;
auto index = s.indexOf(re);
left = s.mid(0, index);
right = s.mid(index + 1);
return qMakePair(left, right);
}

View File

@ -37,7 +37,9 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QPair>
#include <QUrl> #include <QUrl>
#include <utility>
namespace StringUtils { namespace StringUtils {
@ -70,8 +72,8 @@ int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
/** /**
* @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path
* @param url Url to truncate * @param url Url to truncate
* @param max_len max lenght of url in charaters * @param max_len max length of url in characters
* @param hard_limit if truncating the path can't get the url short enough, truncate it normaly. * @param hard_limit if truncating the path can't get the url short enough, truncate it normally.
*/ */
QString truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_limit = false); QString truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_limit = false);
@ -79,4 +81,9 @@ QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_poi
QString getRandomAlphaNumeric(); QString getRandomAlphaNumeric();
QPair<QString, QString> splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re);
} // namespace StringUtils } // namespace StringUtils

View File

@ -0,0 +1,206 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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, see <https://www.gnu.org/licenses/>.
*
*/
#include "PrismExternalUpdater.h"
#include <memory>
#include <QDateTime>
#include <QProgressDialog>
#include <QDir>
#include <QProcess>
#include <QTimer>
#include <QSettings>
#include "StringUtils.h"
#include "BuildConfig.h"
class PrismExternalUpdater::Private {
public:
QDir appDir;
QDir dataDir;
QTimer updateTimer;
bool allowBeta;
bool autoCheck;
double updateInterval;
QDateTime lastCheck;
std::unique_ptr<QSettings> settings;
};
PrismExternalUpdater::PrismExternalUpdater(const QString& appDir, const QString& dataDir)
{
priv = new PrismExternalUpdater::Private();
priv->appDir = QDir(appDir);
priv->dataDir = QDir(dataDir);
auto settings_file = priv->dataDir.absoluteFilePath("prismlauncher_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;
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);
}
connectTimer();
resetAutoCheckTimer();
}
PrismExternalUpdater::~PrismExternalUpdater()
{
if (priv->updateTimer.isActive())
priv->updateTimer.stop();
disconnectTimer();
priv->settings->sync();
delete priv;
}
void PrismExternalUpdater::checkForUpdates()
{
QProgressDialog progress(tr("Checking for updates..."), "", 0, -1);
progress.setCancelButton(nullptr);
progress.show();
QProcess proc;
auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32
exe_name.append(".exe");
#endif
QStringList args = { "--check-only" };
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 result_finished = proc.waitForFinished(60000);
if (!result_finished) {
auto err = proc.error();
qDebug() << "Updater failed to close after 60 seconds." << "reason:" << err << proc.errorString();
}
auto exit_code = proc.exitCode();
auto std_output = proc.readAllStandardOutput();
auto std_error = proc.readAllStandardError();
switch (exit_code) {
case 0:
// no update available
{
qDebug() << "No update available";
}
break;
case 1:
// there was an error
{
qDebug() << "Updater subprocess error" << std_error;
}
break;
case 100:
// update available
{
auto [first_line, remainder1] = StringUtils::splitFirst(std_output, '\n');
auto [second_line, remainder2] = StringUtils::splitFirst(remainder1, '\n');
auto [third_line, changelog] = StringUtils::splitFirst(remainder2, '\n');
auto version_name = StringUtils::splitFirst(first_line, ": ").second;
auto version_tag = StringUtils::splitFirst(second_line, ": ").second;
auto release_timestamp = QDateTime::fromString(StringUtils::splitFirst(third_line, ": ").second, Qt::ISODate);
qDebug() << "Update available:" << version_name << version_tag << release_timestamp;
qDebug() << "Update changelog:" << changelog;
}
break;
default:
// unknown error code
{
qDebug() << "Updater exited with unknown code" << exit_code;
}
}
priv->lastCheck = QDateTime::currentDateTime();
priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate));
priv->settings->sync();
}
bool PrismExternalUpdater::getAutomaticallyChecksForUpdates() {
return priv->autoCheck;
}
double PrismExternalUpdater::getUpdateCheckInterval() {
return priv->updateInterval;
}
bool PrismExternalUpdater::getBetaAllowed() {
return priv->allowBeta;
}
void PrismExternalUpdater::setAutomaticallyChecksForUpdates(bool check) {
priv->autoCheck = check;
priv->settings->setValue("auto_check", check);
priv->settings->sync();
resetAutoCheckTimer();
}
void PrismExternalUpdater::setUpdateCheckInterval(double seconds) {
priv->updateInterval = seconds;
priv->settings->setValue("update_interval", seconds);
priv->settings->sync();
resetAutoCheckTimer();
}
void PrismExternalUpdater::setBetaAllowed(bool allowed) {
priv->allowBeta = allowed;
priv->settings->setValue("auto_beta", allowed);
priv->settings->sync();
}
void PrismExternalUpdater::resetAutoCheckTimer() {
int timeoutDuration = 0;
auto now = QDateTime::currentDateTime();
if (priv->autoCheck) {
if (priv->lastCheck.isValid()) {
auto diff = priv->lastCheck.secsTo(now);
auto secs_left = priv->updateInterval - diff;
if (secs_left < 0)
secs_left = 0;
timeoutDuration = secs_left * 1000; // to msec
}
priv->updateTimer.start(timeoutDuration);
} else {
if (priv->updateTimer.isActive())
priv->updateTimer.stop();
}
}
void PrismExternalUpdater::connectTimer() {
connect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired);
}
void PrismExternalUpdater::disconnectTimer() {
disconnect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired);
}

View File

@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QObject>
#include "ExternalUpdater.h"
/*!
* An implementation for the updater on windows and linux that uses out external updater.
*/
class PrismExternalUpdater : public ExternalUpdater {
Q_OBJECT
public:
PrismExternalUpdater(const QString& appDir, const QString& dataDir);
~PrismExternalUpdater() override;
/*!
* Check for updates manually, showing the user a progress bar and an alert if no updates are found.
*/
void checkForUpdates() override;
/*!
* Indicates whether or not to check for updates automatically.
*/
bool getAutomaticallyChecksForUpdates() override;
/*!
* Indicates the current automatic update check interval in seconds.
*/
double getUpdateCheckInterval() override;
/*!
* Indicates whether or not beta updates should be checked for in addition to regular releases.
*/
bool getBetaAllowed() override;
/*!
* Set whether or not to check for updates automatically.
*
* The update schedule cycle will be reset in a short delay after the propertys new value is set. This is to allow
* reverting this property without kicking off a schedule change immediately."
*/
void setAutomaticallyChecksForUpdates(bool check) override;
/*!
* Set the current automatic update check interval in seconds.
*
* The update schedule cycle will be reset in a short delay after the propertys new value is set. This is to allow
* reverting this property without kicking off a schedule change immediately."
*/
void setUpdateCheckInterval(double seconds) override;
/*!
* Set whether or not beta updates should be checked for in addition to regular releases.
*/
void setBetaAllowed(bool allowed) override;
void resetAutoCheckTimer();
void disconnectTimer();
void connectTimer();
void performUpdate();
public slots:
void autoCheckTimerFired();
private:
class Private;
Private* priv;
};

View File

@ -41,6 +41,7 @@
#include <QProgressDialog> #include <QProgressDialog>
#include <sys.h> #include <sys.h>
#include <winbase.h>
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
@ -202,11 +203,17 @@ void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr
PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv)
{ {
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
// attach the parent console // attach the parent console if stdout not already captured
auto stdout_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE));
if (stdout_type == FILE_TYPE_CHAR || stdout_type == FILE_TYPE_UNKNOWN) {
if (AttachConsole(ATTACH_PARENT_PROCESS)) { if (AttachConsole(ATTACH_PARENT_PROCESS)) {
BindCrtHandlesToStdHandles(true, true, true); BindCrtHandlesToStdHandles(true, true, true);
consoleAttached = true; consoleAttached = true;
} }
} else if (stdout_type == FILE_TYPE_DISK || stdout_type == FILE_TYPE_PIPE ) {
BindCrtHandlesToStdHandles(true, true, true);
}
#endif #endif
setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationName(BuildConfig.LAUNCHER_NAME);
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
@ -226,6 +233,7 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
{ { "U", "update-url" }, tr("Update from the specified repo."), tr("github repo url") }, { { "U", "update-url" }, tr("Update from the specified repo."), tr("github repo url") },
{ { "c", "check-only" }, { { "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).") }, 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.") }, { { "F", "force" }, tr("Force an update, even if one is not needed.") },
{ { "l", "list" }, tr("List available releases.") }, { { "l", "list" }, tr("List available releases.") },
{ "debug", tr("Log debug to console.") }, { "debug", tr("Log debug to console.") },
@ -297,6 +305,8 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar
m_prismVersionMinor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt();
} }
m_allowPreRelease = parser.isSet("pre-release");
QString origCwdPath = QDir::currentPath(); QString origCwdPath = QDir::currentPath();
QString binPath = applicationDirPath(); QString binPath = applicationDirPath();
@ -542,11 +552,20 @@ void PrismUpdaterApp::run()
auto need_update = needUpdate(latest); auto need_update = needUpdate(latest);
if (m_checkOnly) { if (m_checkOnly) {
if (need_update) if (need_update) {
QTextStream stdOutStream(stdout);
stdOutStream << "Name: " << latest.name << "\n";
stdOutStream << "Version: " << latest.tag_name << "\n";
stdOutStream << "TimeStamp: " << latest.created_at.toString(Qt::ISODate) << "\n";
stdOutStream << latest.body << "\n";
stdOutStream.flush();
return exit(100); return exit(100);
else }
else {
return exit(0); return exit(0);
} }
}
if (m_isFlatpak) { if (m_isFlatpak) {
showFatalErrorMessage(tr("Updating flatpack not supported"), tr("Actions outside of checking if an update is available are not " showFatalErrorMessage(tr("Updating flatpack not supported"), tr("Actions outside of checking if an update is available are not "
@ -958,10 +977,11 @@ void PrismUpdaterApp::unpackAndInstall(QFileInfo archive)
if (auto loc = unpackArchive(archive)) { if (auto loc = unpackArchive(archive)) {
auto marker_file_path = loc.value().absoluteFilePath(".prism_launcher_updater_unpack.marker"); auto marker_file_path = loc.value().absoluteFilePath(".prism_launcher_updater_unpack.marker");
FS::write(marker_file_path, applicationDirPath().toUtf8()); FS::write(marker_file_path, applicationDirPath().toUtf8());
auto new_updater_path = loc.value().absoluteFilePath("prismlauncher_updater"); auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
new_updater_path.append(".exe"); exe_name.append(".exe");
#endif #endif
auto new_updater_path = loc.value().absoluteFilePath(exe_name);
logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path)); logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path));
QProcess proc = QProcess(); QProcess proc = QProcess();
if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) { if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) {
@ -1110,7 +1130,7 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path)
QProcess proc = QProcess(); QProcess proc = QProcess();
proc.setProcessChannelMode(QProcess::MergedChannels); proc.setProcessChannelMode(QProcess::MergedChannels);
proc.setReadChannel(QProcess::StandardOutput); proc.setReadChannel(QProcess::StandardOutput);
proc.start(exe_path, { "-v" }); proc.start(exe_path, { "--version" });
if (!proc.waitForStarted(5000)) { if (!proc.waitForStarted(5000)) {
showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launcher child launcher process to read version.")); showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launcher child launcher process to read version."));
return false; return false;
@ -1119,7 +1139,7 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path)
showFatalErrorMessage(tr("Failed to Check Version"), tr("Child launcher process failed.")); showFatalErrorMessage(tr("Failed to Check Version"), tr("Child launcher process failed."));
return false; return false;
} }
auto out = proc.readAll(); auto out = proc.readAllStandardOutput();
auto lines = out.split('\n'); auto lines = out.split('\n');
if (lines.length() < 2) if (lines.length() < 2)
return false; return false;
@ -1250,7 +1270,11 @@ GitHubRelease PrismUpdaterApp::getLatestRelease()
{ {
GitHubRelease latest; GitHubRelease latest;
for (auto release : m_releases) { for (auto release : m_releases) {
if (!latest.isValid() || (!release.draft && release.version > latest.version)) { if (release.draft)
continue;
if (release.prerelease && !m_allowPreRelease)
continue;
if (!latest.isValid() || (release.version > latest.version)) {
latest = release; latest = release;
} }
} }

View File

@ -113,6 +113,7 @@ class PrismUpdaterApp : public QApplication {
bool m_printOnly; bool m_printOnly;
bool m_selectUI; bool m_selectUI;
bool m_allowDowngrade; bool m_allowDowngrade;
bool m_allowPreRelease;
QString m_updateLogPath; QString m_updateLogPath;