//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(std::min(timeout.count(), std::numeric_limits::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(&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(timeout); timeval value {}; value.tv_sec = static_cast(seconds.count()); value.tv_usec = static_cast(std::chrono::duration_cast(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(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(status); } } std::vector 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 buffer(bufferSize); const ssize_t read = RecieveRetrying(socketid, buffer.data(), bufferSize); if (read < 0) ThrowRecieveError(); if (read == 0) throw SocketClosedException(); buffer.resize(static_cast(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(read); } std::vector ClientTCP::RecieveSync() const { int count = 0; ioctl(socketid, FIONREAD, &count); if (count <= 0) { count = 1024; } std::vector buffer(static_cast(count)); const ssize_t read = RecieveRetrying(socketid, buffer.data(), buffer.size()); if (read < 0) ThrowRecieveError(); if (read == 0) throw SocketClosedException(); buffer.resize(static_cast(read)); return buffer; } std::vector ClientTCP::RecieveUntilCloseSync() const { std::vector 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(count)); const ssize_t read = RecieveRetrying(socketid, buffer.data() + oldSize, static_cast(count)); if (read < 0) ThrowRecieveError(); buffer.resize(oldSize + static_cast(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 ClientTCP::RecieveUntilFullSync(std::uint32_t bufferSize) const { std::vector 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(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 void EnqueueReceive(std::function receive, std::function onReceived, std::function onError) { ThreadPool::Enqueue([receive = std::move(receive), onReceived = std::move(onReceived), onError = std::move(onError)]() { std::optional 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)> recieveCallback, std::function onError) const { EnqueueReceive>([this, bufferSize] { return RecieveSync(bufferSize); }, std::move(recieveCallback), std::move(onError)); } void ClientTCP::RecieveAsync(std::uint32_t bufferSize, std::function recieveCallback, char* buffer, std::function onError) const { EnqueueReceive([this, bufferSize, buffer] { return RecieveSync(bufferSize, buffer); }, std::move(recieveCallback), std::move(onError)); } void ClientTCP::RecieveUntilFullAsync(std::uint32_t bufferSize, std::function)> recieveCallback, std::function onError) const { EnqueueReceive>([this, bufferSize] { return RecieveUntilFullSync(bufferSize); }, std::move(recieveCallback), std::move(onError)); } void ClientTCP::RecieveAsync(std::function)> recieveCallback, std::function onError) const { EnqueueReceive>([this] { return RecieveSync(); }, std::move(recieveCallback), std::move(onError)); } void ClientTCP::RecieveUntilCloseAsync(std::function)> recieveCallback, std::function onError) const { EnqueueReceive>([this] { return RecieveUntilCloseSync(); }, std::move(recieveCallback), std::move(onError)); }