Crafter.Network/implementations/Crafter.Network-ClientTCP.cpp
Jorijn van der Graaf 16da0a1979
feat(tcp): a failed send or receive says which errno it failed with
Send() and the receives threw a bare std::runtime_error with the
strerror text, so a caller that reports the cause the way Python does -
"the connection broke ([Errno 104] Connection reset by peer)", which a
language model reads in Crafter.Review's SQL helper output - could not
tell the code. They throw SocketErrorException now, as a failed connect
already does: kind System, the errno, and the same message, still a
std::runtime_error. A receive timeout stays SocketTimeoutException and
a closed peer SocketClosedException.

ShouldReportIoErrors: a peer that resets the connection (SO_LINGER 0)
fails the next receive with ECONNRESET and a send into it with EPIPE
or ECONNRESET.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:28:32 +01:00

328 lines
13 KiB
C++
Executable file

//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;
// anything else carries its errno, under the message a receive always
// threw.
[[noreturn]] void ThrowRecieveError() {
const int failure = errno;
if (failure == EAGAIN || failure == EWOULDBLOCK) throw SocketTimeoutException();
throw SocketErrorException(SocketErrorKind::System, failure, std::strerror(failure));
}
// 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;
throw SocketErrorException(SocketErrorKind::System, failure, std::string("Could not open socket: ") + std::strerror(failure));
}
// 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);
throw SocketErrorException(SocketErrorKind::System, failure, std::string("Could not connect to server: ") + std::strerror(failure));
}
if (bounded) {
fcntl(fd, F_SETFL, flags);
}
return fd;
}
}
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.
//
// 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.
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
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 SocketErrorException(SocketErrorKind::Resolver, status,
std::string("Could not resolve host '") + hostName + "': " + gai_strerror(status));
}
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);
}
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), addresses(std::move(other.addresses)) {
other.socketid = -1;
}
ClientTCP::~ClientTCP()
{
if(socketid != -1) {
shutdown(socketid, SHUT_RDWR);
close(socketid);
}
}
void ClientTCP::Connect(std::chrono::milliseconds timeout) {
if (addresses.empty()) {
throw std::logic_error("ClientTCP::Connect: no address to connect to; a socket handed over by accept() cannot connect again");
}
// 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();
}
}
socketid = -1;
std::rethrow_exception(last);
}
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;
const int failure = errno;
throw SocketErrorException(SocketErrorKind::System, failure, std::strerror(failure));
}
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));
}