feat: paliminary updater
- can check for need to update - can select a version to update to - perform update: TODO Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
parent
954d4d701a
commit
1c91d2f242
@ -469,7 +469,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT;
|
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 " << qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
|
||||||
qDebug() << "Version : " << BuildConfig.printableVersionString();
|
qDebug() << "Version : " << BuildConfig.printableVersionString();
|
||||||
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
|
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
|
||||||
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
|
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
|
||||||
|
@ -567,6 +567,25 @@ set(LINKEXE_SOURCES
|
|||||||
DesktopServices.cpp
|
DesktopServices.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(WINDOWSUPDATEREXE_SOURCES
|
||||||
|
updater/windows/WindowsUpdater.h
|
||||||
|
updater/windows/WindowsUpdater.cpp
|
||||||
|
updater/windows/UpdaterDialogs.h
|
||||||
|
updater/windows/UpdaterDialogs.cpp
|
||||||
|
updater/windows/GitHubRelease.h
|
||||||
|
Json.h
|
||||||
|
Json.cpp
|
||||||
|
FileSystem.h
|
||||||
|
FileSystem.cpp
|
||||||
|
StringUtils.h
|
||||||
|
StringUtils.cpp
|
||||||
|
DesktopServices.h
|
||||||
|
DesktopServices.cpp
|
||||||
|
Version.h
|
||||||
|
Version.cpp
|
||||||
|
Markdown.h
|
||||||
|
)
|
||||||
|
|
||||||
######## Logging categories ########
|
######## Logging categories ########
|
||||||
|
|
||||||
ecm_qt_declare_logging_category(CORE_SOURCES
|
ecm_qt_declare_logging_category(CORE_SOURCES
|
||||||
@ -1075,6 +1094,10 @@ qt_add_resources(LAUNCHER_RESOURCES
|
|||||||
../${Launcher_Branding_LogoQRC}
|
../${Launcher_Branding_LogoQRC}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
qt_wrap_ui(WINDOWSUPDATER_UI
|
||||||
|
updater/windows/SelectReleaseDialog.ui
|
||||||
|
)
|
||||||
|
|
||||||
######## Windows resource files ########
|
######## Windows resource files ########
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
|
set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
|
||||||
@ -1158,7 +1181,41 @@ install(TARGETS ${Launcher_Name}
|
|||||||
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
|
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32 OR (DEFINED Launcher_BUILD_UPDATER AND Launcher_BUILD_UPDATER) )
|
||||||
|
# Updater
|
||||||
|
add_library(windows_updater_logic STATIC ${WINDOWSUPDATEREXE_SOURCES} ${WINDOWSUPDATER_UI})
|
||||||
|
target_include_directories(windows_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(windows_updater_logic
|
||||||
|
systeminfo
|
||||||
|
BuildConfig
|
||||||
|
ghcFilesystem::ghc_filesystem
|
||||||
|
Qt${QT_VERSION_MAJOR}::Widgets
|
||||||
|
Qt${QT_VERSION_MAJOR}::Core
|
||||||
|
Qt${QT_VERSION_MAJOR}::Network
|
||||||
|
${Launcher_QT_LIBS}
|
||||||
|
cmark::cmark
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable("${Launcher_Name}_updater" WIN32 updater/windows/windows_main.cpp)
|
||||||
|
target_link_libraries("${Launcher_Name}_updater" windows_updater_logic)
|
||||||
|
|
||||||
|
if(DEFINED Launcher_APP_BINARY_NAME)
|
||||||
|
set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater")
|
||||||
|
endif()
|
||||||
|
if(DEFINED Launcher_BINARY_RPATH)
|
||||||
|
SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
install(TARGETS "${Launcher_Name}_updater"
|
||||||
|
BUNDLE DESTINATION "." COMPONENT Runtime
|
||||||
|
LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime
|
||||||
|
RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime
|
||||||
|
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER))
|
||||||
|
# File link
|
||||||
add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
|
add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
|
||||||
target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(filelink_logic
|
target_link_libraries(filelink_logic
|
||||||
|
@ -56,6 +56,7 @@ class Version {
|
|||||||
bool operator!=(const Version &other) const;
|
bool operator!=(const Version &other) const;
|
||||||
|
|
||||||
QString toString() const { return m_string; }
|
QString toString() const { return m_string; }
|
||||||
|
bool isEmpty() const { return m_string.isEmpty(); }
|
||||||
|
|
||||||
friend QDebug operator<<(QDebug debug, const Version& v);
|
friend QDebug operator<<(QDebug debug, const Version& v);
|
||||||
|
|
||||||
|
@ -111,6 +111,7 @@ FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv),
|
|||||||
joinServer(serverToJoin);
|
joinServer(serverToJoin);
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "no server to join";
|
qDebug() << "no server to join";
|
||||||
|
m_status = Failed;
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,6 +127,7 @@ void FileLinkApp::joinServer(QString server)
|
|||||||
connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs);
|
connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs);
|
||||||
|
|
||||||
connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) {
|
connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) {
|
||||||
|
m_status = Failed;
|
||||||
switch (socketError) {
|
switch (socketError) {
|
||||||
case QLocalSocket::ServerNotFoundError:
|
case QLocalSocket::ServerNotFoundError:
|
||||||
qDebug()
|
qDebug()
|
||||||
@ -150,6 +152,7 @@ void FileLinkApp::joinServer(QString server)
|
|||||||
|
|
||||||
connect(&socket, &QLocalSocket::disconnected, this, [&]() {
|
connect(&socket, &QLocalSocket::disconnected, this, [&]() {
|
||||||
qDebug() << "disconnected from server, should exit";
|
qDebug() << "disconnected from server, should exit";
|
||||||
|
m_status = Succeeded;
|
||||||
exit();
|
exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,8 +41,17 @@ class FileLinkApp : public QCoreApplication {
|
|||||||
// friends for the purpose of limiting access to deprecated stuff
|
// friends for the purpose of limiting access to deprecated stuff
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
enum Status {
|
||||||
|
Starting,
|
||||||
|
Failed,
|
||||||
|
Succeeded,
|
||||||
|
Initialized
|
||||||
|
};
|
||||||
FileLinkApp(int& argc, char** argv);
|
FileLinkApp(int& argc, char** argv);
|
||||||
virtual ~FileLinkApp();
|
virtual ~FileLinkApp();
|
||||||
|
Status status() const {
|
||||||
|
return m_status;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void joinServer(QString server);
|
void joinServer(QString server);
|
||||||
@ -50,6 +59,8 @@ class FileLinkApp : public QCoreApplication {
|
|||||||
void runLink();
|
void runLink();
|
||||||
void sendResults();
|
void sendResults();
|
||||||
|
|
||||||
|
Status m_status = Status::Starting;
|
||||||
|
|
||||||
bool m_useHardLinks = false;
|
bool m_useHardLinks = false;
|
||||||
|
|
||||||
QDateTime m_startTime;
|
QDateTime m_startTime;
|
||||||
|
@ -26,5 +26,17 @@ int main(int argc, char* argv[])
|
|||||||
{
|
{
|
||||||
FileLinkApp ldh(argc, argv);
|
FileLinkApp ldh(argc, argv);
|
||||||
|
|
||||||
return ldh.exec();
|
switch(ldh.status()) {
|
||||||
|
case FileLinkApp::Starting:
|
||||||
|
case FileLinkApp::Initialized:
|
||||||
|
{
|
||||||
|
return ldh.exec();
|
||||||
|
}
|
||||||
|
case FileLinkApp::Failed:
|
||||||
|
return 1;
|
||||||
|
case FileLinkApp::Succeeded:
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
34
launcher/updater/windows/GitHubRelease.h
Normal file
34
launcher/updater/windows/GitHubRelease.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Version.h"
|
||||||
|
|
||||||
|
struct GitHubReleaseAsset {
|
||||||
|
int id = -1;
|
||||||
|
QString name;
|
||||||
|
QString label;
|
||||||
|
QString content_type;
|
||||||
|
int size;
|
||||||
|
QDateTime created_at;
|
||||||
|
QDateTime updated_at;
|
||||||
|
QString browser_download_url;
|
||||||
|
|
||||||
|
bool isValid() { return id > 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GitHubRelease {
|
||||||
|
int id = -1;
|
||||||
|
QString name;
|
||||||
|
QString tag_name;
|
||||||
|
QDateTime created_at;
|
||||||
|
QDateTime published_at;
|
||||||
|
bool prerelease;
|
||||||
|
bool draft;
|
||||||
|
QString body;
|
||||||
|
QList<GitHubReleaseAsset> assets;
|
||||||
|
Version version;
|
||||||
|
|
||||||
|
bool isValid() const { return id > 0; }
|
||||||
|
};
|
89
launcher/updater/windows/SelectReleaseDialog.ui
Normal file
89
launcher/updater/windows/SelectReleaseDialog.ui
Normal file
@ -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>478</width>
|
||||||
|
<height>517</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>
|
78
launcher/updater/windows/UpdaterDialogs.cpp
Normal file
78
launcher/updater/windows/UpdaterDialogs.cpp
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#include "UpdaterDialogs.h"
|
||||||
|
|
||||||
|
#include "ui_SelectReleaseDialog.h"
|
||||||
|
|
||||||
|
#include <QTextBrowser>
|
||||||
|
#include "Markdown.h"
|
||||||
|
|
||||||
|
SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList<GitHubRelease>& 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::Stretch);
|
||||||
|
ui->versionsTree->setHeaderLabels({tr("Verison"), 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectReleaseDialog::~SelectReleaseDialog()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectReleaseDialog::loadReleases()
|
||||||
|
{
|
||||||
|
for (auto rls : m_releases) {
|
||||||
|
appendRelease(rls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectReleaseDialog::appendRelease(GitHubRelease 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.id));
|
||||||
|
|
||||||
|
ui->versionsTree->addTopLevelItem(rls_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
GitHubRelease SelectReleaseDialog::getRelease(QTreeWidgetItem* item) {
|
||||||
|
int id = item->data(0, Qt::UserRole).toInt();
|
||||||
|
GitHubRelease release;
|
||||||
|
for (auto rls: m_releases) {
|
||||||
|
if (rls.id == id)
|
||||||
|
release = rls;
|
||||||
|
}
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
|
||||||
|
{
|
||||||
|
GitHubRelease release = getRelease(current);
|
||||||
|
QString body = markdownToHTML(release.body.toUtf8());
|
||||||
|
m_selectedRelease = release;
|
||||||
|
|
||||||
|
ui->changelogTextBrowser->setHtml(body);
|
||||||
|
}
|
||||||
|
|
33
launcher/updater/windows/UpdaterDialogs.h
Normal file
33
launcher/updater/windows/UpdaterDialogs.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
|
||||||
|
#include "Version.h"
|
||||||
|
#include "updater/windows/GitHubRelease.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class SelectReleaseDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectReleaseDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SelectReleaseDialog(const Version& cur_version, const QList<GitHubRelease>& releases, QWidget* parent = 0);
|
||||||
|
~SelectReleaseDialog();
|
||||||
|
|
||||||
|
void loadReleases();
|
||||||
|
void appendRelease(GitHubRelease const& release);
|
||||||
|
GitHubRelease selectedRelease() { return m_selectedRelease; }
|
||||||
|
private slots:
|
||||||
|
GitHubRelease getRelease(QTreeWidgetItem* item);
|
||||||
|
void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QList<GitHubRelease> m_releases;
|
||||||
|
GitHubRelease m_selectedRelease;
|
||||||
|
Version m_currentVersion;
|
||||||
|
|
||||||
|
Ui::SelectReleaseDialog* ui;
|
||||||
|
};
|
647
launcher/updater/windows/WindowsUpdater.cpp
Normal file
647
launcher/updater/windows/WindowsUpdater.cpp
Normal file
@ -0,0 +1,647 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "WindowsUpdater.h"
|
||||||
|
#include "BuildConfig.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <QAccessible>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QProcess>
|
||||||
|
|
||||||
|
#include <qglobal.h>
|
||||||
|
#include <sys.h>
|
||||||
|
|
||||||
|
#if defined Q_OS_WIN32
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
|
||||||
|
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
|
||||||
|
#define GHC_USE_STD_FS
|
||||||
|
#include <filesystem>
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
#endif // MacOS min version check
|
||||||
|
#endif // Other OSes version check
|
||||||
|
|
||||||
|
#ifndef GHC_USE_STD_FS
|
||||||
|
#include <ghc/filesystem.hpp>
|
||||||
|
namespace fs = ghc::filesystem;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <DesktopServices.h>
|
||||||
|
|
||||||
|
#include "UpdaterDialogs.h"
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
#include "Json.h"
|
||||||
|
#include "StringUtils.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;
|
||||||
|
|
||||||
|
WindowsUpdaterApp* app = static_cast<WindowsUpdaterApp*>(QCoreApplication::instance());
|
||||||
|
app->logFile->write(out.toUtf8());
|
||||||
|
app->logFile->flush();
|
||||||
|
if (app->logToConsole) {
|
||||||
|
QTextStream(stderr) << out.toLocal8Bit();
|
||||||
|
fflush(stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsUpdaterApp::WindowsUpdaterApp(int& argc, char** argv) : QApplication(argc, argv)
|
||||||
|
{
|
||||||
|
#if defined Q_OS_WIN32
|
||||||
|
// attach the parent console
|
||||||
|
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||||
|
// if attach succeeds, reopen and sync all the i/o
|
||||||
|
if (freopen("CON", "w", stdout)) {
|
||||||
|
std::cout.sync_with_stdio();
|
||||||
|
}
|
||||||
|
if (freopen("CON", "w", stderr)) {
|
||||||
|
std::cerr.sync_with_stdio();
|
||||||
|
}
|
||||||
|
if (freopen("CON", "r", stdin)) {
|
||||||
|
std::cin.sync_with_stdio();
|
||||||
|
}
|
||||||
|
auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
DWORD written;
|
||||||
|
const char* endline = "\n";
|
||||||
|
WriteConsole(out, endline, strlen(endline), &written, NULL);
|
||||||
|
consoleAttached = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
setOrganizationName(BuildConfig.LAUNCHER_NAME);
|
||||||
|
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
|
||||||
|
setApplicationName(BuildConfig.LAUNCHER_NAME + "Updater");
|
||||||
|
setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
|
||||||
|
|
||||||
|
// Commandline parsing
|
||||||
|
QCommandLineParser parser;
|
||||||
|
parser.setApplicationDescription(QObject::tr("An auto-updater for Prism Launcher"));
|
||||||
|
|
||||||
|
parser.addOptions({ { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory).", "directory" },
|
||||||
|
{ { "I", "install-version" }, "Install a spesfic version.", "version name" },
|
||||||
|
{ { "U", "update-url" }, "Update from the spesified repo.", "github repo url" },
|
||||||
|
{ { "e", "executable" }, "Path to the prismluancher executable.", "path" },
|
||||||
|
{ { "c", "check-only" },
|
||||||
|
"Only check if an update is needed. Exit status 100 if true, 0 if false (or non 0 if there was an error)." },
|
||||||
|
{ { "F", "force" }, "Force an update, even if one is not needed." },
|
||||||
|
{ { "l", "list" }, "List avalible releases." },
|
||||||
|
{ "debug", "Log debug to console." },
|
||||||
|
{ { "L", "latest" }, "Update to the latest avalible version." },
|
||||||
|
{ { "D", "allow-downgrade" }, "Allow the updater to downgrade to previous verisons." } });
|
||||||
|
|
||||||
|
parser.addHelpOption();
|
||||||
|
parser.addVersionOption();
|
||||||
|
parser.process(arguments());
|
||||||
|
|
||||||
|
logToConsole = parser.isSet("debug");
|
||||||
|
|
||||||
|
auto prism_executable = parser.value("executable");
|
||||||
|
if (prism_executable.isEmpty()) {
|
||||||
|
prism_executable = QCoreApplication::applicationDirPath() + "/" + BuildConfig.LAUNCHER_APP_BINARY_NAME;
|
||||||
|
#if defined(Q_OS_WIN32)
|
||||||
|
prism_executable += ".exe";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
m_prismExecutable = prism_executable;
|
||||||
|
|
||||||
|
auto prism_update_url = parser.value("update-url");
|
||||||
|
if (prism_update_url.isEmpty())
|
||||||
|
prism_update_url = "https://github.com/PrismLauncher/PrismLauncher";
|
||||||
|
m_prismRepoUrl = QUrl::fromUserInput(prism_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_updateLatest = parser.isSet("latest");
|
||||||
|
m_allowDowngrade = parser.isSet("allow-downgrade");
|
||||||
|
|
||||||
|
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;
|
||||||
|
QString dataPath;
|
||||||
|
// change folder
|
||||||
|
QString dirParam = parser.value("dir");
|
||||||
|
if (!dirParam.isEmpty()) {
|
||||||
|
// the dir param. it makes multimc data path point to whatever the user specified
|
||||||
|
// on command line
|
||||||
|
adjustedBy = "Command line";
|
||||||
|
dataPath = dirParam;
|
||||||
|
} else {
|
||||||
|
QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), ".."));
|
||||||
|
dataPath = foo.absolutePath();
|
||||||
|
adjustedBy = "Persistent data path";
|
||||||
|
|
||||||
|
#ifndef Q_OS_MACOS
|
||||||
|
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
|
||||||
|
dataPath = m_rootPath;
|
||||||
|
adjustedBy = "Portable data path";
|
||||||
|
m_portable = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
m_network = new QNetworkAccessManager();
|
||||||
|
|
||||||
|
{ // setup logging
|
||||||
|
static const QString logBase = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log";
|
||||||
|
auto moveFile = [](const QString& oldName, const QString& newName) {
|
||||||
|
QFile::remove(newName);
|
||||||
|
QFile::copy(oldName, newName);
|
||||||
|
QFile::remove(oldName);
|
||||||
|
};
|
||||||
|
|
||||||
|
moveFile(logBase.arg(3), logBase.arg(4));
|
||||||
|
moveFile(logBase.arg(2), logBase.arg(3));
|
||||||
|
moveFile(logBase.arg(1), logBase.arg(2));
|
||||||
|
moveFile(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("The launcher data folder is not writable!",
|
||||||
|
QString("The launcher 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 launcher cannot continue until you fix this problem.")
|
||||||
|
.arg(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(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"
|
||||||
|
<< ", (c) 2022-2023 " << qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
|
||||||
|
qDebug() << "Version : " << BuildConfig.printableVersionString();
|
||||||
|
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
|
||||||
|
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
|
||||||
|
if (adjustedBy.size()) {
|
||||||
|
qDebug() << "Work dir before adjustment : " << origcwdPath;
|
||||||
|
qDebug() << "Work dir after adjustment : " << QDir::currentPath();
|
||||||
|
qDebug() << "Adjusted by : " << adjustedBy;
|
||||||
|
} else {
|
||||||
|
qDebug() << "Work dir : " << QDir::currentPath();
|
||||||
|
}
|
||||||
|
qDebug() << "Binary path : " << binPath;
|
||||||
|
qDebug() << "Application root path : " << m_rootPath;
|
||||||
|
qDebug() << "<> Paths set.";
|
||||||
|
}
|
||||||
|
|
||||||
|
loadReleaseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsUpdaterApp::~WindowsUpdaterApp()
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|
||||||
|
m_network->deleteLater();
|
||||||
|
if (m_reply)
|
||||||
|
m_reply->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::fail(const QString& reason)
|
||||||
|
{
|
||||||
|
qCritical() << qPrintable(reason);
|
||||||
|
m_status = Failed;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::abort(const QString& reason)
|
||||||
|
{
|
||||||
|
qCritical() << qPrintable(reason);
|
||||||
|
m_status = Aborted;
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::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->exec();
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::run()
|
||||||
|
{
|
||||||
|
qDebug() << "found" << m_releases.length() << "releases on github";
|
||||||
|
qDebug() << "loading exe at " << m_prismExecutable;
|
||||||
|
|
||||||
|
if (m_printOnly) {
|
||||||
|
printReleases();
|
||||||
|
m_status = Succeeded;
|
||||||
|
return exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPrismVersionFromExe(m_prismExecutable);
|
||||||
|
m_status = Succeeded;
|
||||||
|
|
||||||
|
qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVerison;
|
||||||
|
qDebug() << "Version major:" << m_prismVersionMajor;
|
||||||
|
qDebug() << "Verison minor:" << m_prismVersionMinor;
|
||||||
|
qDebug() << "Verison channel:" << m_prsimVersionChannel;
|
||||||
|
qDebug() << "Git Commit:" << m_prismGitCommit;
|
||||||
|
|
||||||
|
auto latest = getLatestRelease();
|
||||||
|
qDebug() << "Latest release" << latest.version;
|
||||||
|
auto need_update = needUpdate(latest);
|
||||||
|
|
||||||
|
if (m_checkOnly) {
|
||||||
|
if (need_update)
|
||||||
|
return exit(100);
|
||||||
|
else
|
||||||
|
return exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) {
|
||||||
|
GitHubRelease update_release = latest;
|
||||||
|
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 github relase for user spesified verison %1").arg(m_userSelectedVersion.toString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!m_updateLatest) {
|
||||||
|
update_release = selectRelease();
|
||||||
|
if (!update_release.isValid()) {
|
||||||
|
showFatalErrorMessage("No version selected.", "No version was selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
performUpdate(update_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::printReleases()
|
||||||
|
{
|
||||||
|
for (auto release : m_releases) {
|
||||||
|
std::cout << release.name.toStdString() << " Version: " << release.tag_name.toStdString() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<GitHubRelease> WindowsUpdaterApp::nonDraftReleases()
|
||||||
|
{
|
||||||
|
QList<GitHubRelease> nonDraft;
|
||||||
|
for (auto rls : m_releases) {
|
||||||
|
if (rls.isValid() && !rls.draft)
|
||||||
|
nonDraft.append(rls);
|
||||||
|
}
|
||||||
|
return nonDraft;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<GitHubRelease> WindowsUpdaterApp::newerReleases()
|
||||||
|
{
|
||||||
|
QList<GitHubRelease> newer;
|
||||||
|
for (auto rls : nonDraftReleases()) {
|
||||||
|
if (rls.version > m_prismVerison)
|
||||||
|
newer.append(rls);
|
||||||
|
}
|
||||||
|
return newer;
|
||||||
|
}
|
||||||
|
|
||||||
|
GitHubRelease WindowsUpdaterApp::selectRelease()
|
||||||
|
{
|
||||||
|
QList<GitHubRelease> releases;
|
||||||
|
|
||||||
|
if (m_allowDowngrade) {
|
||||||
|
releases = nonDraftReleases();
|
||||||
|
} else {
|
||||||
|
releases = newerReleases();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releases.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
SelectReleaseDialog dlg(Version(m_prismVerison), releases);
|
||||||
|
auto result = dlg.exec();
|
||||||
|
|
||||||
|
GitHubRelease release = dlg.selectedRelease();
|
||||||
|
if (result == QDialog::Rejected) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::performUpdate(const GitHubRelease& release)
|
||||||
|
{
|
||||||
|
qDebug() << "Updating to" << release.tag_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::loadPrismVersionFromExe(const QString& exe_path)
|
||||||
|
{
|
||||||
|
QProcess proc = QProcess();
|
||||||
|
proc.start(exe_path, { "-v" });
|
||||||
|
proc.waitForFinished();
|
||||||
|
auto out = proc.readAll();
|
||||||
|
auto lines = out.split('\n');
|
||||||
|
if (lines.length() < 2)
|
||||||
|
return;
|
||||||
|
auto first = lines.takeFirst();
|
||||||
|
auto first_parts = first.split(' ');
|
||||||
|
if (first_parts.length() < 2)
|
||||||
|
return;
|
||||||
|
m_prismBinaryName = first_parts.takeFirst();
|
||||||
|
auto version = first_parts.takeFirst();
|
||||||
|
m_prismVerison = 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('.');
|
||||||
|
m_prismVersionMajor = version_parts.takeFirst().toInt();
|
||||||
|
m_prismVersionMinor = version_parts.takeFirst().toInt();
|
||||||
|
m_prismGitCommit = lines.takeFirst().simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::loadReleaseList()
|
||||||
|
{
|
||||||
|
auto github_repo = m_prismRepoUrl;
|
||||||
|
if (github_repo.host() != "github.com")
|
||||||
|
return fail("updating from a non github url is not supported");
|
||||||
|
|
||||||
|
auto path_parts = github_repo.path().split('/');
|
||||||
|
path_parts.removeFirst(); // empty segment from leading /
|
||||||
|
auto repo_owner = path_parts.takeFirst();
|
||||||
|
auto repo_name = path_parts.takeFirst();
|
||||||
|
auto api_url = QString("https://api.github.com/repos/%1/%2/releases").arg(repo_owner, repo_name);
|
||||||
|
|
||||||
|
qDebug() << "Fetching release list from" << api_url;
|
||||||
|
|
||||||
|
downloadReleasePage(api_url, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::downloadReleasePage(const QString& api_url, int page)
|
||||||
|
{
|
||||||
|
int per_page = 30;
|
||||||
|
auto page_url = QString("%1?per_page=%2&page=%3").arg(api_url).arg(QString::number(per_page)).arg(QString::number(page));
|
||||||
|
QNetworkRequest request(page_url);
|
||||||
|
request.setRawHeader("Accept", "application/vnd.github+json");
|
||||||
|
request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
|
||||||
|
|
||||||
|
QNetworkReply* rep = m_network->get(request);
|
||||||
|
m_reply = rep;
|
||||||
|
auto responce = new QByteArray();
|
||||||
|
|
||||||
|
connect(rep, &QNetworkReply::finished, this, [this, responce, per_page, api_url, page]() {
|
||||||
|
int num_found = parseReleasePage(responce);
|
||||||
|
delete responce;
|
||||||
|
|
||||||
|
if (!(num_found < per_page)) { // there may be more, fetch next page
|
||||||
|
downloadReleasePage(api_url, page + 1);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||||
|
connect(rep, &QNetworkReply::errorOccurred, this, &WindowsUpdaterApp::downloadError);
|
||||||
|
#else
|
||||||
|
connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &WindowsUpdaterApp::downloadError);
|
||||||
|
#endif
|
||||||
|
connect(rep, &QNetworkReply::sslErrors, this, &WindowsUpdaterApp::sslErrors);
|
||||||
|
connect(rep, &QNetworkReply::readyRead, this, [this, responce]() {
|
||||||
|
auto data = m_reply->readAll();
|
||||||
|
responce->append(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int WindowsUpdaterApp::parseReleasePage(const QByteArray* responce)
|
||||||
|
{
|
||||||
|
if (responce->isEmpty()) // empty page
|
||||||
|
return 0;
|
||||||
|
int num_releases = 0;
|
||||||
|
try {
|
||||||
|
auto doc = Json::requireDocument(*responce);
|
||||||
|
auto release_list = Json::requireArray(doc);
|
||||||
|
for (auto release_json : release_list) {
|
||||||
|
auto release_obj = Json::requireObject(release_json);
|
||||||
|
|
||||||
|
GitHubRelease release = {};
|
||||||
|
release.id = Json::requireInteger(release_obj, "id");
|
||||||
|
release.name = Json::ensureString(release_obj, "name");
|
||||||
|
release.tag_name = Json::requireString(release_obj, "tag_name");
|
||||||
|
release.created_at = QDateTime::fromString(Json::requireString(release_obj, "created_at"), Qt::ISODate);
|
||||||
|
release.published_at = QDateTime::fromString(Json::ensureString(release_obj, "published_at"), Qt::ISODate);
|
||||||
|
release.draft = Json::requireBoolean(release_obj, "draft");
|
||||||
|
release.prerelease = Json::requireBoolean(release_obj, "prerelease");
|
||||||
|
release.body = Json::ensureString(release_obj, "body");
|
||||||
|
release.version = Version(release.tag_name);
|
||||||
|
|
||||||
|
auto release_assets_obj = Json::requireArray(release_obj, "assets");
|
||||||
|
for (auto asset_json : release_assets_obj) {
|
||||||
|
auto asset_obj = Json::requireObject(asset_json);
|
||||||
|
GitHubReleaseAsset asset = {};
|
||||||
|
asset.id = Json::requireInteger(asset_obj, "id");
|
||||||
|
asset.name = Json::requireString(asset_obj, "name");
|
||||||
|
asset.label = Json::ensureString(asset_obj, "label");
|
||||||
|
asset.content_type = Json::requireString(asset_obj, "content_type");
|
||||||
|
asset.size = Json::requireInteger(asset_obj, "size");
|
||||||
|
asset.created_at = QDateTime::fromString(Json::requireString(asset_obj, "created_at"), Qt::ISODate);
|
||||||
|
asset.updated_at = QDateTime::fromString(Json::requireString(asset_obj, "updated_at"), Qt::ISODate);
|
||||||
|
asset.browser_download_url = Json::requireString(asset_obj, "browser_download_url");
|
||||||
|
release.assets.append(asset);
|
||||||
|
}
|
||||||
|
m_releases.append(release);
|
||||||
|
num_releases++;
|
||||||
|
}
|
||||||
|
} catch (Json::JsonException& e) {
|
||||||
|
auto err_msg =
|
||||||
|
QString("Failed to parse releases from github: %1\n%2").arg(e.what()).arg(QString::fromStdString(responce->toStdString()));
|
||||||
|
fail(err_msg);
|
||||||
|
}
|
||||||
|
return num_releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
GitHubRelease WindowsUpdaterApp::getLatestRelease()
|
||||||
|
{
|
||||||
|
GitHubRelease latest;
|
||||||
|
for (auto release : m_releases) {
|
||||||
|
if (!latest.isValid() || (!release.draft && release.version > latest.version)) {
|
||||||
|
latest = release;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WindowsUpdaterApp::needUpdate(const GitHubRelease& release)
|
||||||
|
{
|
||||||
|
auto current_ver = Version(QString("%1.%2").arg(QString::number(m_prismVersionMajor)).arg(QString::number(m_prismVersionMinor)));
|
||||||
|
return current_ver < release.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::downloadError(QNetworkReply::NetworkError error)
|
||||||
|
{
|
||||||
|
if (error == QNetworkReply::OperationCanceledError) {
|
||||||
|
abort(QString("Aborted %1").arg(m_reply->url().toString()));
|
||||||
|
} else {
|
||||||
|
fail(QString("Network request Failed: %1 with reason %2").arg(m_reply->url().toString()).arg(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsUpdaterApp::sslErrors(const QList<QSslError>& errors)
|
||||||
|
{
|
||||||
|
int i = 1;
|
||||||
|
QString err_msg;
|
||||||
|
for (auto error : errors) {
|
||||||
|
err_msg.append(QString("Network request %1 SSL Error %2: %3\n").arg(m_reply->url().toString()).arg(i).arg(error.errorString()));
|
||||||
|
auto cert = error.certificate();
|
||||||
|
err_msg.append(QString("Certificate in question:\n%1").arg(cert.toText()));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
fail(err_msg);
|
||||||
|
}
|
110
launcher/updater/windows/WindowsUpdater.h
Normal file
110
launcher/updater/windows/WindowsUpdater.h
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// 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 <QtCore>
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFlag>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#define PRISM_EXTERNAL_EXE
|
||||||
|
#include "FileSystem.h"
|
||||||
|
|
||||||
|
#include "updater/windows/GitHubRelease.h"
|
||||||
|
|
||||||
|
class WindowsUpdaterApp : public QApplication {
|
||||||
|
// friends for the purpose of limiting access to deprecated stuff
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum Status { Starting, Failed, Succeeded, Initialized, Aborted };
|
||||||
|
WindowsUpdaterApp(int& argc, char** argv);
|
||||||
|
virtual ~WindowsUpdaterApp();
|
||||||
|
void loadReleaseList();
|
||||||
|
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);
|
||||||
|
|
||||||
|
void loadPrismVersionFromExe(const QString& exe_path);
|
||||||
|
|
||||||
|
void downloadReleasePage(const QString& api_url, int page);
|
||||||
|
int parseReleasePage(const QByteArray* responce);
|
||||||
|
GitHubRelease getLatestRelease();
|
||||||
|
bool needUpdate(const GitHubRelease& release);
|
||||||
|
GitHubRelease selectRelease();
|
||||||
|
void performUpdate(const GitHubRelease& release);
|
||||||
|
void printReleases();
|
||||||
|
QList<GitHubRelease> newerReleases();
|
||||||
|
QList<GitHubRelease> nonDraftReleases();
|
||||||
|
|
||||||
|
void downloadError(QNetworkReply::NetworkError error);
|
||||||
|
void sslErrors(const QList<QSslError>& errors);
|
||||||
|
|
||||||
|
const QString& root() { return m_rootPath; }
|
||||||
|
|
||||||
|
bool isPortable() { return m_portable; }
|
||||||
|
|
||||||
|
QString m_rootPath;
|
||||||
|
bool m_portable = false;
|
||||||
|
QString m_prismExecutable;
|
||||||
|
QUrl m_prismRepoUrl;
|
||||||
|
Version m_userSelectedVersion;
|
||||||
|
bool m_checkOnly;
|
||||||
|
bool m_forceUpdate;
|
||||||
|
bool m_printOnly;
|
||||||
|
bool m_updateLatest;
|
||||||
|
bool m_allowDowngrade;
|
||||||
|
|
||||||
|
QString m_prismBinaryName;
|
||||||
|
QString m_prismVerison;
|
||||||
|
int m_prismVersionMajor;
|
||||||
|
int m_prismVersionMinor;
|
||||||
|
QString m_prsimVersionChannel;
|
||||||
|
QString m_prismGitCommit;
|
||||||
|
|
||||||
|
Status m_status = Status::Starting;
|
||||||
|
QNetworkAccessManager* m_network;
|
||||||
|
QNetworkReply* m_reply;
|
||||||
|
QList<GitHubRelease> 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
|
||||||
|
};
|
44
launcher/updater/windows/windows_main.cpp
Normal file
44
launcher/updater/windows/windows_main.cpp
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#include "updater/windows/WindowsUpdater.h"
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
WindowsUpdaterApp wUpApp(argc, argv);
|
||||||
|
|
||||||
|
switch(wUpApp.status()) {
|
||||||
|
case WindowsUpdaterApp::Starting:
|
||||||
|
case WindowsUpdaterApp::Initialized:
|
||||||
|
{
|
||||||
|
return wUpApp.exec();
|
||||||
|
}
|
||||||
|
case WindowsUpdaterApp::Failed:
|
||||||
|
return 1;
|
||||||
|
case WindowsUpdaterApp::Succeeded:
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user