/* * Copyright 2022 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NHttpClientLibHC.h" #include #include 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::Warning; 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; } void NHttpClientLibHC::setTimeout(std::chrono::milliseconds timeout) { m_timeout = timeout; if (m_timeout >= std::chrono::milliseconds(0)) { HRESULT hr = HCHttpCallRequestSetTimeout(nullptr, std::chrono::duration_cast(m_timeout).count()); if (FAILED(hr)) { HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "HCHttpCallRequestSetTimeout default failed"); } } } #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; }; if (m_timeout >= std::chrono::seconds(0)) { CHECK_CB( HCHttpCallRequestSetTimeout( std::get<0>(*ctx).get(), std::chrono::duration_cast(m_timeout).count()), "HCHttpCallPerformAsync failed", callback); } 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 m_queue_terminated.store(false, std::memory_order_relaxed); 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); } // XTaskQueueTerminate permanently kills the queue. Create a fresh one // so the client can be reused after cancellation. m_queue.reset(nullptr); XTaskQueueHandle q = nullptr; HRESULT hr = XTaskQueueCreate(XTaskQueueDispatchMode::ThreadPool, XTaskQueueDispatchMode::Manual, &q); if (FAILED(hr)) { HC_TRACE_ERROR(httpTransportLibHC, "Failed to re-create XTaskQueue after cancel: hr=0x%08x", hr); } else { m_queue.reset(q); } } } // namespace Nakama