diff --git a/amxx/scripting/include/easy_http.inc b/amxx/scripting/include/easy_http.inc index b59b807..6bf87c7 100644 --- a/amxx/scripting/include/easy_http.inc +++ b/amxx/scripting/include/easy_http.inc @@ -55,11 +55,22 @@ enum EzHttpPluginEndBehaviour /** * Creates new options object. This object allows you to configure your request by specifying * such parameters as user agent, query parameters, headers, and etc. + * Options are reusable request templates. Their values are copied into a request when it is sent. * * @return EzHttpOptions handle. */ native EzHttpOptions:ezhttp_create_options(); +/** + * Destroys an options object previously created via ezhttp_create_options(). + * It is safe to destroy an options object after sending a request because the request keeps its own snapshot. + * + * @param options_id Options identifier created via ezhttp_create_options(). + * + * @return True if the options object existed and was destroyed, false otherwise. + */ +native bool:ezhttp_destroy_options(EzHttpOptions:options_id); + /** * Sets user-agent string for a request. * @@ -205,6 +216,7 @@ native ezhttp_option_set_auth(EzHttpOptions:options_id, const user[], const pass /** * Sets a custom request data for the HTTP request. + * The data is copied into the request when it is sent, so reusing or destroying the options later is safe. * * @param options_id Options identifier created via ezhttp_create_options(). * @param data The user data to set. @@ -546,6 +558,7 @@ native ezhttp_get_downloaded_bytes(EzHttpRequest:request_id); /** * Returns the custom data associated with the request set by ezhttp_option_set_user_data. + * This is the snapshot captured when the request was sent. * * @param request_id The request identifier. * @param data The buffer to store the user data. diff --git a/amxx_test/scripting/ez_http_mapchange_test.sma b/amxx_test/scripting/ez_http_mapchange_test.sma new file mode 100644 index 0000000..0ba10a3 --- /dev/null +++ b/amxx_test/scripting/ez_http_mapchange_test.sma @@ -0,0 +1,910 @@ +#include +#include + +#pragma semicolon 1 + +/* + * Integration test suite for requests that are still in flight during a map change. + * + * The suite persists its state to: + * addons/amxmodx/data/ezhttp_mapchange_test.ini + * + * By default it runs one scenario, changes the map, verifies that the server + * survived, sends a probe request on the next map, and only then advances to + * the next scenario. + * + * Useful server commands: + * ezhttp_mapchange_test_reset + * ezhttp_mapchange_test_start + */ + +#define PLUGIN_NAME "EasyHttp MapChange Test" +#define PLUGIN_VERSION "1.0" +#define PLUGIN_AUTHOR "Wilian M." + +#define STATE_FILE_PATH "addons/amxmodx/data/ezhttp_mapchange_test.ini" + +#define PHASE_IDLE "idle" +#define PHASE_VERIFY_WAIT "verify_wait" +#define PHASE_PROBE_PENDING "probe_pending" +#define PHASE_DONE "done" + +#define TASK_START_SCENARIO 4101 +#define TASK_PERFORM_CHANGELEVEL 4102 +#define TASK_VERIFY_AFTER_CHANGE 4103 +#define TASK_NEXT_SCENARIO 4104 +#define TASK_EXECUTE_CHANGELEVEL 4105 + +#define CALLBACK_KIND_INFLIGHT 1 +#define CALLBACK_KIND_PROBE 2 + +#define REQUEST_TIMEOUT_BUFFER_SEC 12 + +enum MapChangeScenarioId +{ + Scenario_CancelSingleMain = 0, + Scenario_ForgetSingleMain, + Scenario_CancelStressMain, + Scenario_ForgetStressMain, + Scenario_CancelSequentialQueue, + Scenario_ForgetSequentialQueue, + Scenario_Count +}; + +new const g_ScenarioNames[Scenario_Count][] = { + "cancel_single_main", + "forget_single_main", + "cancel_stress_main", + "forget_stress_main", + "cancel_sequential_queue", + "forget_sequential_queue" +}; + +new const EzHttpPluginEndBehaviour:g_ScenarioBehaviours[Scenario_Count] = { + EZH_CANCEL_REQUEST, + EZH_FORGET_REQUEST, + EZH_CANCEL_REQUEST, + EZH_FORGET_REQUEST, + EZH_CANCEL_REQUEST, + EZH_FORGET_REQUEST +}; + +new const g_ScenarioRequestCounts[Scenario_Count] = { + 1, + 1, + 6, + 6, + 4, + 4 +}; + +new const bool:g_ScenarioUsesQueue[Scenario_Count] = { + false, + false, + false, + false, + true, + true +}; + +new g_CvarAutostart; +new g_CvarBaseUrl; +new g_CvarChangeAfter; +new g_CvarVerifyWait; +new g_CvarNextScenarioDelay; +new g_CvarRequestDelay; +new g_CvarScenarioRepeats; +new g_CvarStressRequests; +new g_CvarQueueRequests; +new g_CvarProbeRequests; +new g_CvarTargetMap; + +new g_StatePhase[32]; +new g_StateScenarioName[64]; +new g_StateCurrentMap[64]; +new g_StateTargetMap[64]; +new g_StateLastResult[32]; +new g_StateLastError[192]; + +new g_StateScenarioIndex; +new g_StateScenarioRepeat; +new g_StatePassCount; +new g_StateFailCount; +new g_StateStartedRequests; +new g_StateDispatchFailures; +new g_StateUnexpectedCallbacks; +new bool:g_StateProbeOk; + +new g_RuntimeProbeExpected; +new g_RuntimeProbeCompleted; +new g_RuntimeProbeFailures; +new g_RuntimeProbeDispatchFailures; + +public plugin_init() +{ + register_plugin(PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_AUTHOR); + + g_CvarAutostart = register_cvar("ezhttp_mapchange_test_autostart", "1"); + g_CvarBaseUrl = register_cvar("ezhttp_mapchange_test_base_url", "https://httpbin.org"); + g_CvarChangeAfter = register_cvar("ezhttp_mapchange_test_change_after", "1.0"); + g_CvarVerifyWait = register_cvar("ezhttp_mapchange_test_verify_wait", "8.0"); + g_CvarNextScenarioDelay = register_cvar("ezhttp_mapchange_test_next_scenario_delay", "2.0"); + g_CvarRequestDelay = register_cvar("ezhttp_mapchange_test_request_delay", "6"); + g_CvarScenarioRepeats = register_cvar("ezhttp_mapchange_test_repeat", "1"); + g_CvarStressRequests = register_cvar("ezhttp_mapchange_test_stress_requests", "6"); + g_CvarQueueRequests = register_cvar("ezhttp_mapchange_test_queue_requests", "4"); + g_CvarProbeRequests = register_cvar("ezhttp_mapchange_test_probe_requests", "1"); + g_CvarTargetMap = register_cvar("ezhttp_mapchange_test_target_map", ""); + + register_srvcmd("ezhttp_mapchange_test_reset", "cmd_reset_state"); + register_srvcmd("ezhttp_mapchange_test_start", "cmd_start_suite"); +} + +public plugin_cfg() +{ + load_state(); + + if (equal(g_StatePhase, PHASE_VERIFY_WAIT) || equal(g_StatePhase, PHASE_PROBE_PENDING)) + { + announce_resume(); + set_task(get_pcvar_float(g_CvarVerifyWait), "begin_post_map_verification", TASK_VERIFY_AFTER_CHANGE); + return; + } + + if (equal(g_StatePhase, PHASE_DONE)) + { + print_suite_summary("suite already completed"); + return; + } + + if (g_StateScenarioIndex >= _:Scenario_Count) + { + finish_suite("scenario index is already complete"); + return; + } + + if (get_pcvar_num(g_CvarAutostart)) + { + server_print("[ez_http_mapchange_test] Starting suite at scenario %d (%s)", + g_StateScenarioIndex, + g_ScenarioNames[MapChangeScenarioId:g_StateScenarioIndex] + ); + set_task(2.0, "start_current_scenario", TASK_START_SCENARIO); + } + else + { + server_print("[ez_http_mapchange_test] Autostart disabled. Use server command 'ezhttp_mapchange_test_start'."); + } +} + +public plugin_end() +{ + log_amx("[ez_http_mapchange_test] plugin_end phase=%s scenario=%s", g_StatePhase, g_StateScenarioName); +} + +public cmd_reset_state() +{ + remove_runtime_tasks(); + reset_state(); + save_state(); + + server_print("[ez_http_mapchange_test] State reset. File: %s", STATE_FILE_PATH); + log_amx("[ez_http_mapchange_test] state reset"); +} + +public cmd_start_suite() +{ + remove_runtime_tasks(); + reset_state(); + save_state(); + + server_print("[ez_http_mapchange_test] Starting suite from the first scenario."); + log_amx("[ez_http_mapchange_test] suite started manually"); + + set_task(1.0, "start_current_scenario", TASK_START_SCENARIO); +} + +public start_current_scenario() +{ + if (g_StateScenarioIndex >= _:Scenario_Count) + { + finish_suite("all scenarios already processed"); + return; + } + + new MapChangeScenarioId:scenario = MapChangeScenarioId:g_StateScenarioIndex; + new EzHttpOptions:opt = ezhttp_create_options(); + new EzHttpQueue:queue_id = EzHttpQueue:0; + new url[256]; + new data[3]; + new request_count = get_request_count_for_scenario(scenario); + new request_delay = get_pcvar_num(g_CvarRequestDelay); + new repeat_total = get_scenario_repeat_count(); + + begin_scenario_state(scenario); + + ezhttp_option_set_plugin_end_behaviour(opt, g_ScenarioBehaviours[scenario]); + ezhttp_option_set_timeout(opt, (request_delay + REQUEST_TIMEOUT_BUFFER_SEC) * 1000); + ezhttp_option_set_connect_timeout(opt, 5000); + + if (g_ScenarioUsesQueue[scenario]) + { + queue_id = ezhttp_create_queue(); + ezhttp_option_set_queue(opt, queue_id); + } + + for (new i = 0; i < request_count; ++i) + { + build_inflight_url(url, charsmax(url), scenario, i); + + data[0] = CALLBACK_KIND_INFLIGHT; + data[1] = _:scenario; + data[2] = i; + + new EzHttpRequest:request_id = ezhttp_get( + url, + "mapchange_request_complete", + opt, + data, + sizeof(data) + ); + + if (request_id != EzHttpRequest:0) + { + ++g_StateStartedRequests; + } + else + { + ++g_StateDispatchFailures; + set_last_error_fmt("Dispatch failed for request slot %d in scenario %s", i, g_ScenarioNames[scenario]); + } + } + + save_state(); + + server_print( + "[ez_http_mapchange_test] Scenario %s repeat %d/%d armed: started=%d failed_dispatch=%d queue=%d behaviour=%d", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + repeat_total, + g_StateStartedRequests, + g_StateDispatchFailures, + _:queue_id, + _:g_ScenarioBehaviours[scenario] + ); + log_amx( + "[ez_http_mapchange_test] scenario=%s repeat=%d/%d started=%d dispatch_failures=%d use_queue=%d behaviour=%d", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + repeat_total, + g_StateStartedRequests, + g_StateDispatchFailures, + g_ScenarioUsesQueue[scenario], + _:g_ScenarioBehaviours[scenario] + ); + + if (g_StateStartedRequests == 0) + { + finalize_current_scenario(false, "no requests were dispatched"); + return; + } + + set_task(get_pcvar_float(g_CvarChangeAfter), "perform_map_change", TASK_PERFORM_CHANGELEVEL); +} + +public perform_map_change() +{ + if (!equal(g_StatePhase, PHASE_VERIFY_WAIT)) + return; + + resolve_target_map(g_StateTargetMap, charsmax(g_StateTargetMap)); + save_state(); + + server_print( + "[ez_http_mapchange_test] Changing map from %s to %s during scenario %s", + g_StateCurrentMap, + g_StateTargetMap, + g_StateScenarioName + ); + log_amx( + "[ez_http_mapchange_test] changelevel current=%s target=%s scenario=%s", + g_StateCurrentMap, + g_StateTargetMap, + g_StateScenarioName + ); + + /* + * Avoid forcing the map change synchronously from inside the current AMXX public. + * Let the function unwind first, then queue the actual changelevel command on a + * short follow-up task so AMXX does not keep stale public metadata on the stack. + */ + set_task(0.1, "execute_changelevel_command", TASK_EXECUTE_CHANGELEVEL); +} + +public execute_changelevel_command() +{ + if (!equal(g_StatePhase, PHASE_VERIFY_WAIT)) + return; + + server_cmd("changelevel %s", g_StateTargetMap); +} + +public begin_post_map_verification() +{ + if (g_StateScenarioIndex >= _:Scenario_Count) + { + finish_suite("verification resumed with no scenarios left"); + return; + } + + if (!equal(g_StatePhase, PHASE_VERIFY_WAIT) && !equal(g_StatePhase, PHASE_PROBE_PENDING)) + return; + + new MapChangeScenarioId:scenario = MapChangeScenarioId:g_StateScenarioIndex; + new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; + new data[3]; + new probe_count = get_probe_request_count(); + + g_RuntimeProbeExpected = 0; + g_RuntimeProbeCompleted = 0; + g_RuntimeProbeFailures = 0; + g_RuntimeProbeDispatchFailures = 0; + + ezhttp_option_set_timeout(opt, 10000); + ezhttp_option_set_connect_timeout(opt, 5000); + + copy(g_StatePhase, charsmax(g_StatePhase), PHASE_PROBE_PENDING); + save_state(); + + for (new i = 0; i < probe_count; ++i) + { + build_probe_url(url, charsmax(url), scenario, i); + + data[0] = CALLBACK_KIND_PROBE; + data[1] = _:scenario; + data[2] = i; + + new EzHttpRequest:request_id = ezhttp_get( + url, + "mapchange_request_complete", + opt, + data, + sizeof(data) + ); + + if (request_id == EzHttpRequest:0) + { + ++g_RuntimeProbeDispatchFailures; + set_last_error_fmt( + "Probe dispatch failed after map change for scenario %s slot=%d", + g_ScenarioNames[scenario], + i + ); + continue; + } + + ++g_RuntimeProbeExpected; + + server_print( + "[ez_http_mapchange_test] Probe %d/%d dispatched for scenario %s", + i + 1, + probe_count, + g_StateScenarioName + ); + log_amx( + "[ez_http_mapchange_test] probe dispatched scenario=%s repeat=%d/%d slot=%d request_id=%d", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + get_scenario_repeat_count(), + i, + _:request_id + ); + } + + if (g_RuntimeProbeExpected == 0) + { + finalize_current_scenario(false, "probe request dispatch failed after map change"); + return; + } +} + +public mapchange_request_complete(EzHttpRequest:request_id, const data[]) +{ + new callback_kind = data[0]; + new callback_scenario = data[1]; + new callback_slot = data[2]; + + if (callback_kind == CALLBACK_KIND_INFLIGHT) + { + ++g_StateUnexpectedCallbacks; + set_last_error_fmt( + "Unexpected in-flight callback in phase=%s scenario=%s callback_scenario=%d slot=%d err=%d", + g_StatePhase, + g_StateScenarioName, + callback_scenario, + callback_slot, + _:ezhttp_get_error_code(request_id) + ); + save_state(); + + server_print( + "[ez_http_mapchange_test] Unexpected in-flight callback: scenario=%s callback_scenario=%d slot=%d error=%d", + g_StateScenarioName, + callback_scenario, + callback_slot, + _:ezhttp_get_error_code(request_id) + ); + log_amx( + "[ez_http_mapchange_test] unexpected callback phase=%s scenario=%s callback_scenario=%d slot=%d error=%d", + g_StatePhase, + g_StateScenarioName, + callback_scenario, + callback_slot, + _:ezhttp_get_error_code(request_id) + ); + return; + } + + if (callback_kind != CALLBACK_KIND_PROBE) + { + set_last_error_fmt("Unknown callback kind %d", callback_kind); + finalize_current_scenario(false, "unknown callback kind received"); + return; + } + + if (callback_scenario != g_StateScenarioIndex) + { + set_last_error_fmt( + "Probe callback scenario mismatch: current=%d callback=%d", + g_StateScenarioIndex, + callback_scenario + ); + finalize_current_scenario(false, "probe callback scenario mismatch"); + return; + } + + if (!verify_probe_response(request_id, MapChangeScenarioId:callback_scenario, callback_slot)) + { + ++g_RuntimeProbeFailures; + } + + ++g_RuntimeProbeCompleted; + g_StateProbeOk = + g_RuntimeProbeCompleted == g_RuntimeProbeExpected && + g_RuntimeProbeFailures == 0 && + g_RuntimeProbeDispatchFailures == 0; + + if (g_RuntimeProbeCompleted < g_RuntimeProbeExpected) + return; + + finalize_current_scenario( + g_StateProbeOk, + g_StateProbeOk ? "all post-map probes succeeded" : "one or more post-map probes failed" + ); +} + +stock begin_scenario_state(MapChangeScenarioId:scenario) +{ + get_mapname(g_StateCurrentMap, charsmax(g_StateCurrentMap)); + resolve_target_map(g_StateTargetMap, charsmax(g_StateTargetMap)); + + copy(g_StatePhase, charsmax(g_StatePhase), PHASE_VERIFY_WAIT); + copy(g_StateScenarioName, charsmax(g_StateScenarioName), g_ScenarioNames[scenario]); + copy(g_StateLastResult, charsmax(g_StateLastResult), "running"); + g_StateLastError[0] = '^0'; + g_StateStartedRequests = 0; + g_StateDispatchFailures = 0; + g_StateUnexpectedCallbacks = 0; + g_StateProbeOk = false; + g_RuntimeProbeExpected = 0; + g_RuntimeProbeCompleted = 0; + g_RuntimeProbeFailures = 0; + g_RuntimeProbeDispatchFailures = 0; + + save_state(); +} + +stock bool:verify_probe_response(EzHttpRequest:request_id, MapChangeScenarioId:scenario, probe_slot) +{ + new EzHttpErrorCode:error_code = ezhttp_get_error_code(request_id); + if (error_code != EZH_OK) + { + set_last_error_fmt( + "Probe error for scenario %s slot=%d: err=%d", + g_ScenarioNames[scenario], + probe_slot, + _:error_code + ); + return false; + } + + new http_code = ezhttp_get_http_code(request_id); + if (http_code != 204) + { + new response_preview[160]; + ezhttp_get_data(request_id, response_preview, charsmax(response_preview)); + trim(response_preview); + + set_last_error_fmt( + "Probe HTTP code mismatch for scenario %s slot=%d: code=%d body=%s", + g_ScenarioNames[scenario], + probe_slot, + http_code, + response_preview + ); + return false; + } + + g_StateLastError[0] = '^0'; + return true; +} + +stock finalize_current_scenario(bool:probe_step_succeeded, const reason[]) +{ + new bool:scenario_passed = + probe_step_succeeded && + g_StateDispatchFailures == 0 && + g_StateUnexpectedCallbacks == 0 && + g_RuntimeProbeDispatchFailures == 0; + + copy(g_StateLastResult, charsmax(g_StateLastResult), scenario_passed ? "passed" : "failed"); + + if (!scenario_passed && !strlen(g_StateLastError)) + copy(g_StateLastError, charsmax(g_StateLastError), reason); + + if (scenario_passed) + ++g_StatePassCount; + else + ++g_StateFailCount; + + server_print( + "[ez_http_mapchange_test] Scenario %s repeat %d/%d %s. started=%d dispatch_failures=%d unexpected_callbacks=%d probe_ok=%d probes=%d/%d probe_failures=%d probe_dispatch_failures=%d", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + get_scenario_repeat_count(), + scenario_passed ? "PASSED" : "FAILED", + g_StateStartedRequests, + g_StateDispatchFailures, + g_StateUnexpectedCallbacks, + g_StateProbeOk, + g_RuntimeProbeCompleted, + g_RuntimeProbeExpected, + g_RuntimeProbeFailures, + g_RuntimeProbeDispatchFailures + ); + log_amx( + "[ez_http_mapchange_test] scenario=%s repeat=%d/%d result=%s started=%d dispatch_failures=%d unexpected_callbacks=%d probe_ok=%d probe_completed=%d probe_expected=%d probe_failures=%d probe_dispatch_failures=%d reason=%s error=%s", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + get_scenario_repeat_count(), + scenario_passed ? "passed" : "failed", + g_StateStartedRequests, + g_StateDispatchFailures, + g_StateUnexpectedCallbacks, + g_StateProbeOk, + g_RuntimeProbeCompleted, + g_RuntimeProbeExpected, + g_RuntimeProbeFailures, + g_RuntimeProbeDispatchFailures, + reason, + g_StateLastError + ); + + if (g_StateScenarioRepeat + 1 < get_scenario_repeat_count()) + { + ++g_StateScenarioRepeat; + } + else + { + g_StateScenarioRepeat = 0; + ++g_StateScenarioIndex; + } + + if (g_StateScenarioIndex >= _:Scenario_Count) + { + finish_suite(reason); + return; + } + + copy(g_StatePhase, charsmax(g_StatePhase), PHASE_IDLE); + save_state(); + + set_task(get_pcvar_float(g_CvarNextScenarioDelay), "start_current_scenario", TASK_NEXT_SCENARIO); +} + +stock finish_suite(const reason[]) +{ + remove_runtime_tasks(); + + copy(g_StatePhase, charsmax(g_StatePhase), PHASE_DONE); + g_StateTargetMap[0] = '^0'; + + if (!strlen(g_StateLastResult)) + copy(g_StateLastResult, charsmax(g_StateLastResult), "done"); + + save_state(); + print_suite_summary(reason); +} + +stock print_suite_summary(const reason[]) +{ + server_print("[ez_http_mapchange_test] Suite finished. pass=%d fail=%d reason=%s state=%s", + g_StatePassCount, + g_StateFailCount, + reason, + STATE_FILE_PATH + ); + log_amx( + "[ez_http_mapchange_test] suite finished pass=%d fail=%d reason=%s state=%s", + g_StatePassCount, + g_StateFailCount, + reason, + STATE_FILE_PATH + ); +} + +stock announce_resume() +{ + server_print( + "[ez_http_mapchange_test] Resuming after map change: scenario=%s repeat=%d/%d phase=%s pass=%d fail=%d", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + get_scenario_repeat_count(), + g_StatePhase, + g_StatePassCount, + g_StateFailCount + ); + log_amx( + "[ez_http_mapchange_test] resume scenario=%s repeat=%d/%d phase=%s pass=%d fail=%d", + g_StateScenarioName, + g_StateScenarioRepeat + 1, + get_scenario_repeat_count(), + g_StatePhase, + g_StatePassCount, + g_StateFailCount + ); +} + +stock remove_runtime_tasks() +{ + remove_task(TASK_START_SCENARIO); + remove_task(TASK_PERFORM_CHANGELEVEL); + remove_task(TASK_EXECUTE_CHANGELEVEL); + remove_task(TASK_VERIFY_AFTER_CHANGE); + remove_task(TASK_NEXT_SCENARIO); +} + +stock reset_state() +{ + copy(g_StatePhase, charsmax(g_StatePhase), PHASE_IDLE); + g_StateScenarioName[0] = '^0'; + g_StateCurrentMap[0] = '^0'; + g_StateTargetMap[0] = '^0'; + copy(g_StateLastResult, charsmax(g_StateLastResult), "not_started"); + g_StateLastError[0] = '^0'; + g_StateScenarioIndex = 0; + g_StateScenarioRepeat = 0; + g_StatePassCount = 0; + g_StateFailCount = 0; + g_StateStartedRequests = 0; + g_StateDispatchFailures = 0; + g_StateUnexpectedCallbacks = 0; + g_StateProbeOk = false; + g_RuntimeProbeExpected = 0; + g_RuntimeProbeCompleted = 0; + g_RuntimeProbeFailures = 0; + g_RuntimeProbeDispatchFailures = 0; +} + +stock load_state() +{ + reset_state(); + + if (!file_exists(STATE_FILE_PATH)) + return; + + new line[256]; + new key[64]; + new value[192]; + new line_no = 0; + new line_len = 0; + + while (read_file(STATE_FILE_PATH, line_no++, line, charsmax(line), line_len)) + { + trim(line); + + if (!line[0] || line[0] == ';' || line[0] == '#' || line[0] == '[') + continue; + + if (!split_key_value(line, key, charsmax(key), value, charsmax(value))) + continue; + + if (equal(key, "phase")) + copy(g_StatePhase, charsmax(g_StatePhase), value); + else if (equal(key, "scenario_name")) + copy(g_StateScenarioName, charsmax(g_StateScenarioName), value); + else if (equal(key, "current_map")) + copy(g_StateCurrentMap, charsmax(g_StateCurrentMap), value); + else if (equal(key, "target_map")) + copy(g_StateTargetMap, charsmax(g_StateTargetMap), value); + else if (equal(key, "last_result")) + copy(g_StateLastResult, charsmax(g_StateLastResult), value); + else if (equal(key, "last_error")) + copy(g_StateLastError, charsmax(g_StateLastError), value); + else if (equal(key, "scenario_index")) + g_StateScenarioIndex = str_to_num(value); + else if (equal(key, "scenario_repeat")) + g_StateScenarioRepeat = str_to_num(value); + else if (equal(key, "pass_count")) + g_StatePassCount = str_to_num(value); + else if (equal(key, "fail_count")) + g_StateFailCount = str_to_num(value); + else if (equal(key, "started_requests")) + g_StateStartedRequests = str_to_num(value); + else if (equal(key, "dispatch_failures")) + g_StateDispatchFailures = str_to_num(value); + else if (equal(key, "unexpected_callbacks")) + g_StateUnexpectedCallbacks = str_to_num(value); + else if (equal(key, "probe_ok")) + g_StateProbeOk = bool:str_to_num(value); + } +} + +stock save_state() +{ + new line[256]; + + delete_file(STATE_FILE_PATH); + + write_file(STATE_FILE_PATH, "[ez_http_mapchange_test]", -1); + + formatex(line, charsmax(line), "phase=%s", g_StatePhase); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "scenario_index=%d", g_StateScenarioIndex); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "scenario_repeat=%d", g_StateScenarioRepeat); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "scenario_name=%s", g_StateScenarioName); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "current_map=%s", g_StateCurrentMap); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "target_map=%s", g_StateTargetMap); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "pass_count=%d", g_StatePassCount); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "fail_count=%d", g_StateFailCount); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "started_requests=%d", g_StateStartedRequests); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "dispatch_failures=%d", g_StateDispatchFailures); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "unexpected_callbacks=%d", g_StateUnexpectedCallbacks); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "probe_ok=%d", _:g_StateProbeOk); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "last_result=%s", g_StateLastResult); + write_file(STATE_FILE_PATH, line, -1); + + formatex(line, charsmax(line), "last_error=%s", g_StateLastError); + write_file(STATE_FILE_PATH, line, -1); +} + +stock bool:split_key_value(line[], key[], key_len, value[], value_len) +{ + new pos = contain(line, "="); + if (pos < 0) + return false; + + line[pos] = '^0'; + + copy(key, key_len, line); + copy(value, value_len, line[pos + 1]); + + trim(key); + trim(value); + + return true; +} + +stock build_inflight_url(url[], max_len, MapChangeScenarioId:scenario, request_slot) +{ + new base_url[128]; + get_base_url(base_url, charsmax(base_url)); + + formatex( + url, + max_len, + "%s/delay/%d?scenario=%s&request=%d", + base_url, + get_pcvar_num(g_CvarRequestDelay), + g_ScenarioNames[scenario], + request_slot + ); +} + +stock build_probe_url(url[], max_len, MapChangeScenarioId:scenario, probe_slot) +{ + new base_url[128]; + get_base_url(base_url, charsmax(base_url)); + + formatex( + url, + max_len, + "%s/status/204?probe=%s&slot=%d", + base_url, + g_ScenarioNames[scenario], + probe_slot + ); +} + +stock get_request_count_for_scenario(MapChangeScenarioId:scenario) +{ + switch (scenario) + { + case Scenario_CancelSingleMain, Scenario_ForgetSingleMain: + return 1; + case Scenario_CancelStressMain, Scenario_ForgetStressMain: + { + new count = get_pcvar_num(g_CvarStressRequests); + return count > 0 ? count : g_ScenarioRequestCounts[scenario]; + } + case Scenario_CancelSequentialQueue, Scenario_ForgetSequentialQueue: + { + new count = get_pcvar_num(g_CvarQueueRequests); + return count > 0 ? count : g_ScenarioRequestCounts[scenario]; + } + } + + return g_ScenarioRequestCounts[scenario]; +} + +stock get_scenario_repeat_count() +{ + new count = get_pcvar_num(g_CvarScenarioRepeats); + return count > 0 ? count : 1; +} + +stock get_probe_request_count() +{ + new count = get_pcvar_num(g_CvarProbeRequests); + return count > 0 ? count : 1; +} + +stock get_base_url(buffer[], max_len) +{ + get_pcvar_string(g_CvarBaseUrl, buffer, max_len); + trim(buffer); + + new len = strlen(buffer); + while (len > 0 && buffer[len - 1] == '/') + { + buffer[--len] = '^0'; + } +} + +stock resolve_target_map(buffer[], max_len) +{ + get_pcvar_string(g_CvarTargetMap, buffer, max_len); + trim(buffer); + + if (!strlen(buffer)) + get_mapname(buffer, max_len); +} + +stock set_last_error_fmt(const fmt[], any:...) +{ + vformat(g_StateLastError, charsmax(g_StateLastError), fmt, 2); +} diff --git a/amxx_test/scripting/ez_http_test.sma b/amxx_test/scripting/ez_http_test.sma index f4a66dc..5f606e9 100644 --- a/amxx_test/scripting/ez_http_test.sma +++ b/amxx_test/scripting/ez_http_test.sma @@ -23,8 +23,12 @@ TEST_LIST_ASYNC = { { "test_auth", "test auth" }, { "test_save_to_file", "test save to file" }, { "test_fail_by_timeout", "test timeout" }, + { "test_options_reuse_concurrent", "test reusing the same options in concurrent requests" }, + { "test_user_data_snapshot", "test request keeps user data snapshot from dispatch time" }, + { "test_destroy_options_after_dispatch", "test options can be destroyed after dispatch" }, { "test_ftp_download", "test ftp download" }, { "test_ftp_download_wildcard", "test ftp download wildcard" }, + { "test_ftp_returns_request_id", "test ftp natives return a valid request id" }, { "test_ftp_upload", "test ftp upload" }, { "test_ftp_upload2", "test ftp upload by uri with special chars in credentials" }, TEST_LIST_END @@ -34,6 +38,96 @@ TEST_LIST_ASYNC = { #define FTP_WILDCARD_FILE_1 "ezhttp_test_ftp_wildcard/hello-2.12.tar.gz.sig" #define FTP_WILDCARD_FILE_2 "ezhttp_test_ftp_wildcard/hello-2.12.1.tar.gz.sig" #define FTP_WILDCARD_FILE_3 "ezhttp_test_ftp_wildcard/hello-2.12.2.tar.gz.sig" +#define FTP_RETURN_ID_FILE "ezhttp_test_ftp_download_return_id.txt" + +new g_CvarEzHttpTestAutostart; +new g_CvarEzHttpTestBaseUrl; + +new g_TestOptionsReuseConcurrent[_TestT]; +new g_TestOptionsReuseConcurrentCompleted = 0; +new bool:g_TestOptionsReuseConcurrentFinished = false; + +new g_TestUserDataSnapshot[_TestT]; +new g_TestUserDataSnapshotCompleted = 0; +new bool:g_TestUserDataSnapshotFinished = false; + +new bool:g_TestDestroyOptionsAfterDispatchDestroyed = false; +new EzHttpRequest:g_TestDestroyOptionsAfterDispatchRequestId = EzHttpRequest:0; + +new bool:g_TestFtpReturnsRequestIdExistsAtDispatch = false; +new EzHttpRequest:g_TestFtpReturnsRequestIdRequestId = EzHttpRequest:0; + +stock copy_test_state(dest[_TestT], const source[_TestT]) +{ + for (new i = 0; i < _TestT; ++i) + dest[i] = source[i]; +} + +stock finish_async_aggregate_test(test[_TestT]) +{ + utest_run_async_one_test_end(test, g_utlist_async); + utest_run_async_one_test_begin(g_utlist_async); +} + +stock bool:prepare_json_response(test[_TestT], EzHttpRequest:request_id, &EzJSON:json_root, line, const fail_message[] = "request must succeed") +{ + json_root = EzInvalid_JSON; + + new EzHttpErrorCode:error_code = ezhttp_get_error_code(request_id); + _test_assert(test, error_code == EZH_OK, line, fail_message); + if (error_code != EZH_OK) + return false; + + json_root = ezhttp_parse_json_response(request_id); + if (json_root == EzInvalid_JSON) + log_invalid_json_response(request_id, line); + + _test_assert(test, json_root != EzInvalid_JSON, line, "response must be valid JSON"); + + return json_root != EzInvalid_JSON; +} + +stock log_invalid_json_response(EzHttpRequest:request_id, line) +{ + new http_code = ezhttp_get_http_code(request_id); + new content_type[96]; + new response_preview[192]; + + if (!ezhttp_get_headers(request_id, "Content-Type", content_type, charsmax(content_type))) + copy(content_type, charsmax(content_type), ""); + + ezhttp_get_data(request_id, response_preview, charsmax(response_preview)); + trim(response_preview); + + log_amx( + "[ez_http_test] invalid json at line=%d http_code=%d content_type=%s body=%s", + line, + http_code, + content_type, + response_preview + ); + server_print( + "[ez_http_test] invalid json at line=%d http_code=%d content_type=%s body=%s", + line, + http_code, + content_type, + response_preview + ); +} + +stock build_test_url(url[], max_len, const path[]) +{ + get_pcvar_string(g_CvarEzHttpTestBaseUrl, url, max_len); + trim(url); + + new len = strlen(url); + while (len > 0 && url[len - 1] == '/') + { + url[--len] = '^0'; + } + + formatex(url[len], max_len - len, "%s", path); +} stock cleanup_ftp_wildcard_download_dir() { @@ -45,9 +139,13 @@ stock cleanup_ftp_wildcard_download_dir() public plugin_init() { - register_plugin("EasyHttp Test", "Polarhigh", "1.1"); + register_plugin("EasyHttp Test", "Polarhigh", "1.2"); + + g_CvarEzHttpTestAutostart = register_cvar("ezhttp_test_autostart", "1"); + g_CvarEzHttpTestBaseUrl = register_cvar("ezhttp_test_base_url", "https://httpbin.org"); - set_task(2.0, "run_tests"); + if (get_pcvar_num(g_CvarEzHttpTestAutostart)) + set_task(2.0, "run_tests"); } public plugin_end() @@ -63,13 +161,15 @@ public run_tests() START_ASYNC_TEST(test_get_parameters) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_add_url_parameter(opt, "MyParam1", "ParamVal1"); ezhttp_option_add_url_parameter(opt, "MyParam2", "ParamVal2"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_get("https://httpbin.org/get", "test_get_parameters_complete", opt); + build_test_url(url, charsmax(url), "/get"); + ezhttp_get(url, "test_get_parameters_complete", opt); } public test_get_parameters_complete(EzHttpRequest:request_id) @@ -78,7 +178,13 @@ public test_get_parameters_complete(EzHttpRequest:request_id) new tmp_data[256]; - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + new EzJSON:json_args = ezjson_object_get_value(json_root, "args"); server_print("test_get_parameters request elapsed: %f", ezhttp_get_elapsed(request_id)); @@ -104,11 +210,13 @@ public test_get_parameters_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_fail_by_timeout) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_timeout(opt, 2 * 1000); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_get("https://httpbin.org/delay/5", "test_get_fail_by_timeout_complete", opt); + build_test_url(url, charsmax(url), "/delay/5"); + ezhttp_get(url, "test_get_fail_by_timeout_complete", opt); } public test_get_fail_by_timeout_complete(EzHttpRequest:request_id) @@ -126,23 +234,187 @@ public test_get_fail_by_timeout_complete(EzHttpRequest:request_id) END_ASYNC_TEST() } +START_ASYNC_TEST(test_options_reuse_concurrent) +{ + copy_test_state(g_TestOptionsReuseConcurrent, __test); + g_TestOptionsReuseConcurrentCompleted = 0; + g_TestOptionsReuseConcurrentFinished = false; + + new EzHttpOptions:opt = ezhttp_create_options(); + ezhttp_option_set_header(opt, "X-Reused-Option", "shared-header"); + + new data_a[1] = { 1 }; + new data_b[1] = { 2 }; + new url_a[256]; + new url_b[256]; + + build_test_url(url_a, charsmax(url_a), "/delay/1?request=reuse_a"); + build_test_url(url_b, charsmax(url_b), "/delay/1?request=reuse_b"); + + new EzHttpRequest:request_a = ezhttp_get( + url_a, + "test_options_reuse_concurrent_complete", + opt, + data_a, + sizeof(data_a) + ); + new EzHttpRequest:request_b = ezhttp_get( + url_b, + "test_options_reuse_concurrent_complete", + opt, + data_b, + sizeof(data_b) + ); + + _test_assert(g_TestOptionsReuseConcurrent, request_a != EzHttpRequest:0, __LINE__, "first request id must not be zero"); + _test_assert(g_TestOptionsReuseConcurrent, request_b != EzHttpRequest:0, __LINE__, "second request id must not be zero"); + + if (request_a == EzHttpRequest:0 || request_b == EzHttpRequest:0) + { + g_TestOptionsReuseConcurrentFinished = true; + finish_async_aggregate_test(g_TestOptionsReuseConcurrent); + } +} + +public test_options_reuse_concurrent_complete(EzHttpRequest:request_id, const data[]) +{ + if (g_TestOptionsReuseConcurrentFinished) + return; + + new EzJSON:json_root; + if (!prepare_json_response(g_TestOptionsReuseConcurrent, request_id, json_root, __LINE__)) + { + if (++g_TestOptionsReuseConcurrentCompleted == 2 && !g_TestOptionsReuseConcurrentFinished) + { + g_TestOptionsReuseConcurrentFinished = true; + finish_async_aggregate_test(g_TestOptionsReuseConcurrent); + } + return; + } + + new EzJSON:json_args = ezjson_object_get_value(json_root, "args"); + new EzJSON:json_headers = ezjson_object_get_value(json_root, "headers"); + + new request_name[64]; + new header_value[64]; + ezjson_object_get_string(json_args, "request", request_name, charsmax(request_name)); + ezjson_object_get_string(json_headers, "X-Reused-Option", header_value, charsmax(header_value)); + + _test_assert(g_TestOptionsReuseConcurrent, equal(header_value, "shared-header"), __LINE__, "reused options header was not preserved"); + + switch (data[0]) + { + case 1: _test_assert(g_TestOptionsReuseConcurrent, equal(request_name, "reuse_a"), __LINE__, "first callback returned unexpected request marker"); + case 2: _test_assert(g_TestOptionsReuseConcurrent, equal(request_name, "reuse_b"), __LINE__, "second callback returned unexpected request marker"); + default: _test_assert(g_TestOptionsReuseConcurrent, false, __LINE__, "unexpected callback payload"); + } + + ezjson_free(json_headers); + ezjson_free(json_args); + ezjson_free(json_root); + + if (++g_TestOptionsReuseConcurrentCompleted == 2 && !g_TestOptionsReuseConcurrentFinished) + { + g_TestOptionsReuseConcurrentFinished = true; + finish_async_aggregate_test(g_TestOptionsReuseConcurrent); + } +} + +START_ASYNC_TEST(test_user_data_snapshot) +{ + copy_test_state(g_TestUserDataSnapshot, __test); + g_TestUserDataSnapshotCompleted = 0; + g_TestUserDataSnapshotFinished = false; + + new EzHttpOptions:opt = ezhttp_create_options(); + new url_a[256]; + new url_b[256]; + + new user_data_a[1] = { 111 }; + new request_data_a[1] = { 1 }; + ezhttp_option_set_user_data(opt, user_data_a, sizeof(user_data_a)); + build_test_url(url_a, charsmax(url_a), "/delay/1?request=user_data_a"); + + new EzHttpRequest:request_a = ezhttp_get( + url_a, + "test_user_data_snapshot_complete", + opt, + request_data_a, + sizeof(request_data_a) + ); + + new user_data_b[1] = { 222 }; + new request_data_b[1] = { 2 }; + ezhttp_option_set_user_data(opt, user_data_b, sizeof(user_data_b)); + build_test_url(url_b, charsmax(url_b), "/delay/1?request=user_data_b"); + + new EzHttpRequest:request_b = ezhttp_get( + url_b, + "test_user_data_snapshot_complete", + opt, + request_data_b, + sizeof(request_data_b) + ); + + _test_assert(g_TestUserDataSnapshot, request_a != EzHttpRequest:0, __LINE__, "first request id must not be zero"); + _test_assert(g_TestUserDataSnapshot, request_b != EzHttpRequest:0, __LINE__, "second request id must not be zero"); + + if (request_a == EzHttpRequest:0 || request_b == EzHttpRequest:0) + { + g_TestUserDataSnapshotFinished = true; + finish_async_aggregate_test(g_TestUserDataSnapshot); + } +} + +public test_user_data_snapshot_complete(EzHttpRequest:request_id, const data[]) +{ + if (g_TestUserDataSnapshotFinished) + return; + + _test_assert(g_TestUserDataSnapshot, ezhttp_get_error_code(request_id) == EZH_OK, __LINE__, "request must succeed"); + + new request_user_data[1]; + ezhttp_get_user_data(request_id, request_user_data); + + switch (data[0]) + { + case 1: _test_assert(g_TestUserDataSnapshot, request_user_data[0] == 111, __LINE__, "first request lost its original user data snapshot"); + case 2: _test_assert(g_TestUserDataSnapshot, request_user_data[0] == 222, __LINE__, "second request did not receive updated user data snapshot"); + default: _test_assert(g_TestUserDataSnapshot, false, __LINE__, "unexpected callback payload"); + } + + if (++g_TestUserDataSnapshotCompleted == 2 && !g_TestUserDataSnapshotFinished) + { + g_TestUserDataSnapshotFinished = true; + finish_async_aggregate_test(g_TestUserDataSnapshot); + } +} + START_ASYNC_TEST(test_post_form) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_add_form_payload(opt, "MyFormEntry1", "FormVal1"); ezhttp_option_add_form_payload(opt, "MyFormEntry2", "FormVal2"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_post("https://httpbin.org/post", "test_post_form_complete", opt); + build_test_url(url, charsmax(url), "/post"); + ezhttp_post(url, "test_post_form_complete", opt); } public test_post_form_complete(EzHttpRequest:request_id) { EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + new EzJSON:json_form = ezjson_object_get_value(json_root, "form"); server_print("test_post_form request elapsed: %f", ezhttp_get_elapsed(request_id)); @@ -170,13 +442,15 @@ public test_post_form_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_post_body) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_body(opt, "MyBody"); ezhttp_option_set_header(opt, "Content-Type", "text/plain"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_post("https://httpbin.org/post", "test_post_body_complete", opt); + build_test_url(url, charsmax(url), "/post"); + ezhttp_post(url, "test_post_body_complete", opt); } public test_post_body_complete(EzHttpRequest:request_id) @@ -185,7 +459,12 @@ public test_post_body_complete(EzHttpRequest:request_id) server_print("test_post_body request elapsed: %f", ezhttp_get_elapsed(request_id)); - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } // asserts @@ -203,6 +482,7 @@ public test_post_body_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_post_body_json) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; new EzJSON:json_root = ezjson_init_object(); ezjson_object_set_string(json_root, "StringField", "TestValue"); @@ -214,14 +494,21 @@ START_ASYNC_TEST(test_post_body_json) EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_post("https://httpbin.org/anything", "test_post_body_json_complete", opt); + build_test_url(url, charsmax(url), "/anything"); + ezhttp_post(url, "test_post_body_json_complete", opt); } public test_post_body_json_complete(EzHttpRequest:request_id) { EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + new EzJSON:json_data = ezjson_object_get_value(json_root, "json"); server_print("test_post_body_json request elapsed: %f", ezhttp_get_elapsed(request_id)); @@ -244,18 +531,26 @@ public test_post_body_json_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_user_agent) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_user_agent(opt, "Easy HTTP User-Agent"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_post("https://httpbin.org/post", "test_user_agent_complete", opt); + build_test_url(url, charsmax(url), "/post"); + ezhttp_post(url, "test_user_agent_complete", opt); } public test_user_agent_complete(EzHttpRequest:request_id) { EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + new EzJSON:json_headers = ezjson_object_get_value(json_root, "headers"); server_print("test_user_agent request elapsed: %f", ezhttp_get_elapsed(request_id)); @@ -277,19 +572,27 @@ public test_user_agent_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_headers) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_header(opt, "Myheader1", "HeaderVal1"); ezhttp_option_set_header(opt, "Myheader2", "HeaderVal2"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_post("https://httpbin.org/post", "test_headers_complete", opt); + build_test_url(url, charsmax(url), "/post"); + ezhttp_post(url, "test_headers_complete", opt); } public test_headers_complete(EzHttpRequest:request_id) { EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + new EzJSON:json_headers = ezjson_object_get_value(json_root, "headers"); server_print("test_headers request elapsed: %f", ezhttp_get_elapsed(request_id)); @@ -315,20 +618,28 @@ public test_headers_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_cookies) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_cookie(opt, "Mycookie1", "CookieVal1"); ezhttp_option_set_cookie(opt, "Mycookie2", "CookieVal20"); ezhttp_option_set_cookie(opt, "Mycookie2", "CookieVal2"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_get("https://httpbin.org/cookies", "test_cookies_complete", opt); + build_test_url(url, charsmax(url), "/cookies"); + ezhttp_get(url, "test_cookies_complete", opt); } public test_cookies_complete(EzHttpRequest:request_id) { EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + new EzJSON:json_cookies = ezjson_object_get_value(json_root, "cookies"); server_print("test_cookies request elapsed: %f", ezhttp_get_elapsed(request_id)); @@ -354,13 +665,15 @@ public test_cookies_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_save_to_file) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_body(opt, "MyBody"); ezhttp_option_set_header(opt, "Content-Type", "text/plain"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_get("https://httpbin.org/robots.txt", "test_save_to_file_complete", opt); + build_test_url(url, charsmax(url), "/robots.txt"); + ezhttp_get(url, "test_save_to_file_complete", opt); } public test_save_to_file_complete(EzHttpRequest:request_id) @@ -391,11 +704,13 @@ public test_save_to_file_complete(EzHttpRequest:request_id) START_ASYNC_TEST(test_auth) { new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; ezhttp_option_set_auth(opt, "user1", "pswd1"); EZHTTP_OPTION_SET_TEST_DATA(opt) - ezhttp_get("https://httpbin.org/basic-auth/user1/pswd1", "test_auth_complete", opt); + build_test_url(url, charsmax(url), "/basic-auth/user1/pswd1"); + ezhttp_get(url, "test_auth_complete", opt); } public test_auth_complete(EzHttpRequest:request_id) @@ -404,7 +719,12 @@ public test_auth_complete(EzHttpRequest:request_id) server_print("test_auth_complete request elapsed: %f", ezhttp_get_elapsed(request_id)); - new EzJSON:json_root = ezhttp_parse_json_response(request_id); + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } // asserts @@ -446,6 +766,62 @@ public test_ftp_download_complete(EzHttpRequest:request_id) END_ASYNC_TEST() } +START_ASYNC_TEST(test_destroy_options_after_dispatch) +{ + new EzHttpOptions:opt = ezhttp_create_options(); + new url[256]; + ezhttp_option_set_header(opt, "X-Destroyed-Option", "still-works"); + + EZHTTP_OPTION_SET_TEST_DATA(opt) + + build_test_url(url, charsmax(url), "/delay/1?request=destroy_after_dispatch"); + g_TestDestroyOptionsAfterDispatchRequestId = ezhttp_get( + url, + "test_destroy_options_after_dispatch_complete", + opt + ); + g_TestDestroyOptionsAfterDispatchDestroyed = ezhttp_destroy_options(opt); + + ASSERT_INT_NEQ_MSG(EzHttpRequest:0, g_TestDestroyOptionsAfterDispatchRequestId, "request id must not be zero"); + + if (g_TestDestroyOptionsAfterDispatchRequestId == EzHttpRequest:0) + { + END_ASYNC_TEST() + } +} + +public test_destroy_options_after_dispatch_complete(EzHttpRequest:request_id) +{ + EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) + + ASSERT_TRUE_MSG(g_TestDestroyOptionsAfterDispatchDestroyed, "options must be destroyable after dispatch"); + ASSERT_INT_EQ(g_TestDestroyOptionsAfterDispatchRequestId, request_id); + + new EzJSON:json_root; + if (!prepare_json_response(__test, request_id, json_root, __LINE__)) + { + END_ASYNC_TEST() + return; + } + + new EzJSON:json_args = ezjson_object_get_value(json_root, "args"); + new EzJSON:json_headers = ezjson_object_get_value(json_root, "headers"); + + new request_name[64]; + new header_value[64]; + ezjson_object_get_string(json_args, "request", request_name, charsmax(request_name)); + ezjson_object_get_string(json_headers, "X-Destroyed-Option", header_value, charsmax(header_value)); + + ASSERT_STRING_EQ(request_name, "destroy_after_dispatch"); + ASSERT_STRING_EQ(header_value, "still-works"); + + ezjson_free(json_headers); + ezjson_free(json_args); + ezjson_free(json_root); + + END_ASYNC_TEST() +} + START_ASYNC_TEST(test_ftp_download_wildcard) { new EzHttpOptions:opt = ezhttp_create_options(); @@ -472,6 +848,50 @@ public test_ftp_download_wildcard_complete(EzHttpRequest:request_id) END_ASYNC_TEST() } +START_ASYNC_TEST(test_ftp_returns_request_id) +{ + new EzHttpOptions:opt = ezhttp_create_options(); + + delete_file(FTP_RETURN_ID_FILE); + EZHTTP_OPTION_SET_TEST_DATA(opt) + + g_TestFtpReturnsRequestIdRequestId = ezhttp_ftp_download2( + "ftp://demo:password@test.rebex.net/readme.txt", + FTP_RETURN_ID_FILE, + "test_ftp_returns_request_id_complete", + EZH_UNSECURE, + opt + ); + g_TestFtpReturnsRequestIdExistsAtDispatch = + g_TestFtpReturnsRequestIdRequestId != EzHttpRequest:0 && + ezhttp_is_request_exists(g_TestFtpReturnsRequestIdRequestId); + + ASSERT_INT_NEQ_MSG(EzHttpRequest:0, g_TestFtpReturnsRequestIdRequestId, "ftp request id must not be zero"); + + if (g_TestFtpReturnsRequestIdRequestId == EzHttpRequest:0) + { + END_ASYNC_TEST() + } +} + +public test_ftp_returns_request_id_complete(EzHttpRequest:request_id) +{ + const expected_file_size = 379; + + EZHTTP_OPTION_EXTRACT_TEST_DATA(request_id) + + ASSERT_INT_NEQ(EzHttpRequest:0, g_TestFtpReturnsRequestIdRequestId); + ASSERT_TRUE_MSG(g_TestFtpReturnsRequestIdExistsAtDispatch, "returned ftp request id must exist after dispatch"); + ASSERT_INT_EQ(g_TestFtpReturnsRequestIdRequestId, request_id); + ASSERT_INT_EQ(EzHttpErrorCode:EZH_OK, ezhttp_get_error_code(request_id)); + + new size = filesize(FTP_RETURN_ID_FILE); + ASSERT_INT_EQ_MSG(expected_file_size, size, fmt("expected %d but was %d", expected_file_size, size)); + + delete_file(FTP_RETURN_ID_FILE); + END_ASYNC_TEST() +} + START_ASYNC_TEST(test_ftp_upload) { new EzHttpOptions:opt = ezhttp_create_options(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 147faf4..bd0dccb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,6 +43,8 @@ add_library(${TARGET_NAME} ${LIBRARY_BUILD_TYPE} easy_http/datetime_service/DateTimeService.cpp easy_http/datetime_service/DateTimeService.h utils/ContainerWithHandles.h + utils/TraceLog.cpp + utils/TraceLog.h utils/ftp_utils.h utils/ftp_utils.cpp utils/string_utils.h diff --git a/src/EasyHttpModule.cpp b/src/EasyHttpModule.cpp index aa11503..f2981fd 100644 --- a/src/EasyHttpModule.cpp +++ b/src/EasyHttpModule.cpp @@ -1,5 +1,6 @@ #include "EasyHttpModule.h" #include "easy_http/EasyHttp.h" +#include "utils/TraceLog.h" #include using namespace ezhttp; @@ -9,57 +10,91 @@ EasyHttpModule::EasyHttpModule(std::string ca_cert_path) : { // as this is a first insertion in queue then these EasyHttps will have QueueId == 1 and therefore QueueId == QueueId::Main CreateQueue(); + ezhttp::trace::Writef("EasyHttpModule", "ctor this=%p main_queue_created queues=%zu", this, easy_http_pack_.size()); } EasyHttpModule::~EasyHttpModule() { - ResetMainAndRemoveUsersQueues(); + ezhttp::trace::Writef("EasyHttpModule", "dtor begin this=%p forgotten=%zu requests=%zu queues=%zu", this, forgotten_easy_http_.size(), requests_.size(), easy_http_pack_.size()); + ShutdownWithoutCallbacks(); while (!forgotten_easy_http_.empty()) RunCleanupFrameForForgottenEasyHttp(); + + ezhttp::trace::Writef("EasyHttpModule", "dtor end this=%p", this); } void EasyHttpModule::RunFrame() { RunFrameEasyHttp(); RunCleanupFrameForForgottenEasyHttp(); + CleanupCompletedForgottenRequests(); } void EasyHttpModule::ServerDeactivate() { - ResetMainAndRemoveUsersQueues(); + ezhttp::trace::Writef("EasyHttpModule", "ServerDeactivate enter requests=%zu queues=%zu forgotten=%zu options=%zu", requests_.size(), easy_http_pack_.size(), forgotten_easy_http_.size(), options_.size()); + ResetForMapChangeWithoutCallbacks(); + ezhttp::trace::Writef("EasyHttpModule", "ServerDeactivate exit requests=%zu queues=%zu forgotten=%zu options=%zu", requests_.size(), easy_http_pack_.size(), forgotten_easy_http_.size(), options_.size()); } -RequestId EasyHttpModule::SendRequest(RequestMethod method, const std::string& url, OptionsId options_id, const ModuleRequestCallback& callback) +RequestId EasyHttpModule::SendRequest(RequestMethod method, const std::string& url, OptionsData options, int callback_id, std::unique_ptr callback_data, int callback_data_len) { - if (options_id == OptionsId::Null) - options_id = CreateOptions(); - OptionsData& options = GetOptions(options_id); - RequestId request_id = requests_.Add(RequestData()); RequestData& request = GetRequest(request_id); + const uint32_t request_generation = ++next_request_generation_; + + request.generation = request_generation; + request.user_data = options.user_data; + request.callback_data = std::move(callback_data); + request.callback_data_len = callback_data_len; + request.callback_id = callback_id; QueueId queue_id = options.queue_id; if (!IsQueueExists(queue_id)) // not so good, error suppression is going on, need to fix this in future queue_id = QueueId::Main; auto& easy_http = GetEasyHttp(queue_id, options.plugin_end_behaviour); + const RequestOptions request_options = options.options_builder.BuildOptions(); + + EasyHttpInterface::ResponseCallback cb_proxy = [this, request_id, request_generation](const Response& response) { + ezhttp::trace::Writef("EasyHttpModule", "callback enter request=%d generation=%u", static_cast(request_id), request_generation); + if (!IsRequestExists(request_id)) + { + ezhttp::trace::Writef("EasyHttpModule", "callback skip missing request=%d generation=%u", static_cast(request_id), request_generation); + return; + } + + RequestData& current_request = GetRequest(request_id); + if (current_request.generation != request_generation) + { + ezhttp::trace::Writef("EasyHttpModule", "callback skip generation mismatch request=%d current=%u expected=%u", static_cast(request_id), current_request.generation, request_generation); + return; + } - EasyHttpInterface::ResponseCallback cb_proxy = [this, request_id, callback](const Response& response) { - GetRequest(request_id).response = response; - callback(request_id); + current_request.response = response; + ezhttp::trace::Writef("EasyHttpModule", "callback finalize request=%d generation=%u status=%ld error=%d", static_cast(request_id), request_generation, response.status_code, static_cast(response.error.code)); + FinalizeRequest(request_id); }; - request.request_control = easy_http->SendRequest(method, url, options.options_builder.BuildOptions(), cb_proxy); - request.options_id = options_id; + request.request_control = easy_http->SendRequest(method, url, request_options, cb_proxy); + + ezhttp::trace::Writef( + "EasyHttpModule", + "SendRequest request=%d generation=%u queue=%d behaviour=%d callback_id=%d control=%p url=%s", + static_cast(request_id), + request_generation, + static_cast(queue_id), + static_cast(options.plugin_end_behaviour), + callback_id, + request.request_control.get(), + url.c_str() + ); return request_id; } -bool EasyHttpModule::DeleteRequest(RequestId handle, bool delete_related_options) +bool EasyHttpModule::DeleteRequest(RequestId handle) { - if (delete_related_options) - DeleteOptions(GetRequest(handle).options_id); - return requests_.Remove(handle); } @@ -73,40 +108,137 @@ bool EasyHttpModule::DeleteOptions(OptionsId handle) return options_.Remove(handle); } +void EasyHttpModule::FinalizeRequest(RequestId handle) +{ + if (!IsRequestExists(handle)) + return; + + RequestData& request = GetRequest(handle); + ezhttp::trace::Writef("EasyHttpModule", "FinalizeRequest request=%d callback_id=%d control=%p", static_cast(handle), request.callback_id, request.request_control.get()); + if (request.callback_id != -1) + { + if (request.callback_data) + MF_ExecuteForward(request.callback_id, handle, MF_PrepareCellArray(request.callback_data.get(), request.callback_data_len)); + else + MF_ExecuteForward(request.callback_id, handle); + + MF_UnregisterSPForward(request.callback_id); + request.callback_id = -1; + } + + DeleteRequest(handle); + ezhttp::trace::Writef("EasyHttpModule", "FinalizeRequest done request=%d", static_cast(handle)); +} + +void EasyHttpModule::CleanupCompletedForgottenRequests() +{ + for (auto it = requests_.begin(); it != requests_.end();) + { + auto& request = it->second; + if (!request.request_control || !request.request_control->completed.load() || !request.request_control->forgotten.load()) + { + ++it; + continue; + } + + if (request.callback_id != -1) + MF_UnregisterSPForward(request.callback_id); + + ezhttp::trace::Writef("EasyHttpModule", "CleanupCompletedForgottenRequests remove request=%d control=%p", static_cast(it->first), request.request_control.get()); + it = requests_.Remove(it); + } +} -void EasyHttpModule::ResetMainAndRemoveUsersQueues() +void EasyHttpModule::ShutdownWithoutCallbacks() { - for (auto it = easy_http_pack_.begin(); it != easy_http_pack_.end(); ) + ezhttp::trace::Writef("EasyHttpModule", "ShutdownWithoutCallbacks begin forgotten=%zu queues=%zu requests=%zu options=%zu", forgotten_easy_http_.size(), easy_http_pack_.size(), requests_.size(), options_.size()); + for (auto& pack_kv : easy_http_pack_) { - auto& terminating_ez = it->second.terminating_easy_http; - auto& forgettable_ez = it->second.forgettable_easy_http; + auto& terminating_ez = pack_kv.second.terminating_easy_http; + auto& forgettable_ez = pack_kv.second.forgettable_easy_http; if (terminating_ez) { - terminating_ez->CancelAllRequests(); - - if (it->first == QueueId::Main) - terminating_ez = std::make_unique(ca_cert_path_, kMainQueueThreads); - else - terminating_ez = nullptr; + terminating_ez->ForgetAllRequests(); + forgotten_easy_http_.emplace_back(std::move(terminating_ez)); } if (forgettable_ez) { forgettable_ez->ForgetAllRequests(); forgotten_easy_http_.emplace_back(std::move(forgettable_ez)); + } + } - if (it->first == QueueId::Main) - forgettable_ez = std::make_unique(ca_cert_path_, kMainQueueThreads); - else - forgettable_ez = nullptr; + for (auto& request_kv : requests_) + { + request_kv.second.callback_id = -1; + } + + easy_http_pack_.clear(); + requests_.clear(); + options_.clear(); + ezhttp::trace::Writef("EasyHttpModule", "ShutdownWithoutCallbacks end forgotten=%zu queues=%zu requests=%zu options=%zu", forgotten_easy_http_.size(), easy_http_pack_.size(), requests_.size(), options_.size()); +} + + +void EasyHttpModule::ResetForMapChangeWithoutCallbacks() +{ + ezhttp::trace::Writef("EasyHttpModule", "ResetForMapChangeWithoutCallbacks begin forgotten=%zu queues=%zu requests=%zu options=%zu", forgotten_easy_http_.size(), easy_http_pack_.size(), requests_.size(), options_.size()); + for (auto& request_kv : requests_) + { + ezhttp::trace::Writef( + "EasyHttpModule", + "mapchange forget request=%d callback_id=%d control=%p", + static_cast(request_kv.first), + request_kv.second.callback_id, + request_kv.second.request_control.get() + ); + if (request_kv.second.callback_id != -1) + { + MF_UnregisterSPForward(request_kv.second.callback_id); + request_kv.second.callback_id = -1; } - if (it->first != QueueId::Main) - it = easy_http_pack_.Remove(it); - else - ++it; + if (request_kv.second.request_control) + request_kv.second.request_control->forgotten.store(true); + } + + for (auto& forgotten_ez : forgotten_easy_http_) + { + if (!forgotten_ez) + continue; + + forgotten_ez->ForgetAllRequests(); + forgotten_ez->DropCompletedRequestsWithoutCallbacks(); } + + for (auto& pack_kv : easy_http_pack_) + { + auto& terminating_ez = pack_kv.second.terminating_easy_http; + auto& forgettable_ez = pack_kv.second.forgettable_easy_http; + + if (terminating_ez) + { + terminating_ez->CancelAllRequests(); + terminating_ez->ForgetAllRequests(); + terminating_ez->DropCompletedRequestsWithoutCallbacks(); + forgotten_easy_http_.emplace_back(std::move(terminating_ez)); + } + + if (forgettable_ez) + { + forgettable_ez->ForgetAllRequests(); + forgettable_ez->DropCompletedRequestsWithoutCallbacks(); + forgotten_easy_http_.emplace_back(std::move(forgettable_ez)); + } + } + + easy_http_pack_.clear(); + requests_.clear(); + options_.clear(); + CreateQueue(); + ezhttp::trace::Writef("EasyHttpModule", "ResetForMapChangeWithoutCallbacks end forgotten=%zu queues=%zu requests=%zu options=%zu", forgotten_easy_http_.size(), easy_http_pack_.size(), requests_.size(), options_.size()); } void EasyHttpModule::RunFrameEasyHttp() @@ -128,10 +260,13 @@ void EasyHttpModule::RunCleanupFrameForForgottenEasyHttp() { for (auto it = forgotten_easy_http_.begin(); it != forgotten_easy_http_.end(); ) { - it->get()->RunFrame(); + it->get()->DropCompletedRequestsWithoutCallbacks(); if (it->get()->GetActiveRequestCount() == 0) + { + ezhttp::trace::Writef("EasyHttpModule", "RunCleanupFrameForForgottenEasyHttp erase easy_http=%p", it->get()); it = forgotten_easy_http_.erase(it); + } else it++; } diff --git a/src/EasyHttpModule.h b/src/EasyHttpModule.h index d89de9b..b0f16a2 100644 --- a/src/EasyHttpModule.h +++ b/src/EasyHttpModule.h @@ -4,6 +4,8 @@ #include "easy_http/EasyHttpOptionsBuilder.h" #include "utils/ContainerWithHandles.h" #include "sdk/amxxmodule.h" +#include +#include #include enum class PluginEndBehaviour @@ -28,10 +30,13 @@ struct OptionsData struct RequestData { + uint32_t generation = 0; std::shared_ptr request_control; - // options_id is always valid as long as the RequestId associated with the object exists - OptionsId options_id; ezhttp::Response response; + std::optional> user_data; + std::unique_ptr callback_data; + int callback_data_len = 0; + int callback_id = -1; }; struct EasyHttpPack @@ -58,13 +63,12 @@ struct EasyHttpPack } }; -using ModuleRequestCallback = std::function; - class EasyHttpModule { const int kMainQueueThreads = 6; std::string ca_cert_path_; + uint32_t next_request_generation_ = 0; std::vector> forgotten_easy_http_; utils::ContainerWithHandles easy_http_pack_; @@ -78,24 +82,36 @@ class EasyHttpModule void RunFrame(); void ServerDeactivate(); - RequestId SendRequest(ezhttp::RequestMethod method, const std::string& url, OptionsId options_id, const ModuleRequestCallback& callback); + RequestId SendRequest( + ezhttp::RequestMethod method, + const std::string& url, + OptionsData options, + int callback_id = -1, + std::unique_ptr callback_data = nullptr, + int callback_data_len = 0 + ); - // When using delete_related_options, make sure that other requests do not use the same options as this one - bool DeleteRequest(RequestId handle, bool delete_related_options = false); + bool DeleteRequest(RequestId handle); [[nodiscard]] bool IsRequestExists(RequestId handle) { return requests_.contains(handle); } [[nodiscard]] RequestData& GetRequest(RequestId handle) { return requests_.at(handle); } + [[nodiscard]] const RequestData& GetRequest(RequestId handle) const { return requests_.at(handle); } OptionsId CreateOptions(); bool DeleteOptions(OptionsId handle); [[nodiscard]] bool IsOptionsExists(OptionsId handle) const { return options_.contains(handle); } [[nodiscard]] OptionsData& GetOptions(OptionsId handle) { return options_.at(handle); } + [[nodiscard]] const OptionsData& GetOptions(OptionsId handle) const { return options_.at(handle); } [[nodiscard]] ezhttp::EasyHttpOptionsBuilder& GetOptionsBuilder(OptionsId handle) { return options_.at(handle).options_builder; } + [[nodiscard]] OptionsData CreateOptionsSnapshot(OptionsId handle) const { return options_.at(handle); } QueueId CreateQueue(); [[nodiscard]] bool IsQueueExists(QueueId handle) const { return easy_http_pack_.contains(handle); } private: - void ResetMainAndRemoveUsersQueues(); + void FinalizeRequest(RequestId handle); + void CleanupCompletedForgottenRequests(); + void ShutdownWithoutCallbacks(); + void ResetForMapChangeWithoutCallbacks(); void RunFrameEasyHttp(); void RunCleanupFrameForForgottenEasyHttp(); std::unique_ptr& GetEasyHttp(QueueId queue_id, PluginEndBehaviour end_map_behaviour); diff --git a/src/easy_http/EasyHttp.cpp b/src/easy_http/EasyHttp.cpp index 05d0fb7..812a975 100644 --- a/src/easy_http/EasyHttp.cpp +++ b/src/easy_http/EasyHttp.cpp @@ -11,6 +11,7 @@ #include "datetime_service/DateTimeService.h" #include "session_factory/CprSessionFactory.h" #include "utils/ftp_utils.h" +#include "utils/TraceLog.h" using namespace ezhttp; @@ -53,6 +54,9 @@ namespace void MarkCancelledResponse(Response &response, std::string message) { + const cpr::Url response_url = response.url; + response = Response{}; + response.url = response_url; response.error.code = cpr::ErrorCode::REQUEST_CANCELLED; response.error.message = std::move(message); } @@ -107,6 +111,8 @@ EasyHttp::EasyHttp(std::string ca_cert_path, int threads) : ca_cert_path_(std::m for (int i = 0; i < worker_count; ++i) worker_threads_.emplace_back(&EasyHttp::WorkerLoop, this); + + ezhttp::trace::Writef("EasyHttp", "ctor this=%p workers=%d", this, worker_count); } std::shared_ptr EasyHttp::SendRequest(RequestMethod method, const cpr::Url &url, const RequestOptions &options, const ResponseCallback &on_complete) @@ -120,9 +126,10 @@ std::shared_ptr EasyHttp::SendRequest(RequestMethod method, cons { std::lock_guard lock_guard(pending_requests_mutex_); pending_requests_.push_back(PendingRequest{request_control, method, normalized_url, options, on_complete}); + ezhttp::trace::Writef("EasyHttp", "SendRequest this=%p control=%p method=%d pending=%zu url=%s", this, request_control.get(), static_cast(method), pending_requests_.size(), normalized_url.str().c_str()); } - requests_.emplace_back(request_control); + TrackRequest(request_control); pending_requests_cv_.notify_one(); return request_control; @@ -130,6 +137,10 @@ std::shared_ptr EasyHttp::SendRequest(RequestMethod method, cons EasyHttp::~EasyHttp() { + ezhttp::trace::Writef("EasyHttp", "dtor begin this=%p active=%d", this, GetActiveRequestCount()); + ForgetAllRequests(); + CancelAllRequests(); + { std::lock_guard lock_guard(pending_requests_mutex_); stop_requested_ = true; @@ -143,14 +154,15 @@ EasyHttp::~EasyHttp() worker_thread.join(); } - while (true) - { - RunFrame(); + DropCompletedRequestsWithoutCallbacks(); + ClearTrackedRequestsWithoutCallbacks(); - std::lock_guard lock_guard(completed_requests_mutex_); - if (completed_requests_.empty() && requests_.empty()) - break; + { + std::lock_guard lock_guard(pending_requests_mutex_); + pending_requests_.clear(); } + + ezhttp::trace::Writef("EasyHttp", "dtor end this=%p", this); } void EasyHttp::WorkerLoop() @@ -171,15 +183,44 @@ void EasyHttp::WorkerLoop() pending_requests_.pop_front(); } + ezhttp::trace::Writef( + "EasyHttp", + "WorkerLoop dequeued this=%p control=%p canceled=%d forgotten=%d url=%s", + this, + pending_request.request_control.get(), + pending_request.request_control->canceled.load(), + pending_request.request_control->forgotten.load(), + pending_request.url.str().c_str() + ); + Response response = pending_request.request_control->canceled.load() ? CreateErrorResponse(pending_request.url, cpr::ErrorCode::REQUEST_CANCELLED, "Request canceled before dispatch") : SendRequest(pending_request.request_control, pending_request.method, pending_request.url, pending_request.options); - std::lock_guard lock_guard(completed_requests_mutex_); - completed_requests_.push_back(CompletedRequest{ - pending_request.request_control, - std::move(response), - std::move(pending_request.on_complete)}); + if (pending_request.request_control->canceled.load()) + response = CreateErrorResponse(pending_request.url, cpr::ErrorCode::REQUEST_CANCELLED, "Request canceled before completion"); + else if (pending_request.request_control->forgotten.load()) + response = CreateErrorResponse(pending_request.url, cpr::ErrorCode::REQUEST_CANCELLED, "Request forgotten before completion"); + + bool forgotten = pending_request.request_control->forgotten.load(); + if (!forgotten) + { + std::lock_guard lock_guard(completed_requests_mutex_); + forgotten = pending_request.request_control->forgotten.load(); + if (!forgotten) + { + completed_requests_.push_back(CompletedRequest{ + pending_request.request_control, + std::move(response), + std::move(pending_request.on_complete)}); + ezhttp::trace::Writef("EasyHttp", "WorkerLoop queued completion this=%p control=%p completed=%zu", this, pending_request.request_control.get(), completed_requests_.size()); + continue; + } + } + + pending_request.request_control->completed.store(true); + FinishTrackedRequest(pending_request.request_control); + ezhttp::trace::Writef("EasyHttp", "WorkerLoop dropped forgotten completion this=%p control=%p", this, pending_request.request_control.get()); } } @@ -194,6 +235,48 @@ bool EasyHttp::TryPopCompletedRequest(CompletedRequest &completed_request) return true; } +void EasyHttp::DropCompletedRequestsWithoutCallbacks() +{ + std::vector> completed_request_controls; + + { + std::lock_guard lock_guard(completed_requests_mutex_); + completed_request_controls.reserve(completed_requests_.size()); + for (auto &completed_request : completed_requests_) + { + if (completed_request.request_control) + { + completed_request.request_control->forgotten.store(true); + completed_request.request_control->completed.store(true); + completed_request_controls.emplace_back(completed_request.request_control); + } + } + + completed_requests_.clear(); + } + + for (auto &request_control : completed_request_controls) + FinishTrackedRequest(request_control); + + if (!completed_request_controls.empty()) + ezhttp::trace::Writef("EasyHttp", "DropCompletedRequestsWithoutCallbacks this=%p dropped=%zu", this, completed_request_controls.size()); +} + +void EasyHttp::ClearTrackedRequestsWithoutCallbacks() +{ + std::lock_guard lock_guard(requests_mutex_); + for (auto &request : requests_) + { + if (!request) + continue; + + request->forgotten.store(true); + request->completed.store(true); + } + + requests_.clear(); +} + void EasyHttp::RunFrame() { for (int i = 0; i < kMaxTasksExecPerFrame; ++i) @@ -202,35 +285,61 @@ void EasyHttp::RunFrame() if (!TryPopCompletedRequest(completed_request)) break; + ezhttp::trace::Writef( + "EasyHttp", + "RunFrame pop this=%p control=%p forgotten=%d canceled=%d status=%ld error=%d", + this, + completed_request.request_control.get(), + completed_request.request_control->forgotten.load(), + completed_request.request_control->canceled.load(), + completed_request.response.status_code, + static_cast(completed_request.response.error.code) + ); completed_request.request_control->completed.store(true); if (!completed_request.request_control->forgotten.load()) + { + ezhttp::trace::Writef("EasyHttp", "RunFrame invoking callback this=%p control=%p", this, completed_request.request_control.get()); completed_request.on_complete(std::move(completed_request.response)); - } - - for (auto it = requests_.begin(); it != requests_.end();) - { - if ((*it)->completed.load()) - it = requests_.erase(it); + } else - ++it; + ezhttp::trace::Writef("EasyHttp", "RunFrame skipping forgotten callback this=%p control=%p", this, completed_request.request_control.get()); + + FinishTrackedRequest(completed_request.request_control); + ezhttp::trace::Writef("EasyHttp", "RunFrame finished this=%p control=%p", this, completed_request.request_control.get()); } } void EasyHttp::ForgetAllRequests() { + std::lock_guard lock_guard(requests_mutex_); for (auto &request : requests_) request->forgotten.store(true); } void EasyHttp::CancelAllRequests() { + std::lock_guard lock_guard(requests_mutex_); for (auto &request : requests_) request->canceled.store(true); pending_requests_cv_.notify_all(); } +void EasyHttp::TrackRequest(const std::shared_ptr& request_control) +{ + std::lock_guard lock_guard(requests_mutex_); + requests_.emplace_back(request_control); +} + +void EasyHttp::FinishTrackedRequest(const std::shared_ptr& request_control) +{ + std::lock_guard lock_guard(requests_mutex_); + auto it = std::find(requests_.begin(), requests_.end(), request_control); + if (it != requests_.end()) + requests_.erase(it); +} + Response EasyHttp::CreateErrorResponse(const cpr::Url &url, cpr::ErrorCode code, std::string message) const { Response response; @@ -274,10 +383,7 @@ Response EasyHttp::SendRequest(const std::shared_ptr &request_co return CreateErrorResponse(url, cpr::ErrorCode::INVALID_URL_FORMAT, "Invalid URL"); if (request_control->canceled.load()) - { - session_cache_.ReturnSession(*session); return CreateErrorResponse(url, cpr::ErrorCode::REQUEST_CANCELLED, "Request canceled before transfer"); - } SetSessionCommonOptions(*session, request_control, url, options); @@ -297,10 +403,20 @@ Response EasyHttp::SendRequest(const std::shared_ptr &request_co break; } - session_cache_.ReturnSession(*session); + if (ShouldReuseSession(request_control, response)) + session_cache_.ReturnSession(*session); + return response; } +bool EasyHttp::ShouldReuseSession(const std::shared_ptr& request_control, const Response& response) const +{ + if (request_control->canceled.load() || request_control->forgotten.load()) + return false; + + return response.error.code == cpr::ErrorCode::OK; +} + Response EasyHttp::SendHttpRequest(cpr::Session &session, const std::shared_ptr &request_control, RequestMethod method, const cpr::Url &url, const RequestOptions &options) { if (options.user_agent) diff --git a/src/easy_http/EasyHttp.h b/src/easy_http/EasyHttp.h index 4fd75ee..49ef43a 100644 --- a/src/easy_http/EasyHttp.h +++ b/src/easy_http/EasyHttp.h @@ -45,6 +45,7 @@ namespace ezhttp std::mutex completed_requests_mutex_; std::deque completed_requests_; + mutable std::mutex requests_mutex_; std::vector worker_threads_; std::vector> requests_; bool stop_requested_{false}; @@ -55,13 +56,22 @@ namespace ezhttp std::shared_ptr SendRequest(RequestMethod method, const cpr::Url &url, const RequestOptions &options, const ResponseCallback &on_complete) override; void RunFrame() override; - int GetActiveRequestCount() override { return static_cast(requests_.size()); } + int GetActiveRequestCount() override + { + std::lock_guard lock_guard(requests_mutex_); + return static_cast(requests_.size()); + } + void DropCompletedRequestsWithoutCallbacks() override; void ForgetAllRequests() override; void CancelAllRequests() override; private: void WorkerLoop(); bool TryPopCompletedRequest(CompletedRequest &completed_request); + void ClearTrackedRequestsWithoutCallbacks(); + void TrackRequest(const std::shared_ptr& request_control); + void FinishTrackedRequest(const std::shared_ptr& request_control); + bool ShouldReuseSession(const std::shared_ptr& request_control, const Response& response) const; Response CreateErrorResponse(const cpr::Url &url, cpr::ErrorCode code, std::string message) const; Response SendRequest(const std::shared_ptr &request_control, RequestMethod method, const cpr::Url &url, const RequestOptions &options); void SetSessionCommonOptions(cpr::Session &session, const std::shared_ptr &request_control, const cpr::Url &url, const RequestOptions &options); diff --git a/src/easy_http/EasyHttpInterface.h b/src/easy_http/EasyHttpInterface.h index 9aa199e..fce6eee 100644 --- a/src/easy_http/EasyHttpInterface.h +++ b/src/easy_http/EasyHttpInterface.h @@ -19,6 +19,7 @@ namespace ezhttp virtual std::shared_ptr SendRequest(RequestMethod method, const cpr::Url &url, const RequestOptions &options, const ResponseCallback& on_complete) = 0; virtual void RunFrame() = 0; virtual int GetActiveRequestCount() = 0; + virtual void DropCompletedRequestsWithoutCallbacks() = 0; // No callback functions will be called for all current requests virtual void ForgetAllRequests() = 0; diff --git a/src/module.cpp b/src/module.cpp index 77bd604..4b4876c 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -10,6 +10,7 @@ #include "utils/ftp_utils.h" #include "utils/string_utils.h" #include "utils/amxx_utils.h" +#include "utils/TraceLog.h" using namespace ezhttp; @@ -18,21 +19,50 @@ bool ValidateRequestId(AMX* amx, RequestId request_id); bool ValidateQueueId(AMX* amx, QueueId queue_id); template void SetKeyValueOption(AMX* amx, cell* params, TMethod method); template void SetStringOption(AMX* amx, cell* params, TMethod method); -RequestId SendRequest(AMX* amx, RequestMethod method, OptionsId options_id, const std::string& url, const std::string& callback, cell* data = nullptr, int data_len = 0); +using OptionsConfigurer = std::function; +RequestId DispatchRequest( + AMX* amx, + RequestMethod method, + OptionsId options_id, + const std::string& url, + const std::string& callback, + std::unique_ptr data = nullptr, + int data_len = 0, + const OptionsConfigurer& configure = {} +); std::unique_ptr g_EasyHttpModule; std::unique_ptr g_JsonManager; +bool g_MapChangeResetDone = false; + +namespace +{ + cvar_t cvar_ezhttp_trace = { "ezhttp_trace_log", "0", FCVAR_SERVER | FCVAR_SPONLY }; + + void RefreshTraceLogSetting() + { + ezhttp::trace::SetEnabled(CVAR_GET_FLOAT("ezhttp_trace_log") != 0.0f); + } +} void CreateModules() { + ezhttp::trace::Initialize(MF_BuildPathname("addons/amxmodx/logs/ezhttp_trace.log")); + RefreshTraceLogSetting(); + ezhttp::trace::Writef("module", "CreateModules begin"); g_EasyHttpModule = std::make_unique(MF_BuildPathname("addons/amxmodx/data/amxx_easy_http_cacert.pem")); g_JsonManager = std::make_unique(); + g_MapChangeResetDone = false; + ezhttp::trace::Writef("module", "CreateModules done easy_http=%p json=%p", g_EasyHttpModule.get(), g_JsonManager.get()); } void DestroyModules() { + ezhttp::trace::Writef("module", "DestroyModules begin easy_http=%p json=%p", g_EasyHttpModule.get(), g_JsonManager.get()); g_EasyHttpModule.reset(); g_JsonManager.reset(); + ezhttp::trace::Writef("module", "DestroyModules done"); + ezhttp::trace::Shutdown(); } // native EzHttpOptions:ezhttp_create_options(); @@ -248,7 +278,8 @@ cell AMX_NATIVE_CALL ezhttp_get(AMX* amx, cell* params) auto options_id = (OptionsId)params[arg_option_id]; - return (cell)SendRequest(amx, RequestMethod::HttpGet, options_id, std::string(url, url_len), std::string(callback, callback_len), data, data_len); + std::unique_ptr request_data(data); + return (cell)DispatchRequest(amx, RequestMethod::HttpGet, options_id, std::string(url, url_len), std::string(callback, callback_len), std::move(request_data), data_len); } // native EzHttpRequest:ezhttp_post(const url[], const on_complete[], EzHttpOptions:options_id = EzHttpOptions:0); @@ -276,7 +307,8 @@ cell AMX_NATIVE_CALL ezhttp_post(AMX* amx, cell* params) auto options_id = (OptionsId)params[arg_option_id]; - return (cell)SendRequest(amx, RequestMethod::HttpPost, options_id, std::string(url, url_len), std::string(callback, callback_len), data, data_len); + std::unique_ptr request_data(data); + return (cell)DispatchRequest(amx, RequestMethod::HttpPost, options_id, std::string(url, url_len), std::string(callback, callback_len), std::move(request_data), data_len); } // native EzHttpRequest:ezhttp_put(const url[], const on_complete[], EzHttpOptions:options_id = EzHttpOptions:0); @@ -304,7 +336,8 @@ cell AMX_NATIVE_CALL ezhttp_put(AMX* amx, cell* params) auto options_id = (OptionsId)params[arg_option_id]; - return (cell)SendRequest(amx, RequestMethod::HttpPut, options_id, std::string(url, url_len), std::string(callback, callback_len), data, data_len); + std::unique_ptr request_data(data); + return (cell)DispatchRequest(amx, RequestMethod::HttpPut, options_id, std::string(url, url_len), std::string(callback, callback_len), std::move(request_data), data_len); } // native EzHttpRequest:ezhttp_patch(const url[], const on_complete[], EzHttpOptions:options_id = EzHttpOptions:0); @@ -332,7 +365,8 @@ cell AMX_NATIVE_CALL ezhttp_patch(AMX* amx, cell* params) auto options_id = (OptionsId)params[arg_option_id]; - return (cell)SendRequest(amx, RequestMethod::HttpPatch, options_id, std::string(url, url_len), std::string(callback, callback_len), data, data_len); + std::unique_ptr request_data(data); + return (cell)DispatchRequest(amx, RequestMethod::HttpPatch, options_id, std::string(url, url_len), std::string(callback, callback_len), std::move(request_data), data_len); } // native EzHttpRequest:ezhttp_delete(const url[], const on_complete[], EzHttpOptions:options_id = EzHttpOptions:0); @@ -360,7 +394,8 @@ cell AMX_NATIVE_CALL ezhttp_delete(AMX* amx, cell* params) auto options_id = (OptionsId)params[arg_option_id]; - return (cell)SendRequest(amx, RequestMethod::HttpDelete, options_id, std::string(url, url_len), std::string(callback, callback_len), data, data_len); + std::unique_ptr request_data(data); + return (cell)DispatchRequest(amx, RequestMethod::HttpDelete, options_id, std::string(url, url_len), std::string(callback, callback_len), std::move(request_data), data_len); } // native ezhttp_is_request_exists(EzHttpRequest:request_id); @@ -685,10 +720,7 @@ cell AMX_NATIVE_CALL ezhttp_get_user_data(AMX* amx, cell* params) if (!ValidateRequestId(amx, request_id)) return 0; - OptionsId options_id = g_EasyHttpModule->GetRequest(request_id).options_id; - OptionsData options = g_EasyHttpModule->GetOptions(options_id); - - const std::optional>& user_data = options.user_data; + const std::optional>& user_data = g_EasyHttpModule->GetRequest(request_id).user_data; if (!user_data) return 0; @@ -712,18 +744,21 @@ cell AMX_NATIVE_CALL ezhttp_ftp_upload(AMX* amx, cell* params) std::string url = utils::ConstructFtpUrl(user, password, host, remote_file); - if (options_id == OptionsId::Null) - options_id = g_EasyHttpModule->CreateOptions(); - else if (!ValidateOptionsId(amx, options_id)) - return 0; - - auto& builder = g_EasyHttpModule->GetOptionsBuilder(options_id); - builder.SetFilePath(MF_BuildPathname("%s", local_file)); - builder.SetSecure(secure); - - SendRequest(amx, RequestMethod::FtpUpload, options_id, url, callback); - - return 0; + const std::string local_file_path = MF_BuildPathname("%s", local_file); + + return (cell)DispatchRequest( + amx, + RequestMethod::FtpUpload, + options_id, + url, + callback, + nullptr, + 0, + [local_file_path, secure](OptionsData& request_options) { + request_options.options_builder.SetFilePath(local_file_path); + request_options.options_builder.SetSecure(secure); + } + ); } cell AMX_NATIVE_CALL ezhttp_ftp_upload2(AMX* amx, cell* params) @@ -740,18 +775,21 @@ cell AMX_NATIVE_CALL ezhttp_ftp_upload2(AMX* amx, cell* params) bool secure = params[4]; auto options_id = (OptionsId)params[5]; - if (options_id == OptionsId::Null) - options_id = g_EasyHttpModule->CreateOptions(); - else if (!ValidateOptionsId(amx, options_id)) - return 0; - - auto& builder = g_EasyHttpModule->GetOptionsBuilder(options_id); - builder.SetFilePath(MF_BuildPathname("%s", local_file)); - builder.SetSecure(secure); - - SendRequest(amx, RequestMethod::FtpUpload, options_id, std::string(url_str, url_str_len), std::string(callback, callback_len)); - - return 0; + const std::string local_file_path = MF_BuildPathname("%s", local_file); + + return (cell)DispatchRequest( + amx, + RequestMethod::FtpUpload, + options_id, + std::string(url_str, url_str_len), + std::string(callback, callback_len), + nullptr, + 0, + [local_file_path, secure](OptionsData& request_options) { + request_options.options_builder.SetFilePath(local_file_path); + request_options.options_builder.SetSecure(secure); + } + ); } cell AMX_NATIVE_CALL ezhttp_ftp_download(AMX* amx, cell* params) @@ -769,18 +807,21 @@ cell AMX_NATIVE_CALL ezhttp_ftp_download(AMX* amx, cell* params) std::string url = utils::ConstructFtpUrl(user, password, host, remote_file); - if (options_id == OptionsId::Null) - options_id = g_EasyHttpModule->CreateOptions(); - else if (!ValidateOptionsId(amx, options_id)) - return 0; - - auto& builder = g_EasyHttpModule->GetOptionsBuilder(options_id); - builder.SetFilePath(MF_BuildPathname("%s", local_file)); - builder.SetSecure(secure); - - SendRequest(amx, RequestMethod::FtpDownload, options_id, url, callback); - - return 0; + const std::string local_file_path = MF_BuildPathname("%s", local_file); + + return (cell)DispatchRequest( + amx, + RequestMethod::FtpDownload, + options_id, + url, + callback, + nullptr, + 0, + [local_file_path, secure](OptionsData& request_options) { + request_options.options_builder.SetFilePath(local_file_path); + request_options.options_builder.SetSecure(secure); + } + ); } cell AMX_NATIVE_CALL ezhttp_ftp_download2(AMX* amx, cell* params) @@ -797,18 +838,21 @@ cell AMX_NATIVE_CALL ezhttp_ftp_download2(AMX* amx, cell* params) bool secure = params[4]; auto options_id = (OptionsId)params[5]; - if (options_id == OptionsId::Null) - options_id = g_EasyHttpModule->CreateOptions(); - else if (!ValidateOptionsId(amx, options_id)) - return 0; - - auto& builder = g_EasyHttpModule->GetOptionsBuilder(options_id); - builder.SetFilePath(MF_BuildPathname("%s", local_file)); - builder.SetSecure(secure); - - SendRequest(amx, RequestMethod::FtpDownload, options_id, std::string(url_str, url_str_len), std::string(callback, callback_len)); - - return 0; + const std::string local_file_path = MF_BuildPathname("%s", local_file); + + return (cell)DispatchRequest( + amx, + RequestMethod::FtpDownload, + options_id, + std::string(url_str, url_str_len), + std::string(callback, callback_len), + nullptr, + 0, + [local_file_path, secure](OptionsData& request_options) { + request_options.options_builder.SetFilePath(local_file_path); + request_options.options_builder.SetSecure(secure); + } + ); } cell AMX_NATIVE_CALL ezhttp_create_queue(AMX* amx, cell* params) @@ -849,18 +893,24 @@ cell AMX_NATIVE_CALL ezhttp_steam_to_steam64(AMX* amx, cell* params) return 1; } -RequestId SendRequest(AMX* amx, RequestMethod method, OptionsId options_id, const std::string& url, const std::string& callback, cell* data, const int data_len) +RequestId DispatchRequest( + AMX* amx, + RequestMethod method, + OptionsId options_id, + const std::string& url, + const std::string& callback, + std::unique_ptr data, + const int data_len, + const OptionsConfigurer& configure +) { if (options_id != OptionsId::Null && !ValidateOptionsId(amx, options_id)) - { - delete[] data; return RequestId::Null; - } int callback_id = -1; if (!callback.empty()) { - if (data == nullptr) + if (!data) { callback_id = MF_RegisterSPForwardByName(amx, callback.c_str(), FP_CELL, FP_DONE); } else { @@ -869,33 +919,26 @@ RequestId SendRequest(AMX* amx, RequestMethod method, OptionsId options_id, cons if (callback_id == -1) { - delete[] data; MF_LogError(amx, AMX_ERR_NATIVE, "Callback function \"%s\" is not exists", callback.c_str()); return RequestId::Null; } } - auto on_complete = [callback_id, data, data_len](RequestId request_id) { - if (callback_id == -1) - { - delete[] data; - g_EasyHttpModule->DeleteRequest(request_id, true); - return; - } + OptionsData request_options = options_id == OptionsId::Null + ? OptionsData{} + : g_EasyHttpModule->CreateOptionsSnapshot(options_id); - if (data == nullptr) - { - MF_ExecuteForward(callback_id, request_id); - } else { - MF_ExecuteForward(callback_id, request_id, MF_PrepareCellArray(data, data_len)); - } - MF_UnregisterSPForward(callback_id); + if (configure) + configure(request_options); - delete[] data; - g_EasyHttpModule->DeleteRequest(request_id, true); - }; - - RequestId request_id = g_EasyHttpModule->SendRequest(method, url, options_id, on_complete); + RequestId request_id = g_EasyHttpModule->SendRequest( + method, + url, + std::move(request_options), + callback_id, + std::move(data), + data_len + ); return request_id; } @@ -1035,9 +1078,10 @@ void OnAmxxAttach() MF_AddNatives(g_Natives); MF_AddNatives(g_JsonNatives); - CreateModules(); - CVAR_REGISTER(&cvar_ezhttp_version); + CVAR_REGISTER(&cvar_ezhttp_trace); + + CreateModules(); } void OnAmxxDetach() @@ -1045,8 +1089,32 @@ void OnAmxxDetach() DestroyModules(); } +void OnPluginsUnloading() +{ + RefreshTraceLogSetting(); + ezhttp::trace::Writef("module", "OnPluginsUnloading enter easy_http=%p json=%p mapchange_reset_done=%d", g_EasyHttpModule.get(), g_JsonManager.get(), g_MapChangeResetDone); + + if (g_EasyHttpModule && !g_MapChangeResetDone) + g_EasyHttpModule->ServerDeactivate(); + + if (g_JsonManager) + g_JsonManager->FreeAllHandles(); + + ezhttp::trace::Writef("module", "OnPluginsUnloading exit"); +} + +void ServerActivate(edict_t* /*pEdictList*/, int /*edictCount*/, int /*clientMax*/) +{ + g_MapChangeResetDone = false; + RefreshTraceLogSetting(); + ezhttp::trace::Writef("module", "Metamod ServerActivate mapchange_reset_done=%d", g_MapChangeResetDone); + SET_META_RESULT(MRES_IGNORED); +} + void StartFrame() { + RefreshTraceLogSetting(); + if (g_EasyHttpModule) g_EasyHttpModule->RunFrame(); @@ -1055,16 +1123,25 @@ void StartFrame() void ServerDeactivate() { + RefreshTraceLogSetting(); + ezhttp::trace::Writef("module", "Metamod ServerDeactivate enter easy_http=%p json=%p", g_EasyHttpModule.get(), g_JsonManager.get()); + if (g_EasyHttpModule) g_EasyHttpModule->ServerDeactivate(); + g_MapChangeResetDone = true; + if (g_JsonManager) g_JsonManager->FreeAllHandles(); + ezhttp::trace::Writef("module", "Metamod ServerDeactivate exit mapchange_reset_done=%d", g_MapChangeResetDone); + SET_META_RESULT(MRES_IGNORED); } void GameShutdown() { + RefreshTraceLogSetting(); + ezhttp::trace::Writef("module", "GameShutdown"); DestroyModules(); } diff --git a/src/sdk/moduleconfig.h b/src/sdk/moduleconfig.h index 1dfdbaf..2311e7d 100644 --- a/src/sdk/moduleconfig.h +++ b/src/sdk/moduleconfig.h @@ -74,7 +74,7 @@ //#define FN_AMXX_PLUGINSLOADED OnPluginsLoaded /** All plugins are about to be unloaded */ -//#define FN_AMXX_PLUGINSUNLOADING OnPluginsUnloading +#define FN_AMXX_PLUGINSUNLOADING OnPluginsUnloading /** All plugins are now unloaded */ //#define FN_AMXX_PLUGINSUNLOADED OnPluginsUnloaded @@ -119,7 +119,7 @@ // #define FN_ClientPutInServer ClientPutInServer /* pfnClientPutInServer() (wd) Client is entering the game */ // #define FN_ClientCommand ClientCommand /* pfnClientCommand() (wd) Player has sent a command (typed or from a bind) */ // #define FN_ClientUserInfoChanged ClientUserInfoChanged /* pfnClientUserInfoChanged() (wd) Client has updated their setinfo structure */ -// #define FN_ServerActivate ServerActivate /* pfnServerActivate() (wd) Server is starting a new map */ + #define FN_ServerActivate ServerActivate /* pfnServerActivate() (wd) Server is starting a new map */ #define FN_ServerDeactivate ServerDeactivate /* pfnServerDeactivate() (wd) Server is leaving the map (shutdown or changelevel); SDK2 */ // #define FN_PlayerPreThink PlayerPreThink /* pfnPlayerPreThink() */ // #define FN_PlayerPostThink PlayerPostThink /* pfnPlayerPostThink() */ diff --git a/src/utils/ContainerWithHandles.h b/src/utils/ContainerWithHandles.h index 4d1d508..8f386bc 100644 --- a/src/utils/ContainerWithHandles.h +++ b/src/utils/ContainerWithHandles.h @@ -102,6 +102,11 @@ namespace utils return values_.at(handle); } + const TValue& at(THandle handle) const + { + return values_.at(handle); + } + bool contains(THandle handle) const { return values_.count(handle) == 1; @@ -118,11 +123,11 @@ namespace utils } else { - while (values_.count(handle) > 1) + while (values_.count(handle) > 0) handle = (THandle)((int)handle + 1); } return handle; } }; -} \ No newline at end of file +} diff --git a/src/utils/TraceLog.cpp b/src/utils/TraceLog.cpp new file mode 100644 index 0000000..ab1c816 --- /dev/null +++ b/src/utils/TraceLog.cpp @@ -0,0 +1,140 @@ +#include "TraceLog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace ezhttp::trace +{ + namespace + { + std::mutex g_trace_mutex; + std::string g_trace_path; + bool g_initialized = false; + bool g_enabled = false; + bool g_header_written = false; + + unsigned long GetThreadIdForLog() + { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(std::hash{}(std::this_thread::get_id())); +#endif + } + + void FillLocalTime(std::time_t now, std::tm &local_tm) + { +#ifdef _WIN32 + localtime_s(&local_tm, &now); +#else + localtime_r(&now, &local_tm); +#endif + } + + void EnsureTraceHeaderLocked() + { + if (!g_initialized || !g_enabled || g_trace_path.empty() || g_header_written) + return; + + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(g_trace_path).parent_path(), ec); + + if (FILE *file = std::fopen(g_trace_path.c_str(), "w")) + { + std::fputs("==== ezhttp trace start ====\n", file); + std::fflush(file); + std::fclose(file); + g_header_written = true; + } + } + } + + void Initialize(const char *path) + { + std::lock_guard lock_guard(g_trace_mutex); + + g_trace_path = path == nullptr ? std::string() : std::string(path); + g_initialized = !g_trace_path.empty(); + g_header_written = false; + + if (!g_initialized) + return; + + EnsureTraceHeaderLocked(); + } + + void SetEnabled(bool enabled) + { + std::lock_guard lock_guard(g_trace_mutex); + g_enabled = enabled; + EnsureTraceHeaderLocked(); + } + + bool IsEnabled() + { + std::lock_guard lock_guard(g_trace_mutex); + return g_enabled; + } + + void Shutdown() + { + std::lock_guard lock_guard(g_trace_mutex); + g_initialized = false; + g_enabled = false; + g_header_written = false; + g_trace_path.clear(); + } + + void Writef(const char *component, const char *format, ...) + { + char message_buffer[1024]; + va_list args; + va_start(args, format); + std::vsnprintf(message_buffer, sizeof(message_buffer), format, args); + va_end(args); + + std::lock_guard lock_guard(g_trace_mutex); + if (!g_initialized || !g_enabled || g_trace_path.empty()) + return; + + EnsureTraceHeaderLocked(); + + FILE *file = std::fopen(g_trace_path.c_str(), "a"); + if (file == nullptr) + return; + + const auto now = std::chrono::system_clock::now(); + const auto time_t_now = std::chrono::system_clock::to_time_t(now); + const auto millis = std::chrono::duration_cast(now.time_since_epoch()) % std::chrono::seconds(1); + + std::tm local_tm{}; + FillLocalTime(time_t_now, local_tm); + + char timestamp_buffer[64]; + std::strftime(timestamp_buffer, sizeof(timestamp_buffer), "%Y-%m-%d %H:%M:%S", &local_tm); + + std::fprintf( + file, + "[%s.%03lld][tid=%lu][%s] %s\n", + timestamp_buffer, + static_cast(millis.count()), + GetThreadIdForLog(), + component == nullptr ? "trace" : component, + message_buffer + ); + std::fflush(file); + std::fclose(file); + } +} diff --git a/src/utils/TraceLog.h b/src/utils/TraceLog.h new file mode 100644 index 0000000..57aa91b --- /dev/null +++ b/src/utils/TraceLog.h @@ -0,0 +1,10 @@ +#pragma once + +namespace ezhttp::trace +{ + void Initialize(const char *path); + void SetEnabled(bool enabled); + bool IsEnabled(); + void Shutdown(); + void Writef(const char *component, const char *format, ...); +}