feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived

- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
  them msquic: a consumer that only needs TCP builds no msquic. The flag
  isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
  Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
  SocketTimeoutException.
- TCP receives return the bytes that arrived:
  - RecieveSync(n) returned the whole buffer when fewer had come;
  - RecieveUntilCloseSync truncated what it had read;
  - a signal (EINTR) during a receive threw where it should wait on;
  - RecieveSync(size, buffer) returns the count and throws like the others;
  - a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
  exception escaping a ThreadPool task ended the program. An empty onError
  drops the error.
- QUIC streams:
  - async receives deliver an empty chunk on close, as the browser build does;
  - RecieveUntilFullAsync gives what arrived on a stream that closes early;
  - RecieveUntilFullSync takes all it asked for or nothing, under one hold
    of the lock, so what did arrive stays for the next receive and no
    other receive can take bytes from among it;
  - SendAsync's callback hears whether the send went out (bool sent): native,
    browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
  (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
  openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
  config_diagnostics on) made it fail to initialise. The README says to use
  --no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
  x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
  ShouldRecieveTCP, ShouldReportClosedQUICStream.

Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-20 19:07:29 +02:00
commit f683216850
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
14 changed files with 1216 additions and 382 deletions

View file

@ -7,36 +7,52 @@ namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
constexpr std::array<std::string_view, 15> networkInterfaces = {
// TCP, HTTP/1.1 and TLS: what every native build has.
constexpr std::array<std::string_view, 9> networkInterfaces = {
"interfaces/Crafter.Network",
"interfaces/Crafter.Network-ClientTCP",
"interfaces/Crafter.Network-ListenerTCP",
"interfaces/Crafter.Network-ClientHTTP",
"interfaces/Crafter.Network-ListenerHTTP",
"interfaces/Crafter.Network-HTTP",
"interfaces/Crafter.Network-HTTP1",
"interfaces/Crafter.Network-Stream",
"interfaces/Crafter.Network-TLS",
"interfaces/Crafter.Network-ClientHTTP1",
"interfaces/Crafter.Network-ListenerHTTP1",
};
// HTTP/3, QUIC and WebTransport, over msquic. --no-quic leaves them out.
constexpr std::array<std::string_view, 6> quicInterfaces = {
"interfaces/Crafter.Network-ClientHTTP",
"interfaces/Crafter.Network-ListenerHTTP",
"interfaces/Crafter.Network-HTTP3",
"interfaces/Crafter.Network-ClientQUIC",
"interfaces/Crafter.Network-ListenerQUIC",
"interfaces/Crafter.Network-WebTransport",
};
std::vector<std::string> depArgs(args.begin(), args.end());
Configuration* thread = GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Thread.git" },
.args = depArgs,
});
// --no-quic is this project's own flag, so it stays out of the
// dependencies' args: Crafter.Thread would otherwise build a second copy
// of itself, hashed apart from the one the consumer imports.
std::vector<std::string> depArgs;
for (std::string_view arg : args) {
if (arg != "--no-quic") depArgs.emplace_back(arg);
}
// Crafter.Thread comes from forgejo, or with --local from the checkout
// next to this one. --local stays in depArgs, so Crafter.Thread resolves
// its own dependencies the same way.
const bool useLocal = std::ranges::find(args, std::string_view("--local")) != args.end();
Configuration* thread = useLocal
? LocalProject({ .projectFile = "../Crafter.Thread/project.cpp", .args = depArgs })
: GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Thread.git" },
.args = depArgs,
});
Configuration cfg;
cfg.path = "./";
cfg.name = "crafter-network";
cfg.outputName = "crafter-network";
cfg.type = ConfigurationType::LibraryStatic;
ApplyStandardArgs(cfg, args);
ArgQuery opts = ApplyStandardArgs(cfg, args);
cfg.dependencies = { thread };
// Browser path: any wasm32-* target gets the browser network stack
@ -74,44 +90,65 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
return cfg;
}
constexpr std::array<std::string_view, 11> networkImplementations = {
constexpr std::array<std::string_view, 6> networkImplementations = {
"implementations/Crafter.Network-ClientTCP",
"implementations/Crafter.Network-ListenerTCP",
"implementations/Crafter.Network-ClientHTTP",
"implementations/Crafter.Network-ListenerHTTP",
"implementations/Crafter.Network-Stream",
"implementations/Crafter.Network-TLS",
"implementations/Crafter.Network-ClientHTTP1",
"implementations/Crafter.Network-ListenerHTTP1",
};
constexpr std::array<std::string_view, 5> quicImplementations = {
"implementations/Crafter.Network-ClientHTTP",
"implementations/Crafter.Network-ListenerHTTP",
"implementations/Crafter.Network-ClientQUIC",
"implementations/Crafter.Network-ListenerQUIC",
"implementations/Crafter.Network-WebTransport",
};
// msquic — provides the QUIC transport used by ClientQUIC / ListenerQUIC.
// Cloned + built via CMake into the per-project external cache; no system
// package required. Submodules (quictls / clog / etc.) come via the
// recursive clone Crafter.Build performs. We disable msquic's own tests,
// tools and perf binaries since we only need the library.
ExternalDependency& msquic = cfg.externalDependencies.emplace_back();
msquic.name = "msquic";
msquic.source.url = "https://github.com/microsoft/msquic.git";
msquic.source.branch = "main";
msquic.builder = ExternalBuilder::CMake;
msquic.options = {
"-DQUIC_TLS_LIB=quictls",
"-DQUIC_BUILD_TEST=OFF",
"-DQUIC_BUILD_TOOLS=OFF",
"-DQUIC_BUILD_PERF=OFF",
"-DQUIC_BUILD_SHARED=ON",
};
msquic.includeDirs = { "src/inc" };
// msquic's CMakeLists overrides CMAKE_LIBRARY_OUTPUT_DIRECTORY with
// QUIC_OUTPUT_DIR (defaults to bin/$<CONFIG>), so libmsquic.so lands in
// a subdir of the cmake build dir rather than at its root. Point the
// linker at the actual output location.
msquic.libDirs = { "bin/Release" };
msquic.libs = { "msquic" };
// --no-quic: TCP, HTTP/1.1 and TLS only, for consumers that need no more.
// msquic takes a recursive clone and a CMake build, so leaving it out is
// most of a fresh build. The browser build above has no msquic to leave
// out and ignores the flag.
const bool quic = !opts.Has("--no-quic");
if (quic) {
// msquic — provides the QUIC transport used by ClientQUIC / ListenerQUIC.
// Cloned + built via CMake into the per-project external cache; no system
// package required. Pinned to a release (v2.6.3): main moves, and its
// submodules (quictls / clog / etc.) with it. Crafter.Build checks the
// submodules out at the revisions the pinned commit records. We disable
// msquic's own tests, tools and perf binaries since we only need the
// library.
ExternalDependency& msquic = cfg.externalDependencies.emplace_back();
msquic.name = "msquic";
msquic.source.url = "https://github.com/microsoft/msquic.git";
msquic.source.commit = "16ca4d8d785d34fb22285e7c652e477ac16475a5";
msquic.builder = ExternalBuilder::CMake;
// TLS from the system's OpenSSL (3.5 or newer, which has the QUIC
// API), the same libssl the HTTP/1.1 TLS code links: one OpenSSL in
// the process. The quictls fork msquic otherwise builds and bundles is
// an older OpenSSL that still reads the system's openssl.cnf, and the
// Flatpak runtime's (an engine section for the runtime's own OpenSSL,
// config_diagnostics on) makes its initialisation fail: MsQuicOpen2
// returned QUIC_STATUS_TLS_ERROR (0x7e) there.
msquic.options = {
"-DQUIC_TLS_LIB=openssl",
"-DQUIC_USE_EXTERNAL_OPENSSL=ON",
"-DQUIC_BUILD_TEST=OFF",
"-DQUIC_BUILD_TOOLS=OFF",
"-DQUIC_BUILD_PERF=OFF",
"-DQUIC_BUILD_SHARED=ON",
};
msquic.includeDirs = { "src/inc" };
// msquic's CMakeLists overrides CMAKE_LIBRARY_OUTPUT_DIRECTORY with
// QUIC_OUTPUT_DIR (defaults to bin/$<CONFIG>), so libmsquic.so lands in
// a subdir of the cmake build dir rather than at its root. Point the
// linker at the actual output location.
msquic.libDirs = { "bin/Release" };
msquic.libs = { "msquic" };
} else {
cfg.defines.push_back({"CRAFTER_NETWORK_NO_QUIC", ""});
}
// libssl/libcrypto — the TLS transport behind :TLS, i.e. https:// on the
// HTTP/1.1 client and listener. A system package rather than a built
@ -121,36 +158,48 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.linkFlags.push_back("-lssl");
cfg.linkFlags.push_back("-lcrypto");
std::array<fs::path, 15> ifaces;
std::ranges::copy(networkInterfaces, ifaces.begin());
std::array<fs::path, 11> impls;
std::ranges::copy(networkImplementations, impls.begin());
std::vector<fs::path> ifaces(networkInterfaces.begin(), networkInterfaces.end());
std::vector<fs::path> impls(networkImplementations.begin(), networkImplementations.end());
if (quic) {
ifaces.insert(ifaces.end(), quicInterfaces.begin(), quicInterfaces.end());
impls.insert(impls.end(), quicImplementations.begin(), quicImplementations.end());
}
cfg.GetInterfacesAndImplementations(ifaces, impls);
// Linux-only: msquic + POSIX socket backends. The browser path above
// returns early, so wasm builds skip these. Each test links the local
// crafter-network static lib via .Dependencies({ &cfg }).
if (cfg.target == "x86_64-pc-linux-gnu") {
cfg.AddTest("ShouldEchoWebTransport").Dependencies({ &cfg });
cfg.AddTest("ShouldFallbackUnknownRoutes").Dependencies({ &cfg });
cfg.AddTest("ShouldHonourQUICSettings").Dependencies({ &cfg });
// Linux-only: the POSIX socket backends, and msquic unless --no-quic.
// The browser path above returns early, so wasm builds skip these. Each
// test links the local crafter-network static lib via
// .Dependencies({ &cfg }). Run the suite with --no-quic as well. Any
// Linux triple: the toolchain's own is x86_64-unknown-linux-gnu.
if (cfg.target.find("linux") != std::string::npos) {
cfg.AddTest("ShouldConfigureTCP").Dependencies({ &cfg });
cfg.AddTest("ShouldRecieveTCP").Dependencies({ &cfg });
cfg.AddTest("ShouldInteropCurlHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldInteropCurlHTTPS1").Dependencies({ &cfg });
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
cfg.AddTest("ShouldParseHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldRequireClientCertificateHTTPS1").Dependencies({ &cfg });
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTPS1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveLargeHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveLargeHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveQUICDatagram").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveQUICStream").Dependencies({ &cfg });
cfg.AddTest("ShouldSurviveAbuseHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });
if (quic) {
cfg.AddTest("ShouldEchoWebTransport").Dependencies({ &cfg });
cfg.AddTest("ShouldFallbackUnknownRoutes").Dependencies({ &cfg });
cfg.AddTest("ShouldHonourQUICSettings").Dependencies({ &cfg });
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveLargeHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveQUICDatagram").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveQUICStream").Dependencies({ &cfg });
cfg.AddTest("ShouldReportClosedQUICStream").Dependencies({ &cfg });
cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });
} else {
// Only a --no-quic build can show that msquic stayed out.
cfg.AddTest("ShouldBuildWithoutQUIC").Dependencies({ &cfg });
}
}
return cfg;