fix resources not being downloaded by fallbacking to github api

- also add an overload to AsyncWebRequest::expect that gives you the http status code
This commit is contained in:
HJfod 2023-01-24 18:17:03 +02:00
parent 8551071ac9
commit a418828394
4 changed files with 131 additions and 44 deletions

View file

@ -56,6 +56,7 @@ namespace geode::utils::web {
using AsyncProgress = std::function<void(SentAsyncWebRequest&, double, double)>; using AsyncProgress = std::function<void(SentAsyncWebRequest&, double, double)>;
using AsyncExpect = std::function<void(std::string const&)>; using AsyncExpect = std::function<void(std::string const&)>;
using AsyncExpectCode = std::function<void(std::string const&, int)>;
using AsyncThen = std::function<void(SentAsyncWebRequest&, ByteVector const&)>; using AsyncThen = std::function<void(SentAsyncWebRequest&, ByteVector const&)>;
using AsyncCancelled = std::function<void(SentAsyncWebRequest&)>; using AsyncCancelled = std::function<void(SentAsyncWebRequest&)>;
@ -74,7 +75,7 @@ namespace geode::utils::web {
void pause(); void pause();
void resume(); void resume();
void error(std::string const& error); void error(std::string const& error, int code);
void doCancel(); void doCancel();
public: public:
@ -112,7 +113,7 @@ namespace geode::utils::web {
std::optional<std::string> m_joinID; std::optional<std::string> m_joinID;
std::string m_url; std::string m_url;
AsyncThen m_then = nullptr; AsyncThen m_then = nullptr;
AsyncExpect m_expect = nullptr; AsyncExpectCode m_expect = nullptr;
AsyncProgress m_progress = nullptr; AsyncProgress m_progress = nullptr;
AsyncCancelled m_cancelled = nullptr; AsyncCancelled m_cancelled = nullptr;
bool m_sent = false; bool m_sent = false;
@ -157,25 +158,32 @@ namespace geode::utils::web {
*/ */
AsyncWebResponse fetch(std::string const& url); AsyncWebResponse fetch(std::string const& url);
/** /**
* Specify a callback to run if the download fails. Runs in the GD * Specify a callback to run if the download fails. The callback is
* thread, so interacting with UI is safe * always ran in the GD thread, so interacting with UI is safe
* @param handler Callback to run if the download fails * @param handler Callback to run if the download fails
* @returns Same AsyncWebRequest * @returns Same AsyncWebRequest
*/ */
AsyncWebRequest& expect(AsyncExpect handler); AsyncWebRequest& expect(AsyncExpect handler);
/** /**
* Specify a callback to run when the download progresses. Runs in the * Specify a callback to run if the download fails. The callback is
* GD thread, so interacting with UI is safe * 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 * @param handler Callback to run when the download progresses
* @returns Same AsyncWebRequest * @returns Same AsyncWebRequest
*/ */
AsyncWebRequest& progress(AsyncProgress handler); AsyncWebRequest& progress(AsyncProgress handler);
/** /**
* Specify a callback to run if the download is cancelled. Runs in the * Specify a callback to run if the download is cancelled. The callback is
* GD thread, so interacting with UI is safe. Web requests may be * always ran in the GD thread, so interacting with UI is safe. Web
* cancelled after they are finished (for example, if downloading files * requests may be cancelled after they are finished (for example, if
* in bulk and one fails). In that case, handle freeing up the results * downloading files in bulk and one fails). In that case, handle
* of `then` in this handler * freeing up the results of `then` in this handler
* @param handler Callback to run if the download is cancelled * @param handler Callback to run if the download is cancelled
* @returns Same AsyncWebRequest * @returns Same AsyncWebRequest
*/ */
@ -294,7 +302,7 @@ namespace geode::utils::web {
handle(conv.unwrap()); handle(conv.unwrap());
} }
else { else {
req.error("Unable to convert value: " + conv.unwrapErr()); req.error("Unable to convert value: " + conv.unwrapErr(), -1);
} }
}; };
return m_request; return m_request;
@ -309,7 +317,7 @@ namespace geode::utils::web {
handle(req, conv.value()); handle(req, conv.value());
} }
else { else {
req.error("Unable to convert value: " + conv.error()); req.error("Unable to convert value: " + conv.error(), -1);
} }
}; };
return m_request; return m_request;

