From a4188283943dc4556868de7b5041cd4295925134 Mon Sep 17 00:00:00 2001 From: HJfod <60038575+HJfod@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:17:03 +0200 Subject: [PATCH] fix resources not being downloaded by fallbacking to github api - also add an overload to AsyncWebRequest::expect that gives you the http status code --- loader/include/Geode/utils/web.hpp | 34 ++++++---- loader/src/loader/LoaderImpl.cpp | 99 ++++++++++++++++++++++++------ loader/src/loader/LoaderImpl.hpp | 10 ++- loader/src/utils/web.cpp | 32 ++++++---- 4 files changed, 131 insertions(+), 44 deletions(-) diff --git a/loader/include/Geode/utils/web.hpp b/loader/include/Geode/utils/web.hpp index ffa20b11..6c8c3324 100644 --- a/loader/include/Geode/utils/web.hpp +++ b/loader/include/Geode/utils/web.hpp @@ -56,6 +56,7 @@ namespace geode::utils::web { using AsyncProgress = std::function; using AsyncExpect = std::function; + using AsyncExpectCode = std::function; using AsyncThen = std::function; using AsyncCancelled = std::function; @@ -74,7 +75,7 @@ namespace geode::utils::web { void pause(); void resume(); - void error(std::string const& error); + void error(std::string const& error, int code); void doCancel(); public: @@ -112,7 +113,7 @@ namespace geode::utils::web { std::optional m_joinID; std::string m_url; AsyncThen m_then = nullptr; - AsyncExpect m_expect = nullptr; + AsyncExpectCode m_expect = nullptr; AsyncProgress m_progress = nullptr; AsyncCancelled m_cancelled = nullptr; bool m_sent = false; @@ -157,25 +158,32 @@ namespace geode::utils::web { */ AsyncWebResponse fetch(std::string const& url); /** - * Specify a callback to run if the download fails. Runs in the GD - * thread, so interacting with UI is safe + * Specify a callback to run if the download fails. The callback is + * always ran in the GD thread, so interacting with UI is safe * @param handler Callback to run if the download fails * @returns Same AsyncWebRequest */ AsyncWebRequest& expect(AsyncExpect handler); /** - * Specify a callback to run when the download progresses. Runs in the - * GD thread, so interacting with UI is safe + * Specify a callback to run if the download fails. The callback is + * always ran in the GD thread, so interacting with UI is safe + * @param handler Callback to run if the download fails + * @returns Same AsyncWebRequest + */ + AsyncWebRequest& expect(AsyncExpectCode handler); + /** + * Specify a callback to run when the download progresses. The callback is + * always ran in the GD thread, so interacting with UI is safe * @param handler Callback to run when the download progresses * @returns Same AsyncWebRequest */ AsyncWebRequest& progress(AsyncProgress handler); /** - * Specify a callback to run if the download is cancelled. Runs in the - * GD thread, so interacting with UI is safe. Web requests may be - * cancelled after they are finished (for example, if downloading files - * in bulk and one fails). In that case, handle freeing up the results - * of `then` in this handler + * Specify a callback to run if the download is cancelled. The callback is + * always ran in the GD thread, so interacting with UI is safe. Web + * requests may be cancelled after they are finished (for example, if + * downloading files in bulk and one fails). In that case, handle + * freeing up the results of `then` in this handler * @param handler Callback to run if the download is cancelled * @returns Same AsyncWebRequest */ @@ -294,7 +302,7 @@ namespace geode::utils::web { handle(conv.unwrap()); } else { - req.error("Unable to convert value: " + conv.unwrapErr()); + req.error("Unable to convert value: " + conv.unwrapErr(), -1); } }; return m_request; @@ -309,7 +317,7 @@ namespace geode::utils::web { handle(req, conv.value()); } else { - req.error("Unable to convert value: " + conv.error()); + req.error("Unable to convert value: " + conv.error(), -1); } }; return m_request; diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 8d398630..b3342323 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -466,16 +466,35 @@ bool Loader::Impl::platformConsoleOpen() const { return m_platformConsoleOpen; } -void Loader::Impl::downloadLoaderResources() { - auto version = this->getVersion().toString(); +void Loader::Impl::fetchLatestGithubRelease( + std::function then, + std::function expect +) { + if (m_latestGithubRelease) { + return then(m_latestGithubRelease.value()); + } + web::AsyncWebRequest() + .join("loader-auto-update-check") + .fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest") + .json() + .then([this, then](nlohmann::json const& json) { + m_latestGithubRelease = json; + then(json); + }) + .expect(expect); +} + +void Loader::Impl::tryDownloadLoaderResources( + std::string const& url, + bool tryLatestOnError +) { auto tempResourcesZip = dirs::getTempDir() / "new.zip"; auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID(); web::AsyncWebRequest() - .join("update-geode-loader-resources") - .fetch(fmt::format( - "https://github.com/geode-sdk/geode/releases/download/{}/resources.zip", version - )) + // use the url as a join handle + .join(url) + .fetch(url) .into(tempResourcesZip) .then([tempResourcesZip, resourcesDir](auto) { // unzip resources zip @@ -487,10 +506,17 @@ void Loader::Impl::downloadLoaderResources() { } ResourceDownloadEvent(UpdateFinished()).post(); }) - .expect([](std::string const& info) { - ResourceDownloadEvent( - UpdateFailed("Unable to download resources: " + info) - ).post(); + .expect([this, tryLatestOnError](std::string const& info, int code) { + // if the url was not found, try downloading latest release instead + // (for development versions) + if (code == 404 && tryLatestOnError) { + this->downloadLoaderResources(true); + } + else { + ResourceDownloadEvent( + UpdateFailed("Unable to download resources: " + info) + ).post(); + } }) .progress([](auto&, double now, double total) { ResourceDownloadEvent( @@ -502,6 +528,45 @@ void Loader::Impl::downloadLoaderResources() { }); } +void Loader::Impl::downloadLoaderResources(bool useLatestRelease) { + if (!useLatestRelease) { + this->tryDownloadLoaderResources(fmt::format( + "https://github.com/geode-sdk/geode/releases/download/{}/resources.zip", + this->getVersion().toString() + )); + } + else { + fetchLatestGithubRelease( + [this](nlohmann::json const& raw) { + auto json = raw; + JsonChecker checker(json); + auto root = checker.root("[]").obj(); + + // find release asset + for (auto asset : root.needs("assets").iterate()) { + auto obj = asset.obj(); + if (obj.needs("name").template get() == "resources.zip") { + this->tryDownloadLoaderResources( + obj.needs("browser_download_url").template get(), + false + ); + return; + } + } + + ResourceDownloadEvent( + UpdateFailed("Unable to find resources in latest GitHub release") + ).post(); + }, + [this](std::string const& info) { + ResourceDownloadEvent( + UpdateFailed("Unable to download resources: " + info) + ).post(); + } + ); + } +} + bool Loader::Impl::verifyLoaderResources() { static std::optional CACHED = std::nullopt; if (CACHED.has_value()) { @@ -587,11 +652,8 @@ void Loader::Impl::downloadLoaderUpdate(std::string const& url) { void Loader::Impl::checkForLoaderUpdates() { // Check for updates in the background - web::AsyncWebRequest() - .join("loader-auto-update-check") - .fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest") - .json() - .then([this](nlohmann::json const& raw) { + fetchLatestGithubRelease( + [this](nlohmann::json const& raw) { auto json = raw; JsonChecker checker(json); auto root = checker.root("[]").obj(); @@ -626,12 +688,13 @@ void Loader::Impl::checkForLoaderUpdates() { LoaderUpdateEvent( UpdateFailed("Unable to find release asset for " GEODE_PLATFORM_NAME) ).post(); - }) - .expect([](std::string const& info) { + }, + [](std::string const& info) { LoaderUpdateEvent( UpdateFailed("Unable to check for updates: " + info) ).post(); - }); + } + ); } bool Loader::Impl::isNewUpdateDownloaded() const { diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 34720918..bf52a2a9 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -59,6 +59,9 @@ namespace geode { std::vector m_texturePaths; bool m_isSetup = false; + // cache for the json of the latest github release to avoid hitting + // the github api too much + std::optional m_latestGithubRelease; bool m_isNewUpdateDownloaded = false; std::condition_variable m_earlyLoadFinishedCV; @@ -87,8 +90,13 @@ namespace geode { Result getHandler(void* address); Result<> removeHandler(void* address); - void downloadLoaderResources(); + void tryDownloadLoaderResources(std::string const& url, bool tryLatestOnError = true); + void downloadLoaderResources(bool useLatestRelease = false); void downloadLoaderUpdate(std::string const& url); + void fetchLatestGithubRelease( + std::function then, + std::function expect + ); bool loadHooks(); void setupIPC(); diff --git a/loader/src/utils/web.cpp b/loader/src/utils/web.cpp index 26c6b1f6..b379cc81 100644 --- a/loader/src/utils/web.cpp +++ b/loader/src/utils/web.cpp @@ -140,7 +140,6 @@ Result web::fetch(std::string const& url) { class SentAsyncWebRequest::Impl { private: enum class Status { - Paused, Running, Finished, @@ -150,7 +149,7 @@ private: std::string m_id; std::string m_url; std::vector m_thens; - std::vector m_expects; + std::vector m_expects; std::vector m_progresses; std::vector m_cancelleds; Status m_status = Status::Paused; @@ -173,7 +172,7 @@ private: void pause(); void resume(); - void error(std::string const& error); + void error(std::string const& error, int code); void doCancel(); public: @@ -212,7 +211,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const auto curl = curl_easy_init(); if (!curl) { - return this->error("Curl not initialized"); + return this->error("Curl not initialized", -1); } // resulting byte array @@ -290,8 +289,10 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &data); auto res = curl_easy_perform(curl); if (res != CURLE_OK) { + long code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); curl_easy_cleanup(curl); - return this->error("Fetch failed: " + std::string(curl_easy_strerror(res))); + return this->error("Fetch failed: " + std::string(curl_easy_strerror(res)), code); } curl_easy_cleanup(curl); @@ -335,7 +336,7 @@ void SentAsyncWebRequest::Impl::doCancel() { } }); - this->error("Request cancelled"); + this->error("Request cancelled", -1); } void SentAsyncWebRequest::Impl::cancel() { @@ -360,16 +361,16 @@ bool SentAsyncWebRequest::Impl::finished() const { return m_finished; } -void SentAsyncWebRequest::Impl::error(std::string const& error) { +void SentAsyncWebRequest::Impl::error(std::string const& error, int code) { auto lock = std::unique_lock(m_statusMutex); m_statusCV.wait(lock, [this]() { return !m_paused; }); - Loader::get()->queueInGDThread([this, error]() { + Loader::get()->queueInGDThread([this, error, code]() { { std::lock_guard _(m_mutex); for (auto& expect : m_expects) { - expect(error); + expect(error, code); } } std::lock_guard _(RUNNING_REQUESTS_MUTEX); @@ -405,8 +406,8 @@ bool SentAsyncWebRequest::finished() const { return m_impl->finished(); } -void SentAsyncWebRequest::error(std::string const& error) { - return m_impl->error(error); +void SentAsyncWebRequest::error(std::string const& error, int code) { + return m_impl->error(error, code); } AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) { @@ -424,7 +425,14 @@ AsyncWebResponse AsyncWebRequest::fetch(std::string const& url) { return AsyncWebResponse(*this); } -AsyncWebRequest& AsyncWebRequest::expect(std::function handler) { +AsyncWebRequest& AsyncWebRequest::expect(AsyncExpect handler) { + m_expect = [handler](std::string const& info, auto) { + return handler(info); + }; + return *this; +} + +AsyncWebRequest& AsyncWebRequest::expect(AsyncExpectCode handler) { m_expect = handler; return *this; }