mirror of
https://github.com/heroiclabs/nakama-cpp.git
synced 2026-11-13 20:12:24 -10:00
* fix crash add test, add worker thread * Improve HTTP/WS cleanup and test timing Resolves cleanup/race issues and speeds up integration tests: - impl/httpCurl/NHttpClientLibCurl.cpp: Remove curl handle earlier and deduplicate calls to curl_multi_remove_handle(), logging errors immediately if removal fails to avoid double-remove races. - impl/wsWslay/NWebsocketWslay.cpp: Avoid heap allocation for received message by capturing a moved stack NBytes in the callback; ensure destructor always stops IO, joins the thread and cleans up the connection to prevent lingering IO. - integrationtests/src/NTest.cpp: Run deleteAccountAsync calls concurrently and pump only the HTTP client while waiting to avoid RT client races; consume results best-effort. Also reduce tick/check timeouts from 50ms to 5ms to speed test loops. * pr comments * Add exception destructors and Android test updates Provide out-of-line destructor definitions for NException and NRtException and declare their destructors in headers to satisfy the ABI and avoid inline dtor issues. Revamp Android integration test setup: add gradle.properties and .gitignore entries, switch build.gradle to use prebuilt native libraries staged under jniLibs (avoid Gradle/CMake integration conflicts), add usesCleartextTraffic to AndroidManifest, and update integration main to run tests with client/rt client factories and server params. The deploy.sh script was rewritten to support building native libs via CMake presets, staging .so and libc++_shared, assembling the APK, installing to a device/emulator, setting up adb reverse, streaming logcat and parsing test results. Also increase tournament test durations to 60s to be more tolerant of slow environments (e.g. emulators). * Refactor test cleanup, logging and Taskfile Multiple changes to improve test reliability and logging behavior: - integrationtests/src/NTest.cpp: Use a detached cleanup thread to pump the HTTP client with a 10s timeout, wait/join or detach based on completion, and avoid hangs when tick() blocks. Added <future> and best-effort future get handling; emit warning if cleanup times out. - impl/httpLibHttpClient/NHttpClientLibHC.cpp and impl/wsLibHttpClient/NWebsocketLibHC.cpp: Map NLogLevel::Debug to HCTraceLevel::Warning (was Verbose) to adjust trace level mapping. - Taskfile.yml: Only apply Xcode code-signing overrides on macOS, add docker compose --wait to service startup in multiple tasks, make the test task start the server automatically and copy built .dlls into the integrationtests folder (best-effort) so tests can find native binaries. - interface headers: add <stdexcept> includes in NException.h and realtime/rtdata/NRtException.h to ensure exception types are available. These changes aim to prevent test hangs during shutdown, ensure dependent services are ready before tests run, and fix missing includes and logging-level mapping. * Add cancellation and retry integration tests Introduce integrationtests/src/test_cancellation.cpp with a suite of tests covering request cancellation, disconnect recovery, and retry/idempotency scenarios (cancel_noPendingRequests, cancel_inflightRequests, cancel_recoveryAfterDisconnect, retry_afterConnectionError, retry_afterUnauthenticated, retry_afterSessionExpiry, retry_storageIdempotent, retry_authAndGetAccount). Also update integrationtests/src/NTestLib.cpp to declare and run test_cancellation so the new tests are included in the test run. These tests improve coverage around client disconnect behavior, CancelledByUser handling, and recovery/retry patterns. * Add configure status; recreate XTaskQueue NHttpClientLibHC.cpp: ensure m_queue_terminated is reset before terminating, then recreate the XTaskQueue after XTaskQueueTerminate so the client can be reused after cancellation; log an error if queue creation fails. * Make Android deploy script cross-platform Taskfile: loosened tests-android invocation (DEVICE is optional, ABI default kept) and delegate build/deploy to integrationtests/android/deploy.sh with an --abi flag. deploy.sh: added OS detection and portability fixes (Windows/macOS/Linux), auto-detect ANDROID_HOME (including LOCALAPPDATA on Windows) and normalize backslashes, prefer adb from PATH with fallback to ANDROID_HOME, auto-detect/suggest JDK 17 and set JAVA_HOME if found, and normalize ANDROID_NDK_HOME. Build now runs from repo root (pushd/popd) and uses HOST_OS for NDK STL path. All adb calls now go through a resolved ADB variable. These changes improve robustness and cross-platform support for Android integration tests. * linux build * inline deploy.sh * rename * run android tests windows * dis own proces on powershell * ensure exported interfaces have out-of-line key functions avoiding linker RTTI issues * revert to private wslay * change to 10 ms
319 lines
9.9 KiB
C++
319 lines
9.9 KiB
C++
#include "NHttpClientLibCurl.h"
|
|
#include "nakama-cpp/log/NLogger.h"
|
|
#include <curl/curl.h>
|
|
#include <memory.h>
|
|
#include <nakama-cpp/NHttpTransportInterface.h>
|
|
#include <nakama-cpp/log/NLogger.h>
|
|
#include <string>
|
|
|
|
#if __ANDROID__
|
|
#include "AndroidCA.h"
|
|
#endif
|
|
|
|
static int debug_callback(CURL* handle, curl_infotype type, char* data, size_t size, void* userp) {
|
|
const char* text;
|
|
(void)handle; /* prevent compiler warning */
|
|
(void)userp;
|
|
|
|
switch (type) {
|
|
case CURLINFO_TEXT:
|
|
NLOG(Nakama::NLogLevel::Debug, "libcurl debug info <=> Text: %s", data);
|
|
default: /* in case a new one is introduced */
|
|
return 0;
|
|
|
|
case CURLINFO_HEADER_OUT:
|
|
text = "=> Send header";
|
|
break;
|
|
case CURLINFO_DATA_OUT:
|
|
text = "=> Send data";
|
|
break;
|
|
case CURLINFO_SSL_DATA_OUT:
|
|
text = "=> Send SSL data";
|
|
break;
|
|
case CURLINFO_HEADER_IN:
|
|
text = "<= Recv header";
|
|
break;
|
|
case CURLINFO_DATA_IN:
|
|
text = "<= Recv data";
|
|
break;
|
|
case CURLINFO_SSL_DATA_IN:
|
|
text = "<= Recv SSL data";
|
|
break;
|
|
}
|
|
|
|
NLOG(Nakama::NLogLevel::Debug, "libcurl debug info: %s", text);
|
|
return 0;
|
|
}
|
|
|
|
static size_t write_callback(char* buffer, size_t size, size_t nmemb, void* user_ctx) {
|
|
Nakama::NHttpClientLibCurlContext* curl_ctx = (Nakama::NHttpClientLibCurlContext*)user_ctx;
|
|
|
|
if (buffer != NULL) {
|
|
// char buffer from libcurl is not null terminated.
|
|
curl_ctx->append_body(std::string(buffer, nmemb * size));
|
|
}
|
|
|
|
return nmemb * size;
|
|
}
|
|
|
|
namespace Nakama {
|
|
|
|
NHttpClientLibCurl::NHttpClientLibCurl(const NPlatformParameters& platformParameters)
|
|
: _curl_multi(curl_multi_init(), curl_multi_cleanup) {}
|
|
|
|
void NHttpClientLibCurl::request(const NHttpRequest& req, const NHttpResponseCallback& callback) noexcept {
|
|
std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl_easy(curl_easy_init(), curl_easy_cleanup);
|
|
|
|
std::string uri = _base_uri + req.path + (req.queryArgs.empty() ? "" : "?");
|
|
for (auto p : req.queryArgs) {
|
|
uri += p.first + "=" + p.second + "&";
|
|
};
|
|
|
|
curl_slist* headers_list = NULL;
|
|
|
|
for (auto it = req.headers.begin(); it != req.headers.end(); it++) {
|
|
headers_list = curl_slist_append(headers_list, (it->first + ": " + it->second).c_str());
|
|
if (headers_list == NULL) {
|
|
NLOG(Nakama::NLogLevel::Error, "error writing header: %s", it->first.c_str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
const char* callMethod = nullptr;
|
|
switch (req.method) {
|
|
case NHttpReqMethod::POST:
|
|
callMethod = "POST";
|
|
// manually set content-length if there's no body.
|
|
if (req.body.empty()) {
|
|
headers_list = curl_slist_append(headers_list, "Content-Length: 0");
|
|
if (headers_list == NULL) {
|
|
NLOG(Nakama::NLogLevel::Error, "error writing header: Content-Length");
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
case NHttpReqMethod::GET:
|
|
callMethod = "GET";
|
|
break;
|
|
case NHttpReqMethod::PUT:
|
|
callMethod = "PUT";
|
|
break;
|
|
case NHttpReqMethod::DEL:
|
|
callMethod = "DELETE";
|
|
break;
|
|
}
|
|
|
|
CURLcode curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_URL, uri.c_str());
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("setting url", curl_code, callback);
|
|
return;
|
|
}
|
|
|
|
if (_timeout >= std::chrono::milliseconds(0)) {
|
|
curl_code = curl_easy_setopt(
|
|
curl_easy.get(), CURLOPT_TIMEOUT_MS, std::chrono::duration_cast<std::chrono::milliseconds>(_timeout).count());
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("setting timeout", curl_code, callback);
|
|
return;
|
|
}
|
|
}
|
|
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_HTTPHEADER, headers_list);
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("adding headers", curl_code, callback);
|
|
return;
|
|
}
|
|
|
|
if (req.body != "") {
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_COPYPOSTFIELDS, req.body.c_str());
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("copying post fields", curl_code, callback);
|
|
return;
|
|
}
|
|
}
|
|
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_CUSTOMREQUEST, callMethod);
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("adding call method", curl_code, callback);
|
|
return;
|
|
}
|
|
|
|
#if __ANDROID__
|
|
CACertificateData* data = Nakama::getCaCertificates();
|
|
if (data == nullptr) {
|
|
// Has System.loadLibrary("nakama-sdk") been called?
|
|
NLOG(Nakama::NLogLevel::Error, "error obtaining CA Certificates.");
|
|
return;
|
|
} else {
|
|
struct curl_blob blob;
|
|
blob.data = reinterpret_cast<char*>(data->data);
|
|
blob.len = data->len;
|
|
blob.flags = CURL_BLOB_COPY;
|
|
curl_easy_setopt(curl_easy.get(), CURLOPT_CAINFO_BLOB, &blob);
|
|
}
|
|
#endif
|
|
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_WRITEFUNCTION, write_callback);
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("adding write function", curl_code, callback);
|
|
return;
|
|
}
|
|
|
|
std::unique_ptr<NHttpClientLibCurlContext> curl_ctx(new NHttpClientLibCurlContext(callback, headers_list));
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_WRITEDATA, curl_ctx.get());
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("adding curl context", curl_code, callback);
|
|
return;
|
|
}
|
|
|
|
/* ask libcurl to show us the verbose output */
|
|
#ifdef CURL_DEBUG
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_VERBOSE, 1L);
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("setting verbose output", curl_code, callback);
|
|
return;
|
|
}
|
|
|
|
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_DEBUGFUNCTION, debug_callback);
|
|
if (curl_code != CURLE_OK) {
|
|
handle_curl_easy_set_opt_error("adding debug function", curl_code, callback);
|
|
return;
|
|
}
|
|
#endif
|
|
const std::lock_guard lock(_mutex);
|
|
|
|
CURLMcode curl_multi_code = curl_multi_add_handle(_curl_multi.get(), curl_easy.get());
|
|
|
|
if (curl_multi_code != CURLM_OK) {
|
|
auto response = std::shared_ptr<NHttpResponse>(new NHttpResponse());
|
|
response->errorMessage =
|
|
"curl_multi_add_handle() error while adding handle, code: " + std::string(curl_multi_strerror(curl_multi_code));
|
|
response->statusCode = -1;
|
|
callback(response);
|
|
return;
|
|
};
|
|
|
|
std::pair<std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>, std::unique_ptr<NHttpClientLibCurlContext>> pair(
|
|
std::move(curl_easy), std::move(curl_ctx));
|
|
_contexts.emplace_back(std::move(pair));
|
|
}
|
|
|
|
void NHttpClientLibCurl::setBaseUri(const std::string& uri) { _base_uri = uri; }
|
|
|
|
void NHttpClientLibCurl::setTimeout(std::chrono::milliseconds timeout) { _timeout = timeout; }
|
|
|
|
void NHttpClientLibCurl::tick() {
|
|
std::unique_lock lock(_mutex, std::try_to_lock);
|
|
if (!lock.owns_lock()) {
|
|
return; // return immediately, tick() is expected to get called again. no reason to make tick block.
|
|
}
|
|
|
|
int running_handles = 0;
|
|
CURLMcode mc = curl_multi_perform(_curl_multi.get(), &running_handles);
|
|
|
|
lock.unlock();
|
|
if (mc) {
|
|
// log but do not return -- still see if any pending messages
|
|
NLOG(Nakama::NLogLevel::Error, "curl_multi_perform() failed, code %d.\n", (int)mc);
|
|
}
|
|
|
|
struct CURLMsg* m = nullptr;
|
|
CURLcode result;
|
|
|
|
do {
|
|
int msgq = 0;
|
|
lock.lock();
|
|
m = curl_multi_info_read(_curl_multi.get(), &msgq);
|
|
lock.unlock();
|
|
|
|
if (m && (m->msg == CURLMSG_DONE)) {
|
|
lock.lock();
|
|
result = m->data.result; // cache here because when we remove the easy handle, m is invalidated.
|
|
CURL* e = m->easy_handle;
|
|
long response_code;
|
|
CURLcode curl_code = curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &response_code);
|
|
|
|
std::unique_ptr<NHttpClientLibCurlContext> context = NULL;
|
|
|
|
auto it = _contexts.begin();
|
|
while (it != _contexts.end()) {
|
|
if (it->first.get() == e) {
|
|
break;
|
|
}
|
|
|
|
it++;
|
|
}
|
|
|
|
if (it == _contexts.end()) {
|
|
NLOG(Nakama::NLogLevel::Error, "curl_multi_info_read() error: untracked easy handle.");
|
|
continue;
|
|
}
|
|
|
|
context = std::move(it->second);
|
|
|
|
mc = curl_multi_remove_handle(_curl_multi.get(), e);
|
|
if (mc) {
|
|
NLOG(Nakama::NLogLevel::Error, "curl_multi_remove_handle() failed, code %d.\n", (int)mc);
|
|
}
|
|
|
|
_contexts.remove(*it);
|
|
|
|
lock.unlock();
|
|
|
|
auto callback = context->get_callback();
|
|
if (callback != NULL) {
|
|
NLOG_DEBUG("invoking curl callback");
|
|
|
|
auto response = std::shared_ptr<NHttpResponse>(new NHttpResponse());
|
|
response->body = context->get_body();
|
|
|
|
if (result != CURLE_OK) {
|
|
NLOG(Nakama::NLogLevel::Error, "curl easy handle returned code: %d \n", (int)result);
|
|
response->statusCode = InternalStatusCodes::CONNECTION_ERROR;
|
|
} else {
|
|
response->statusCode = static_cast<int>(response_code);
|
|
|
|
if (curl_code != CURLE_OK) {
|
|
NLOG(
|
|
Nakama::NLogLevel::Error, "curl_easy_getinfo() failed when getting response code, code %d.\n",
|
|
(int)curl_code);
|
|
}
|
|
}
|
|
|
|
callback(response);
|
|
}
|
|
}
|
|
} while (m);
|
|
}
|
|
|
|
void NHttpClientLibCurl::cancelAllRequests() {
|
|
const std::lock_guard lock(_mutex);
|
|
for (auto it = _contexts.begin(); it != _contexts.end(); it++) {
|
|
NHttpResponsePtr responsePtr(new NHttpResponse());
|
|
|
|
responsePtr->statusCode = InternalStatusCodes::CANCELLED_BY_USER;
|
|
responsePtr->errorMessage = "cancelled by user";
|
|
|
|
it->second->get_callback()(responsePtr);
|
|
CURLMcode mc = curl_multi_remove_handle(_curl_multi.get(), it->first.get());
|
|
if (mc != CURLM_OK) {
|
|
NLOG(Nakama::NLogLevel::Error, "curl_multi_remove_handle() failed, code %d.\n", (int)mc);
|
|
}
|
|
}
|
|
|
|
_contexts.clear();
|
|
}
|
|
|
|
void NHttpClientLibCurl::handle_curl_easy_set_opt_error(
|
|
std::string action,
|
|
CURLcode code,
|
|
const NHttpResponseCallback& callback) {
|
|
auto response = std::shared_ptr<NHttpResponse>(new NHttpResponse());
|
|
response->errorMessage =
|
|
"curl_easy_getinfo() error while : " + action + ", code: " + std::string(curl_easy_strerror(code));
|
|
response->statusCode = -1;
|
|
callback(response);
|
|
}
|
|
|
|
}; // namespace Nakama
|