add self updater (#1352)

* add self updater

* remove updater tests after validation
This commit is contained in:
nmzik
2026-10-10 05:42:32 +02:00
committed by GitHub
parent 679be25c37
commit 307fdcf7ad
12 changed files with 1025 additions and 119 deletions
+13 -1
View File
@@ -1,5 +1,6 @@
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
add_subdirectory("${KYTY_SOURCE_DIR}/updater" "${CMAKE_CURRENT_BINARY_DIR}/updater")
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@@ -42,7 +43,13 @@ elseif(LINUX)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher kyty_emulator)
add_custom_target(stage_updater
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:launcher>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:kyty_updater>
$<TARGET_FILE_DIR:launcher>/$<TARGET_FILE_NAME:kyty_updater>
DEPENDS kyty_updater
VERBATIM)
add_dependencies(launcher kyty_emulator stage_updater)
if(KYTY_CLANG_CL)
add_custom_command(TARGET launcher POST_BUILD
@@ -96,6 +103,7 @@ install(TARGETS launcher
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
@@ -112,6 +120,10 @@ if(APPLE)
")
endif()
if(NOT APPLE)
install(TARGETS kyty_updater RUNTIME DESTINATION .)
endif()
if(WIN32)
install(CODE "
execute_process(
+41 -17
View File
@@ -1,12 +1,21 @@
#ifndef UPDATE_CHECKER_H
#define UPDATE_CHECKER_H
#include "updateRelease.h"
#include <QCryptographicHash>
#include <QFile>
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QPointer>
#include <QProcess>
class QByteArray;
#include <functional>
#include <memory>
class QNetworkReply;
class QProgressDialog;
class QTemporaryDir;
class QWidget;
class UpdateChecker final: public QObject {
@@ -14,28 +23,43 @@ class UpdateChecker final: public QObject {
public:
explicit UpdateChecker(QWidget* parent);
~UpdateChecker() override;
[[nodiscard]] static bool IsSupported();
void Check(bool manual);
static void ShowPreviousResult(QWidget* parent);
void Check(bool manual);
void SetGameRunning(bool running) { m_game_running = running; }
[[nodiscard]] bool IsInstalling() const { return m_installing; }
signals:
void CheckingChanged(bool checking);
void InstallingChanged(bool installing);
private:
struct UpdateInfo;
void FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done);
void CheckCurrent(const UpdateRelease::Info& current, bool manual);
void OfferUpdate();
void Download();
void ReadDownload(QNetworkReply* reply);
void PreparePackage();
void RunPreparation(const QString& program, const QStringList& arguments, bool validate);
void Restart();
void Finish(QString error = {}, bool report = true);
static UpdateInfo ParseUpdateInfo(const QByteArray& data);
void FetchUpdateInfo(const char* url, bool fallback, bool manual);
void ShowUpdateResult(const UpdateInfo& info, bool manual);
QWidget* m_parent = nullptr;
QNetworkAccessManager m_network;
bool m_checking_updates = false;
// Answer from the primary update feed, kept while the GitHub fallback feed is
// being consulted, so it can still be shown if the fallback request fails.
QString m_primary_tag;
QUrl m_primary_page_url;
QWidget* m_parent;
QNetworkAccessManager m_network;
QPointer<QNetworkReply> m_reply;
QPointer<QProgressDialog> m_progress;
QProcess m_prepare;
std::unique_ptr<QTemporaryDir> m_workspace;
QFile m_download;
QCryptographicHash m_hash {QCryptographicHash::Sha256};
UpdateRelease::Info m_release;
QString m_download_error;
bool m_busy = false;
bool m_installing = false;
bool m_game_running = false;
bool m_canceled = false;
};
#endif // UPDATE_CHECKER_H
+38
View File
@@ -0,0 +1,38 @@
#ifndef UPDATE_RELEASE_H
#define UPDATE_RELEASE_H
#include <QByteArray>
#include <QDateTime>
#include <QString>
#include <QUrl>
namespace UpdateRelease {
inline const QString LATEST_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest");
inline const QString TAG_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/tags/%1");
inline const QString PAGE_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/tag/%1");
inline const QString DOWNLOAD_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/download/%1/%2");
struct Info {
QString tag;
QUrl page_url;
QUrl download_url;
QByteArray sha256;
qint64 size = 0;
QDateTime published_at;
QString error;
};
enum class VersionRelation { Current, Newer, Older, Unknown };
[[nodiscard]] QString PlatformSuffix();
[[nodiscard]] Info Parse(const QByteArray& data, const QString& platform_suffix);
[[nodiscard]] VersionRelation Compare(const Info& latest, const Info& current);
} // namespace UpdateRelease
#endif // UPDATE_RELEASE_H
+8 -1
View File
@@ -136,6 +136,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
[this](const QString&) { m_update_checker->Check(true); });
connect(m_update_checker, &UpdateChecker::CheckingChanged, m_ui->check_updates_link,
&QLabel::setDisabled);
connect(m_update_checker, &UpdateChecker::InstallingChanged, this, &MainDialogPrivate::Update);
connect(m_ui->check_updates_on_startup, &QCheckBox::toggled, this, [this](bool checked) {
g_check_updates_on_startup = checked;
m_ui->widget->WriteSettings();
@@ -146,6 +147,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
});
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
m_update_checker->SetGameRunning(state != QProcess::NotRunning);
if (state == QProcess::NotRunning) {
if (m_running_item != nullptr) {
m_running_item->SetRunning(false);
@@ -162,6 +164,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
}
void MainDialogPrivate::FindInterpreter() {
UpdateChecker::ShowPreviousResult(m_main_dialog);
QDir search_dir(QApplication::applicationDirPath());
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
@@ -509,6 +512,9 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
}
void MainDialogPrivate::Run() {
if (m_update_checker->IsInstalling()) {
return;
}
m_running_item = m_ui->widget->GetSelectedItem();
if (m_running_item == nullptr) {
return;
@@ -526,7 +532,8 @@ void MainDialogPrivate::Run() {
void MainDialogPrivate::Update() {
const auto* item = m_ui->widget->GetSelectedItem();
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr &&
!m_update_checker->IsInstalling());
if (run_enabled) {
const auto& info = item->GetInfo();
+374 -99
View File
@@ -2,143 +2,418 @@
#include "kytyGitVersion.h"
#include <QByteArray>
#include <QAbstractButton>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QString>
#include <QUrl>
#include <QWidget>
#include <QProcessEnvironment>
#include <QProgressDialog>
#include <QTemporaryDir>
namespace {
constexpr char DEFAULT_FEED_URL[] = "https://kytyps5.github.io/data/updates.json";
constexpr char FALLBACK_FEED_URL[] =
"https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest";
constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
QString LauncherRelativePath() {
#if defined(_WIN32)
return QStringLiteral("launcher.exe");
#elif defined(__APPLE__)
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
#else
return QStringLiteral("launcher");
#endif
}
QString InstallDirectory() {
QDir dir(QCoreApplication::applicationDirPath());
#if defined(__APPLE__)
// Official macOS releases are a bundle; never update a flat development build.
if (!dir.cdUp() || !dir.cdUp() || dir.dirName() != QStringLiteral("KytyPS5.app") ||
!dir.cdUp()) {
return {};
}
#endif
return dir.absolutePath();
}
QString HelperName() {
#if defined(_WIN32)
return QStringLiteral("kyty_updater.exe");
#else
return QStringLiteral("kyty_updater");
#endif
}
QNetworkRequest Request(const QUrl& url) {
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "KytyPS5-Updater");
request.setTransferTimeout(30000);
return request;
}
QString TarProgram() {
#if defined(_WIN32)
// Use the OS tool, not a tar.exe from the download or current directory.
return QDir(qEnvironmentVariable("SystemRoot", QStringLiteral("C:/Windows")))
.filePath(QStringLiteral("System32/tar.exe"));
#else
return QStringLiteral("/usr/bin/tar");
#endif
}
} // namespace
struct UpdateChecker::UpdateInfo {
QString tag;
QUrl page_url;
QString error;
};
UpdateChecker::UpdateChecker(QWidget* parent): QObject(parent), m_parent(parent), m_network(this) {}
UpdateChecker::~UpdateChecker() {
if (m_reply) {
m_reply->disconnect(this);
m_reply->abort();
}
// Extraction must stop before QTemporaryDir removes its output.
m_prepare.disconnect(this);
if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
m_prepare.waitForFinished();
}
}
bool UpdateChecker::IsSupported() {
#if defined(KYTY_OFFICIAL_BUILD) && defined(NDEBUG)
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty"));
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty")) &&
!UpdateRelease::PlatformSuffix().isEmpty();
#else
return false;
#endif
}
UpdateChecker::UpdateInfo UpdateChecker::ParseUpdateInfo(const QByteArray& data) {
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
return {{}, {}, tr("Invalid update feed")};
}
const auto root = document.object();
UpdateInfo info {root.value(QStringLiteral("tag")).toString(),
QUrl(root.value(QStringLiteral("html_url")).toString()), {}};
if (info.tag.isEmpty()) {
info.tag = root.value(QStringLiteral("tag_name")).toString();
}
if (info.tag.isEmpty() || !info.page_url.isValid() ||
info.page_url.scheme() != QStringLiteral("https")) {
info.error = tr("Incomplete update feed");
}
return info;
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_checking_updates) {
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
const auto directory = InstallDirectory();
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
return;
}
m_checking_updates = true;
emit CheckingChanged(true);
FetchUpdateInfo(DEFAULT_FEED_URL, false, manual);
const auto status = result.readLine().trimmed();
auto message = QString::fromUtf8(result.read(64 * 1024)).trimmed();
result.close();
result.remove();
if (status != "success" && status != "failure") {
return;
}
const bool success = status == "success";
if (message.isEmpty()) {
message = success ? tr("The update was installed successfully.")
: tr("The update could not be installed.");
}
QMessageBox(success ? QMessageBox::Information : QMessageBox::Warning, tr("KytyPS5 Update"),
message, QMessageBox::Ok, parent)
.exec();
}
void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) {
QNetworkRequest request(QUrl(QString::fromLatin1(url)));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
void UpdateChecker::FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done) {
auto request = Request(url);
request.setRawHeader("Accept", "application/vnd.github+json");
request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
request.setTransferTimeout(15000);
auto* reply = m_network.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, fallback, manual]() {
UpdateInfo info;
if (reply->error() == QNetworkReply::NoError) {
info = ParseUpdateInfo(reply->readAll());
} else {
m_reply = reply;
reply->setReadBufferSize(MAX_METADATA_SIZE + 1);
connect(reply, &QNetworkReply::readyRead, this, [reply]() {
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
reply->abort();
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply, done = std::move(done)]() {
UpdateRelease::Info info;
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
info.error = tr("The update response is too large.");
} else if (reply->error() != QNetworkReply::NoError) {
info.error = reply->errorString();
} else {
info = UpdateRelease::Parse(reply->readAll(), UpdateRelease::PlatformSuffix());
}
m_reply = nullptr;
reply->deleteLater();
if (!fallback) {
const bool up_to_date =
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (!up_to_date) {
if (info.error.isEmpty()) {
// The primary feed is reachable but disagrees with the build tag
// (e.g. a stale mirror); remember its answer in case the fallback
// request also fails.
m_primary_tag = info.tag;
m_primary_page_url = info.page_url;
}
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
}
}
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
// The fallback feed failed, but the primary feed already returned a
// usable answer; prefer it over reporting a network error.
info = {m_primary_tag, m_primary_page_url, {}};
}
m_primary_tag.clear();
m_primary_page_url.clear();
m_checking_updates = false;
emit CheckingChanged(false);
ShowUpdateResult(info, manual);
done(info);
});
}
void UpdateChecker::ShowUpdateResult(const UpdateInfo& info, bool manual) {
if (!info.error.isEmpty()) {
qWarning() << "Could not check for updates:" << info.error;
if (manual) {
QMessageBox::warning(m_parent, tr("Update Check"),
tr("Could not check for updates:\n%1").arg(info.error));
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_busy) {
return;
}
m_busy = true;
emit CheckingChanged(true);
FetchRelease(QUrl(UpdateRelease::LATEST_URL), [this, manual](UpdateRelease::Info latest) {
m_release = std::move(latest);
if (!m_release.published_at.isValid()) {
Finish(m_release.error, manual);
} else if (m_release.tag == QString::fromLatin1(KYTY_RELEASE_TAG)) {
CheckCurrent(m_release, manual);
} else {
// Date/hash tags cannot order releases published on the same day.
const auto tag = QUrl::toPercentEncoding(QString::fromLatin1(KYTY_RELEASE_TAG));
FetchRelease(QUrl(UpdateRelease::TAG_URL.arg(QString::fromLatin1(tag))),
[this, manual](const UpdateRelease::Info& current) {
CheckCurrent(current, manual);
});
}
});
}
const bool current = info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (current) {
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("You are using the latest version (%1).").arg(info.tag));
void UpdateChecker::CheckCurrent(const UpdateRelease::Info& current, bool manual) {
if (current.tag != QString::fromLatin1(KYTY_RELEASE_TAG) || !current.published_at.isValid()) {
Finish(tr("Could not verify the currently installed release. Please try again later."),
manual);
return;
}
const auto relation = UpdateRelease::Compare(m_release, current);
if (relation == UpdateRelease::VersionRelation::Newer) {
if (m_release.error.isEmpty()) {
OfferUpdate();
} else {
Finish(m_release.error, manual);
}
return;
}
const auto message =
tr("An update is available.\n\nCurrent: %1\nLatest: %2\n\n"
"Open the release page?")
.arg(QString::fromLatin1(KYTY_RELEASE_TAG), info.tag);
if (QMessageBox::question(m_parent, tr("KytyPS5 Update"), message,
QMessageBox::Open | QMessageBox::Cancel,
QMessageBox::Open) == QMessageBox::Open) {
QDesktopServices::openUrl(info.page_url);
if (relation == UpdateRelease::VersionRelation::Unknown) {
Finish(tr("Could not determine whether the available release is newer."), manual);
return;
}
Finish();
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("No newer release is available."));
}
}
void UpdateChecker::OfferUpdate() {
QMessageBox question(QMessageBox::Information, tr("KytyPS5 Update"),
tr("Version %1 is available.\n\nCurrent: %2\n\n"
"Download and install it now? The launcher will restart. Your settings "
"and games are kept.")
.arg(m_release.tag, QString::fromLatin1(KYTY_RELEASE_TAG)),
QMessageBox::Yes | QMessageBox::Open | QMessageBox::Cancel, m_parent);
question.button(QMessageBox::Yes)->setText(tr("Install Update"));
question.button(QMessageBox::Open)->setText(tr("Release Notes"));
question.setDefaultButton(QMessageBox::Yes);
const auto answer = question.exec();
if (answer == QMessageBox::Yes) {
Download();
return;
}
if (answer == QMessageBox::Open) {
QDesktopServices::openUrl(m_release.page_url);
}
Finish();
}
void UpdateChecker::Download() {
if (m_game_running) {
Finish(tr("Close the running game before installing an update."));
return;
}
const auto directory = InstallDirectory();
const auto helper = QDir(QCoreApplication::applicationDirPath()).filePath(HelperName());
if (directory.isEmpty() || !QFileInfo::exists(helper) || !QFileInfo::exists(TarProgram())) {
Finish(tr("This installation cannot update itself. Download the release from GitHub and "
"extract it manually."));
return;
}
m_workspace = std::make_unique<QTemporaryDir>(
QDir(directory).filePath(QStringLiteral(".kyty-update-XXXXXX")));
if (!m_workspace->isValid()) {
Finish(tr("The installation folder is not writable. Move KytyPS5 to a writable folder and "
"try again."));
return;
}
if (!QFile::copy(helper, m_workspace->filePath(HelperName())) ||
!QDir(m_workspace->path()).mkdir(QStringLiteral("payload"))) {
Finish(tr("Could not prepare the update folder."));
return;
}
m_download.setFileName(
m_workspace->filePath(QStringLiteral("package") + UpdateRelease::PlatformSuffix()));
if (!m_download.open(QIODevice::WriteOnly)) {
Finish(m_download.errorString());
return;
}
m_hash.reset();
m_download_error.clear();
m_canceled = false;
m_installing = true;
emit InstallingChanged(true);
m_progress = new QProgressDialog(tr("Downloading %1…").arg(m_release.tag), tr("Cancel"), 0, 100,
m_parent);
m_progress->setWindowTitle(tr("KytyPS5 Update"));
m_progress->setWindowModality(Qt::ApplicationModal);
m_progress->setAutoClose(false);
m_progress->setAutoReset(false);
m_progress->setMinimumDuration(0);
connect(m_progress, &QProgressDialog::canceled, this, [this]() {
m_canceled = true;
if (m_reply) {
m_reply->abort();
} else if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
}
});
auto* reply = m_network.get(Request(m_release.download_url));
m_reply = reply;
reply->setReadBufferSize(1024 * 1024);
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { ReadDownload(reply); });
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64) {
if (m_progress) {
m_progress->setValue(
static_cast<int>(qMin<qint64>(99, received * 100 / m_release.size)));
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
ReadDownload(reply);
const auto error = reply->error();
const auto error_text = reply->errorString();
m_reply = nullptr;
reply->deleteLater();
const auto size = m_download.size();
const bool flushed = m_download.flush();
m_download.close();
if (m_canceled) {
Finish();
} else if (!m_download_error.isEmpty()) {
Finish(m_download_error);
} else if (error != QNetworkReply::NoError) {
Finish(error_text);
} else if (!flushed || size != m_release.size || m_hash.result() != m_release.sha256) {
Finish(tr("The update failed its size or SHA-256 verification. Please try again."));
} else {
PreparePackage();
}
});
}
void UpdateChecker::ReadDownload(QNetworkReply* reply) {
const auto data = reply->readAll();
if (data.isEmpty() || m_canceled || !m_download_error.isEmpty()) {
return;
}
if (m_download.size() + data.size() > m_release.size) {
m_download_error = tr("The downloaded file is larger than the release asset.");
} else if (m_download.write(data) != data.size()) {
m_download_error = m_download.errorString();
} else {
m_hash.addData(data);
}
if (!m_download_error.isEmpty() && !reply->isFinished()) {
reply->abort();
}
}
void UpdateChecker::PreparePackage() {
m_progress->setLabelText(tr("Preparing the update…"));
m_progress->setRange(0, 0);
RunPreparation(TarProgram(),
{QStringLiteral("-xf"), m_download.fileName(), QStringLiteral("-C"),
m_workspace->filePath(QStringLiteral("payload")),
QStringLiteral("--no-same-owner"), QStringLiteral("--no-same-permissions")},
false);
}
void UpdateChecker::RunPreparation(const QString& program, const QStringList& arguments,
bool validate) {
m_prepare.disconnect(this);
m_prepare.setProgram(program);
m_prepare.setArguments(arguments);
m_prepare.setWorkingDirectory(m_workspace->path());
auto environment = QProcessEnvironment::systemEnvironment();
environment.remove(QStringLiteral("TAR_OPTIONS"));
m_prepare.setProcessEnvironment(environment);
connect(&m_prepare, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (error == QProcess::FailedToStart) {
Finish(m_prepare.errorString());
}
});
connect(&m_prepare, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
[this, validate](int exit_code, QProcess::ExitStatus status) {
if (m_canceled) {
Finish();
} else if (exit_code != 0 || status != QProcess::NormalExit) {
Finish(
tr("Could not prepare the update:\n%1")
.arg(QString::fromUtf8(m_prepare.readAllStandardError()).left(4096)));
} else if (!validate) {
RunPreparation(m_workspace->filePath(HelperName()),
{QStringLiteral("--validate"),
m_workspace->filePath(QStringLiteral("payload")),
InstallDirectory(), LauncherRelativePath()},
true);
} else {
Restart();
}
});
m_prepare.start();
}
void UpdateChecker::Restart() {
if (m_game_running || m_canceled) {
Finish(tr("The update was canceled. Close any running game before trying again."));
return;
}
m_progress->setLabelText(tr("Restarting to install the update…"));
m_progress->setCancelButton(nullptr);
QProcess helper;
helper.setProgram(m_workspace->filePath(HelperName()));
helper.setArguments({QStringLiteral("--install"),
m_workspace->filePath(QStringLiteral("payload")), InstallDirectory(),
LauncherRelativePath(),
QString::number(QCoreApplication::applicationPid()), QDir::currentPath()});
helper.setWorkingDirectory(m_workspace->path());
helper.setStandardOutputFile(m_workspace->filePath(QStringLiteral("helper.log")));
helper.setStandardErrorFile(m_workspace->filePath(QStringLiteral("helper.log")),
QIODevice::Append);
if (!helper.startDetached()) {
Finish(tr("Could not start the update installer."));
return;
}
m_workspace->setAutoRemove(false);
QApplication::quit();
}
void UpdateChecker::Finish(QString error, bool report) {
const auto release_page = m_release.page_url;
m_release = {};
m_download.close();
m_workspace.reset();
if (m_progress) {
m_progress->disconnect(this);
m_progress->hide();
m_progress->deleteLater();
m_progress = nullptr;
}
m_busy = false;
m_installing = false;
emit CheckingChanged(false);
emit InstallingChanged(false);
if (error.isEmpty()) {
return;
}
qWarning() << "KytyPS5 updater:" << error;
if (!report) {
return;
}
QMessageBox message(QMessageBox::Warning, tr("KytyPS5 Update"), error, QMessageBox::Close,
m_parent);
if (!release_page.isEmpty()) {
message.addButton(QMessageBox::Open);
message.button(QMessageBox::Open)->setText(tr("Open Release Page"));
}
if (message.exec() == QMessageBox::Open) {
QDesktopServices::openUrl(release_page);
}
}
+89
View File
@@ -0,0 +1,89 @@
#include "updateRelease.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QRegularExpression>
namespace UpdateRelease {
QString PlatformSuffix() {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
return QStringLiteral("-Windows-x64.zip");
#elif defined(__linux__) && defined(__x86_64__)
return QStringLiteral("-Linux-x86_64.tar.gz");
#elif defined(__APPLE__) && defined(__x86_64__)
return QStringLiteral("-macOS-x86_64.zip");
#else
return {};
#endif
}
Info Parse(const QByteArray& data, const QString& platform_suffix) {
Info info;
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
info.error = QObject::tr("Invalid GitHub release response.");
return info;
}
const auto root = document.object();
info.tag = root.value(QStringLiteral("tag_name")).toString();
const auto published_at =
QDateTime::fromString(root.value(QStringLiteral("published_at")).toString(), Qt::ISODate);
if (info.tag.isEmpty() || !published_at.isValid() ||
root.value(QStringLiteral("draft")).toBool() ||
root.value(QStringLiteral("prerelease")).toBool()) {
info.error = QObject::tr("The release is not a published stable release.");
return info;
}
info.published_at = published_at;
const auto encoded_tag = QString::fromLatin1(QUrl::toPercentEncoding(info.tag));
info.page_url = QUrl(PAGE_URL.arg(encoded_tag));
if (platform_suffix.isEmpty()) {
info.error = QObject::tr("Built-in updates are unavailable for this platform.");
return info;
}
const auto asset_name = info.tag + platform_suffix;
for (const auto& value: root.value(QStringLiteral("assets")).toArray()) {
const auto asset = value.toObject();
if (asset.value(QStringLiteral("name")).toString() != asset_name) {
continue;
}
const auto digest = asset.value(QStringLiteral("digest")).toString();
const auto size = asset.value(QStringLiteral("size")).toInteger();
static const QRegularExpression digest_pattern(
QStringLiteral("\\Asha256:[A-Fa-f0-9]{64}\\z"));
if (!digest_pattern.match(digest).hasMatch() || size <= 0) {
info.error = QObject::tr("The update package size or SHA-256 checksum is invalid.");
return info;
}
const auto encoded_asset = QString::fromLatin1(QUrl::toPercentEncoding(asset_name));
info.download_url = QUrl(DOWNLOAD_URL.arg(encoded_tag, encoded_asset));
info.sha256 = QByteArray::fromHex(digest.mid(7).toLatin1());
info.size = size;
return info;
}
info.error = QObject::tr("This release has no update package for your platform.");
return info;
}
VersionRelation Compare(const Info& latest, const Info& current) {
if (latest.tag.isEmpty() || current.tag.isEmpty()) {
return VersionRelation::Unknown;
}
if (latest.tag == current.tag) {
return VersionRelation::Current;
}
if (!latest.published_at.isValid() || !current.published_at.isValid() ||
latest.published_at == current.published_at) {
return VersionRelation::Unknown;
}
return latest.published_at > current.published_at ? VersionRelation::Newer
: VersionRelation::Older;
}
} // namespace UpdateRelease