Organize scripts and add TWS plugins with HTTP timeouts
Vendor pinned T5 and T6 plugin sources under the new plugins layout. Add native HTTP deadlines, status and timing diagnostics, telemetry integration, and build/test tooling.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
param(
|
||||
[ValidateSet('t5', 't6', 'all')]
|
||||
[string]$Game = 'all',
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = Split-Path -Parent $PSScriptRoot
|
||||
$games = if ($Game -eq 'all') { @('t5', 't6') } else { @($Game) }
|
||||
foreach ($current in $games) {
|
||||
& python (Join-Path $PSScriptRoot 'fetch_plugin_deps.py') $current
|
||||
if ($LASTEXITCODE -ne 0) { throw "Dependency restore failed for $current" }
|
||||
$plugin = Join-Path $repo "$current/plugins/tws-$current-gsc-utils"
|
||||
Push-Location $plugin
|
||||
try {
|
||||
& .\tools\premake5.exe vs2022
|
||||
if ($LASTEXITCODE -ne 0) { throw "Project generation failed for $current" }
|
||||
& msbuild /m /v:minimal "/p:Configuration=$Configuration" /p:PlatformTarget=x86 "build/tws-$current-gsc-utils.sln"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Build failed for $current" }
|
||||
$binary = Join-Path $plugin "build/bin/$Configuration/tws-$current-gsc-utils.dll"
|
||||
if (!(Test-Path -LiteralPath $binary)) { throw "Missing output: $binary" }
|
||||
Write-Output "Built $binary"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ function functions(source) {
|
||||
}
|
||||
|
||||
function engine(game) {
|
||||
const source = readFileSync(new URL(`${game}/tws_${game}_leaderboard.gsc`, root), 'utf8');
|
||||
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_leaderboard.gsc`, root), 'utf8');
|
||||
const all = functions(source);
|
||||
const excluded = new Set(['lb_worker', 'lb_welcome', 'lb_get', 'lb_collect', 'lb_log', 'lb_show_hud', 'lb_hud_help_visible', 'lb_hud_end_game', 'lb_hud_destroy', 'lb_chat_listener', 'lb_request', 'lb_hud_disconnect']);
|
||||
const table = [];
|
||||
@@ -90,7 +90,7 @@ for (const game of ['t5', 't6']) {
|
||||
const { context: c, source, all, dvars, responses, queries } = engine(game);
|
||||
const option = mapOption(game);
|
||||
check(`${game} exactly one connect listener across modules`, () => {
|
||||
const common = readFileSync(new URL(`${game}/tws_${game}_common.gsc`, root), 'utf8');
|
||||
const common = readFileSync(new URL(`${game}/scripts/tws_${game}_common.gsc`, root), 'utf8');
|
||||
assert.equal((common.match(/waittill\("connected"/g) ?? []).length, 1);
|
||||
assert(!source.includes('waittill("connected"'));
|
||||
assert(!all.get('lb_welcome').includes('spawned_player'));
|
||||
@@ -227,8 +227,8 @@ for (const game of ['t5', 't6']) {
|
||||
assert.equal(responses.length, 0, `${game} unused responses`);
|
||||
}
|
||||
check('paired data helpers stay identical', () => {
|
||||
const t5 = functions(readFileSync(new URL('t5/tws_t5_leaderboard.gsc', root), 'utf8'));
|
||||
const t6 = functions(readFileSync(new URL('t6/tws_t6_leaderboard.gsc', root), 'utf8'));
|
||||
const t5 = functions(readFileSync(new URL('t5/scripts/tws_t5_leaderboard.gsc', root), 'utf8'));
|
||||
const t6 = functions(readFileSync(new URL('t6/scripts/tws_t6_leaderboard.gsc', root), 'utf8'));
|
||||
for (const [name, fn] of t5) {
|
||||
if (!['lb_selection', 'lb_welcome', 'lb_get', 'lb_log'].includes(name)) {
|
||||
const normalized = t6.get(name).replaceAll('scripts\\zm\\tws_t6_common::', 'scripts\\sp\\tws_t5_common::');
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Restore the exact upstream dependency revisions for the vendored plugins."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def git(directory, *args, capture=False):
|
||||
return subprocess.check_output(["git", "-C", str(directory), *args], text=True).strip() if capture else subprocess.check_call(["git", "-C", str(directory), *args])
|
||||
|
||||
|
||||
def restore(game, check_only):
|
||||
plugin = ROOT / game / "plugins" / f"tws-{game}-gsc-utils"
|
||||
lock = json.loads((plugin / "upstream.json").read_text())
|
||||
for dependency in lock["dependencies"]:
|
||||
target = plugin / dependency["path"]
|
||||
if not target.resolve().is_relative_to(plugin.resolve()):
|
||||
raise RuntimeError(f"Dependency path leaves the plugin: {target}")
|
||||
if not (target / ".git").exists():
|
||||
if check_only:
|
||||
raise RuntimeError(f"Missing dependency: {target}")
|
||||
if target.exists() and any(target.iterdir()):
|
||||
raise RuntimeError(f"Refusing to replace nonempty directory: {target}")
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
git(target, "init", "--quiet")
|
||||
git(target, "remote", "add", "origin", dependency["url"])
|
||||
if git(target, "remote", "get-url", "origin", capture=True) != dependency["url"]:
|
||||
raise RuntimeError(f"Unexpected dependency remote: {target}")
|
||||
if git(target, "status", "--porcelain", capture=True):
|
||||
raise RuntimeError(f"Dependency has local changes: {target}")
|
||||
current = subprocess.run(["git", "-C", str(target), "rev-parse", "--verify", "HEAD"], capture_output=True, text=True)
|
||||
if current.stdout.strip() != dependency["commit"]:
|
||||
if check_only:
|
||||
raise RuntimeError(f"Wrong dependency revision: {target}")
|
||||
git(target, "fetch", "--depth", "1", "origin", dependency["commit"])
|
||||
git(target, "checkout", "--quiet", "--detach", dependency["commit"])
|
||||
print(f"OK {game}/{dependency['path']} {dependency['commit']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("game", choices=["t5", "t6", "all"])
|
||||
parser.add_argument("--check", action="store_true", help="Verify existing checkouts without fetching or changing them")
|
||||
args = parser.parse_args()
|
||||
for game in ("t5", "t6") if args.game == "all" else (args.game,):
|
||||
restore(game, args.check)
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile actual HTTP component logic using host shims for engine interfaces."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
with tempfile.TemporaryDirectory(prefix="tws-http-component-") as temp:
|
||||
for game in ("t5", "t6"):
|
||||
plugin = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" / "src"
|
||||
# Only includes are substituted; compile the entire component unchanged.
|
||||
component = re.sub(r'^#include[^\n]*\n', '', (plugin / "component/http.cpp").read_text(), flags=re.MULTILINE)
|
||||
source = Path(temp) / f"{game}.cpp"
|
||||
source.write_text('#include "http_component_stub.hpp"\n' + component + (ROOT / "tools/tests/http_component_cases.cpp").read_text())
|
||||
executable = Path(temp) / game
|
||||
subprocess.run([os.environ.get("CXX", "c++"), "-std=c++20", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined",
|
||||
"-I", str(plugin / "utils"), "-I", str(ROOT / "tools/tests"), str(source), "-lcurl", "-o", str(executable)], check=True)
|
||||
print(game, flush=True)
|
||||
subprocess.run([str(executable)], check=True)
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile both real HTTP utilities and exercise native deadlines on loopback."""
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socketserver
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
||||
assert body == b'{"event":"heartbeat"}'
|
||||
assert self.headers.get("Content-Type") == "application/json"
|
||||
assert self.headers.get("Authorization") == "Bearer test-only"
|
||||
if self.path == "/stall":
|
||||
time.sleep(2)
|
||||
return
|
||||
if self.path == "/partial":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", "100")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{")
|
||||
self.wfile.flush()
|
||||
time.sleep(2)
|
||||
return
|
||||
if self.path == "/slow":
|
||||
time.sleep(0.1)
|
||||
status = int(self.path[1:]) if self.path[1:].isdigit() else 200
|
||||
payload = b'{"ok":true}' if status == 200 else b'{"ok":false}'
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
|
||||
class StallTLS(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
# Accept TCP but never finish TLS: the connection deadline must fire.
|
||||
time.sleep(6)
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingTCPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
def main():
|
||||
with Server(("127.0.0.1", 0), Handler) as http, Server(("127.0.0.1", 0), StallTLS) as tls, tempfile.TemporaryDirectory(prefix="tws-http-tests-") as temp:
|
||||
for server in (http, tls):
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
try:
|
||||
for game in ("t5", "t6"):
|
||||
source = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" / "src" / "utils"
|
||||
executable = Path(temp) / game
|
||||
subprocess.run([os.environ.get("CXX", "c++"), "-std=c++20", "-Wall", "-Wextra", "-Werror", "-I", str(source),
|
||||
str(ROOT / "tools/tests/http_probe.cpp"), str(source / "http.cpp"), "-lcurl", "-o", str(executable)], check=True)
|
||||
base = f"http://127.0.0.1:{http.server_address[1]}"
|
||||
env = {**os.environ, "NO_PROXY": "*", "no_proxy": "*"}
|
||||
|
||||
def probe(path, connect="1000", total="1000"):
|
||||
url = path if "://" in path else base + path
|
||||
completed = subprocess.run([str(executable), url, connect, total], env=env, capture_output=True, text=True, check=True, timeout=8)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
normal = probe("/200", "-", "-")
|
||||
assert normal["curl_code"] == 0 and normal["http_status"] == 200 and normal["body_size"] == 11, normal
|
||||
assert 0 <= normal["dns_ms"] <= normal["connect_ms"] <= normal["first_byte_ms"] <= normal["total_ms"], normal
|
||||
for status in (401, 429, 500):
|
||||
rejected = probe(f"/{status}")
|
||||
assert rejected["curl_code"] == 0 and rejected["http_status"] == status and rejected["body_size"] == 12, rejected
|
||||
for path, status in (("/stall", 0), ("/partial", 200)):
|
||||
timed_out = probe(path, total="200")
|
||||
assert timed_out["curl_code"] == 28 and timed_out["http_status"] == status, timed_out
|
||||
assert 150 <= timed_out["total_ms"] < 1500 and timed_out["wall_ms"] < 1500, timed_out
|
||||
assert timed_out["body_size"] == 0, timed_out
|
||||
if status: assert timed_out["first_byte_ms"] > 0, timed_out
|
||||
assert probe("/200")["curl_code"] == 0 # The next request completes after the stall.
|
||||
slow = probe("/slow")
|
||||
assert slow["curl_code"] == 0 and slow["first_byte_ms"] >= 90, slow
|
||||
tls_url = f"https://127.0.0.1:{tls.server_address[1]}/"
|
||||
stalled_connect = probe(tls_url, connect="150", total="1000")
|
||||
assert stalled_connect["curl_code"] == 28 and 100 <= stalled_connect["total_ms"] < 800, stalled_connect
|
||||
# Total timeout must also bound connection setup when it is shorter.
|
||||
total_connect = probe(tls_url, connect="1000", total="150")
|
||||
assert total_connect["curl_code"] == 28 and 100 <= total_connect["total_ms"] < 800, total_connect
|
||||
for connect, total in (("0", "100"), ("100", "0"), ("-1", "100")):
|
||||
invalid = probe("/200", connect, total)
|
||||
assert invalid["curl_code"] == 43 and invalid["http_status"] == 0, invalid
|
||||
print(f"PASS {game}: POST body/headers, HTTP errors, stalled/partial responses, recovery, TLS/connect and total deadlines, invalid limits", flush=True)
|
||||
finally:
|
||||
http.shutdown()
|
||||
tls.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
int main()
|
||||
{
|
||||
using namespace scripting;
|
||||
http::http_request_params_t params{};
|
||||
array options;
|
||||
http::parse_request_options(params, options);
|
||||
assert(params.limits.connect_timeout_ms == 5000 && params.limits.timeout_ms == 15000);
|
||||
options["connect_timeout_ms"] = 120;
|
||||
options["timeout_ms"] = 450;
|
||||
options["method"] = "POST";
|
||||
options["body"] = "snapshot";
|
||||
array headers;
|
||||
headers["Authorization"] = "Bearer test-only";
|
||||
options["headers"] = headers;
|
||||
http::parse_request_options(params, options);
|
||||
assert(params.limits.connect_timeout_ms == 120 && params.limits.timeout_ms == 450);
|
||||
assert(params.method == "POST" && params.fields == "snapshot" && params.headers.at("Authorization") == "Bearer test-only");
|
||||
for (const auto* key : {"connect_timeout_ms", "timeout_ms"})
|
||||
{
|
||||
for (script_value value : {script_value(0), script_value(-1), script_value(1.5), script_value("150")})
|
||||
{
|
||||
array bad;
|
||||
bad[key] = value;
|
||||
bool rejected = false;
|
||||
try { http::parse_request_options(params, bad); }
|
||||
catch (const std::runtime_error&) { rejected = true; }
|
||||
assert(rejected);
|
||||
}
|
||||
}
|
||||
http::parse_request_options(params, options);
|
||||
auto send = [&]()
|
||||
{
|
||||
notifications.clear();
|
||||
http::create_request(params);
|
||||
http::check_requests();
|
||||
assert(notifications.empty()); // Incomplete work never notifies early.
|
||||
scheduler::thread_pool.run();
|
||||
http::check_requests();
|
||||
assert(notifications.size() == 1 && notifications.front().size() == 4);
|
||||
assert(utils::http::observed_limits.connect_timeout_ms == 120 && utils::http::observed_limits.timeout_ms == 450);
|
||||
auto diagnostics = notifications.front()[3].as<array>();
|
||||
assert(diagnostics["worker_queue_ms"].as<double>() >= 0 && diagnostics["notify_ms"].as<double>() >= 0);
|
||||
assert(diagnostics["connect_timeout_ms"].as<int>() == 120 && diagnostics["timeout_ms"].as<int>() == 450);
|
||||
assert(diagnostics.data->size() == 11);
|
||||
http::check_requests();
|
||||
assert(notifications.size() == 1); // Exactly one completion.
|
||||
return diagnostics;
|
||||
};
|
||||
utils::http::result successful;
|
||||
successful.code = CURLE_OK;
|
||||
successful.status = 200;
|
||||
successful.buffer = "accepted";
|
||||
successful.dns_ms = 1;
|
||||
successful.connect_ms = 2;
|
||||
successful.tls_ms = 3;
|
||||
successful.first_byte_ms = 4;
|
||||
successful.total_ms = 5;
|
||||
utils::http::next_result = successful;
|
||||
auto diagnostics = send();
|
||||
assert(notifications.front()[0].as<std::string>() == "accepted" && notifications.front()[1].as<bool>());
|
||||
assert(notifications.front()[2].get_raw().type == game::SCRIPT_NONE);
|
||||
assert(diagnostics["http_status"].as<int>() == 200 && diagnostics["curl_code"].as<int>() == 0);
|
||||
assert(diagnostics["dns_ms"].as<double>() == 1 && diagnostics["connect_ms"].as<double>() == 2);
|
||||
assert(diagnostics["tls_ms"].as<double>() == 3 && diagnostics["first_byte_ms"].as<double>() == 4 && diagnostics["total_ms"].as<double>() == 5);
|
||||
utils::http::next_result->status = 500;
|
||||
assert(send()["http_status"].as<int>() == 500 && notifications.front()[1].as<bool>());
|
||||
utils::http::next_result->code = CURLE_OPERATION_TIMEDOUT;
|
||||
assert(send()["curl_code"].as<int>() == 28 && !notifications.front()[1].as<bool>());
|
||||
assert(notifications.front()[0].get_raw().type == game::SCRIPT_NONE && notifications.front()[2].is<std::string>());
|
||||
utils::http::next_result = std::nullopt;
|
||||
send();
|
||||
assert(!notifications.front()[1].as<bool>());
|
||||
utils::http::throw_from_transfer = true;
|
||||
send();
|
||||
assert(!notifications.front()[1].as<bool>());
|
||||
utils::http::throw_from_transfer = false;
|
||||
utils::http::next_result = successful;
|
||||
utils::http::next_result->buffer = std::string(0x5001, 'x');
|
||||
send();
|
||||
assert(notifications.front()[0].as<std::string>().size() == 0x5000);
|
||||
// A resumed waiter can enqueue enough work to reallocate the pending vector.
|
||||
utils::http::next_result = successful;
|
||||
notifications.clear();
|
||||
http::create_request(params);
|
||||
scheduler::thread_pool.run();
|
||||
after_notify = [&]() { for (int i = 0; i < 100; ++i) http::create_request(params); };
|
||||
http::check_requests();
|
||||
after_notify = {};
|
||||
assert(http::requests.size() == 100 && notifications.size() == 1);
|
||||
scheduler::thread_pool.run();
|
||||
http::check_requests();
|
||||
assert(http::requests.empty() && notifications.size() == 101);
|
||||
puts("\nPASS component: option validation, metadata, compatible argument order, single completion on failures/exceptions, reentrant queue");
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Minimal host interfaces for compiling the real component without the game VM.
|
||||
#pragma once
|
||||
#include "http.hpp"
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace game { constexpr int SCRIPT_NONE = 0; }
|
||||
namespace plugin { class plugin {}; }
|
||||
class component_interface
|
||||
{
|
||||
public:
|
||||
virtual ~component_interface() = default;
|
||||
virtual void on_startup(plugin::plugin*) {}
|
||||
virtual void on_shutdown(plugin::plugin*) {}
|
||||
};
|
||||
#define REGISTER_COMPONENT(...)
|
||||
|
||||
namespace scripting
|
||||
{
|
||||
class array;
|
||||
class script_value;
|
||||
using fields = std::map<std::string, script_value>;
|
||||
class script_value
|
||||
{
|
||||
public:
|
||||
std::variant<std::monostate, int, bool, double, std::string, std::shared_ptr<fields>> data;
|
||||
script_value() = default;
|
||||
script_value(int v) : data(v) {}
|
||||
script_value(bool v) : data(v) {}
|
||||
script_value(double v) : data(v) {}
|
||||
script_value(const char* v) : data(std::string(v)) {}
|
||||
script_value(std::string v) : data(std::move(v)) {}
|
||||
script_value(const array& v);
|
||||
template <typename T> bool is() const { return std::holds_alternative<T>(data); }
|
||||
template <typename T> T as() const { return std::get<T>(data); }
|
||||
struct raw { int type; };
|
||||
raw get_raw() const { return {static_cast<int>(data.index())}; }
|
||||
std::string to_string() const { return as<std::string>(); }
|
||||
};
|
||||
class array
|
||||
{
|
||||
public:
|
||||
std::shared_ptr<fields> data = std::make_shared<fields>();
|
||||
script_value& operator[](const script_value& key) const { return (*data)[key.as<std::string>()]; }
|
||||
std::vector<script_value> get_keys() const
|
||||
{
|
||||
std::vector<script_value> keys;
|
||||
for (const auto& [key, value] : *data) keys.emplace_back(key);
|
||||
return keys;
|
||||
}
|
||||
};
|
||||
inline script_value::script_value(const array& v) : data(v.data) {}
|
||||
template <> inline bool script_value::is<array>() const { return std::holds_alternative<std::shared_ptr<fields>>(data); }
|
||||
template <> inline array script_value::as<array>() const
|
||||
{
|
||||
array result;
|
||||
result.data = std::get<std::shared_ptr<fields>>(data);
|
||||
return result;
|
||||
}
|
||||
using variadic_args = std::vector<script_value>;
|
||||
class object
|
||||
{
|
||||
inline static unsigned next_id = 1;
|
||||
unsigned id = next_id++;
|
||||
public:
|
||||
unsigned get_entity_id() const { return id; }
|
||||
};
|
||||
inline std::vector<std::vector<script_value>> notifications;
|
||||
inline std::function<void()> after_notify;
|
||||
inline void notify(unsigned, const std::string& event, const std::vector<script_value>& args)
|
||||
{
|
||||
assert(event == "done");
|
||||
notifications.push_back(args);
|
||||
if (after_notify) after_notify();
|
||||
}
|
||||
inline void on_shutdown(const std::function<void()>&) {}
|
||||
}
|
||||
namespace scheduler
|
||||
{
|
||||
struct pool
|
||||
{
|
||||
std::vector<std::function<void()>> tasks;
|
||||
void push(std::function<void()> f) { tasks.push_back(std::move(f)); }
|
||||
void run()
|
||||
{
|
||||
auto work = std::move(tasks);
|
||||
tasks.clear();
|
||||
for (auto& task : work) task();
|
||||
}
|
||||
};
|
||||
inline pool thread_pool;
|
||||
constexpr int server = 0;
|
||||
inline void loop(const std::function<void()>&, int) {}
|
||||
}
|
||||
namespace gsc::function
|
||||
{
|
||||
template <typename F> void add(const char*, F) {}
|
||||
template <typename F, typename... Names> void add_multiple(F, Names...) {}
|
||||
}
|
||||
namespace utils::http
|
||||
{
|
||||
inline bool throw_from_transfer = false;
|
||||
inline std::optional<result> next_result = result{};
|
||||
inline timeouts observed_limits;
|
||||
inline std::optional<result> get_data(const std::string&, const std::string&, const headers&, const std::string&, const timeouts& limits)
|
||||
{
|
||||
observed_limits = limits;
|
||||
if (throw_from_transfer) throw std::runtime_error("simulated worker error");
|
||||
return next_result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc != 4 || curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
|
||||
return 2;
|
||||
utils::http::timeouts limits;
|
||||
// A dash exercises the defaults compiled into the actual plugin source.
|
||||
if (std::string(argv[2]) != "-") limits.connect_timeout_ms = std::stol(argv[2]);
|
||||
if (std::string(argv[3]) != "-") limits.timeout_ms = std::stol(argv[3]);
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
const auto result = utils::http::get_data(argv[1], "{\"event\":\"heartbeat\"}",
|
||||
{{"Content-Type", "application/json"}, {"Authorization", "Bearer test-only"}}, "POST", limits);
|
||||
const auto wall_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
|
||||
if (!result) return 3;
|
||||
std::cout << "{\"curl_code\":" << result->code << ",\"http_status\":" << result->status
|
||||
<< ",\"dns_ms\":" << result->dns_ms << ",\"connect_ms\":" << result->connect_ms
|
||||
<< ",\"tls_ms\":" << result->tls_ms << ",\"first_byte_ms\":" << result->first_byte_ms
|
||||
<< ",\"total_ms\":" << result->total_ms << ",\"wall_ms\":" << wall_ms
|
||||
<< ",\"body_size\":" << result->buffer.size() << "}\n";
|
||||
curl_global_cleanup();
|
||||
}
|
||||
Reference in New Issue
Block a user