// // Created by yamlcoder on 17/03/2022. // #include #include #include "NHttpClientLibHC.h" namespace Nakama { HC_DEFINE_TRACE_AREA(httpTransportLibHC, HCTraceLevel::Error); // Converts our loglevel to HCTracelevel static HCTraceLevel HCLevelForNLevel(NLogLevel lvl) { switch (lvl) { case NLogLevel::Debug: return HCTraceLevel::Verbose; case NLogLevel::Info: return HCTraceLevel::Information; case NLogLevel::Warn: return HCTraceLevel::Warning; case NLogLevel::Error: return HCTraceLevel::Error; case NLogLevel::Fatal: return HCTraceLevel::Error; } return HCTraceLevel::Off; } static void configureNLogger() { static bool configured = false; auto l = NLogger::getSink(); if (!l) { return; } if (configured) return; configured = true; HCSettingsSetTraceLevel(HCLevelForNLevel(l->getLevel())); HCTraceSetClientCallback([]( const char* areaName, HCTraceLevel level, uint64_t /*threadId*/, uint64_t /*timestamp*/, const char* message) { switch (level) { case HCTraceLevel::Verbose: return NLogger::Debug(message, areaName); case HCTraceLevel::Information: return NLogger::Info(message, areaName); case HCTraceLevel::Important: case HCTraceLevel::Warning: return NLogger::Warn(message, areaName); case HCTraceLevel::Error: return NLogger::Error(message, areaName); case HCTraceLevel::Off: return; } }); } NHttpClientLibHC::NHttpClientLibHC(const NPlatformParameters& platformParams): m_queue(nullptr, &XTaskQueueCloseHandle), m_baseUri(), m_queue_terminated(false) { configureNLogger(); #ifdef __ANDROID__ #error libhttpclient is not currently supported on Android in libnakama! #else (void)platformParams; HCInitArgs* initArgsParam = nullptr; #endif HRESULT hr = HCInitialize(initArgsParam); if (FAILED(hr)) { HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "HCInitialize failed"); } XTaskQueueHandle q; XTaskQueueCreate(XTaskQueueDispatchMode::ThreadPool, XTaskQueueDispatchMode::Manual, // callbacks from ticks &q ); m_queue.reset(q); } NHttpClientLibHC::~NHttpClientLibHC() noexcept { HCCleanup(); } void NHttpClientLibHC::setBaseUri(const std::string& uri) { m_baseUri = uri; } #define CHECK_CB(exp, msg, cb) { \ HRESULT hr = exp; if (FAILED(hr)) { \ HC_TRACE_ERROR_HR(httpTransportLibHC, hr, msg); \ submit_cb(cb, InternalStatusCodes::INTERNAL_TRANSPORT_ERROR, "", msg); \ return; \ }} #define CHECK_AND_LOG(hr, msg) { if (FAILED(hr)) { HC_TRACE_ERROR_HR(httpTransportLibHC, hr, msg); return hr; } } using call_ptr = std::unique_ptr; HRESULT NHttpClientLibHC::prep_hc_call(const NHttpRequest& req, call_ptr& call) { HCCallHandle pcall = nullptr; CHECK_AND_LOG(HCHttpCallCreate(&pcall), "HCHttpCallCreate failed"); call.reset(pcall); const char *callMethod = nullptr; switch (req.method) { case NHttpReqMethod::POST: callMethod = "POST"; break; case NHttpReqMethod::GET : callMethod = "GET"; break; case NHttpReqMethod::PUT : callMethod = "PUT"; break; case NHttpReqMethod::DEL : callMethod = "DELETE"; break; } std::string uri = m_baseUri + req.path + (req.queryArgs.empty() ? "" : "?"); for (auto p: req.queryArgs) { uri += p.first + "=" + p.second + "&"; }; CHECK_AND_LOG(HCHttpCallRequestSetUrl(pcall, callMethod, uri.c_str()), "HCHttpCallRequestSetUrl failed"); if (req.body.size()) { CHECK_AND_LOG(HCHttpCallRequestSetRequestBodyBytes(pcall, reinterpret_cast(req.body.data()), uint32_t(req.body.size())), "HCHttpCallRequestSetRequestBodyBytes"); } for (auto p: req.headers) { HCHttpCallRequestSetHeader(pcall, p.first.c_str(), p.second.c_str(), true); } CHECK_AND_LOG(HCHttpCallRequestSetRetryAllowed(pcall, false), "HCHttpCallRequestSetRetryAllowed failed"); return S_OK; } using ctx_t = std::tuple; static HRESULT setup_hc_response_to_string(ctx_t &ctx) { HC_CALL *call = std::get<0>(ctx).get(); HCHttpCallResponseBodyWriteFunction write_func = [](HCCallHandle /*call*/, const uint8_t* source, size_t bytesAvailable, void* context) { ctx_t *ctx = static_cast(context); std::get<2>(*ctx).append(reinterpret_cast(source), bytesAvailable); return S_OK; }; return HCHttpCallResponseSetResponseBodyWriteFunction(call, write_func, static_cast(&ctx)); } // This call back is called libHC when request is completed or failed // Prior to performing request we stashed ctx_t (HC_CALL, SDK callbacks, body) // in the asyncBlock context. Here we extract relevant information // to call SDK callback. Once this function is done final cleanup is // done via unique_ptr destructor implicitly. static void __stdcall hc_req_completed_cb(XAsyncBlock *ab) noexcept { const std::unique_ptr asyncBlock(ab); const std::unique_ptr ctx(static_cast(asyncBlock->context)); HC_CALL* call = std::get<0>(*ctx).get(); auto &callback = std::get<1>(*ctx); { NHttpResponse resp{InternalStatusCodes::CANCELLED_BY_USER, "", ""}; HRESULT hr = XAsyncGetStatus(asyncBlock.get(), false); if (E_ABORT == hr) { HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "Aborted XAsyncGetStatus"); resp.statusCode = InternalStatusCodes::CANCELLED_BY_USER; callback(std::make_shared(resp)); } // else if (FAILED(hr)) { // HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "XAsyncGetStatus"); // resp.statusCode = InternalStatusCodes::INTERNAL_TRANSPORT_ERROR; // callback(std::make_shared(resp)); // } } HRESULT errCode = 0; uint32_t platErrCode = 0; uint32_t hc_statusCode = 0; int statusCode = 0; std::string responseString; std::string errMessage; HCHttpCallResponseGetNetworkErrorCode(call, &errCode, &platErrCode); HCHttpCallResponseGetStatusCode(call, &hc_statusCode); statusCode = hc_statusCode; if (FAILED(errCode)) { const char *errMsg; HCHttpCallResponseGetPlatformNetworkErrorMessage(call, &errMsg); errMessage = errMsg; statusCode = InternalStatusCodes::CONNECTION_ERROR; //FIXME: need a way to indicate request timeout } callback(std::make_shared( NHttpResponse{statusCode, std::move(std::get<2>(*ctx)), std::move(errMessage)})); }; void NHttpClientLibHC::request(const NHttpRequest& req, const NHttpResponseCallback& callback) { if (m_baseUri.empty()) { submit_cb(callback, InternalStatusCodes::NOT_INITIALIZED_ERROR, "", "[NHttpClientLibHC::request] base uri is not set"); return; } call_ptr call(nullptr, &HCHttpCallCloseHandle); CHECK_CB(prep_hc_call(req, call), "Error preparing HC_CALL", callback); XAsyncBlock *asyncBlock = new XAsyncBlock{}; ctx_t *ctx = new ctx_t(std::move(call), callback, std::string()); asyncBlock->context = ctx; asyncBlock->queue = m_queue.get(); //unique_ptr m_queue will outlive asyncBlock, so thats fine if (callback) { CHECK_CB(setup_hc_response_to_string(*ctx), "HCHttpCallResponseSetResponseBodyWriteFunction failed", callback); asyncBlock->callback = &hc_req_completed_cb; }; CHECK_CB(HCHttpCallPerformAsync(std::get<0>(*ctx).get(), asyncBlock), "HCHttpCallPerformAsync failed", callback); } void NHttpClientLibHC::tick() { while(XTaskQueueDispatch(m_queue.get(), XTaskQueuePort::Completion, 0)) {}; } void NHttpClientLibHC::submit_cb(const NHttpResponseCallback &cb, int statusCode, std::string body, std::string err) noexcept { if (cb) { using xtask_cb_ctx_t = std::tuple; xtask_cb_ctx_t* ctx = new auto(std::make_tuple(cb, NHttpResponse{statusCode, body, err})); XTaskQueueSubmitCallback(m_queue.get(), XTaskQueuePort::Completion, ctx, [](void *ctx, bool /*cancelled*/) { //We deliberately ignore _cancelled and deliver callback as it was requested std::unique_ptr context(static_cast(ctx)); auto &cb = std::get<0>(*context); auto &response = std::get<1>(*context); cb(std::make_shared(std::move(response))); }); } } void NHttpClientLibHC::cancelAllRequests() { // We can't use `wait` because if we are called from the same thread tick is being called, // then we deadlock should there be any outstanding requests. So instead we use termination // callback which will be a last callback on the Completion port XTaskQueueTerminate(m_queue.get(), false, &m_queue_terminated, [](void *ctx){ static_cast*>(ctx)->store(true, std::memory_order_relaxed); }); // Pump completion queue until our callback is executed. tick() can still be called in // parallel thread, then it will be set from there, but we do our pumping here in case // tick is not called anymore. That is also why we have atomic bool, not just bool. while(!m_queue_terminated.load(std::memory_order_relaxed)) { XTaskQueueDispatch(m_queue.get(), XTaskQueuePort::Completion, 50); } } }