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

@ -101,8 +101,8 @@ namespace Crafter::NetworkBrowser {
static std::unordered_map<std::int32_t, ConnectionState*> m;
return m;
}
inline std::unordered_map<std::int32_t, std::function<void()>>& WriteCallbacks() {
static std::unordered_map<std::int32_t, std::function<void()>> m;
inline std::unordered_map<std::int32_t, std::function<void(bool)>>& WriteCallbacks() {
static std::unordered_map<std::int32_t, std::function<void(bool)>> m;
return m;
}
inline std::int32_t NextWriteCallbackId() {
@ -200,9 +200,9 @@ QUICStream::~QUICStream() {
}
void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish,
std::function<void()> onSent) {
std::function<void(bool sent)> onSent) {
if (!impl || impl->state.handle == 0 || impl->state.closed) {
if (onSent) onSent();
if (onSent) onSent(false);
return;
}
std::int32_t cbId = 0;
@ -402,13 +402,14 @@ extern "C" {
TryDispatchRecv(s);
}
// `sent` is nonzero when the write (and the close, with finish) resolved.
__attribute__((export_name("CrafterNetworkOnWtStreamWriteComplete")))
void CrafterNetworkOnWtStreamWriteComplete(std::int32_t callbackId) {
void CrafterNetworkOnWtStreamWriteComplete(std::int32_t callbackId, std::int32_t sent) {
auto it = WriteCallbacks().find(callbackId);
if (it == WriteCallbacks().end()) return;
auto cb = std::move(it->second);
WriteCallbacks().erase(it);
if (cb) cb();
if (cb) cb(sent != 0);
}
__attribute__((export_name("CrafterNetworkOnWtDatagram")))

View file

@ -123,6 +123,53 @@ struct QUICStream::Impl {
bool shutdownComplete = false;
bool sendInFlight = false;
// Up to `count` received bytes, waiting for them; fewer once the peer
// has closed its side and the rest is taken.
std::vector<char> TakeUpTo(std::uint32_t count) {
std::vector<char> out;
out.reserve(count);
std::unique_lock lk(mtx);
while (out.size() < count) {
cv.wait(lk, [&]{ return !pending.empty() || peerSendClosed || shutdownComplete; });
Take(out, count);
if (pending.empty() && (peerSendClosed || shutdownComplete)) break;
}
return out;
}
// Exactly `count` received bytes, once they have all arrived; nothing,
// leaving what did arrive where it is, once the peer has closed its side
// short of them. One hold of the lock throughout, so no other receive
// takes bytes from among them.
std::optional<std::vector<char>> TakeExactly(std::uint32_t count) {
std::unique_lock lk(mtx);
auto arrived = [&] {
std::size_t bytes = 0;
for (const std::vector<char>& chunk : pending) bytes += chunk.size();
return bytes;
};
cv.wait(lk, [&]{ return arrived() >= count || peerSendClosed || shutdownComplete; });
if (arrived() < count) return std::nullopt;
std::vector<char> out;
out.reserve(count);
Take(out, count);
return out;
}
// Moves pending bytes onto `out` until it holds `count`. Under mtx.
void Take(std::vector<char>& out, std::size_t count) {
while (!pending.empty() && out.size() < count) {
auto& chunk = pending.front();
std::size_t want = std::min<std::size_t>(chunk.size(), count - out.size());
out.insert(out.end(), chunk.begin(), chunk.begin() + want);
if (want == chunk.size()) {
pending.pop_front();
} else {
chunk.erase(chunk.begin(), chunk.begin() + want);
}
}
}
// Lifetime: msquic delivers stream callbacks (RECEIVE, SHUTDOWN_COMPLETE, …)
// possibly AFTER the owning QUICStream wrapper is destroyed (the wrapper only
// initiates a graceful shutdown; completion is async). To make that safe, the
@ -401,48 +448,53 @@ std::vector<char> QUICStream::RecieveUntilCloseSync() {
std::vector<char> QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) {
if (!handle || !canReceive) throw QUICClosedException();
std::vector<char> out;
out.reserve(bufferSize);
while (out.size() < bufferSize) {
std::unique_lock lk(impl->mtx);
impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; });
while (!impl->pending.empty() && out.size() < bufferSize) {
auto& chunk = impl->pending.front();
std::size_t want = std::min<std::size_t>(chunk.size(), bufferSize - out.size());
out.insert(out.end(), chunk.begin(), chunk.begin() + want);
if (want == chunk.size()) {
impl->pending.pop_front();
} else {
chunk.erase(chunk.begin(), chunk.begin() + want);
}
}
if (out.size() < bufferSize && (impl->peerSendClosed || impl->shutdownComplete)) {
throw QUICClosedException();
}
}
return out;
// Closed early: what did arrive stays for the next receive.
std::optional<std::vector<char>> out = impl->TakeExactly(bufferSize);
if (!out) throw QUICClosedException();
return std::move(*out);
}
void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish,
std::function<void()> onSent) {
std::function<void(bool sent)> onSent) {
// Copy now: the caller's buffer may not outlive the enqueued task.
std::vector<char> copy(static_cast<const char*>(buffer),
static_cast<const char*>(buffer) + size);
ThreadPool::Enqueue([this, copy = std::move(copy), finish, onSent = std::move(onSent)]() mutable {
try { this->SendSync(copy.data(), static_cast<std::uint32_t>(copy.size()), finish); }
catch (...) { /* swallowed — callback still fires so the caller can move on */ }
if (onSent) onSent();
// Nothing may leave a pool task; the callback hears how it went.
bool sent = true;
try {
this->SendSync(copy.data(), static_cast<std::uint32_t>(copy.size()), finish);
} catch (...) {
sent = false;
}
if (onSent) onSent(sent);
});
}
// The async receives deliver a closed stream as the browser build does: an
// empty chunk, as nothing may leave a pool task.
void QUICStream::RecieveAsync(std::function<void(std::vector<char>)> cb) {
ThreadPool::Enqueue([this, cb]{ cb(this->RecieveSync()); });
ThreadPool::Enqueue([this, cb]{
std::vector<char> chunk;
try {
chunk = this->RecieveSync();
} catch (const QUICClosedException&) {}
cb(std::move(chunk));
});
}
void QUICStream::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> cb) {
ThreadPool::Enqueue([this, cb]{ cb(this->RecieveUntilCloseSync()); });
ThreadPool::Enqueue([this, cb]{
std::vector<char> data;
try {
data = this->RecieveUntilCloseSync();
} catch (const QUICClosedException&) {}
cb(std::move(data));
});
}
void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> cb) {
ThreadPool::Enqueue([this, bufferSize, cb]{ cb(this->RecieveUntilFullSync(bufferSize)); });
ThreadPool::Enqueue([this, bufferSize, cb]{
cb(handle && canReceive ? impl->TakeUpTo(bufferSize) : std::vector<char>{});
});
}
void QUICStream::PrependReceived(std::vector<char> bytes) {

View file

@ -1,222 +1,288 @@
//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>
module Crafter.Network:ClientTCP_impl;
import :ClientTCP;
import Crafter.Thread;
import std;
using namespace Crafter;
ClientTCP::ClientTCP(int socketid) : socketid(socketid)
{
}
ClientTCP::ClientTCP(const char* hostName, std::uint16_t port)
{
// 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.
addrinfo hints{};
hints.ai_family = AF_INET;
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) {
throw std::runtime_error(std::string("Could not resolve host '") + hostName + "': "
+ gai_strerror(status));
}
host = nullptr;
serv_addr = *reinterpret_cast<sockaddr_in*>(resolved->ai_addr);
freeaddrinfo(resolved);
Connect();
}
ClientTCP::ClientTCP(std::string hostName, std::uint16_t port): ClientTCP(hostName.c_str(), port)
{
}
// 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.
ClientTCP::ClientTCP(ClientTCP&& other) noexcept : socketid(other.socketid) {
other.socketid = -1;
}
ClientTCP::~ClientTCP()
{
if(socketid != -1) {
shutdown(socketid, SHUT_RDWR);
close(socketid);
}
}
void ClientTCP::Connect() {
if((socketid = socket(AF_INET, SOCK_STREAM, 0)) == -1){
throw std::runtime_error(std::string("Could not open socket: ") + std::strerror(errno));
}
if(connect(socketid,(sockaddr*)&serv_addr, sizeof(sockaddr)) == -1){
// 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.
const std::string reason = std::strerror(errno);
close(socketid);
socketid = -1;
throw std::runtime_error("Could not connect to server: " + reason);
}
}
void ClientTCP::Stop() {
shutdown(socketid, SHUT_RDWR);
close(socketid);
socketid = -1;
}
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;
throw std::runtime_error(std::strerror(errno));
}
sent += static_cast<std::uint32_t>(status);
}
}
std::vector<char> ClientTCP::RecieveSync(std::uint32_t bufferSize) const {
std::vector<char> totalBuffer(bufferSize);
int read = recv(socketid, totalBuffer.data(), bufferSize, 0);
if(read < 0) {
throw std::runtime_error(std::strerror(errno));
} else if(read == 0) {
throw SocketClosedException();
}
return totalBuffer;
}
int ClientTCP::RecieveSync(std::uint32_t bufferSize, void* buffer) const {
return recv(socketid, reinterpret_cast<char*>(buffer), bufferSize, 0);
}
std::vector<char> ClientTCP::RecieveSync() const {
int count;
ioctl(socketid, FIONREAD, &count);
if(count == 0){
count = 1024;
}
std::vector<char> buffer(count);
int read = recv(socketid, buffer.data(), count, 0);
if(read < 0) {
throw std::runtime_error(std::strerror(errno));
} else if(read == 0) {
throw SocketClosedException();
} else if(count != read) {
buffer.resize(read);
}
return buffer;
}
std::vector<char> ClientTCP::RecieveUntilCloseSync() const {
int count;
ioctl(socketid, FIONREAD, &count);
if(count == 0){
count = 1024;
}
std::vector<char> buffer(count);
int read = recv(socketid, buffer.data(), count, 0);
if(read < 0) {
throw std::runtime_error(std::strerror(errno));
} else if(read == 0) {
throw SocketClosedException();
}
while(true) {
ioctl(socketid, FIONREAD, &count);
if(count == 0){
count = 1024;
}
unsigned int oldSize = buffer.size();
buffer.resize(buffer.size()+count);
int read = recv(socketid, buffer.data()+oldSize, count, 0);
if(read < 0) {
throw std::runtime_error(std::strerror(errno));
} else if(read == 0) {
return buffer;
} else if(count != read) {
buffer.resize(read);
}
}
return buffer;
}
std::vector<char> ClientTCP::RecieveUntilFullSync(std::uint32_t bufferSize) const {
std::vector<char> buffer(bufferSize);
int read = 0;
while(read < bufferSize) {
int newRead = recv(socketid, buffer.data()+read, bufferSize-read, 0);
if(newRead < 0) {
throw std::runtime_error(std::strerror(errno));
} else if(newRead == 0) {
throw SocketClosedException();
}
read+=newRead;
}
buffer.resize(read);
return buffer;
}
void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback) const {
ThreadPool::Enqueue([recieveCallback, this, bufferSize](){
recieveCallback(this->RecieveSync(bufferSize));
});
}
void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function<void(int)> recieveCallback, char* buffer) const {
ThreadPool::Enqueue([recieveCallback, this, bufferSize, buffer](){
recieveCallback(this->RecieveSync(bufferSize, buffer));
});
}
void ClientTCP::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback) const {
ThreadPool::Enqueue([recieveCallback, this, bufferSize](){
recieveCallback(this->RecieveUntilFullSync(bufferSize));
});
}
void ClientTCP::RecieveAsync(std::function<void(std::vector<char>)> recieveCallback) const {
ThreadPool::Enqueue([this, recieveCallback](){
recieveCallback(this->RecieveSync());
});
}
void ClientTCP::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback) const {
ThreadPool::Enqueue([this, recieveCallback](){
recieveCallback(this->RecieveUntilCloseSync());
});
}
//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;
}
// A failed recv. EAGAIN is the receive timeout (SO_RCVTIMEO) running out.
[[noreturn]] void ThrowRecieveError() {
if (errno == EAGAIN || errno == EWOULDBLOCK) throw SocketTimeoutException();
throw std::runtime_error(std::strerror(errno));
}
}
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.
addrinfo hints{};
hints.ai_family = AF_INET;
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) {
throw std::runtime_error(std::string("Could not resolve host '") + hostName + "': "
+ gai_strerror(status));
}
serv_addr = *reinterpret_cast<sockaddr_in*>(resolved->ai_addr);
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.
ClientTCP::ClientTCP(ClientTCP&& other) noexcept : socketid(other.socketid) {
other.socketid = -1;
}
ClientTCP::~ClientTCP()
{
if(socketid != -1) {
shutdown(socketid, SHUT_RDWR);
close(socketid);
}
}
void ClientTCP::Connect(std::chrono::milliseconds timeout) {
if((socketid = socket(AF_INET, SOCK_STREAM, 0)) == -1){
throw std::runtime_error(std::string("Could not open socket: ") + std::strerror(errno));
}
// 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(socketid, F_GETFL, 0);
if (bounded) {
fcntl(socketid, F_SETFL, flags | O_NONBLOCK);
}
int failure = 0;
if(connect(socketid,(sockaddr*)&serv_addr, sizeof(sockaddr)) == -1){
failure = errno;
if (failure == EINPROGRESS) {
pollfd connecting { .fd = socketid, .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(socketid);
socketid = -1;
throw SocketTimeoutException();
}
if (ready == -1) {
failure = errno;
} else {
// The connect's own result.
socklen_t length = sizeof(failure);
getsockopt(socketid, SOL_SOCKET, SO_ERROR, &failure, &length);
}
}
}
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(socketid);
socketid = -1;
throw std::runtime_error(std::string("Could not connect to server: ") + std::strerror(failure));
}
if (bounded) {
fcntl(socketid, F_SETFL, flags);
}
}
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;
throw std::runtime_error(std::strerror(errno));
}
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));
}