//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. [[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(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(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(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(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; throw std::runtime_error(std::strerror(errno)); } 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)); }