From af86e5d058f62c25f41a937b6829160d20c19f0c Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 24 Nov 2023 16:33:03 +1000 Subject: [PATCH] Qt: Use HTTPDownloader instead of QtNetwork for updates --- src/duckstation-qt/autoupdaterdialog.cpp | 236 +++++++++++++---------- src/duckstation-qt/autoupdaterdialog.h | 38 ++-- src/duckstation-qt/mainwindow.cpp | 2 +- 3 files changed, 159 insertions(+), 117 deletions(-) diff --git a/src/duckstation-qt/autoupdaterdialog.cpp b/src/duckstation-qt/autoupdaterdialog.cpp index 04bb138a3..367bab29d 100644 --- a/src/duckstation-qt/autoupdaterdialog.cpp +++ b/src/duckstation-qt/autoupdaterdialog.cpp @@ -4,10 +4,13 @@ #include "autoupdaterdialog.h" #include "mainwindow.h" #include "qthost.h" +#include "qtprogresscallback.h" #include "qtutils.h" #include "scmversion/scmversion.h" #include "unzip.h" +#include "util/http_downloader.h" + #include "common/file_system.h" #include "common/log.h" #include "common/minizip_helpers.h" @@ -22,13 +25,13 @@ #include #include #include -#include -#include -#include #include #include #include +// Interval at which HTTP requests are polled. +static constexpr u32 HTTP_POLL_INTERVAL = 10; + #ifdef __APPLE__ #include "common/cocoa_tools.h" #endif @@ -45,8 +48,8 @@ #ifdef AUTO_UPDATER_SUPPORTED static const char* LATEST_TAG_URL = "https://api.github.com/repos/stenzek/duckstation/tags"; -static const char* LATEST_RELEASE_URL = "https://api.github.com/repos/stenzek/duckstation/releases/tags/%s"; -static const char* CHANGES_URL = "https://api.github.com/repos/stenzek/duckstation/compare/%s...%s"; +static const char* LATEST_RELEASE_URL = "https://api.github.com/repos/stenzek/duckstation/releases/tags/{}"; +static const char* CHANGES_URL = "https://api.github.com/repos/stenzek/duckstation/compare/{}...{}"; static const char* UPDATE_ASSET_FILENAME = SCM_RELEASE_ASSET; static const char* UPDATE_TAGS[] = SCM_RELEASE_TAGS; static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG; @@ -55,11 +58,8 @@ static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG; Log_SetChannel(AutoUpdaterDialog); -AutoUpdaterDialog::AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent /* = nullptr */) - : QDialog(parent), m_host_interface(host_interface) +AutoUpdaterDialog::AutoUpdaterDialog(QWidget* parent /* = nullptr */) : QDialog(parent) { - m_network_access_mgr = new QNetworkAccessManager(this); - m_ui.setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -67,6 +67,10 @@ AutoUpdaterDialog::AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent 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) + Log_ErrorPrint("Failed to create HTTP downloader, auto updater will not be available."); } AutoUpdaterDialog::~AutoUpdaterDialog() = default; @@ -129,16 +133,52 @@ 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() +{ + Assert(m_http); + m_http->PollRequests(); + + if (!m_http->HasAnyRequests()) + { + Log_VerbosePrint("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::getLatestTagComplete); + if (!ensureHttpReady()) + { + emit updateCheckCompleted(); + return; + } - QUrl url(QUrl::fromEncoded(QByteArray(LATEST_TAG_URL))); - QNetworkRequest request(url); - m_network_access_mgr->get(request); + m_http->CreateRequest(LATEST_TAG_URL, std::bind(&AutoUpdaterDialog::getLatestTagComplete, this, std::placeholders::_1, + std::placeholders::_3)); #else emit updateCheckCompleted(); #endif @@ -147,32 +187,29 @@ void AutoUpdaterDialog::queueUpdateCheck(bool display_message) void AutoUpdaterDialog::queueGetLatestRelease() { #ifdef AUTO_UPDATER_SUPPORTED - connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getLatestReleaseComplete); + if (!ensureHttpReady()) + { + emit updateCheckCompleted(); + return; + } - SmallString url_string; - url_string.format(LATEST_RELEASE_URL, getCurrentUpdateTag().c_str()); - - QUrl url(QUrl::fromEncoded(QByteArray(url_string))); - QNetworkRequest request(url); - m_network_access_mgr->get(request); + std::string url = fmt::format(fmt::runtime(LATEST_RELEASE_URL), getCurrentUpdateTag()); + m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterDialog::getLatestReleaseComplete, this, + std::placeholders::_1, std::placeholders::_3)); #endif } -void AutoUpdaterDialog::getLatestTagComplete(QNetworkReply* reply) +void AutoUpdaterDialog::getLatestTagComplete(s32 status_code, std::vector response) { #ifdef AUTO_UPDATER_SUPPORTED const std::string selected_tag(getCurrentUpdateTag()); const QString selected_tag_qstr = QString::fromStdString(selected_tag); - // 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(); - - 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)); + const QJsonDocument doc = QJsonDocument::fromJson( + QByteArray(reinterpret_cast(response.data()), response.size()), &parse_error); if (doc.isArray()) { const QJsonArray doc_array(doc.array()); @@ -215,24 +252,21 @@ void AutoUpdaterDialog::getLatestTagComplete(QNetworkReply* reply) else { if (m_display_messages) - reportError("Failed to download latest tag info: %d", static_cast(reply->error())); + reportError("Failed to download latest tag info: HTTP %d", status_code); } emit updateCheckCompleted(); #endif } -void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply) +void AutoUpdaterDialog::getLatestReleaseComplete(s32 status_code, std::vector response) { #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)); + const QJsonDocument doc = QJsonDocument::fromJson( + QByteArray(reinterpret_cast(response.data()), response.size()), &parse_error); if (doc.isObject()) { const QJsonObject doc_object(doc.object()); @@ -253,8 +287,12 @@ void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply) m_ui.newVersion->setText( tr("New Version: %1 (%2)").arg(m_latest_sha).arg(doc_object["published_at"].toString())); m_ui.updateNotes->setText(tr("Loading...")); + m_ui.downloadAndInstall->setEnabled(true); queueGetChanges(); - exec(); + + // We have to defer this, because it comes back through the timer/HTTP callback... + QMetaObject::invokeMethod(this, "exec", Qt::QueuedConnection); + emit updateCheckCompleted(); return; } @@ -272,7 +310,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: HTTP %d", status_code); } #endif } @@ -280,27 +318,23 @@ 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 std::string url_string( - StringUtil::StdStringFromFormat(CHANGES_URL, g_scm_hash_str, getCurrentUpdateTag().c_str())); - QUrl url(QUrl::fromEncoded(QByteArray(url_string.c_str(), static_cast(url_string.size())))); - QNetworkRequest request(url); - m_network_access_mgr->get(request); + std::string url = fmt::format(fmt::runtime(CHANGES_URL), g_scm_hash_str, getCurrentUpdateTag()); + m_http->CreateRequest(std::move(url), 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 response) { #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)); + const QJsonDocument doc = QJsonDocument::fromJson( + QByteArray(reinterpret_cast(response.data()), response.size()), &parse_error); if (doc.isObject()) { const QJsonObject doc_object(doc.object()); @@ -362,67 +396,59 @@ void AutoUpdaterDialog::getChangesComplete(QNetworkReply* reply) } else { - reportError("Failed to download change list: %d", static_cast(reply->error())); + reportError("Failed to download change list: HTTP %d", status_code); } #endif - - m_ui.downloadAndInstall->setEnabled(true); } void AutoUpdaterDialog::downloadUpdateClicked() { - QUrl url(m_download_url); - QNetworkRequest request(url); - QNetworkReply* reply = m_network_access_mgr->get(request); + m_display_messages = true; - 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_sha).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](s32 status_code, const std::string&, std::vector response) { + if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED) + return; - connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, [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 (response.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(response); + }, + &progress); - if (processUpdate(data)) - 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 - g_main_window->requestExit(); + // 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(); } bool AutoUpdaterDialog::updateNeeded() const @@ -456,7 +482,7 @@ void AutoUpdaterDialog::remindMeLaterClicked() #ifdef _WIN32 -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +bool AutoUpdaterDialog::processUpdate(const std::vector& update_data) { const QString update_directory = QCoreApplication::applicationDirPath(); const QString update_zip_path = update_directory + QStringLiteral("\\update.zip"); @@ -472,7 +498,9 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) { 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(update_data.data()), + static_cast(update_data.size())) != static_cast(update_data.size())) { reportError("Writing update zip to '%s' failed", update_zip_path.toUtf8().constData()); return false; @@ -587,7 +615,7 @@ void AutoUpdaterDialog::cleanupAfterUpdate() #elif defined(__APPLE__) -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +bool AutoUpdaterDialog::processUpdate(const std::vector& update_data) { std::optional bundle_path = CocoaTools::GetNonTranslocatedBundlePath(); if (!bundle_path.has_value()) @@ -628,7 +656,9 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) // Save update. { QFile zip_file(QString::fromStdString(zip_path)); - if (!zip_file.open(QIODevice::WriteOnly) || zip_file.write(update_data) != update_data.size()) + if (!zip_file.open(QIODevice::WriteOnly) || + zip_file.write(reinterpret_cast(update_data.data()), static_cast(update_data.size())) != + static_cast(update_data.size())) { reportError("Writing update zip to '%s' failed", zip_path.c_str()); return false; @@ -659,7 +689,7 @@ void AutoUpdaterDialog::cleanupAfterUpdate() #elif defined(__linux__) -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +bool AutoUpdaterDialog::processUpdate(const std::vector& update_data) { const char* appimage_path = std::getenv("APPIMAGE"); if (!appimage_path || !FileSystem::FileExists(appimage_path)) @@ -699,7 +729,9 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) 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() || + if (!new_file.open(QIODevice::WriteOnly) || + new_file.write(reinterpret_cast(update_data.data()), static_cast(update_data.size())) != + static_cast(update_data.size()) || !new_file.setPermissions(old_permissions)) { QFile::remove(new_appimage_path); @@ -759,7 +791,7 @@ void AutoUpdaterDialog::cleanupAfterUpdate() #else -bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +bool AutoUpdaterDialog::processUpdate(const std::vector& update_data) { return false; } diff --git a/src/duckstation-qt/autoupdaterdialog.h b/src/duckstation-qt/autoupdaterdialog.h index 9e856e1b0..084a4175c 100644 --- a/src/duckstation-qt/autoupdaterdialog.h +++ b/src/duckstation-qt/autoupdaterdialog.h @@ -3,14 +3,19 @@ #pragma once +#include "common/types.h" + #include "ui_autoupdaterdialog.h" -#include -#include +#include #include -class QNetworkAccessManager; -class QNetworkReply; +#include +#include +#include +#include + +class HTTPDownloader; class EmuThread; @@ -19,7 +24,7 @@ class AutoUpdaterDialog final : public QDialog Q_OBJECT public: - explicit AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent = nullptr); + explicit AutoUpdaterDialog(QWidget* parent = nullptr); ~AutoUpdaterDialog(); static bool isSupported(); @@ -35,11 +40,7 @@ public Q_SLOTS: void queueGetLatestRelease(); private Q_SLOTS: - void getLatestTagComplete(QNetworkReply* reply); - void getLatestReleaseComplete(QNetworkReply* reply); - - void queueGetChanges(); - void getChangesComplete(QNetworkReply* reply); + void httpPollTimerPoll(); void downloadUpdateClicked(); void skipThisUpdateClicked(); @@ -47,21 +48,30 @@ private Q_SLOTS: private: void reportError(const char* msg, ...); + + bool ensureHttpReady(); + bool updateNeeded() const; std::string getCurrentUpdateTag() const; + void getLatestTagComplete(s32 status_code, std::vector response); + void getLatestReleaseComplete(s32 status_code, std::vector response); + + void queueGetChanges(); + void getChangesComplete(s32 status_code, std::vector response); + #ifdef _WIN32 - bool processUpdate(const QByteArray& update_data); + bool processUpdate(const std::vector& update_data); bool extractUpdater(const QString& zip_path, const QString& destination_path); bool doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path); #else - bool processUpdate(const QByteArray& update_data); + bool processUpdate(const std::vector& update_data); #endif Ui::AutoUpdaterDialog m_ui; - EmuThread* m_host_interface; - QNetworkAccessManager* m_network_access_mgr = nullptr; + std::unique_ptr m_http; + QTimer* m_http_poll_timer = nullptr; QString m_latest_sha; QString m_download_url; int m_download_size = 0; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 92bfee9e3..701b3727e 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2871,7 +2871,7 @@ void MainWindow::checkForUpdates(bool display_message) if (m_auto_updater_dialog) return; - m_auto_updater_dialog = new AutoUpdaterDialog(g_emu_thread, this); + m_auto_updater_dialog = new AutoUpdaterDialog(this); connect(m_auto_updater_dialog, &AutoUpdaterDialog::updateCheckCompleted, this, &MainWindow::onUpdateCheckComplete); m_auto_updater_dialog->queueUpdateCheck(display_message); }