core: net/http_client — libcurl multi wrapper (stage 2)
One HttpClient owns a small pool of workers, each with its own CURLM; an easy handle lives on one worker for its life. Public start/pause/resume/ cancel enqueue a command + curl_multi_wakeup(); callbacks (on_head / on_data / on_finished) run on the worker thread and return a DataAction (proceed / pause / abort). Covers redirects (final-response head only), ranges (inclusive ByteRange -> CURLOPT_RANGE), proxy/SOCKS5, basic/digest auth, cookies, verbatim headers, stall detection, a coarse recv-rate cap, and a curl_share DNS/TLS cache across workers. CURLcode + HTTP status -> vdm::Error in net/curl_error. A probe is on_head returning abort: it finishes successfully (head_complete), not canceled. Tests drive tools/testserver: full GET, ranged 206, redirect chain, 404 -> not_found, connection refused -> connect_failed, HEAD probe + ranged 0-0 probe (no body), cancel mid-transfer, pause/resume completes. Skip cleanly if testserver isn't in the tree. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// testserver_fixture.hpp — spawn tools/testserver for a test, tear it down after.
|
||||
//
|
||||
// Linux-only (fork/exec/pipe/kill). The path to testserver.py is injected by CMake as
|
||||
// VDM_TESTSERVER_PY; if it's empty or missing the fixture reports unavailable() and the
|
||||
// test should skip.
|
||||
|
||||
#ifndef VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP
|
||||
#define VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#ifndef VDM_TESTSERVER_PY
|
||||
#define VDM_TESTSERVER_PY ""
|
||||
#endif
|
||||
|
||||
namespace vdm::testing {
|
||||
|
||||
class TestServer {
|
||||
public:
|
||||
TestServer() {
|
||||
const char *script = VDM_TESTSERVER_PY;
|
||||
if (!script || !*script || ::access(script, R_OK) != 0)
|
||||
return;
|
||||
|
||||
int pipefd[2];
|
||||
if (::pipe(pipefd) != 0)
|
||||
return;
|
||||
|
||||
pid_ = ::fork();
|
||||
if (pid_ < 0) {
|
||||
::close(pipefd[0]);
|
||||
::close(pipefd[1]);
|
||||
return;
|
||||
}
|
||||
if (pid_ == 0) {
|
||||
::dup2(pipefd[1], STDOUT_FILENO);
|
||||
::close(pipefd[0]);
|
||||
::close(pipefd[1]);
|
||||
int devnull = ::open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0)
|
||||
::dup2(devnull, STDERR_FILENO);
|
||||
::execlp("python3", "python3", script, "--port", "0", "--seed", "9", "--loris-seconds",
|
||||
"1", "--throttle-bps", "131072", static_cast<char *>(nullptr));
|
||||
::_exit(127);
|
||||
}
|
||||
::close(pipefd[1]);
|
||||
|
||||
// Read the port line the server prints to stdout.
|
||||
std::string line;
|
||||
char c = 0;
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
ssize_t r = ::read(pipefd[0], &c, 1);
|
||||
if (r == 1) {
|
||||
if (c == '\n')
|
||||
break;
|
||||
line += c;
|
||||
} else if (r == 0) {
|
||||
break;
|
||||
} else if (errno != EINTR) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
::close(pipefd[0]);
|
||||
if (!line.empty())
|
||||
port_ = std::atoi(line.c_str());
|
||||
|
||||
// Give the listener a moment to accept.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
||||
}
|
||||
|
||||
~TestServer() {
|
||||
if (pid_ > 0) {
|
||||
::kill(pid_, SIGTERM);
|
||||
int status = 0;
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
if (::waitpid(pid_, &status, WNOHANG) == pid_)
|
||||
return;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
::kill(pid_, SIGKILL);
|
||||
::waitpid(pid_, &status, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TestServer(const TestServer &) = delete;
|
||||
TestServer &operator=(const TestServer &) = delete;
|
||||
|
||||
[[nodiscard]] bool available() const { return port_ > 0; }
|
||||
[[nodiscard]] int port() const { return port_; }
|
||||
[[nodiscard]] std::string url(const std::string &path) const {
|
||||
return "http://127.0.0.1:" + std::to_string(port_) + path;
|
||||
}
|
||||
|
||||
private:
|
||||
pid_t pid_ = -1;
|
||||
int port_ = 0;
|
||||
};
|
||||
|
||||
} // namespace vdm::testing
|
||||
|
||||
#endif // VDM_TESTS_NET_TESTSERVER_FIXTURE_HPP
|
||||
Reference in New Issue
Block a user