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:
parent
05f7652770
commit
f683216850
14 changed files with 1216 additions and 382 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue