feat(seatpool): the seat pool and the board several drivers share

The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.

:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.

The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 08:15:03 +01:00
commit c63c898833
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
10 changed files with 2562 additions and 8 deletions

View file

@ -0,0 +1,108 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <netdb.h>
module Crafter.Review:Http_impl;
import :Http;
import :Json;
import :Text;
import Crafter.Network;
import std;
namespace Crafter::Review::Http {
namespace {
std::string Wrapped(std::string_view reason) {
return std::format("<urlopen error {}>", reason);
}
// ClientTCP's resolve, socket() and connect failures, which happen
// while the request goes out, apart from a send's or a receive's.
bool WhileSending(const SocketErrorException& failure) {
const std::string_view text = failure.what();
return failure.kind == SocketErrorKind::Resolver || text.starts_with("Could not connect") || text.starts_with("Could not open socket");
}
std::string SocketText(const SocketErrorException& failure) {
if (failure.kind == SocketErrorKind::Resolver) return std::format("[Errno {}] {}", failure.code, gai_strerror(failure.code));
return Dyn::ErrnoText(failure.code);
}
constexpr std::array<std::pair<std::int32_t, std::string_view>, 62> Phrases = {{
{100, "Continue"}, {101, "Switching Protocols"}, {102, "Processing"}, {103, "Early Hints"}, {200, "OK"}, {201, "Created"}, {202, "Accepted"},
{203, "Non-Authoritative Information"}, {204, "No Content"}, {205, "Reset Content"}, {206, "Partial Content"}, {207, "Multi-Status"},
{208, "Already Reported"}, {226, "IM Used"}, {300, "Multiple Choices"}, {301, "Moved Permanently"}, {302, "Found"}, {303, "See Other"},
{304, "Not Modified"}, {305, "Use Proxy"}, {307, "Temporary Redirect"}, {308, "Permanent Redirect"}, {400, "Bad Request"}, {401, "Unauthorized"},
{402, "Payment Required"}, {403, "Forbidden"}, {404, "Not Found"}, {405, "Method Not Allowed"}, {406, "Not Acceptable"},
{407, "Proxy Authentication Required"}, {408, "Request Timeout"}, {409, "Conflict"}, {410, "Gone"}, {411, "Length Required"},
{412, "Precondition Failed"}, {413, "Content Too Large"}, {414, "URI Too Long"}, {415, "Unsupported Media Type"}, {416, "Range Not Satisfiable"},
{417, "Expectation Failed"}, {418, "I'm a Teapot"}, {421, "Misdirected Request"}, {422, "Unprocessable Content"}, {423, "Locked"},
{424, "Failed Dependency"}, {425, "Too Early"}, {426, "Upgrade Required"}, {428, "Precondition Required"}, {429, "Too Many Requests"},
{431, "Request Header Fields Too Large"}, {451, "Unavailable For Legal Reasons"}, {500, "Internal Server Error"}, {501, "Not Implemented"},
{502, "Bad Gateway"}, {503, "Service Unavailable"}, {504, "Gateway Timeout"}, {505, "HTTP Version Not Supported"}, {506, "Variant Also Negotiates"},
{507, "Insufficient Storage"}, {508, "Loop Detected"}, {510, "Not Extended"}, {511, "Network Authentication Required"},
}};
}
HttpError::HttpError(std::int32_t code, const std::string& message) : std::runtime_error(std::format("HTTP Error {}: {}", code, message)), code(code) {}
HttpError::HttpError(std::int32_t code) : HttpError(code, std::string(Reason(code))) {}
std::int32_t HttpError::Code() const {
return code;
}
std::string_view Reason(std::int32_t code) {
for (const auto& [value, phrase] : Phrases) {
if (value == code) return phrase;
}
return "";
}
Response UrlOpen(std::string_view url, const std::map<std::string, std::string>& headers, double timeout) {
FetchOptionsHTTP1 options;
options.headers = {{"accept-encoding", "identity"}, {"connection", "close"}, {"user-agent", "crafter-review"}};
for (const auto& [name, value] : headers) options.headers[Dyn::Lower(name)] = value;
// The timeout bounds each blocking step: the connect, the handshake,
// every read.
const std::chrono::milliseconds budget(static_cast<std::int64_t>(timeout * 1000));
options.connectTimeout = budget;
options.handshakeTimeout = budget;
options.timeout = budget;
FetchResultHTTP1 result;
try {
result = FetchHTTP1(url, options);
} catch (const SocketTimeoutException&) {
throw Error(Wrapped("timed out"));
} catch (const SocketErrorException& failure) {
throw Error(WhileSending(failure) ? Wrapped(SocketText(failure)) : SocketText(failure));
} catch (const SocketClosedException&) {
throw Error("Remote end closed connection without response");
} catch (const TLSException& failure) {
throw Error(Wrapped(failure.what()));
} catch (const std::invalid_argument& failure) {
throw Error(failure.what());
} catch (const std::runtime_error& failure) {
const std::string_view text = failure.what();
if (text.starts_with("timed out")) throw Error("timed out");
if (text.starts_with("connection closed")) throw Error("Remote end closed connection without response");
throw Error(failure.what());
} catch (const std::exception& failure) {
throw Error(failure.what());
}
const std::optional<std::int64_t> status = Dyn::ParseInt(result.response.status);
const auto code = static_cast<std::int32_t>(status.value_or(0));
if (result.limited) {
throw HttpError(code, std::format("The HTTP server returned a redirect error that would lead to an infinite loop.\nThe last 30x error message was:\n{}", Reason(code)));
}
if (code < 200 || code >= 300) throw HttpError(code);
return {code, std::move(result.response.body)};
}
Json JsonBody(const Response& response) {
if (const std::optional<std::string> error = Dyn::Utf8DecodeError(response.body)) throw Dyn::Error("UnicodeDecodeError", *error);
if (const std::optional<std::string> error = Dyn::JsonDecodeError(response.body)) throw Dyn::Error("JSONDecodeError", *error);
return ParseLoose(response.body);
}
}