View file

@ -466,16 +466,35 @@ bool Loader::Impl::platformConsoleOpen() const {
return m_platformConsoleOpen; return m_platformConsoleOpen;
} }
void Loader::Impl::downloadLoaderResources() { void Loader::Impl::fetchLatestGithubRelease(
auto version = this->getVersion().toString(); std::function<void(nlohmann::json const&)> then,
std::function<void(std::string const&)> 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 tempResourcesZip = dirs::getTempDir() / "new.zip";
auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID(); auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID();
web::AsyncWebRequest() web::AsyncWebRequest()
.join("update-geode-loader-resources") // use the url as a join handle
.fetch(fmt::format( .join(url)
"https://github.com/geode-sdk/geode/releases/download/{}/resources.zip", version .fetch(url)
))
.into(tempResourcesZip) .into(tempResourcesZip)
.then([tempResourcesZip, resourcesDir](auto) { .then([tempResourcesZip, resourcesDir](auto) {
// unzip resources zip // unzip resources zip
@ -487,10 +506,17 @@ void Loader::Impl::downloadLoaderResources() {
} }
ResourceDownloadEvent(UpdateFinished()).post(); ResourceDownloadEvent(UpdateFinished()).post();
}) })
.expect([](std::string const& info) { .expect([this, tryLatestOnError](std::string const& info, int code) {
ResourceDownloadEvent( // if the url was not found, try downloading latest release instead
UpdateFailed("Unable to download resources: " + info) // (for development versions)
).post(); if (code == 404 && tryLatestOnError) {
this->downloadLoaderResources(true);
}
else {
ResourceDownloadEvent(
UpdateFailed("Unable to download resources: " + info)
).post();
}
}) })
.progress([](auto&, double now, double total) { .progress([](auto&, double now, double total) {
ResourceDownloadEvent( 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<std::string>() == "resources.zip") {
this->tryDownloadLoaderResources(
obj.needs("browser_download_url").template get<std::string>(),
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() { bool Loader::Impl::verifyLoaderResources() {
static std::optional<bool> CACHED = std::nullopt; static std::optional<bool> CACHED = std::nullopt;
if (CACHED.has_value()) { if (CACHED.has_value()) {
@ -587,11 +652,8 @@ void Loader::Impl::downloadLoaderUpdate(std::string const& url) {
void Loader::Impl::checkForLoaderUpdates() { void Loader::Impl::checkForLoaderUpdates() {
// Check for updates in the background // Check for updates in the background
web::AsyncWebRequest() fetchLatestGithubRelease(
.join("loader-auto-update-check") [this](nlohmann::json const& raw) {
.fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest")
.json()
.then([this](nlohmann::json const& raw) {
auto json = raw; auto json = raw;
JsonChecker checker(json); JsonChecker checker(json);
auto root = checker.root("[]").obj(); auto root = checker.root("[]").obj();
@ -626,12 +688,13 @@ void Loader::Impl::checkForLoaderUpdates() {
LoaderUpdateEvent( LoaderUpdateEvent(
UpdateFailed("Unable to find release asset for " GEODE_PLATFORM_NAME) UpdateFailed("Unable to find release asset for " GEODE_PLATFORM_NAME)
).post(); ).post();
}) },
.expect([](std::string const& info) { [](std::string const& info) {
LoaderUpdateEvent( LoaderUpdateEvent(
UpdateFailed("Unable to check for updates: " + info) UpdateFailed("Unable to check for updates: " + info)
).post(); ).post();
}); }
);
} }
bool Loader::Impl::isNewUpdateDownloaded() const { bool Loader::Impl::isNewUpdateDownloaded() const {

View file

@ -59,6 +59,9 @@ namespace geode {
std::vector<ghc::filesystem::path> m_texturePaths; std::vector<ghc::filesystem::path> m_texturePaths;
bool m_isSetup = false; bool m_isSetup = false;
// cache for the json of the latest github release to avoid hitting
// the github api too much
std::optional<nlohmann::json> m_latestGithubRelease;
bool m_isNewUpdateDownloaded = false; bool m_isNewUpdateDownloaded = false;
std::condition_variable m_earlyLoadFinishedCV; std::condition_variable m_earlyLoadFinishedCV;
@ -87,8 +90,13 @@ namespace geode {
Result<tulip::hook::HandlerHandle> getHandler(void* address); Result<tulip::hook::HandlerHandle> getHandler(void* address);
Result<> removeHandler(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 downloadLoaderUpdate(std::string const& url);
void fetchLatestGithubRelease(
std::function<void(nlohmann::json const&)> then,
std::function<void(std::string const&)> expect
);
bool loadHooks(); bool loadHooks();
void setupIPC(); void setupIPC();

View file

@ -140,7 +140,6 @@ Result<std::string> web::fetch(std::string const& url) {
class SentAsyncWebRequest::Impl { class SentAsyncWebRequest::Impl {
private: private:
enum class Status { enum class Status {
Paused, Paused,
Running, Running,
Finished, Finished,
@ -150,7 +149,7 @@ private:
std::string m_id; std::string m_id;
std::string m_url; std::string m_url;
std::vector<AsyncThen> m_thens; std::vector<AsyncThen> m_thens;
std::vector<AsyncExpect> m_expects; std::vector<AsyncExpectCode> m_expects;
std::vector<AsyncProgress> m_progresses; std::vector<AsyncProgress> m_progresses;
std::vector<AsyncCancelled> m_cancelleds; std::vector<AsyncCancelled> m_cancelleds;
Status m_status = Status::Paused; Status m_status = Status::Paused;
@ -173,7 +172,7 @@ private:
void pause(); void pause();
void resume(); void resume();
void error(std::string const& error); void error(std::string const& error, int code);
void doCancel(); void doCancel();
public: public:
@ -212,7 +211,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
auto curl = curl_easy_init(); auto curl = curl_easy_init();
if (!curl) { if (!curl) {
return this->error("Curl not initialized"); return this->error("Curl not initialized", -1);
} }
// resulting byte array // resulting byte array
@ -290,8 +289,10 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &data); curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &data);
auto res = curl_easy_perform(curl); auto res = curl_easy_perform(curl);
if (res != CURLE_OK) { if (res != CURLE_OK) {
long code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
curl_easy_cleanup(curl); 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); 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() { void SentAsyncWebRequest::Impl::cancel() {
@ -360,16 +361,16 @@ bool SentAsyncWebRequest::Impl::finished() const {
return m_finished; 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); auto lock = std::unique_lock(m_statusMutex);
m_statusCV.wait(lock, [this]() { m_statusCV.wait(lock, [this]() {
return !m_paused; return !m_paused;
}); });
Loader::get()->queueInGDThread([this, error]() { Loader::get()->queueInGDThread([this, error, code]() {
{ {
std::lock_guard _(m_mutex); std::lock_guard _(m_mutex);
for (auto& expect : m_expects) { for (auto& expect : m_expects) {
expect(error); expect(error, code);
} }
} }
std::lock_guard _(RUNNING_REQUESTS_MUTEX); std::lock_guard _(RUNNING_REQUESTS_MUTEX);
@ -405,8 +406,8 @@ bool SentAsyncWebRequest::finished() const {
return m_impl->finished(); return m_impl->finished();
} }
void SentAsyncWebRequest::error(std::string const& error) { void SentAsyncWebRequest::error(std::string const& error, int code) {
return m_impl->error(error); return m_impl->error(error, code);
} }
AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) { AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) {
@ -424,7 +425,14 @@ AsyncWebResponse AsyncWebRequest::fetch(std::string const& url) {
return AsyncWebResponse(*this); return AsyncWebResponse(*this);
} }
AsyncWebRequest& AsyncWebRequest::expect(std::function<void(std::string const&)> 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; m_expect = handler;
return *this; return *this;
} }