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>
2026-10-20 19:07:29 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
module;
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <netinet/in.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <sys/uio.h>
|
|
|
|
|
#include <sys/time.h>
|
|
|
|
|
#include <sys/wait.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#include <netdb.h>
|
|
|
|
|
#include <strings.h>
|
|
|
|
|
#include <cerrno>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <netinet/tcp.h>
|
|
|
|
|
#include <poll.h>
|
|
|
|
|
|
|
|
|
|
module Crafter.Network:ClientTCP_impl;
|
|
|
|
|
import :ClientTCP;
|
|
|
|
|
import Crafter.Thread;
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
// recv, retried when a signal interrupts it: a receive waits on through
|
|
|
|
|
// a signal rather than failing.
|
|
|
|
|
ssize_t RecieveRetrying(int socketid, void* buffer, std::size_t size) {
|
|
|
|
|
ssize_t read;
|
|
|
|
|
do {
|
|
|
|
|
read = recv(socketid, buffer, size, 0);
|
|
|
|
|
} while (read < 0 && errno == EINTR);
|
|
|
|
|
return read;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 07:06:24 +01:00
|
|
|
// A failed recv. EAGAIN is the receive timeout (SO_RCVTIMEO) running out;
|
|
|
|
|
// anything else carries its errno, under the message a receive always
|
|
|
|
|
// threw.
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
[[noreturn]] void ThrowRecieveError() {
|
2026-10-31 07:06:24 +01:00
|
|
|
const int failure = errno;
|
|
|
|
|
if (failure == EAGAIN || failure == EWOULDBLOCK) throw SocketTimeoutException();
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
throw SocketErrorException(SocketPhase::Receive, std::error_code(failure, std::generic_category()));
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
// getaddrinfo's results as an error category: the number is one of
|
|
|
|
|
// netdb.h's EAI_* codes and the text is what gai_strerror makes of it.
|
|
|
|
|
class ResolverErrorCategory : public std::error_category {
|
|
|
|
|
public:
|
|
|
|
|
const char* name() const noexcept override {
|
|
|
|
|
return "resolver";
|
|
|
|
|
}
|
|
|
|
|
std::string message(int code) const override {
|
|
|
|
|
return gai_strerror(code);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
// One connect attempt to one address: a connected, blocking socket, or a
|
|
|
|
|
// throw that leaves nothing open.
|
|
|
|
|
int ConnectTo(const sockaddr* address, socklen_t length, std::chrono::milliseconds timeout) {
|
|
|
|
|
const int fd = socket(address->sa_family, SOCK_STREAM, 0);
|
|
|
|
|
if (fd == -1) {
|
|
|
|
|
// An address family the host cannot open — IPv6 switched off,
|
|
|
|
|
// say — fails this address alone; the caller moves on.
|
|
|
|
|
const int failure = errno;
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
throw SocketErrorException(SocketPhase::Open, std::error_code(failure, std::generic_category()), "Could not open socket");
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// With a timeout the connect runs non-blocking, so poll can bound the
|
|
|
|
|
// wait; the socket is made blocking again once it is connected.
|
|
|
|
|
const bool bounded = timeout > std::chrono::milliseconds::zero();
|
|
|
|
|
const int flags = fcntl(fd, F_GETFL, 0);
|
|
|
|
|
if (bounded) {
|
|
|
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
|
|
|
}
|
|
|
|
|
int failure = 0;
|
|
|
|
|
if (connect(fd, address, length) == -1) {
|
|
|
|
|
failure = errno;
|
|
|
|
|
if (failure == EINPROGRESS) {
|
|
|
|
|
pollfd connecting { .fd = fd, .events = POLLOUT, .revents = 0 };
|
|
|
|
|
const int waitMs = static_cast<int>(std::min<std::chrono::milliseconds::rep>(timeout.count(), std::numeric_limits<int>::max()));
|
|
|
|
|
int ready;
|
|
|
|
|
do {
|
|
|
|
|
ready = poll(&connecting, 1, waitMs);
|
|
|
|
|
} while (ready == -1 && errno == EINTR);
|
|
|
|
|
if (ready == 0) {
|
|
|
|
|
close(fd);
|
|
|
|
|
throw SocketTimeoutException();
|
|
|
|
|
}
|
|
|
|
|
if (ready == -1) {
|
|
|
|
|
failure = errno;
|
|
|
|
|
} else {
|
|
|
|
|
// The connect's own result.
|
|
|
|
|
socklen_t size = sizeof(failure);
|
|
|
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, &failure, &size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failure != 0) {
|
|
|
|
|
// Report the failure instead of handing back a socket that is not
|
|
|
|
|
// connected to anything — every later send/recv on it would fail
|
|
|
|
|
// with a far less obvious error.
|
|
|
|
|
close(fd);
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
throw SocketErrorException(SocketPhase::Connect, std::error_code(failure, std::generic_category()), "Could not connect to server");
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
}
|
|
|
|
|
if (bounded) {
|
|
|
|
|
fcntl(fd, F_SETFL, flags);
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
|
|
|
|
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
const std::error_category& Crafter::ResolverCategory() noexcept {
|
|
|
|
|
static const ResolverErrorCategory category;
|
|
|
|
|
return category;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
ClientTCP::ClientTCP(int socketid) : socketid(socketid)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClientTCP::ClientTCP(const char* hostName, std::uint16_t port, std::chrono::milliseconds connectTimeout)
|
|
|
|
|
{
|
|
|
|
|
// getaddrinfo rather than gethostbyname: the latter is not thread safe,
|
|
|
|
|
// and it signals failure by returning null — which was then dereferenced
|
|
|
|
|
// straight into a crash on any unresolvable host.
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
//
|
|
|
|
|
// AF_UNSPEC, and every result kept: `localhost` resolves to ::1 and
|
|
|
|
|
// 127.0.0.1, and a server may listen on either one alone. Asking for
|
|
|
|
|
// AF_INET only, as this used to, made a server bound to ::1 unreachable
|
|
|
|
|
// by name. No AI_ADDRCONFIG, as Python's create_connection passes none
|
|
|
|
|
// either: it hides ::1 on a host whose only IPv6 address is loopback.
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
addrinfo hints{};
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
hints.ai_family = AF_UNSPEC;
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
|
addrinfo* resolved = nullptr;
|
|
|
|
|
const std::string service = std::to_string(port);
|
|
|
|
|
const int status = getaddrinfo(hostName, service.c_str(), &hints, &resolved);
|
|
|
|
|
if (status != 0 || resolved == nullptr) {
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
throw SocketErrorException(SocketPhase::Resolve, std::error_code(status, ResolverCategory()),
|
|
|
|
|
std::string("Could not resolve host '") + hostName + "'");
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
}
|
|
|
|
|
for (const addrinfo* entry = resolved; entry != nullptr; entry = entry->ai_next) {
|
|
|
|
|
Address address {};
|
|
|
|
|
std::memcpy(&address.storage, entry->ai_addr, entry->ai_addrlen);
|
|
|
|
|
address.length = entry->ai_addrlen;
|
|
|
|
|
addresses.push_back(address);
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
|
|
|
|
freeaddrinfo(resolved);
|
|
|
|
|
|
|
|
|
|
Connect(connectTimeout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClientTCP::ClientTCP(std::string hostName, std::uint16_t port, std::chrono::milliseconds connectTimeout): ClientTCP(hostName.c_str(), port, connectTimeout)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The moved-from object gives up ownership; the socket itself stays open —
|
|
|
|
|
// closing it here (as this used to) meant moving a ClientTCP silently
|
|
|
|
|
// dropped the connection it was carrying.
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
ClientTCP::ClientTCP(ClientTCP&& other) noexcept : socketid(other.socketid), addresses(std::move(other.addresses)) {
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
other.socketid = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClientTCP::~ClientTCP()
|
|
|
|
|
{
|
|
|
|
|
if(socketid != -1) {
|
|
|
|
|
shutdown(socketid, SHUT_RDWR);
|
|
|
|
|
close(socketid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::Connect(std::chrono::milliseconds timeout) {
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
if (addresses.empty()) {
|
|
|
|
|
throw std::logic_error("ClientTCP::Connect: no address to connect to; a socket handed over by accept() cannot connect again");
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
// Each address in turn, in the order getaddrinfo ranked them; when none
|
|
|
|
|
// connects, the caller sees what the last one failed with, as with
|
|
|
|
|
// Python's create_connection.
|
|
|
|
|
std::exception_ptr last;
|
|
|
|
|
for (const Address& address : addresses) {
|
|
|
|
|
try {
|
|
|
|
|
socketid = ConnectTo(reinterpret_cast<const sockaddr*>(&address.storage), address.length, timeout);
|
|
|
|
|
return;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
last = std::current_exception();
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:06:41 +01:00
|
|
|
socketid = -1;
|
|
|
|
|
std::rethrow_exception(last);
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::Stop() {
|
|
|
|
|
shutdown(socketid, SHUT_RDWR);
|
|
|
|
|
close(socketid);
|
|
|
|
|
socketid = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::SetNoDelay(bool enabled) {
|
|
|
|
|
const int value = enabled ? 1 : 0;
|
|
|
|
|
if (setsockopt(socketid, IPPROTO_TCP, TCP_NODELAY, &value, sizeof(value)) == -1) {
|
|
|
|
|
throw std::runtime_error(std::string("Could not set TCP_NODELAY: ") + std::strerror(errno));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::SetReceiveTimeout(std::chrono::milliseconds timeout) {
|
|
|
|
|
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(timeout);
|
|
|
|
|
timeval value {};
|
|
|
|
|
value.tv_sec = static_cast<time_t>(seconds.count());
|
|
|
|
|
value.tv_usec = static_cast<suseconds_t>(std::chrono::duration_cast<std::chrono::microseconds>(timeout - seconds).count());
|
|
|
|
|
if (setsockopt(socketid, SOL_SOCKET, SO_RCVTIMEO, &value, sizeof(value)) == -1) {
|
|
|
|
|
throw std::runtime_error(std::string("Could not set the receive timeout: ") + std::strerror(errno));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::Send(const void* buffer, std::uint32_t size) const {
|
|
|
|
|
// send() is free to accept less than it was offered, and does so
|
|
|
|
|
// routinely once a buffer outgrows the socket's send buffer — a
|
|
|
|
|
// multi-megabyte HTTP body, say. Loop until it is all handed over.
|
|
|
|
|
const char* data = reinterpret_cast<const char*>(buffer);
|
|
|
|
|
std::uint32_t sent = 0;
|
|
|
|
|
while (sent < size) {
|
|
|
|
|
const auto status = send(socketid, data + sent, size - sent, MSG_NOSIGNAL);
|
|
|
|
|
if (status == 0) {
|
|
|
|
|
throw SocketClosedException();
|
|
|
|
|
} else if (status < 0) {
|
|
|
|
|
if (errno == EINTR) continue;
|
2026-10-31 07:06:24 +01:00
|
|
|
const int failure = errno;
|
feat(tcp): socket errors as std::system_error, with the phase that failed
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
2026-10-31 20:33:37 +01:00
|
|
|
throw SocketErrorException(SocketPhase::Send, std::error_code(failure, std::generic_category()));
|
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>
2026-10-20 19:07:29 +02:00
|
|
|
}
|
|
|
|
|
sent += static_cast<std::uint32_t>(status);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::vector<char> ClientTCP::RecieveSync(std::uint32_t bufferSize) const {
|
|
|
|
|
// recv's 0 means a closed peer, but not for a receive of no bytes.
|
|
|
|
|
if (bufferSize == 0) return {};
|
|
|
|
|
std::vector<char> buffer(bufferSize);
|
|
|
|
|
const ssize_t read = RecieveRetrying(socketid, buffer.data(), bufferSize);
|
|
|
|
|
if (read < 0) ThrowRecieveError();
|
|
|
|
|
if (read == 0) throw SocketClosedException();
|
|
|
|
|
buffer.resize(static_cast<std::size_t>(read));
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
std::uint32_t ClientTCP::RecieveSync(std::uint32_t bufferSize, void* buffer) const {
|
|
|
|
|
if (bufferSize == 0) return 0;
|
|
|
|
|
const ssize_t read = RecieveRetrying(socketid, buffer, bufferSize);
|
|
|
|
|
if (read < 0) ThrowRecieveError();
|
|
|
|
|
if (read == 0) throw SocketClosedException();
|
|
|
|
|
return static_cast<std::uint32_t>(read);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<char> ClientTCP::RecieveSync() const {
|
|
|
|
|
int count = 0;
|
|
|
|
|
ioctl(socketid, FIONREAD, &count);
|
|
|
|
|
if (count <= 0) {
|
|
|
|
|
count = 1024;
|
|
|
|
|
}
|
|
|
|
|
std::vector<char> buffer(static_cast<std::size_t>(count));
|
|
|
|
|
const ssize_t read = RecieveRetrying(socketid, buffer.data(), buffer.size());
|
|
|
|
|
if (read < 0) ThrowRecieveError();
|
|
|
|
|
if (read == 0) throw SocketClosedException();
|
|
|
|
|
buffer.resize(static_cast<std::size_t>(read));
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<char> ClientTCP::RecieveUntilCloseSync() const {
|
|
|
|
|
std::vector<char> buffer;
|
|
|
|
|
while (true) {
|
|
|
|
|
int count = 0;
|
|
|
|
|
ioctl(socketid, FIONREAD, &count);
|
|
|
|
|
if (count <= 0) {
|
|
|
|
|
count = 1024;
|
|
|
|
|
}
|
|
|
|
|
const std::size_t oldSize = buffer.size();
|
|
|
|
|
buffer.resize(oldSize + static_cast<std::size_t>(count));
|
|
|
|
|
const ssize_t read = RecieveRetrying(socketid, buffer.data() + oldSize, static_cast<std::size_t>(count));
|
|
|
|
|
if (read < 0) ThrowRecieveError();
|
|
|
|
|
buffer.resize(oldSize + static_cast<std::size_t>(read));
|
|
|
|
|
if (read == 0) {
|
|
|
|
|
// A peer that closes before sending anything is reported the
|
|
|
|
|
// way the other receives report it.
|
|
|
|
|
if (buffer.empty()) throw SocketClosedException();
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<char> ClientTCP::RecieveUntilFullSync(std::uint32_t bufferSize) const {
|
|
|
|
|
std::vector<char> buffer(bufferSize);
|
|
|
|
|
std::size_t read = 0;
|
|
|
|
|
while (read < bufferSize) {
|
|
|
|
|
const ssize_t newRead = RecieveRetrying(socketid, buffer.data() + read, bufferSize - read);
|
|
|
|
|
if (newRead < 0) ThrowRecieveError();
|
|
|
|
|
if (newRead == 0) throw SocketClosedException();
|
|
|
|
|
read += static_cast<std::size_t>(newRead);
|
|
|
|
|
}
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
// Runs `receive` on the ThreadPool and hands its result to `onReceived`,
|
|
|
|
|
// or what it threw to `onError` (dropped without one): a pool task must
|
|
|
|
|
// not let an exception escape, or it ends the program. The callback runs
|
|
|
|
|
// outside the try, so its own exceptions aren't taken for a failed
|
|
|
|
|
// receive.
|
|
|
|
|
template <typename Result>
|
|
|
|
|
void EnqueueReceive(std::function<Result()> receive, std::function<void(Result)> onReceived, std::function<void(std::exception_ptr)> onError) {
|
|
|
|
|
ThreadPool::Enqueue([receive = std::move(receive), onReceived = std::move(onReceived), onError = std::move(onError)]() {
|
|
|
|
|
std::optional<Result> result;
|
|
|
|
|
try {
|
|
|
|
|
result = receive();
|
|
|
|
|
} catch (...) {
|
|
|
|
|
if (onError) onError(std::current_exception());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onReceived(std::move(*result));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const {
|
|
|
|
|
EnqueueReceive<std::vector<char>>([this, bufferSize] { return RecieveSync(bufferSize); }, std::move(recieveCallback), std::move(onError));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function<void(std::uint32_t)> recieveCallback, char* buffer, std::function<void(std::exception_ptr)> onError) const {
|
|
|
|
|
EnqueueReceive<std::uint32_t>([this, bufferSize, buffer] { return RecieveSync(bufferSize, buffer); }, std::move(recieveCallback), std::move(onError));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const {
|
|
|
|
|
EnqueueReceive<std::vector<char>>([this, bufferSize] { return RecieveUntilFullSync(bufferSize); }, std::move(recieveCallback), std::move(onError));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveAsync(std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const {
|
|
|
|
|
EnqueueReceive<std::vector<char>>([this] { return RecieveSync(); }, std::move(recieveCallback), std::move(onError));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClientTCP::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const {
|
|
|
|
|
EnqueueReceive<std::vector<char>>([this] { return RecieveUntilCloseSync(); }, std::move(recieveCallback), std::move(onError));
|
|
|
|
|
}
|