rework Index + add AsyncWebRequest API + add GEODE_DEBUG macro

This commit is contained in:
HJfod 2022-10-13 00:22:43 +03:00
parent 0646ea95f8
commit 2b06316397
16 changed files with 886 additions and 650 deletions

View file

@ -19,6 +19,10 @@ set_target_properties(${PROJECT_NAME} PROPERTIES CMAKE_CONFIGURE_DEPENDS VERSION
target_compile_definitions(${PROJECT_NAME} INTERFACE -DPROJECT_NAME=${CMAKE_PROJECT_NAME})
if (GEODE_DEBUG)
target_compile_definitions(${PROJECT_NAME} INTERFACE GEODE_DEBUG)
endif()
set(GEODE_CODEGEN_PATH ${CMAKE_CURRENT_BINARY_DIR}/codegenned)
set(GEODE_BIN_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
set(GEODE_LOADER_PATH ${CMAKE_CURRENT_SOURCE_DIR}/loader)

View file

@ -1 +1 @@
0.4.5
0.4.5

View file

@ -141,7 +141,7 @@ namespace geode {
template <typename ...Args>
void debug(Args... args) {
#ifndef NDEBUG
#ifdef GEODE_DEBUG
schedule(Severity::Debug, args...);
#endif
}

View file

@ -101,6 +101,16 @@ namespace geode {
std::function<void(FLAlertLayer*, bool)> selected,
bool doShow = true
);
GEODE_DLL FLAlertLayer* createQuickPopup(
const char* title,
std::string const& content,
const char* btn1,
const char* btn2,
float width,
std::function<void(FLAlertLayer*, bool)> selected,
bool doShow = true
);
}

View file

@ -157,6 +157,11 @@ namespace geode {
return *this;
}
JsonMaybeValue<Json> array() {
this->as<value_t::array>();
return *this;
}
template<nlohmann::detail::value_t... T>
JsonMaybeValue<Json> asOneOf() {
if (this->isError()) return *this;
@ -273,6 +278,24 @@ namespace geode {
}
};
JsonMaybeValue<Json> at(size_t i) {
this->as<value_t::array>();
if (this->isError()) return *this;
if (self().m_json.size() <= i) {
this->setError(
self().m_hierarchy + ": has " +
std::to_string(self().m_json.size()) + "items "
", expected to have at least " + std::to_string(i + 1)
);
return *this;
}
return JsonMaybeValue<Json>(
self().m_checker, self().m_json.at(i),
self().m_hierarchy + "." + std::to_string(i),
self().m_hasValue
);
}
Iterator<JsonMaybeValue<Json>> iterate() {
this->as<value_t::array>();
Iterator<JsonMaybeValue<Json>> iter;

View file

@ -4,10 +4,18 @@
#include <fs/filesystem.hpp>
#include "Result.hpp"
#include "json.hpp"
#include <mutex>
namespace geode::utils::web {
using FileProgressCallback = std::function<bool(double, double)>;
/**
* Synchronously fetch data from the internet
* @param url URL to fetch
* @returns Returned data as bytes, or error on error
*/
GEODE_DLL Result<byte_array> fetchBytes(std::string const& url);
/**
* Synchronously fetch data from the internet
* @param url URL to fetch
@ -46,5 +54,154 @@ namespace geode::utils::web {
return Err(e.what());
}
}
class SentAsyncWebRequest;
template<class T>
class AsyncWebResult;
class AsyncWebResponse;
class AsyncWebRequest;
using AsyncProgress = std::function<void(SentAsyncWebRequest&, double, double)>;
using AsyncExpect = std::function<void(std::string const&)>;
using AsyncThen = std::function<void(SentAsyncWebRequest&, byte_array const&)>;
using AsyncCancelled = std::function<void(SentAsyncWebRequest&)>;
class SentAsyncWebRequest {
private:
std::string m_id;
std::string m_url;
std::vector<AsyncThen> m_thens;
std::vector<AsyncExpect> m_expects;
std::vector<AsyncProgress> m_progresses;
std::vector<AsyncCancelled> m_cancelleds;
std::atomic<bool> m_paused = true;
std::atomic<bool> m_cancelled = false;
std::atomic<bool> m_finished = false;
std::atomic<bool> m_cleanedUp = false;
mutable std::mutex m_mutex;
std::variant<
std::monostate,
std::ostream*,
ghc::filesystem::path
> m_target = std::monostate();
template<class T>
friend class AsyncWebResult;
friend class AsyncWebRequest;
void pause();
void resume();
void error(std::string const& error);
void doCancel();
public:
SentAsyncWebRequest(AsyncWebRequest const&, std::string const& id);
void cancel();
bool finished() const;
};
using SentAsyncWebRequestHandle = std::shared_ptr<SentAsyncWebRequest>;
template<class T>
using DataConverter = Result<T>(*)(byte_array const&);
template<class T>
class AsyncWebResult {
private:
AsyncWebRequest& m_request;
DataConverter<T> m_converter;
AsyncWebResult(AsyncWebRequest& request, DataConverter<T> converter)
: m_request(request), m_converter(converter) {}
friend class AsyncWebResponse;
public:
AsyncWebRequest& then(std::function<void(T)> handle) {
m_request.m_then = [
converter = m_converter,
handle
](SentAsyncWebRequest& req, byte_array const& arr) {
auto conv = converter(arr);
if (conv) {
handle(conv.value());
} else {
req.error("Unable to convert value: " + conv.error());
}
};
return m_request;
}
AsyncWebRequest& then(std::function<void(SentAsyncWebRequest&, T)> handle) {
m_request.m_then = [
converter = m_converter,
handle
](SentAsyncWebRequest& req, byte_array const& arr) {
auto conv = converter(arr);
if (conv) {
handle(req, conv.value());
} else {
req.error("Unable to convert value: " + conv.error());
}
};
return m_request;
}
};
class GEODE_DLL AsyncWebResponse {
private:
AsyncWebRequest& m_request;
inline AsyncWebResponse(AsyncWebRequest& request) : m_request(request) {}
friend class AsyncWebRequest;
public:
// Make sure the stream lives for the entire duration of the request.
AsyncWebResult<std::monostate> into(std::ostream& stream);
AsyncWebResult<std::monostate> into(ghc::filesystem::path const& path);
AsyncWebResult<std::string> text();
AsyncWebResult<byte_array> bytes();
AsyncWebResult<nlohmann::json> json();
template<class T>
AsyncWebResult<T> as(DataConverter<T> converter) {
return AsyncWebResult(m_request, converter);
}
};
class GEODE_DLL AsyncWebRequest {
private:
std::optional<std::string> m_joinID;
std::string m_url;
AsyncThen m_then = nullptr;
AsyncExpect m_expect = nullptr;
AsyncProgress m_progress = nullptr;
AsyncCancelled m_cancelled = nullptr;
bool m_sent = false;
std::variant<
std::monostate,
std::ostream*,
ghc::filesystem::path
> m_target;
template<class T>
friend class AsyncWebResult;
friend class SentAsyncWebRequest;
friend class AsyncWebResponse;
public:
AsyncWebRequest& join(std::string const& requestID);
AsyncWebResponse fetch(std::string const& url);
AsyncWebRequest& expect(AsyncExpect handler);
AsyncWebRequest& progress(AsyncProgress progressFunc);
// 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` here
AsyncWebRequest& cancelled(AsyncCancelled cancelledFunc);
SentAsyncWebRequestHandle send();
~AsyncWebRequest();
};
}

View file

@ -176,7 +176,7 @@ namespace geode::utils::vector {
* @returns Reference to vector.
*/
template<class T>
std::vector<T>& erase(std::vector<T>& vec, T& element) {
std::vector<T>& erase(std::vector<T>& vec, T const& element) {
vec.erase(std::remove(vec.begin(), vec.end(), element), vec.end());
return vec;
}

View file

@ -31,42 +31,6 @@ static void addUpdateIcon(const char* icon = "updates-available.png"_spr) {
}
}
static void updateModsProgress(
UpdateStatus status,
std::string const& info,
uint8_t progress
) {
if (status == UpdateStatus::Failed) {
g_indexUpdateNotif->hide();
g_indexUpdateNotif = nullptr;
NotificationBuilder()
.title("Some Updates Failed")
.text("Some mods failed to update, click for details")
.icon("info-alert.png"_spr)
.clicked([info](auto) -> void {
FLAlertLayer::create("Info", info, "OK")->show();
})
.show();
addUpdateIcon("updates-failed.png"_spr);
}
if (status == UpdateStatus::Finished) {
g_indexUpdateNotif->hide();
g_indexUpdateNotif = nullptr;
NotificationBuilder()
.title("Updates Installed")
.text(
"Mods have been updated, please "
"restart to apply changes"
)
.icon("updates-available.png"_spr)
.clicked([info](auto) -> void {
FLAlertLayer::create("Info", info, "OK")->show();
})
.show();
}
}
static void updateIndexProgress(
UpdateStatus status,
std::string const& info,
@ -87,44 +51,14 @@ static void updateIndexProgress(
g_indexUpdateNotif->hide();
g_indexUpdateNotif = nullptr;
if (Index::get()->areUpdatesAvailable()) {
if (Mod::get()->getSettingValue<bool>("auto-update-mods")) {
auto ticket = Index::get()->installUpdates(updateModsProgress);
if (!ticket) {
NotificationBuilder()
.title("Unable to auto-update")
.text("Unable to update mods :(")
.icon("updates-failed.png"_spr)
.show();
} else {
g_indexUpdateNotif = NotificationBuilder()
.title("Installing updates")
.text("Installing updates...")
.clicked([ticket](auto) -> void {
createQuickPopup(
"Cancel Updates",
"Do you want to <cr>cancel</c> updates?",
"Don't Cancel", "Cancel Updates",
[ticket](auto, bool btn2) -> void {
if (g_indexUpdateNotif && btn2) {
ticket.value()->cancel();
}
}
);
}, false)
.loading()
.stay()
.show();
}
} else {
NotificationBuilder()
.title("Updates available")
.text("Some mods have updates available!")
.icon("updates-available.png"_spr)
.clicked([](auto) -> void {
ModListLayer::scene();
})
.show();
}
NotificationBuilder()
.title("Updates available")
.text("Some mods have updates available!")
.icon("updates-available.png"_spr)
.clicked([](auto) -> void {
ModListLayer::scene();
})
.show();
addUpdateIcon();
}
}

View file

@ -3,6 +3,7 @@
#include <Geode/utils/json.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/fetch.hpp>
#include <hash.hpp>
#define GITHUB_DONT_RATE_LIMIT_ME_PLS 0
@ -59,159 +60,17 @@ bool Index::isFeaturedItem(std::string const& item) const {
return m_featured.count(item);
}
void Index::updateIndexThread(bool force) {
auto indexDir = Loader::get()->getGeodeDirectory() / "index";
// download index
#if GITHUB_DONT_RATE_LIMIT_ME_PLS == 0
indexUpdateProgress(
UpdateStatus::Progress, "Fetching index metadata", 0
);
// get all commits in index repo
auto commit = web::fetchJSON(
"https://api.github.com/repos/geode-sdk/mods/commits"
);
if (!commit) {
return indexUpdateProgress(UpdateStatus::Failed, commit.error());
}
auto json = commit.value();
if (
json.is_object() &&
json.contains("documentation_url") &&
json.contains("message")
) {
// whoops! got rate limited
return indexUpdateProgress(
UpdateStatus::Failed,
json["message"].get<std::string>()
);
}
indexUpdateProgress(
UpdateStatus::Progress, "Checking index status", 25
);
// read sha of latest commit
if (!json.is_array()) {
return indexUpdateProgress(
UpdateStatus::Failed,
"Fetched commits, expected 'array', got '" +
std::string(json.type_name()) + "'. "
"Report this bug to the Geode developers!"
);
}
if (!json.at(0).is_object()) {
return indexUpdateProgress(
UpdateStatus::Failed,
"Fetched commits, expected 'array.object', got 'array." +
std::string(json.type_name()) + "'. "
"Report this bug to the Geode developers!"
);
}
if (!json.at(0).contains("sha")) {
return indexUpdateProgress(
UpdateStatus::Failed,
"Fetched commits, missing '0.sha'. "
"Report this bug to the Geode developers!"
);
}
auto upcomingCommitSHA = json.at(0)["sha"];
// read sha of currently installed commit
std::string currentCommitSHA = "";
if (ghc::filesystem::exists(indexDir / "current")) {
auto data = utils::file::readString(indexDir / "current");
if (data) {
currentCommitSHA = data.value();
}
}
// update if forced or latest commit has
// different sha
if (force || currentCommitSHA != upcomingCommitSHA) {
// save new sha in file
utils::file::writeString(indexDir / "current", upcomingCommitSHA);
// download latest commit (by downloading
// the repo as a zip)
indexUpdateProgress(
UpdateStatus::Progress,
"Downloading index",
50
);
auto gotZip = web::fetchFile(
"https://github.com/geode-sdk/mods/zipball/main",
indexDir / "index.zip"
);
if (!gotZip) {
return indexUpdateProgress(
UpdateStatus::Failed,
gotZip.error()
);
}
// delete old index
if (ghc::filesystem::exists(indexDir / "index")) {
ghc::filesystem::remove_all(indexDir / "index");
}
auto unzip = file::unzipTo(indexDir / "index.zip", indexDir);
if (!unzip) {
return indexUpdateProgress(
UpdateStatus::Failed, unzip.error()
);
}
}
#endif
// update index
indexUpdateProgress(
UpdateStatus::Progress,
"Updating index",
75
);
this->updateIndexFromLocalCache();
m_upToDate = true;
m_updating = false;
indexUpdateProgress(
UpdateStatus::Finished,
"",
100
);
}
void Index::indexUpdateProgress(
UpdateStatus status,
std::string const& info,
uint8_t percentage
) {
Loader::get()->queueInGDThread([this, status, info, percentage]() -> void {
m_callbacksMutex.lock();
for (auto& d : m_callbacks) {
d(status, info, percentage);
}
if (
status == UpdateStatus::Finished ||
status == UpdateStatus::Failed
) {
m_callbacks.clear();
}
m_callbacksMutex.unlock();
});
}
void Index::updateIndex(IndexUpdateCallback callback, bool force) {
#define RETURN_ERROR(str) \
std::string err__ = (str); \
if (callback) callback( \
UpdateStatus::Failed, \
err__, \
0 \
); \
log::info("Index update failed: {}", err__);\
return;
// if already updated and no force, let
// delegate know
if (!force && m_upToDate) {
@ -225,36 +84,134 @@ void Index::updateIndex(IndexUpdateCallback callback, bool force) {
return;
}
// add delegate thread-safely if it's not
// added already
if (callback) {
m_callbacksMutex.lock();
m_callbacks.push_back(callback);
m_callbacksMutex.unlock();
}
// if already updating, let delegate know
// and return
if (m_updating) {
if (callback) {
callback(
UpdateStatus::Progress,
"Waiting for update info",
0
);
}
return;
}
m_updating = true;
// create directory for the local clone of
// the index
auto indexDir = Loader::get()->getGeodeDirectory() / "index";
ghc::filesystem::create_directories(indexDir);
// update index in another thread to avoid
// pausing UI
std::thread(&Index::updateIndexThread, this, force).detach();
#if GITHUB_DONT_RATE_LIMIT_ME_PLS == 1
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err);
}
m_upToDate = true;
m_updating = false;
if (callback) callback(UpdateStatus::Finished, "", 100);
return;
#endif
web::AsyncWebRequest()
.join("index-update")
.fetch("https://api.github.com/repos/geode-sdk/mods/commits")
.json()
.then([this, force, callback](nlohmann::json const& json) {
auto indexDir = Loader::get()->getGeodeDirectory() / "index";
// check if rate-limited (returns object)
JsonChecker checkerObj(json);
auto obj = checkerObj.root("[geode-sdk/mods/commits]").obj();
if (obj.has("documentation_url") && obj.has("message")) {
RETURN_ERROR(obj.has("message").get<std::string>());
}
// get sha of latest commit
JsonChecker checker(json);
auto root = checker.root("[geode-sdk/mods/commits]").array();
std::string upcomingCommitSHA;
if (auto first = root.at(0).obj().needs("sha")) {
upcomingCommitSHA = first.get<std::string>();
} else {
RETURN_ERROR("Unable to get hash from latest commit: " + checker.getError());
}
// read sha of currently installed commit
std::string currentCommitSHA = "";
if (ghc::filesystem::exists(indexDir / "current")) {
auto data = utils::file::readString(indexDir / "current");
if (data) {
currentCommitSHA = data.value();
}
}
// update if forced or latest commit has
// different sha
if (force || currentCommitSHA != upcomingCommitSHA) {
// save new sha in file
utils::file::writeString(indexDir / "current", upcomingCommitSHA);
web::AsyncWebRequest()
.join("index-download")
.fetch("https://github.com/geode-sdk/mods/zipball/main")
.into(indexDir / "index.zip")
.then([this, indexDir, callback](auto) {
// delete old index
try {
if (ghc::filesystem::exists(indexDir / "index")) {
ghc::filesystem::remove_all(indexDir / "index");
}
} catch(std::exception& e) {
RETURN_ERROR("Unable to delete old index " + std::string(e.what()));
}
// unzip new index
auto unzip = file::unzipTo(indexDir / "index.zip", indexDir);
if (!unzip) {
RETURN_ERROR(unzip.error());
}
// update index
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err.error());
}
m_upToDate = true;
m_updating = false;
if (callback) callback(
UpdateStatus::Finished, "", 100
);
})
.expect([callback](std::string const& err) {
RETURN_ERROR(err);
})
.progress([callback](web::SentAsyncWebRequest& req, double now, double total) {
if (callback) callback(
UpdateStatus::Progress,
"Downloading",
static_cast<int>(now / total * 100.0)
);
});
} else {
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err.error());
}
m_upToDate = true;
m_updating = false;
if (callback) callback(
UpdateStatus::Finished,
"", 100
);
}
})
.expect([callback](std::string const& err) {
RETURN_ERROR(err);
})
.progress([callback](web::SentAsyncWebRequest& req, double now, double total) {
if (callback) callback(
UpdateStatus::Progress,
"Downloading",
static_cast<int>(now / total * 100.0)
);
});
}
void Index::addIndexItemFromFolder(ghc::filesystem::path const& dir) {
@ -338,7 +295,7 @@ void Index::addIndexItemFromFolder(ghc::filesystem::path const& dir) {
}
}
void Index::updateIndexFromLocalCache() {
Result<> Index::updateIndexFromLocalCache() {
m_items.clear();
auto baseIndexDir = Loader::get()->getGeodeDirectory() / "index";
@ -352,10 +309,19 @@ void Index::updateIndexFromLocalCache() {
// load index mods
auto modsDir = baseIndexDir / "index";
for (auto const& dir : ghc::filesystem::directory_iterator(modsDir)) {
if (ghc::filesystem::is_directory(dir)) {
this->addIndexItemFromFolder(dir);
if (ghc::filesystem::exists(modsDir)) {
for (auto const& dir : ghc::filesystem::directory_iterator(modsDir)) {
if (ghc::filesystem::is_directory(dir)) {
this->addIndexItemFromFolder(dir);
}
}
log::info("Index updated");
return Ok();
} else {
return Err(
"Index appears not to have been "
"downloaded, or is fully empty"
);
}
}
@ -446,9 +412,8 @@ Result<std::vector<std::string>> Index::checkDependenciesForItem(
}
}
Result<InstallTicket*> Index::installItems(
std::vector<IndexItem> const& items,
ItemInstallCallback progress
Result<InstallItems> Index::installItems(
std::vector<IndexItem> const& items
) {
std::vector<std::string> ids {};
for (auto& item : items) {
@ -482,14 +447,13 @@ Result<InstallTicket*> Index::installItems(
}
utils::vector::push(ids, list.value());
}
return Ok(new InstallTicket(this, ids, progress));
return Ok(InstallItems(ids));
}
Result<InstallTicket*> Index::installItem(
IndexItem const& item,
ItemInstallCallback progress
Result<InstallItems> Index::installItem(
IndexItem const& item
) {
return this->installItems({ item }, progress);
return this->installItems({ item });
}
bool Index::isUpdateAvailableForItem(std::string const& id) const {
@ -522,9 +486,7 @@ bool Index::areUpdatesAvailable() const {
return false;
}
Result<InstallTicket*> Index::installUpdates(
IndexUpdateCallback callback, bool force
) {
Result<InstallItems> Index::installAllUpdates() {
// find items that need updating
std::vector<IndexItem> itemsToUpdate {};
for (auto& item : m_items) {
@ -532,52 +494,118 @@ Result<InstallTicket*> Index::installUpdates(
itemsToUpdate.push_back(item);
}
}
return this->installItems(itemsToUpdate);
}
// generate ticket
auto ticket = this->installItems(
itemsToUpdate,
[itemsToUpdate, callback](
InstallTicket*,
UpdateStatus status,
std::string const& info,
uint8_t progress
) -> void {
switch (status) {
case UpdateStatus::Failed: {
callback(
UpdateStatus::Failed,
"Updating failed: " + info,
InstallHandles Index::getRunningInstallations() const {
return map::getValues(m_installations);
}
InstallHandle Index::isInstallingItem(std::string const& id) {
if (m_installations.count(id)) {
return m_installations.at(id);
}
return nullptr;
}
std::vector<std::string> InstallItems::toInstall() const {
return m_toInstall;
}
InstallHandles InstallItems::begin(ItemInstallCallback callback) const {
InstallHandles res {};
for (auto& inst : m_toInstall) {
// by virtue of running this function we know item must be valid
auto item = Index::get()->getKnownItem(inst);
auto indexDir = Loader::get()->getGeodeDirectory() / "index";
auto tempFile = indexDir / item.m_download.m_filename;
auto handle = web::AsyncWebRequest()
.join("install_mod_" + inst)
.fetch(item.m_download.m_url)
.into(tempFile)
.then([callback, item, inst, indexDir, tempFile](auto) {
// check for 404
auto notFound = utils::file::readString(tempFile);
if (notFound && notFound.value() == "Not Found") {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
if (callback) callback(
inst, UpdateStatus::Failed,
"Binary file download returned \"Not found\". Report "
"this to the Geode development team.",
0
);
} break;
}
case UpdateStatus::Finished: {
std::string updatedStr = "";
for (auto& item : itemsToUpdate) {
updatedStr += item.m_info.m_name + " (" +
item.m_info.m_id + ")\n";
}
callback(
UpdateStatus::Finished,
"Updated the following mods: " +
updatedStr +
"Please restart to apply changes.",
100
// verify checksum
if (callback) callback(inst, UpdateStatus::Progress, "Verifying", 100);
if (::calculateHash(tempFile.string()) != item.m_download.m_hash) {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
if (callback) callback(
inst, UpdateStatus::Failed,
"Checksum mismatch! (Downloaded file did not match what "
"was expected. Try again, and if the download fails another time, "
"report this to the Geode development team.",
0
);
} break;
}
case UpdateStatus::Progress: {
callback(UpdateStatus::Progress, info, progress);
} break;
}
}
);
if (!ticket) {
return Err(ticket.error());
// move temp file to geode directory
try {
auto modDir = Loader::get()->getGeodeDirectory() / "mods";
auto targetFile = modDir / item.m_download.m_filename;
// find valid filename that doesn't exist yet
auto filename = ghc::filesystem::path(
item.m_download.m_filename
).replace_extension("").string();
size_t number = 0;
while (ghc::filesystem::exists(targetFile)) {
targetFile = modDir /
(filename + std::to_string(number) + ".geode");
number++;
}
// move file
ghc::filesystem::rename(tempFile, targetFile);
} catch(std::exception& e) {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
if (callback) callback(
inst, UpdateStatus::Failed,
"Unable to move downloaded file to mods directory: \"" +
std::string(e.what()) + " \" "
"(This might be due to insufficient permissions to "
"write files under SteamLibrary, try running GD as "
"administrator)",
0
);
}
// finished
if (callback) callback(inst, UpdateStatus::Finished, "", 100);
})
.expect([inst, callback](std::string const& error) {
if (callback) callback(inst, UpdateStatus::Failed, error, 0);
Index::get()->m_installations.erase(inst);
})
.cancelled([inst](auto&) {
Index::get()->m_installations.erase(inst);
})
.progress([inst, callback](web::SentAsyncWebRequest&, double now, double total) {
if (callback) callback(
inst, UpdateStatus::Progress,
"Downloading binary",
static_cast<uint8_t>(now / total * 100.0)
);
})
.send();
res.push_back(handle);
Index::get()->m_installations.insert({ inst, handle });
}
// install updates concurrently
ticket.value()->start(InstallMode::Concurrent);
return ticket;
return res;
}

View file

@ -3,12 +3,12 @@
#include <Geode/Geode.hpp>
#include <mutex>
#include <optional>
#include <Geode/utils/fetch.hpp>
USE_GEODE_NAMESPACE();
class Index;
struct ModInstallUpdate;
class InstallTicket;
// todo: make index use events
@ -18,8 +18,11 @@ enum class UpdateStatus {
Finished,
};
using InstallHandle = web::SentAsyncWebRequestHandle;
using InstallHandles = std::vector<InstallHandle>;
using ItemInstallCallback = std::function<void(
InstallTicket*, UpdateStatus, std::string const&, uint8_t
std::string const&, UpdateStatus, std::string const&, uint8_t
)>;
using IndexUpdateCallback = std::function<void(
UpdateStatus, std::string const&, uint8_t
@ -39,73 +42,20 @@ struct IndexItem {
std::unordered_set<std::string> m_categories;
};
enum class InstallMode {
Order, // download & install one-by-one
Concurrent, // download & install all simultaneously
};
struct InstallItems final {
private:
std::vector<std::string> m_toInstall;
/**
* Used for working with a currently
* happening mod installation from
* the index. Note that once the
* installation is finished / failed,
* the ticket will free its own memory,
* so make sure to let go of any
* pointers you may have to it.
*/
class InstallTicket {
protected:
ItemInstallCallback m_progress;
const std::vector<std::string> m_installList;
mutable std::mutex m_cancelMutex;
bool m_cancelling = false;
bool m_installing = false;
bool m_replaceFiles = true;
Index* m_index;
void postProgress(
UpdateStatus status,
std::string const& info = "",
uint8_t progress = 0
);
void install(std::string const& id);
inline InstallItems(
std::vector<std::string> const& toInstall
) : m_toInstall(toInstall) {}
friend class Index;
public:
/**
* Create a new ticket for installing a list of mods. This method
* should not be called manually; instead, you should always use
* `Index::installItem`. Note that once the installation is
* finished / failed, the ticket will free its own memory, so make
* sure to let go of any pointers you may have to it.
*/
InstallTicket(
Index* index,
std::vector<std::string> const& list,
ItemInstallCallback progress
);
std::vector<std::string> toInstall() const;
/**
* Get list of mods to install
*/
std::vector<std::string> getInstallList() const;
/**
* Cancel all pending installations and revert finished ones. This
* function is thread-safe
*/
void cancel();
/**
* Begin installation. Note that this function is *not*
* thread-safe
* @param mode Whether to install the list of mods
* provided concurrently or in order
* @note Use InstallTicket::cancel to cancel the
* installation
*/
void start(InstallMode mode = InstallMode::Concurrent);
InstallHandles begin(ItemInstallCallback callback) const;
};
class Index {
@ -113,50 +63,42 @@ protected:
bool m_upToDate = false;
bool m_updating = false;
mutable std::mutex m_callbacksMutex;
std::vector<IndexUpdateCallback> m_callbacks;
std::vector<IndexItem> m_items;
std::unordered_map<std::string, InstallHandle> m_installations;
mutable std::mutex m_ticketsMutex;
std::unordered_set<std::string> m_featured;
std::unordered_set<std::string> m_categories;
void indexUpdateProgress(
UpdateStatus status,
std::string const& info = "",
uint8_t percentage = 0
);
void updateIndexThread(bool force);
void addIndexItemFromFolder(ghc::filesystem::path const& dir);
void updateIndexFromLocalCache();
Result<> updateIndexFromLocalCache();
Result<std::vector<std::string>> checkDependenciesForItem(
IndexItem const& item
);
friend struct InstallItems;
public:
static Index* get();
std::vector<IndexItem> getItems() const;
bool isKnownItem(std::string const& id) const;
IndexItem getKnownItem(std::string const& id) const;
Result<InstallTicket*> installItems(
std::vector<IndexItem> const& item,
ItemInstallCallback progress = nullptr
);
Result<InstallTicket*> installItem(
IndexItem const& item,
ItemInstallCallback progress = nullptr
);
bool isUpdateAvailableForItem(std::string const& id) const;
bool isUpdateAvailableForItem(IndexItem const& item) const;
bool areUpdatesAvailable() const;
Result<InstallTicket*> installUpdates(
IndexUpdateCallback callback = nullptr,
bool force = false
);
std::unordered_set<std::string> getCategories() const;
std::vector<IndexItem> getFeaturedItems() const;
bool isFeaturedItem(std::string const& item) const;
Result<InstallItems> installItems(std::vector<IndexItem> const& item);
Result<InstallItems> installItem(IndexItem const& item);
InstallHandles getRunningInstallations() const;
InstallHandle isInstallingItem(std::string const& id);
bool isUpdateAvailableForItem(std::string const& id) const;
bool isUpdateAvailableForItem(IndexItem const& item) const;
bool areUpdatesAvailable() const;
Result<InstallItems> installAllUpdates();
bool isIndexUpdated() const;
void updateIndex(IndexUpdateCallback callback, bool force = false);
};

View file

@ -1,201 +0,0 @@
#include "Index.hpp"
#include <thread>
#include <Geode/utils/json.hpp>
#include <hash.hpp>
#include <Geode/utils/fetch.hpp>
void InstallTicket::postProgress(
UpdateStatus status,
std::string const& info,
uint8_t percentage
) {
if (m_progress) {
Loader::get()->queueInGDThread([this, status, info, percentage]() -> void {
m_progress(this, status, info, percentage);
});
}
if (status == UpdateStatus::Failed || status == UpdateStatus::Finished) {
log::info("Deleting InstallTicket at {}", this);
// clean up own memory
delete this;
}
}
InstallTicket::InstallTicket(
Index* index,
std::vector<std::string> const& list,
ItemInstallCallback progress
) : m_index(index),
m_installList(list),
m_progress(progress) {}
std::vector<std::string> InstallTicket::getInstallList() const {
return m_installList;
}
void InstallTicket::install(std::string const& id) {
// run installing in another thread in order
// to render progress on screen while installing
auto indexDir = Loader::get()->getGeodeDirectory() / "index";
auto item = m_index->getKnownItem(id);
this->postProgress(UpdateStatus::Progress, "Checking status", 0);
// download to temp file in index dir
auto tempFile = indexDir / item.m_download.m_filename;
this->postProgress(UpdateStatus::Progress, "Fetching binary", 0);
auto res = web::fetchFile(
item.m_download.m_url,
tempFile,
[this, tempFile](double now, double total) -> int {
// check if cancelled
std::lock_guard cancelLock(m_cancelMutex);
if (m_cancelling) {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
return false;
}
// no need to scope the lock guard more as this
// function is going to exit right after anyway
this->postProgress(
UpdateStatus::Progress,
"Downloading binary",
static_cast<uint8_t>(now / total * 100.0)
);
return true;
}
);
if (!res) {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
return this->postProgress(
UpdateStatus::Failed,
"Downloading failed: " + res.error()
);
}
// check if cancelled
{
std::lock_guard cancelLock(m_cancelMutex);
if (m_cancelling) {
ghc::filesystem::remove(tempFile);
return;
}
// scope ends here since we don't need to
// access m_cancelling anymore
}
// check for 404
auto notFound = utils::file::readString(tempFile);
if (notFound && notFound.value() == "Not Found") {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
return this->postProgress(
UpdateStatus::Failed,
"Binary file download returned \"Not found\". Report "
"this to the Geode development team."
);
}
// verify checksum
this->postProgress(UpdateStatus::Progress, "Verifying", 100);
auto checksum = ::calculateHash(tempFile.string());
if (checksum != item.m_download.m_hash) {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
return this->postProgress(
UpdateStatus::Failed,
"Checksum mismatch! (Downloaded file did not match what "
"was expected. Try again, and if the download fails another time, "
"report this to the Geode development team."
);
}
// move temp file to geode directory
try {
auto modDir = Loader::get()->getGeodeDirectory() / "mods";
auto targetFile = modDir / item.m_download.m_filename;
// find valid filename that doesn't exist yet
if (!m_replaceFiles) {
auto filename = ghc::filesystem::path(
item.m_download.m_filename
).replace_extension("").string();
size_t number = 0;
while (ghc::filesystem::exists(targetFile)) {
targetFile = modDir /
(filename + std::to_string(number) + ".geode");
number++;
}
}
// move file
ghc::filesystem::rename(tempFile, targetFile);
} catch(std::exception& e) {
try { ghc::filesystem::remove(tempFile); } catch(...) {}
return this->postProgress(
UpdateStatus::Failed,
"Unable to move downloaded file to mods directory: \"" +
std::string(e.what()) + " \" "
"(This might be due to insufficient permissions to "
"write files under SteamLibrary, try running GD as "
"administrator)"
);
}
// call next in queue or post finish message
Loader::get()->queueInGDThread([this, id]() -> void {
// todo: Loader::get()->refreshMods(m_updateMod);
// where the Loader unloads the mod binary and
// reloads it from disk (this should prolly be
// done only for the installed mods)
Loader::get()->refreshMods();
// already in GD thread, so might aswell do the
// progress posting manually
if (m_progress) {
(m_progress)(
this, UpdateStatus::Finished, "", 100
);
}
// clean up memory
delete this;
});
}
void InstallTicket::cancel() {
// really no point in using std::lock_guard here
// since just plain locking and unlocking the mutex
// will do the job just fine with the same legibility
m_cancelMutex.lock();
m_cancelling = true;
m_cancelMutex.unlock();
}
void InstallTicket::start(InstallMode mode) {
if (m_installing) return;
// make sure we have stuff to install
if (!m_installList.size()) {
return this->postProgress(
UpdateStatus::Failed, "Nothing to install", 0
);
}
m_installing = true;
switch (mode) {
case InstallMode::Concurrent: {
for (auto& id : m_installList) {
std::thread(&InstallTicket::install, this, id).detach();
}
} break;
case InstallMode::Order: {
std::thread([this]() -> void {
for (auto& id : m_installList) {
this->install(id);
}
}).detach();
} break;
}
}

View file

@ -293,7 +293,7 @@ static LONG WINAPI exceptionHandler(LPEXCEPTION_POINTERS info) {
<< "Please submit this crash report to its developer ("
<< faultyMod->getDeveloper() << ") for assistance.\n";
}
// geode info
file << "\n== Geode Information ==\n";
printGeodeInfo(file);

View file

@ -12,7 +12,6 @@
#undef min
#undef max
static constexpr const int TAG_CONFIRM_INSTALL = 4;
static constexpr const int TAG_CONFIRM_UNINSTALL = 5;
static constexpr const int TAG_DELETE_SAVEDATA = 6;
@ -411,6 +410,10 @@ bool ModInfoLayer::init(ModObject* obj, ModListView* list) {
);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
if (auto handle = Index::get()->isInstallingItem(m_info.m_id)) {
m_installations.push_back(handle);
}
}
auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png");
@ -496,10 +499,7 @@ void ModInfoLayer::onRepository(CCObject*) {
void ModInfoLayer::onInstallMod(CCObject*) {
auto ticketRes = Index::get()->installItem(
Index::get()->getKnownItem(m_info.m_id),
[this](InstallTicket* ticket, UpdateStatus status, std::string const& info, uint8_t progress) -> void {
this->modInstallProgress(ticket, status, info, progress);
}
Index::get()->getKnownItem(m_info.m_id)
);
if (!ticketRes) {
return FLAlertLayer::create(
@ -508,26 +508,50 @@ void ModInfoLayer::onInstallMod(CCObject*) {
"OK"
)->show();
}
m_ticket = ticketRes.value();
auto items = ticketRes.value();
auto layer = FLAlertLayer::create(
this,
createQuickPopup(
"Install",
"The following <cb>mods</c> will be installed: " +
utils::vector::join(m_ticket->getInstallList(), ",") + ".",
"Cancel", "OK", 360.f
utils::vector::join(items.toInstall(), ",") + ".",
"Cancel", "OK",
[this, items](FLAlertLayer*, bool btn2) {
if (btn2) {
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(false);
}
this->updateInstallStatus("Starting install", 0);
m_installBtn->setTarget(
this, menu_selector(ModInfoLayer::onCancelInstall)
);
m_installBtnSpr->setString("Cancel");
m_installBtnSpr->setBG("GJ_button_06.png", false);
this->retain();
m_installations = items.begin([this](
std::string const& mod,
UpdateStatus status,
std::string const& value,
uint8_t progress
) {
this->modInstallProgress(mod, status, value, progress);
});
} else {
this->updateInstallStatus("", 0);
}
}
);
layer->setTag(TAG_CONFIRM_INSTALL);
layer->show();
}
void ModInfoLayer::onCancelInstall(CCObject*) {
m_installBtn->setEnabled(false);
m_installBtnSpr->setString("Cancelling");
if (m_ticket) {
m_ticket->cancel();
for (auto& inst : m_installations) {
inst->cancel();
}
m_installations.clear();
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(true);
}
@ -546,14 +570,6 @@ void ModInfoLayer::onUninstall(CCObject*) {
void ModInfoLayer::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
switch (layer->getTag()) {
case TAG_CONFIRM_INSTALL: {
if (btn2) {
this->install();
} else {
this->updateInstallStatus("", 0);
}
} break;
case TAG_CONFIRM_UNINSTALL: {
if (btn2) {
this->uninstall();
@ -596,7 +612,7 @@ void ModInfoLayer::updateInstallStatus(
}
void ModInfoLayer::modInstallProgress(
InstallTicket*,
std::string const& mod,
UpdateStatus status,
std::string const& info,
uint8_t percentage
@ -615,10 +631,22 @@ void ModInfoLayer::modInstallProgress(
m_installBtnSpr->setString("Install");
m_installBtnSpr->setBG("GE_button_01.png"_spr, false);
m_ticket = nullptr;
for (auto& inst : m_installations) {
inst->cancel();
}
m_installations.clear();
this->release();
} break;
case UpdateStatus::Finished: {
// if some installations are still running, keep going
for (auto& inst : m_installations) {
if (!inst->finished()) return;
}
// load mods
Loader::get()->refreshMods();
this->updateInstallStatus("", 100);
FLAlertLayer::create(
@ -629,10 +657,12 @@ void ModInfoLayer::modInstallProgress(
"OK"
)->show();
m_ticket = nullptr;
m_installations.clear();
if (m_list) m_list->refreshList();
this->onClose(nullptr);
this->release();
} break;
default: {
@ -641,23 +671,6 @@ void ModInfoLayer::modInstallProgress(
}
}
void ModInfoLayer::install() {
if (m_ticket) {
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(false);
}
this->updateInstallStatus("Starting install", 0);
m_installBtn->setTarget(
this, menu_selector(ModInfoLayer::onCancelInstall)
);
m_installBtnSpr->setString("Cancel");
m_installBtnSpr->setBG("GJ_button_06.png", false);
m_ticket->start();
}
}
void ModInfoLayer::uninstall() {
auto res = m_mod->uninstall();
if (!res) {
@ -732,6 +745,7 @@ void ModInfoLayer::keyDown(enumKeyCodes key) {
void ModInfoLayer::onClose(CCObject* pSender) {
this->setKeyboardEnabled(false);
this->removeFromParentAndCleanup(true);
m_installations.clear();
};
ModInfoLayer* ModInfoLayer::create(Mod* mod, ModListView* list) {

View file

@ -37,7 +37,7 @@ protected:
IconButtonSprite* m_installBtnSpr;
CCMenuItemSpriteExtra* m_installBtn;
CCLabelBMFont* m_updateVersionLabel = nullptr;
InstallTicket* m_ticket = nullptr;
InstallHandles m_installations;
MDTextArea* m_detailsArea;
MDTextArea* m_changelogArea;
Scrollbar* m_scrollbar;
@ -60,7 +60,7 @@ protected:
void updateInstallStatus(std::string const& status, uint8_t progress);
void modInstallProgress(
InstallTicket*,
std::string const& mod,
UpdateStatus status,
std::string const& info,
uint8_t percentage

View file

@ -18,13 +18,14 @@ public:
std::string const& content,
const char* btn1,
const char* btn2,
float width,
std::function<void(FLAlertLayer*, bool)> selected
) {
auto inst = new QuickPopup;
inst->m_selected = selected;
if (inst && inst->init(
inst, title, content,
btn1, btn2, 350.f, false, .0f
btn1, btn2, width, false, .0f
)) {
inst->autorelease();
return inst;
@ -39,6 +40,7 @@ FLAlertLayer* geode::createQuickPopup(
std::string const& content,
const char* btn1,
const char* btn2,
float width,
std::function<void(FLAlertLayer*, bool)> selected,
bool doShow
) {
@ -46,6 +48,7 @@ FLAlertLayer* geode::createQuickPopup(
title,
content,
btn1, btn2,
width,
selected
);
if (doShow) {
@ -53,3 +56,17 @@ FLAlertLayer* geode::createQuickPopup(
}
return ret;
}
FLAlertLayer* geode::createQuickPopup(
const char* title,
std::string const& content,
const char* btn1,
const char* btn2,
std::function<void(FLAlertLayer*, bool)> selected,
bool doShow
) {
return createQuickPopup(
title, content, btn1, btn2, 350.f,
selected, doShow
);
}

View file

@ -1,17 +1,28 @@
#include <Geode/utils/fetch.hpp>
#include <curl/curl.h>
#include <Geode/utils/casts.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/utils/vector.hpp>
USE_GEODE_NAMESPACE();
using namespace web;
namespace geode::utils::fetch {
static size_t writeData(char* data, size_t size, size_t nmemb, void* str) {
static size_t writeBytes(char* data, size_t size, size_t nmemb, void* str) {
as<byte_array*>(str)->insert(
as<byte_array*>(str)->end(),
data, data + size * nmemb
);
return size * nmemb;
}
static size_t writeString(char* data, size_t size, size_t nmemb, void* str) {
as<std::string*>(str)->append(data, size * nmemb);
return size * nmemb;
}
static size_t writeBinaryData(char* data, size_t size, size_t nmemb, void* file) {
as<std::ofstream*>(file)->write(data, size * nmemb);
as<std::ostream*>(file)->write(data, size * nmemb);
return size * nmemb;
}
@ -62,16 +73,16 @@ Result<> web::fetchFile(
return Err("Error getting info: " + std::string(curl_easy_strerror(res)));
}
Result<std::string> web::fetch(std::string const& url) {
Result<byte_array> web::fetchBytes(std::string const& url) {
auto curl = curl_easy_init();
if (!curl) return Err("Curl not initialized!");
std::string ret;
byte_array ret;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeData);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBytes);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
auto res = curl_easy_perform(curl);
if (res != CURLE_OK) {
@ -89,4 +100,301 @@ Result<std::string> web::fetch(std::string const& url) {
return Err("Error getting info: " + std::string(curl_easy_strerror(res)));
}
Result<std::string> web::fetch(std::string const& url) {
auto curl = curl_easy_init();
if (!curl) return Err("Curl not initialized!");
std::string ret;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeString);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
auto res = curl_easy_perform(curl);
if (res != CURLE_OK) {
curl_easy_cleanup(curl);
return Err("Fetch failed");
}
char* ct;
res = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
if ((res == CURLE_OK) && ct) {
curl_easy_cleanup(curl);
return Ok(ret);
}
curl_easy_cleanup(curl);
return Err("Error getting info: " + std::string(curl_easy_strerror(res)));
}
static std::unordered_map<std::string, SentAsyncWebRequestHandle> RUNNING_REQUESTS {};
static std::mutex RUNNING_REQUESTS_MUTEX;
SentAsyncWebRequest::SentAsyncWebRequest(
AsyncWebRequest const& req,
std::string const& id
) : m_id(id),
m_url(req.m_url),
m_target(req.m_target)
{
#define AWAIT_RESUME() \
while (m_paused) {}\
if (m_cancelled) {\
this->doCancel();\
return;\
}
if (req.m_then) m_thens.push_back(req.m_then);
if (req.m_progress) m_progresses.push_back(req.m_progress);
if (req.m_cancelled) m_cancelleds.push_back(req.m_cancelled);
if (req.m_expect) m_expects.push_back(req.m_expect);
std::thread([this]() {
AWAIT_RESUME();
auto curl = curl_easy_init();
if (!curl) {
return this->error("Curl not initialized");
}
// resulting byte array
byte_array ret;
// output file if downloading to file. unique_ptr because not always
// initialized but don't wanna manually managed memory
std::unique_ptr<std::ofstream> file = nullptr;
// into file
if (std::holds_alternative<ghc::filesystem::path>(m_target)) {
file = std::make_unique<std::ofstream>(
std::get<ghc::filesystem::path>(m_target),
std::ios::out | std::ios::binary
);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, file.get());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
}
// into stream
else if (std::holds_alternative<std::ostream*>(m_target)) {
curl_easy_setopt(curl, CURLOPT_WRITEDATA, std::get<std::ostream*>(m_target));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
}
// into memory
else {
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBytes);
}
curl_easy_setopt(curl, CURLOPT_URL, m_url.c_str());
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
struct ProgressData {
SentAsyncWebRequest* self;
std::ofstream* file;
} data { this, file.get() };
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION,
+[](void* ptr, double total, double now, double, double) -> int {
auto data = reinterpret_cast<ProgressData*>(ptr);
while (data->self->m_paused) {}
if (data->self->m_cancelled) {
if (data->file) {
data->file->close();
}
return 1;
}
Loader::get()->queueInGDThread([self = data->self, now, total]() {
std::lock_guard _(self->m_mutex);
for (auto& prog : self->m_progresses) {
prog(*self, now, total);
}
});
return 0;
}
);
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &data);
auto res = curl_easy_perform(curl);
char* ct;
res = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct);
curl_easy_cleanup(curl);
if ((res != CURLE_OK) || !ct) {
return this->error("Fetch failed: " + std::string(curl_easy_strerror(res)));
}
AWAIT_RESUME();
// if something is still holding a handle to this
// request, then they may still cancel it
m_finished = true;
Loader::get()->queueInGDThread([this, ret]() {
std::lock_guard _(m_mutex);
for (auto& then : m_thens) {
then(*this, ret);
}
std::lock_guard __(RUNNING_REQUESTS_MUTEX);
RUNNING_REQUESTS.erase(m_id);
});
}).detach();
}
void SentAsyncWebRequest::doCancel() {
if (m_cleanedUp) return;
m_cleanedUp = true;
// remove file if downloaded to one
if (std::holds_alternative<ghc::filesystem::path>(m_target)) {
auto path = std::get<ghc::filesystem::path>(m_target);
if (ghc::filesystem::exists(path)) {
try {
ghc::filesystem::remove(path);
} catch(...) {}
}
}
Loader::get()->queueInGDThread([this]() {
std::lock_guard _(m_mutex);
for (auto& canc : m_cancelleds) {
canc(*this);
}
});
this->error("Request cancelled");
}
void SentAsyncWebRequest::cancel() {
m_cancelled = true;
// if already finished, cancel anyway to clean up
if (m_finished) {
this->doCancel();
}
}
void SentAsyncWebRequest::pause() {
m_paused = true;
}
void SentAsyncWebRequest::resume() {
m_paused = false;
}
bool SentAsyncWebRequest::finished() const {
return m_finished;
}
void SentAsyncWebRequest::error(std::string const& error) {
while (m_paused) {};
Loader::get()->queueInGDThread([this, error]() {
std::lock_guard _(m_mutex);
for (auto& expect : m_expects) {
expect(error);
}
std::lock_guard __(RUNNING_REQUESTS_MUTEX);
RUNNING_REQUESTS.erase(m_id);
});
}
AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) {
m_joinID = requestID;
return *this;
}
AsyncWebResponse AsyncWebRequest::fetch(std::string const& url) {
m_url = url;
return AsyncWebResponse(*this);
}
AsyncWebRequest& AsyncWebRequest::expect(std::function<void(std::string const&)> handler) {
m_expect = handler;
return *this;
}
AsyncWebRequest& AsyncWebRequest::progress(AsyncProgress progress) {
m_progress = progress;
return *this;
}
AsyncWebRequest& AsyncWebRequest::cancelled(AsyncCancelled cancelledFunc) {
m_cancelled = cancelledFunc;
return *this;
}
SentAsyncWebRequestHandle AsyncWebRequest::send() {
if (m_sent) return nullptr;
m_sent = true;
std::lock_guard __(RUNNING_REQUESTS_MUTEX);
// pause all running requests
for (auto& [_, req] : RUNNING_REQUESTS) {
req->pause();
}
SentAsyncWebRequestHandle ret;
static size_t COUNTER = 0;
if (m_joinID && RUNNING_REQUESTS.count(m_joinID.value())) {
auto& req = RUNNING_REQUESTS.at(m_joinID.value());
std::lock_guard _(req->m_mutex);
if (m_then) req->m_thens.push_back(m_then);
if (m_progress) req->m_progresses.push_back(m_progress);
if (m_expect) req->m_expects.push_back(m_expect);
if (m_cancelled) req->m_cancelleds.push_back(m_cancelled);
ret = req;
} else {
auto id = m_joinID.value_or("__anon_request_" + std::to_string(COUNTER++));
ret = std::make_shared<SentAsyncWebRequest>(*this, id);
RUNNING_REQUESTS.insert({ id, ret });
}
// resume all running requests
for (auto& [_, req] : RUNNING_REQUESTS) {
req->resume();
}
return ret;
}
AsyncWebRequest::~AsyncWebRequest() {
this->send();
}
AsyncWebResult<std::monostate> AsyncWebResponse::into(std::ostream& stream) {
m_request.m_target = &stream;
return this->as(+[](byte_array const&) {
return Ok(std::monostate());
});
}
AsyncWebResult<std::monostate> AsyncWebResponse::into(ghc::filesystem::path const& path) {
m_request.m_target = path;
return this->as(+[](byte_array const&) {
return Ok(std::monostate());
});
}
AsyncWebResult<std::string> AsyncWebResponse::text() {
return this->as(+[](byte_array const& bytes) {
return Ok(std::string(bytes.begin(), bytes.end()));
});
}
AsyncWebResult<byte_array> AsyncWebResponse::bytes() {
return this->as(+[](byte_array const& bytes) {
return Ok(bytes);
});
}
AsyncWebResult<nlohmann::json> AsyncWebResponse::json() {
return this->as(+[](byte_array const& bytes) -> Result<nlohmann::json> {
try {
return Ok(nlohmann::json::parse(bytes.begin(), bytes.end()));
} catch(std::exception& e) {
return Err(std::string(e.what()));
}
});
}