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>
111 lines
4 KiB
C++
111 lines
4 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// A send or a receive that fails throws SocketErrorException with its errno,
|
|
// as a failed connect does: a caller that reports the cause in its own words
|
|
// — "the connection broke ([Errno 104] Connection reset by peer)" — needs the
|
|
// code, and the message alone is the strerror text a send and a receive
|
|
// always threw. A receive timeout is still SocketTimeoutException and a
|
|
// clean close SocketClosedException.
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
|
|
import Crafter.Network;
|
|
import std;
|
|
using namespace Crafter;
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void Check(bool ok, std::string_view what) {
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
if (!ok) ++failures;
|
|
}
|
|
|
|
// A listener on 127.0.0.1 that accepts one connection and resets it: SO_LINGER
|
|
// with a zero timeout makes close() send an RST instead of a FIN.
|
|
struct Resetter {
|
|
int listener = -1;
|
|
std::uint16_t port = 0;
|
|
std::thread thread;
|
|
|
|
Resetter() {
|
|
listener = socket(AF_INET, SOCK_STREAM, 0);
|
|
sockaddr_in address {};
|
|
address.sin_family = AF_INET;
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
bind(listener, reinterpret_cast<sockaddr*>(&address), sizeof(address));
|
|
listen(listener, 1);
|
|
socklen_t length = sizeof(address);
|
|
getsockname(listener, reinterpret_cast<sockaddr*>(&address), &length);
|
|
port = ntohs(address.sin_port);
|
|
thread = std::thread([this] {
|
|
const int client = accept(listener, nullptr, nullptr);
|
|
linger reset { .l_onoff = 1, .l_linger = 0 };
|
|
setsockopt(client, SOL_SOCKET, SO_LINGER, &reset, sizeof(reset));
|
|
close(client);
|
|
});
|
|
}
|
|
|
|
~Resetter() {
|
|
if (thread.joinable()) thread.join();
|
|
close(listener);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
{
|
|
Resetter peer;
|
|
ClientTCP client("127.0.0.1", peer.port, 2s);
|
|
client.SetReceiveTimeout(2s);
|
|
peer.thread.join();
|
|
bool thrown = false;
|
|
try {
|
|
client.RecieveSync(16);
|
|
} catch (const SocketErrorException& e) {
|
|
thrown = true;
|
|
Check(e.kind == SocketErrorKind::System, "a reset connection fails the receive as a system error");
|
|
Check(e.code == ECONNRESET, std::format("it carries ECONNRESET (got {})", e.code));
|
|
Check(std::string_view(e.what()) == std::strerror(ECONNRESET), std::format("its message is the strerror text (got '{}')", e.what()));
|
|
} catch (const std::exception& e) {
|
|
Check(false, std::format("the receive threw something else: {}", e.what()));
|
|
}
|
|
Check(thrown, "a receive on a reset connection throws SocketErrorException");
|
|
}
|
|
|
|
{
|
|
Resetter peer;
|
|
ClientTCP client("127.0.0.1", peer.port, 2s);
|
|
peer.thread.join();
|
|
// The first send after the reset may still be accepted locally; the
|
|
// kernel answers the next one with EPIPE or ECONNRESET.
|
|
std::int32_t code = 0;
|
|
bool runtime = false;
|
|
const std::string data(4096, 'x');
|
|
for (int i = 0; i < 50 && code == 0; ++i) {
|
|
try {
|
|
client.Send(data.data(), static_cast<std::uint32_t>(data.size()));
|
|
} catch (const SocketErrorException& e) {
|
|
code = e.code;
|
|
runtime = dynamic_cast<const std::runtime_error*>(&e) != nullptr;
|
|
} catch (const std::exception&) {
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(10ms);
|
|
}
|
|
Check(code == EPIPE || code == ECONNRESET, std::format("a send into a reset connection throws SocketErrorException with EPIPE or ECONNRESET (got {})", code));
|
|
Check(runtime, "and it is still a std::runtime_error");
|
|
}
|
|
|
|
std::println("{}", failures == 0 ? "all passed" : std::format("{} failed", failures));
|
|
return failures == 0 ? 0 : 1;
|
|
}
|