diff --git a/CMakeLists.txt b/CMakeLists.txt index a953178fe..9aae8f385 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,10 @@ if(ANDROID) message("Building for Android, disabling Discord Presence support") set(ENABLE_DISCORD_PRESENCE OFF) endif() + if(ENABLE_CHEEVOS) + message("Building for Android. disabling RetroAchievements support") + set(ENABLE_CHEEVOS OFF) + endif() if(USE_SDL2) message("Building for Android, disabling SDL2 support") set(USE_SDL2 OFF) @@ -133,6 +137,13 @@ if(USE_EVDEV) message(STATUS "EVDev Support enabled") find_package(LIBEVDEV REQUIRED) endif() +if(ENABLE_CHEEVOS) + message(STATUS "RetroAchievements support enabled") + if(NOT WIN32) + find_package(CURL REQUIRED) + endif() +endif() + # Set _DEBUG macro for Debug builds. set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG") diff --git a/dep/rcheevos/CMakeLists.txt b/dep/rcheevos/CMakeLists.txt index 4b1b48507..cfefd4c1e 100644 --- a/dep/rcheevos/CMakeLists.txt +++ b/dep/rcheevos/CMakeLists.txt @@ -28,5 +28,5 @@ add_library(rcheevos target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") - +target_compile_definitions(rcheevos PRIVATE "RC_DISABLE_LUA=1") diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d1ece5f72..298fa50fc 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -72,6 +72,8 @@ add_library(common string.h string_util.cpp string_util.h + thirdparty/thread_pool.cpp + thirdparty/thread_pool.h timer.cpp timer.h timestamp.cpp diff --git a/src/common/easing.h b/src/common/easing.h index b5d5c1d54..ae3812a03 100644 --- a/src/common/easing.h +++ b/src/common/easing.h @@ -177,9 +177,10 @@ ALWAYS_INLINE_RELEASE static T InBack(T t) } template -ALWAYS_INLINE_RELEASE static T OutBack(T t) +static T OutBack(T t) { - return 1 + (--t) * t * (2.70158f * t + 1.70158f); + t -= 1; + return 1 + t * t * (2.70158f * t + 1.70158f); } template @@ -254,4 +255,4 @@ ALWAYS_INLINE_RELEASE static T InOutBounce(T t) } } -} // namespace Easing \ No newline at end of file +} // namespace Easing diff --git a/src/common/thirdparty/thread_pool.cpp b/src/common/thirdparty/thread_pool.cpp new file mode 100644 index 000000000..fb497b1e5 --- /dev/null +++ b/src/common/thirdparty/thread_pool.cpp @@ -0,0 +1,111 @@ +// From https://raw.githubusercontent.com/cbraley/threadpool/master/src/thread_pool.cc + +#include "thread_pool.h" + +#include + +namespace cb { + +// static +unsigned int ThreadPool::GetNumLogicalCores() { + // TODO(cbraley): Apparently this is broken in some older stdlib + // implementations? + const unsigned int dflt = std::thread::hardware_concurrency(); + if (dflt == 0) { + // TODO(cbraley): Return some error code instead. + return 16; + } else { + return dflt; + } +} + +ThreadPool::~ThreadPool() { + // TODO(cbraley): The current thread could help out to drain the work_ queue + // faster - for example, if there is work that hasn't yet been scheduled this + // thread could "pitch in" to help finish faster. + + { + std::lock_guard scoped_lock(mu_); + exit_ = true; + } + condvar_.notify_all(); // Tell *all* workers we are ready. + + for (std::thread& thread : workers_) { + thread.join(); + } +} + +void ThreadPool::Wait() { + std::unique_lock lock(mu_); + if (!work_.empty()) { + work_done_condvar_.wait(lock, [this] { return work_.empty(); }); + } +} + +ThreadPool::ThreadPool(int num_workers) + : num_workers_(num_workers), exit_(false) { + assert(num_workers_ > 0); + // TODO(cbraley): Handle thread construction exceptions. + workers_.reserve(num_workers_); + for (int i = 0; i < num_workers_; ++i) { + workers_.emplace_back(&ThreadPool::ThreadLoop, this); + } +} + +void ThreadPool::Schedule(std::function func) { + ScheduleAndGetFuture(std::move(func)); // We ignore the returned std::future. +} + +void ThreadPool::ThreadLoop() { + // Wait until the ThreadPool sends us work. + while (true) { + WorkItem work_item; + + int prev_work_size = -1; + { + std::unique_lock lock(mu_); + condvar_.wait(lock, [this] { return exit_ || (!work_.empty()); }); + // ...after the wait(), we hold the lock. + + // If all the work is done and exit_ is true, break out of the loop. + if (exit_ && work_.empty()) { + break; + } + + // Pop the work off of the queue - we are careful to execute the + // work_item.func callback only after we have released the lock. + prev_work_size = work_.size(); + work_item = std::move(work_.front()); + work_.pop(); + } + + // We are careful to do the work without the lock held! + // TODO(cbraley): Handle exceptions properly. + work_item.func(); // Do work. + + if (work_done_callback_) { + work_done_callback_(prev_work_size - 1); + } + + // Notify a condvar is all work is done. + { + std::unique_lock lock(mu_); + if (work_.empty() && prev_work_size == 1) { + work_done_condvar_.notify_all(); + } + } + } +} + +int ThreadPool::OutstandingWorkSize() const { + std::lock_guard scoped_lock(mu_); + return work_.size(); +} + +int ThreadPool::NumWorkers() const { return num_workers_; } + +void ThreadPool::SetWorkDoneCallback(std::function func) { + work_done_callback_ = std::move(func); +} + +} // namespace cb diff --git a/src/common/thirdparty/thread_pool.h b/src/common/thirdparty/thread_pool.h new file mode 100644 index 000000000..b41a00529 --- /dev/null +++ b/src/common/thirdparty/thread_pool.h @@ -0,0 +1,234 @@ +// From https://raw.githubusercontent.com/cbraley/threadpool/master/src/thread_pool.h + +#ifndef SRC_THREAD_POOL_H_ +#define SRC_THREAD_POOL_H_ + +// A simple thread pool class. +// Usage examples: +// +// { +// ThreadPool pool(16); // 16 worker threads. +// for (int i = 0; i < 100; ++i) { +// pool.Schedule([i]() { +// DoSlowExpensiveOperation(i); +// }); +// } +// +// // `pool` goes out of scope here - the code will block in the ~ThreadPool +// // destructor until all work is complete. +// } +// +// // TODO(cbraley): Add examples with std::future. + +#include +#include +#include +#include +#include +#include +#include + +// We want to use std::invoke if C++17 is available, and fallback to "hand +// crafted" code if std::invoke isn't available. +#if __cplusplus >= 201703L + #define INVOKE_MACRO(CALLABLE, ARGS_TYPE, ARGS) std::invoke(CALLABLE, std::forward(ARGS)...) +#elif __cplusplus >= 201103L + // Update this with http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4169.html. + #define INVOKE_MACRO(CALLABLE, ARGS_TYPE, ARGS) CALLABLE(std::forward(ARGS)...) +#else + #error ("C++ version is too old! C++98 is not supported.") +#endif + +namespace cb { + +class ThreadPool { + public: + // Create a thread pool with `num_workers` dedicated worker threads. + explicit ThreadPool(int num_workers); + + // Default construction is disallowed. + ThreadPool() = delete; + + // Get the number of logical cores on the CPU. This is implemented using + // std::thread::hardware_concurrency(). + // https://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency + static unsigned int GetNumLogicalCores(); + + // The `ThreadPool` destructor blocks until all outstanding work is complete. + ~ThreadPool(); + + // No copying, assigning, or std::move-ing. + ThreadPool& operator=(const ThreadPool&) = delete; + ThreadPool(const ThreadPool&) = delete; + ThreadPool(ThreadPool&&) = delete; + ThreadPool& operator=(ThreadPool&&) = delete; + + // Add the function `func` to the thread pool. `func` will be executed at some + // point in the future on an arbitrary thread. + void Schedule(std::function func); + + // Add `func` to the thread pool, and return a std::future that can be used to + // access the function's return value. + // + // *** Usage example *** + // Don't be alarmed by this function's tricky looking signature - this is + // very easy to use. Here's an example: + // + // int ComputeSum(std::vector& values) { + // int sum = 0; + // for (const int& v : values) { + // sum += v; + // } + // return sum; + // } + // + // ThreadPool pool = ...; + // std::vector numbers = ...; + // + // std::future sum_future = ScheduleAndGetFuture( + // []() { + // return ComputeSum(numbers); + // }); + // + // // Do other work... + // + // std::cout << "The sum is " << sum_future.get() << std::endl; + // + // *** Details *** + // Given a callable `func` that returns a value of type `RetT`, this + // function returns a std::future that can be used to access + // `func`'s results. + template + auto ScheduleAndGetFuture(FuncT&& func, ArgsT&&... args) + -> std::future; + + // Wait for all outstanding work to be completed. + void Wait(); + + // Return the number of outstanding functions to be executed. + int OutstandingWorkSize() const; + + // Return the number of threads in the pool. + int NumWorkers() const; + + void SetWorkDoneCallback(std::function func); + + private: + void ThreadLoop(); + + // Number of worker threads - fixed at construction time. + int num_workers_; + + // The destructor sets `exit_` to true and then notifies all workers. `exit_` + // causes each thread to break out of their work loop. + bool exit_; + + mutable std::mutex mu_; + + // Work queue. Guarded by `mu_`. + struct WorkItem { + std::function func; + }; + std::queue work_; + + // Condition variable used to notify worker threads that new work is + // available. + std::condition_variable condvar_; + + // Worker threads. + std::vector workers_; + + // Condition variable used to notify that all work is complete - the work + // queue has "run dry". + std::condition_variable work_done_condvar_; + + // Whenever a work item is complete, we call this callback. If this is empty, + // nothing is done. + std::function work_done_callback_; +}; + +namespace impl { + +// This helper class simply returns a std::function that executes: +// ReturnT x = func(); +// promise->set_value(x); +// However, this is tricky in the case where T == void. The code above won't +// compile if ReturnT == void, and neither will +// promise->set_value(func()); +// To workaround this, we use a template specialization for the case where +// ReturnT is void. If the "regular void" proposal is accepted, this could be +// simpler: +// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html. + +// The non-specialized `FuncWrapper` implementation handles callables that +// return a non-void value. +template +struct FuncWrapper { + template + std::function GetWrapped( + FuncT&& func, std::shared_ptr> promise, + ArgsT&&... args) { + // TODO(cbraley): Capturing by value is inefficient. It would be more + // efficient to move-capture everything, but we can't do this until C++14 + // generalized lambda capture is available. Can we use std::bind instead to + // make this more efficient and still use C++11? + return [promise, func, args...]() mutable { + promise->set_value(INVOKE_MACRO(func, ArgsT, args)); + }; + } +}; + +template +void InvokeVoidRet(FuncT&& func, std::shared_ptr> promise, + ArgsT&&... args) { + INVOKE_MACRO(func, ArgsT, args); + promise->set_value(); +} + +// This `FuncWrapper` specialization handles callables that return void. +template <> +struct FuncWrapper { + template + std::function GetWrapped(FuncT&& func, + std::shared_ptr> promise, + ArgsT&&... args) { + return [promise, func, args...]() mutable { + INVOKE_MACRO(func, ArgsT, args); + promise->set_value(); + }; + } +}; + +} // namespace impl + +template +auto ThreadPool::ScheduleAndGetFuture(FuncT&& func, ArgsT&&... args) + -> std::future { + using ReturnT = decltype(INVOKE_MACRO(func, ArgsT, args)); + + // We are only allocating this std::promise in a shared_ptr because + // std::promise is non-copyable. + std::shared_ptr> promise = + std::make_shared>(); + std::future ret_future = promise->get_future(); + + impl::FuncWrapper func_wrapper; + std::function wrapped_func = func_wrapper.GetWrapped( + std::move(func), std::move(promise), std::forward(args)...); + + // Acquire the lock, and then push the WorkItem onto the queue. + { + std::lock_guard scoped_lock(mu_); + WorkItem work; + work.func = std::move(wrapped_func); + work_.emplace(std::move(work)); + } + condvar_.notify_one(); // Tell one worker we are ready. + return ret_future; +} + +} // namespace cb + +#undef INVOKE_MACRO + +#endif // SRC_THREAD_POOL_H_ diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 9901cf60e..0aeb07c97 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -103,7 +103,7 @@ set(SRCS settingsdialog.ui ) -if(WITH_CHEEVOS) +if(ENABLE_CHEEVOS) set(SRCS ${SRCS} achievementlogindialog.cpp achievementlogindialog.h diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 11db6deab..7c6d3d190 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -76,7 +76,6 @@ if(SDL2_FOUND) endif() if(USE_EVDEV) - find_package(LIBEVDEV REQUIRED) target_compile_definitions(frontend-common PUBLIC "-DWITH_EVDEV=1") target_include_directories(frontend-common PRIVATE ${LIBEVDEV_INCLUDE_DIRS}) target_link_libraries(frontend-common PRIVATE ${LIBEVDEV_LIBRARIES}) @@ -101,6 +100,14 @@ if(ENABLE_CHEEVOS) http_downloader_winhttp.cpp http_downloader_winhttp.h ) + else() + target_sources(frontend-common PRIVATE + http_downloader_curl.cpp + http_downloader_curl.h + ) + target_link_libraries(frontend-common PRIVATE + CURL::libcurl + ) endif() target_sources(frontend-common PRIVATE diff --git a/src/frontend-common/http_downloader_curl.cpp b/src/frontend-common/http_downloader_curl.cpp new file mode 100644 index 000000000..7178bff95 --- /dev/null +++ b/src/frontend-common/http_downloader_curl.cpp @@ -0,0 +1,283 @@ +#include "http_downloader_curl.h" +#include "common/assert.h" +#include "common/log.h" +#include "common/string_util.h" +#include "common/timer.h" +#include +#include +Log_SetChannel(HTTPDownloaderCurl); + +namespace FrontendCommon { + +HTTPDownloaderCurl::HTTPDownloaderCurl() : HTTPDownloader() {} + +HTTPDownloaderCurl::~HTTPDownloaderCurl() = default; + +std::unique_ptr HTTPDownloader::Create() +{ + std::unique_ptr instance(std::make_unique()); + if (!instance->Initialize()) + return {}; + + return instance; +} + +static bool s_curl_initialized = false; +static std::once_flag s_curl_initialized_once_flag; + +bool HTTPDownloaderCurl::Initialize() +{ + if (!s_curl_initialized) + { + std::call_once(s_curl_initialized_once_flag, []() { + s_curl_initialized = curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK; + if (s_curl_initialized) + { + std::atexit([]() { + curl_global_cleanup(); + s_curl_initialized = false; + }); + } + }); + if (!s_curl_initialized) + { + Log_ErrorPrint("curl_global_init() failed"); + return false; + } + } + m_thread_pool = std::make_unique(m_max_active_requests); + return true; +} +/* +void CALLBACK HTTPDownloaderCurl::HTTPStatusCallback(HINTERNET hRequest, DWORD_PTR dwContext, DWORD dwInternetStatus, + LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) +{ + Request* req = reinterpret_cast(dwContext); + switch (dwInternetStatus) + { + case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED: + return; + + case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING: + { + if (!req) + return; + + DebugAssert(hRequest == req->hRequest); + + HTTPDownloaderCurl* parent = static_cast(req->parent); + std::unique_lock lock(parent->m_pending_http_request_lock); + Assert(std::none_of(parent->m_pending_http_requests.begin(), parent->m_pending_http_requests.end(), + [req](HTTPDownloader::Request* it) { return it == req; })); + + // we can clean up the connection as well + DebugAssert(req->hConnection != NULL); + WinHttpCloseHandle(req->hConnection); + delete req; + return; + } + + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: + { + const WINHTTP_ASYNC_RESULT* res = reinterpret_cast(lpvStatusInformation); + Log_ErrorPrintf("WinHttp async function %p returned error %u", res->dwResult, res->dwError); + req->status_code = -1; + req->state.store(Request::State::Complete); + return; + } + case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: + { + Log_DevPrintf("SendRequest complete"); + if (!WinHttpReceiveResponse(hRequest, nullptr)) + { + Log_ErrorPrintf("WinHttpReceiveResponse() failed: %u", GetLastError()); + req->status_code = -1; + req->state.store(Request::State::Complete); + } + + return; + } + case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: + { + Log_DevPrintf("Headers available"); + + DWORD buffer_size = sizeof(req->status_code); + if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &req->status_code, &buffer_size, WINHTTP_NO_HEADER_INDEX)) + { + Log_ErrorPrintf("WinHttpQueryHeaders() for status code failed: %u", GetLastError()); + req->status_code = -1; + req->state.store(Request::State::Complete); + return; + } + + buffer_size = sizeof(req->content_length); + if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &req->content_length, &buffer_size, + WINHTTP_NO_HEADER_INDEX)) + { + if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND) + Log_WarningPrintf("WinHttpQueryHeaders() for content length failed: %u", GetLastError()); + + req->content_length = 0; + } + + Log_DevPrintf("Status code %d, content-length is %u", req->status_code, req->content_length); + req->data.reserve(req->content_length); + req->state = Request::State::Receiving; + + // start reading + if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING) + { + Log_ErrorPrintf("WinHttpQueryDataAvailable() failed: %u", GetLastError()); + req->status_code = -1; + req->state.store(Request::State::Complete); + } + + return; + } + case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: + { + DWORD bytes_available; + std::memcpy(&bytes_available, lpvStatusInformation, sizeof(bytes_available)); + if (bytes_available == 0) + { + // end of request + Log_DevPrintf("End of request '%s', %zu bytes received", req->url.c_str(), req->data.size()); + req->state.store(Request::State::Complete); + return; + } + + // start the transfer + Log_DevPrintf("%u bytes available", bytes_available); + req->io_position = static_cast(req->data.size()); + req->data.resize(req->io_position + bytes_available); + if (!WinHttpReadData(hRequest, req->data.data() + req->io_position, bytes_available, nullptr) && + GetLastError() != ERROR_IO_PENDING) + { + Log_ErrorPrintf("WinHttpReadData() failed: %u", GetLastError()); + req->status_code = -1; + req->state.store(Request::State::Complete); + } + + return; + } + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: + { + Log_DevPrintf("Read of %u complete", dwStatusInformationLength); + + const u32 new_size = req->io_position + dwStatusInformationLength; + Assert(new_size <= req->data.size()); + req->data.resize(new_size); + req->start_time = Common::Timer::GetValue(); + + if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING) + { + Log_ErrorPrintf("WinHttpQueryDataAvailable() failed: %u", GetLastError()); + req->status_code = -1; + req->state.store(Request::State::Complete); + } + + return; + } + default: + // unhandled, ignore + return; + } +} +*/ + +size_t HTTPDownloaderCurl::WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) +{ + Request* req = static_cast(userdata); + const size_t current_size = req->data.size(); + const size_t transfer_size = size * nmemb; + const size_t new_size = current_size + transfer_size; + req->data.resize(new_size); + std::memcpy(&req->data[current_size], ptr, transfer_size); + return nmemb; +} + +void HTTPDownloaderCurl::ProcessRequest(Request* req) +{ + std::unique_lock cancel_lock(m_cancel_mutex); + if (req->closed.load()) + return; + + cancel_lock.unlock(); + + req->start_time = Common::Timer::GetValue(); + int ret = curl_easy_perform(req->handle); + if (ret == CURLE_OK) + { + long response_code = 0; + curl_easy_getinfo(req->handle, CURLINFO_RESPONSE_CODE, &response_code); + req->status_code = static_cast(response_code); + Log_DevPrintf("Request for '%s' returned status code %d and %zu bytes", + req->url.c_str(), req->status_code, req->data.size()); + } + else + { + Log_ErrorPrintf("Request for '%s' returned %d", req->url.c_str(), ret); + } + + curl_easy_cleanup(req->handle); + + cancel_lock.lock(); + req->state = Request::State::Complete; + if (req->closed.load()) + delete req; + else + req->closed.store(true); +} + +HTTPDownloader::Request* HTTPDownloaderCurl::InternalCreateRequest() +{ + Request* req = new Request(); + req->handle = curl_easy_init(); + if (!req->handle) + { + delete req; + return nullptr; + } + + return req; +} + +void HTTPDownloaderCurl::InternalPollRequests() +{ + // noop - uses thread pool +} + +bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request) +{ + Request* req = static_cast(request); + curl_easy_setopt(req->handle, CURLOPT_URL, request->url.c_str()); + // curl_easy_setopt(req->handle, CURLOPT_USERAGENT, m_user_agent.c_str()); + curl_easy_setopt(req->handle, CURLOPT_WRITEFUNCTION, &HTTPDownloaderCurl::WriteCallback); + curl_easy_setopt(req->handle, CURLOPT_WRITEDATA, req); + + if (request->type == Request::Type::Post) + { + curl_easy_setopt(req->handle, CURLOPT_POST, 1L); + curl_easy_setopt(req->handle, CURLOPT_POSTFIELDS, request->post_data.c_str()); + } + + Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str()); + req->state = Request::State::Started; + req->start_time = Common::Timer::GetValue(); + m_thread_pool->Schedule(std::bind(&HTTPDownloaderCurl::ProcessRequest, this, req)); + return true; +} + +void HTTPDownloaderCurl::CloseRequest(HTTPDownloader::Request* request) +{ + std::unique_lock cancel_lock(m_cancel_mutex); + Request* req = static_cast(request); + if (req->closed.load()) + delete req; + else + req->closed.store(true); +} + +} // namespace FrontendCommon diff --git a/src/frontend-common/http_downloader_curl.h b/src/frontend-common/http_downloader_curl.h new file mode 100644 index 000000000..f2663270e --- /dev/null +++ b/src/frontend-common/http_downloader_curl.h @@ -0,0 +1,39 @@ +#pragma once +#include "http_downloader.h" +#include "common/thirdparty/thread_pool.h" +#include +#include +#include +#include + +namespace FrontendCommon { + +class HTTPDownloaderCurl final : public HTTPDownloader +{ +public: + HTTPDownloaderCurl(); + ~HTTPDownloaderCurl() override; + + bool Initialize(); + +protected: + Request* InternalCreateRequest() override; + void InternalPollRequests() override; + bool StartRequest(HTTPDownloader::Request* request) override; + void CloseRequest(HTTPDownloader::Request* request) override; + +private: + struct Request : HTTPDownloader::Request + { + CURL* handle = nullptr; + std::atomic_bool closed{false}; + }; + + static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata); + void ProcessRequest(Request* req); + + std::unique_ptr m_thread_pool; + std::mutex m_cancel_mutex; +}; + +} // namespace FrontendCommon