diff --git a/pcsx2-qt/AutoUpdaterDialog.cpp b/pcsx2-qt/AutoUpdaterDialog.cpp index c7c0b0d5ee..d8758d57ff 100644 --- a/pcsx2-qt/AutoUpdaterDialog.cpp +++ b/pcsx2-qt/AutoUpdaterDialog.cpp @@ -1,5 +1,5 @@ /* PCSX2 - PS2 Emulator for PCs - * Copyright (C) 2002-2023 PCSX2 Dev Team + * Copyright (C) 2002-2023 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- @@ -18,6 +18,7 @@ #include "AutoUpdaterDialog.h" #include "MainWindow.h" #include "QtHost.h" +#include "QtProgressCallback.h" #include "QtUtils.h" #include "pcsx2/Host.h" @@ -26,11 +27,14 @@ #include "updater/UpdaterExtractor.h" +#include "common/Assertions.h" #include "common/CocoaTools.h" #include "common/Console.h" #include "common/FileSystem.h" +#include "common/HTTPDownloader.h" #include "common/StringUtil.h" +#include #include #include #include @@ -39,34 +43,34 @@ #include #include #include -#include -#include -#include #include #include #include +// Interval at which HTTP requests are polled. +static constexpr u32 HTTP_POLL_INTERVAL = 10; + // Logic to detect whether we can use the auto updater. // We use tagged commit, because this gets set on nightly builds. #if (defined(_WIN32) || defined(__linux__) || defined(__APPLE__)) && defined(GIT_TAG_LO) - #define AUTO_UPDATER_SUPPORTED 1 +#define AUTO_UPDATER_SUPPORTED 1 - #if defined(_WIN32) - #define UPDATE_PLATFORM_STR "Windows" - #elif defined(__linux__) - #define UPDATE_PLATFORM_STR "Linux" - #elif defined(__APPLE__) - #define UPDATE_PLATFORM_STR "MacOS" - #endif +#if defined(_WIN32) +#define UPDATE_PLATFORM_STR "Windows" +#elif defined(__linux__) +#define UPDATE_PLATFORM_STR "Linux" +#elif defined(__APPLE__) +#define UPDATE_PLATFORM_STR "MacOS" +#endif - #ifdef MULTI_ISA_SHARED_COMPILATION - // #undef UPDATE_ADDITIONAL_TAGS - #elif _M_SSE >= 0x501 - #define UPDATE_ADDITIONAL_TAGS "AVX2" - #else - #define UPDATE_ADDITIONAL_TAGS "SSE4" - #endif +#ifdef MULTI_ISA_SHARED_COMPILATION +// #undef UPDATE_ADDITIONAL_TAGS +#elif _M_SSE >= 0x501 +#define UPDATE_ADDITIONAL_TAGS "AVX2" +#else +#define UPDATE_ADDITIONAL_TAGS "SSE4" +#endif #endif @@ -86,8 +90,6 @@ static const char* UPDATE_TAGS[] = {"stable", "nightly"}; AutoUpdaterDialog::AutoUpdaterDialog(QWidget* parent /* = nullptr */) : QDialog(parent) { - m_network_access_mgr = new QNetworkAccessManager(this); - m_ui.setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -95,6 +97,10 @@ AutoUpdaterDialog::AutoUpdaterDialog(QWidget* parent /* = nullptr */) connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterDialog::downloadUpdateClicked); connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterDialog::skipThisUpdateClicked); connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterDialog::remindMeLaterClicked); + + m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent()); + if (!m_http) + Console.Error("Failed to create HTTP downloader, auto updater will not be available."); } AutoUpdaterDialog::~AutoUpdaterDialog() = default; @@ -172,35 +178,66 @@ void AutoUpdaterDialog::reportError(const char* msg, ...) QMessageBox::critical(this, tr("Updater Error"), QString::fromStdString(full_msg)); } +bool AutoUpdaterDialog::ensureHttpReady() +{ + if (!m_http) + return false; + + if (!m_http_poll_timer) + { + m_http_poll_timer = new QTimer(this); + m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterDialog::httpPollTimerPoll); + } + + if (!m_http_poll_timer->isActive()) + { + m_http_poll_timer->setSingleShot(false); + m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL); + m_http_poll_timer->start(); + } + + return true; +} + +void AutoUpdaterDialog::httpPollTimerPoll() +{ + pxAssert(m_http); + m_http->PollRequests(); + + if (!m_http->HasAnyRequests()) + { + Console.WriteLn("(AutoUpdaterDialog) All HTTP requests done."); + m_http_poll_timer->stop(); + } +} + void AutoUpdaterDialog::queueUpdateCheck(bool display_message) { m_display_messages = display_message; #ifdef AUTO_UPDATER_SUPPORTED - connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getLatestReleaseComplete); + if (!ensureHttpReady()) + { + emit updateCheckCompleted(); + return; + } - QUrl url(QStringLiteral(LATEST_RELEASE_URL).arg(getCurrentUpdateTag())); - QNetworkRequest request(url); - m_network_access_mgr->get(request); + m_http->CreateRequest(QStringLiteral(LATEST_RELEASE_URL).arg(getCurrentUpdateTag()).toStdString(), + std::bind(&AutoUpdaterDialog::getLatestReleaseComplete, this, std::placeholders::_1, std::placeholders::_3)); #else emit updateCheckCompleted(); #endif } -void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply) +void AutoUpdaterDialog::getLatestReleaseComplete(s32 status_code, std::vector data) { #ifdef AUTO_UPDATER_SUPPORTED - // this might fail due to a lack of internet connection - in which case, don't spam the user with messages every time. - m_network_access_mgr->disconnect(this); - reply->deleteLater(); - bool found_update_info = false; - if (reply->error() == QNetworkReply::NoError) + if (status_code == HTTPDownloader::HTTP_STATUS_OK) { - const QByteArray reply_json(reply->readAll()); QJsonParseError parse_error; - QJsonDocument doc(QJsonDocument::fromJson(reply_json, &parse_error)); + QJsonDocument doc(QJsonDocument::fromJson(QByteArray(reinterpret_cast(data.data()), data.size()), &parse_error)); if (doc.isObject()) { const QJsonObject doc_object(doc.object()); @@ -304,7 +341,7 @@ void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply) } else { - reportError("Failed to download latest release info: %d", static_cast(reply->error())); + reportError("Failed to download latest release info: %d", status_code); } if (found_update_info) @@ -317,29 +354,21 @@ void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply) void AutoUpdaterDialog::queueGetChanges() { #ifdef AUTO_UPDATER_SUPPORTED - connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getChangesComplete); + if (!ensureHttpReady()) + return; - const QString url_string(QStringLiteral(CHANGES_URL).arg(GIT_HASH).arg(m_latest_version)); - QUrl url(url_string); - QNetworkRequest request(url); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - m_network_access_mgr->get(request); + m_http->CreateRequest(QStringLiteral(CHANGES_URL).arg(GIT_HASH).arg(m_latest_version).toStdString(), + std::bind(&AutoUpdaterDialog::getChangesComplete, this, std::placeholders::_1, std::placeholders::_3)); #endif } -void AutoUpdaterDialog::getChangesComplete(QNetworkReply* reply) +void AutoUpdaterDialog::getChangesComplete(s32 status_code, std::vector data) { #ifdef AUTO_UPDATER_SUPPORTED - m_network_access_mgr->disconnect(this); - reply->deleteLater(); - - if (reply->error() == QNetworkReply::NoError) + if (status_code == HTTPDownloader::HTTP_STATUS_OK) { - const QByteArray reply_json(reply->readAll()); QJsonParseError parse_error; - QJsonDocument doc(QJsonDocument::fromJson(reply_json, &parse_error)); + QJsonDocument doc(QJsonDocument::fromJson(QByteArray(reinterpret_cast(data.data()), data.size()), &parse_error)); if (doc.isObject()) { const QJsonObject doc_object(doc.object()); @@ -400,7 +429,7 @@ void AutoUpdaterDialog::getChangesComplete(QNetworkReply* reply) } else { - reportError("Failed to download change list: %d", static_cast(reply->error())); + reportError("Failed to download change list: %d", status_code); } #endif @@ -427,61 +456,53 @@ void AutoUpdaterDialog::downloadUpdateClicked() } m_display_messages = true; - QUrl url(m_download_url); - QNetworkRequest request(url); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - QNetworkReply* reply = m_network_access_mgr->get(request); - QProgressDialog progress(tr("Downloading %1...").arg(m_download_url), tr("Cancel"), 0, 1); - progress.setWindowTitle(tr("Automatic Updater")); - progress.setWindowIcon(windowIcon()); - progress.setAutoClose(false); + std::optional download_result; + QtModalProgressCallback progress(this); + progress.SetTitle(tr("Automatic Updater").toUtf8().constData()); + progress.SetStatusText(tr("Downloading %1...").arg(m_latest_version).toUtf8().constData()); + progress.GetDialog().setWindowIcon(windowIcon()); + progress.SetCancellable(true); - connect(reply, &QNetworkReply::downloadProgress, [&progress](quint64 received, quint64 total) { - progress.setRange(0, static_cast(total)); - progress.setValue(static_cast(received)); - }); + m_http->CreateRequest( + m_download_url.toStdString(), + [this, &download_result, &progress](s32 status_code, const std::string&, std::vector data) { + if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED) + return; - connect(m_network_access_mgr, &QNetworkAccessManager::finished, [this, &progress](QNetworkReply* reply) { - m_network_access_mgr->disconnect(); + if (status_code != HTTPDownloader::HTTP_STATUS_OK) + { + reportError("Download failed: %d", status_code); + download_result = false; + return; + } - if (reply->error() != QNetworkReply::NoError) - { - reportError("Download failed: %s", reply->errorString().toUtf8().constData()); - progress.done(-1); - return; - } + if (data.empty()) + { + reportError("Download failed: Update is empty"); + download_result = false; + return; + } - const QByteArray data = reply->readAll(); - if (data.isEmpty()) - { - reportError("Download failed: Update is empty"); - progress.done(-1); - return; - } + download_result = processUpdate(data, progress.GetDialog()); + }, + &progress); - if (processUpdate(data, progress)) - progress.done(1); - else - progress.done(-1); - }); - - const int result = progress.exec(); - if (result == 0) + // Block until completion. + while (m_http->HasAnyRequests()) { - // cancelled - reply->abort(); + QApplication::processEvents(QEventLoop::AllEvents, HTTP_POLL_INTERVAL); + m_http->PollRequests(); } - else if (result == 1) + + if (download_result.value_or(false)) { // updater started. since we're a modal on the main window, we have to queue this. QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, true)); done(0); } - reply->deleteLater(); + // download error or cancelled } void AutoUpdaterDialog::checkIfUpdateNeeded() @@ -490,8 +511,8 @@ void AutoUpdaterDialog::checkIfUpdateNeeded() QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion"))); Console.WriteLn(Color_StrongGreen, "Current version: %s", GIT_TAG); - Console.WriteLn(Color_StrongYellow, "Latest SHA: %s", m_latest_version.toUtf8().constData()); - Console.WriteLn(Color_StrongOrange, "Last Checked SHA: %s", last_checked_version.toUtf8().constData()); + Console.WriteLn(Color_StrongYellow, "Latest version: %s", m_latest_version.toUtf8().constData()); + Console.WriteLn(Color_StrongOrange, "Last checked version: %s", last_checked_version.toUtf8().constData()); if (m_latest_version == GIT_TAG || m_latest_version == last_checked_version) { Console.WriteLn(Color_StrongGreen, "No update needed."); @@ -522,7 +543,9 @@ void AutoUpdaterDialog::checkIfUpdateNeeded() m_ui.downloadSize->setText(tr("Download Size: %1 MB").arg(static_cast(m_download_size) / 1048576.0, 0, 'f', 2)); m_ui.updateNotes->setText(tr("Loading...")); queueGetChanges(); - exec(); + + // We have to defer this, because it comes back through the timer/HTTP callback... + QMetaObject::invokeMethod(this, "exec", Qt::QueuedConnection); } void AutoUpdaterDialog::skipThisUpdateClicked() @@ -539,7 +562,7 @@ void AutoUpdaterDialog::remindMeLaterClicked() #if defined(_WIN32) -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog&) +bool AutoUpdaterDialog::processUpdate(const std::vector& data, QProgressDialog&) { const QString update_directory = QCoreApplication::applicationDirPath(); const QString update_zip_path = QStringLiteral("%1" FS_OSPATH_SEPARATOR_STR "%2").arg(update_directory).arg(UPDATER_ARCHIVE_NAME); @@ -555,7 +578,8 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDi { QFile update_zip_file(update_zip_path); - if (!update_zip_file.open(QIODevice::WriteOnly) || update_zip_file.write(update_data) != update_data.size()) + if (!update_zip_file.open(QIODevice::WriteOnly) || + update_zip_file.write(reinterpret_cast(data.data()), static_cast(data.size())) != static_cast(data.size())) { reportError("Writing update zip to '%s' failed", update_zip_path.toUtf8().constData()); return false; @@ -615,7 +639,7 @@ void AutoUpdaterDialog::cleanupAfterUpdate() #elif defined(__linux__) -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog&) +bool AutoUpdaterDialog::processUpdate(const std::vector& data, QProgressDialog&) { const char* appimage_path = std::getenv("APPIMAGE"); if (!appimage_path || !FileSystem::FileExists(appimage_path)) @@ -655,7 +679,9 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDi QFile old_file(qappimage_path); const QFileDevice::Permissions old_permissions = old_file.permissions(); QFile new_file(new_appimage_path); - if (!new_file.open(QIODevice::WriteOnly) || new_file.write(update_data) != update_data.size() || !new_file.setPermissions(old_permissions)) + if (!new_file.open(QIODevice::WriteOnly) || + new_file.write(reinterpret_cast(data.data()), static_cast(data.size())) != static_cast(data.size()) || + !new_file.setPermissions(old_permissions)) { QFile::remove(new_appimage_path); reportError("Failed to write new destination AppImage: %s", new_appimage_path.toUtf8().constData()); @@ -725,7 +751,7 @@ static QString UpdateVersionNumberInName(QString name, QStringView new_version) return name; } -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog& progress) +bool AutoUpdaterDialog::processUpdate(const std::vector& data, QProgressDialog& progress) { std::optional path = CocoaTools::GetNonTranslocatedBundlePath(); if (!path.has_value()) @@ -754,20 +780,21 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDi return false; } - constexpr qsizetype chunk_size = 65536; + constexpr size_t chunk_size = 65536; progress.setLabelText(QStringLiteral("Unpacking update...")); progress.reset(); - progress.setRange(0, static_cast((update_data.size() + chunk_size - 1) / chunk_size)); + progress.setRange(0, static_cast((data.size() + chunk_size - 1) / chunk_size)); QProcess untar; untar.setProgram(QStringLiteral("/usr/bin/tar")); untar.setArguments({QStringLiteral("xC"), temp_dir.path()}); untar.start(); - for (qsizetype i = 0; i < update_data.size(); i += chunk_size) + for (size_t i = 0; i < data.size(); i += chunk_size) { progress.setValue(static_cast(i / chunk_size)); - const qsizetype amt = std::min(update_data.size() - i, chunk_size); - if (progress.wasCanceled() || untar.write(update_data.data() + i, amt) != amt) + const size_t amt = std::min(data.size() - i, chunk_size); + if (progress.wasCanceled() || + untar.write(reinterpret_cast(data.data() + i), static_cast(amt)) != static_cast(amt)) { if (!progress.wasCanceled()) reportError("Failed to unpack update (write stopped short)"); @@ -796,7 +823,7 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDi } QFileInfoList temp_dir_contents = QDir(temp_dir.path()).entryInfoList(QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot); - auto new_app = std::find_if(temp_dir_contents.begin(), temp_dir_contents.end(), [](const QFileInfo& file){ return file.suffix() == QStringLiteral("app"); }); + auto new_app = std::find_if(temp_dir_contents.begin(), temp_dir_contents.end(), [](const QFileInfo& file) { return file.suffix() == QStringLiteral("app"); }); if (new_app == temp_dir_contents.end()) { reportError("Couldn't find application in update package"); @@ -814,7 +841,7 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDi { QFile::rename(QString::fromStdString(*trashed_path), info.filePath()); reportError("Failed to move new application into place (couldn't rename '%s' to '%s')", - new_app->absoluteFilePath().toUtf8().constData(), open_path.toUtf8().constData()); + new_app->absoluteFilePath().toUtf8().constData(), open_path.toUtf8().constData()); return false; } QDir(QString::fromStdString(*trashed_path)).removeRecursively(); diff --git a/pcsx2-qt/AutoUpdaterDialog.h b/pcsx2-qt/AutoUpdaterDialog.h index da3654f552..b5bb56cb0a 100644 --- a/pcsx2-qt/AutoUpdaterDialog.h +++ b/pcsx2-qt/AutoUpdaterDialog.h @@ -1,5 +1,5 @@ /* PCSX2 - PS2 Emulator for PCs - * Copyright (C) 2002-2022 PCSX2 Dev Team + * Copyright (C) 2002-2023 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- @@ -14,14 +14,20 @@ */ #pragma once + +#include "common/Pcsx2Defs.h" + #include "ui_AutoUpdaterDialog.h" + +#include #include + #include #include +#include #include -class QNetworkAccessManager; -class QNetworkReply; +class HTTPDownloader; class QProgressDialog; class AutoUpdaterDialog final : public QDialog @@ -46,28 +52,33 @@ public Q_SLOTS: void queueUpdateCheck(bool display_message); private Q_SLOTS: - void getLatestReleaseComplete(QNetworkReply* reply); - - void queueGetChanges(); - void getChangesComplete(QNetworkReply* reply); - + void httpPollTimerPoll(); void downloadUpdateClicked(); void skipThisUpdateClicked(); void remindMeLaterClicked(); private: void reportError(const char* msg, ...); + + bool ensureHttpReady(); + void checkIfUpdateNeeded(); QString getCurrentUpdateTag() const; - bool processUpdate(const QByteArray& update_data, QProgressDialog& progress); + void getLatestReleaseComplete(s32 status_code, std::vector data); + + void queueGetChanges(); + void getChangesComplete(s32 status_code, std::vector data); + + bool processUpdate(const std::vector& data, QProgressDialog& progress); #if defined(_WIN32) bool doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path); #endif Ui::AutoUpdaterDialog m_ui; - QNetworkAccessManager* m_network_access_mgr = nullptr; + std::unique_ptr m_http; + QTimer* m_http_poll_timer = nullptr; QString m_latest_version; QDateTime m_latest_version_timestamp; QString m_download_url;