272 lines
11 KiB
C++
272 lines
11 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// ClientTCP's receive calls return exactly the bytes that arrived:
|
||
|
|
// - RecieveSync(bufferSize) returned the whole buffer when fewer bytes had
|
||
|
|
// arrived, so the caller read garbage after them.
|
||
|
|
// - RecieveUntilCloseSync kept the unused end of its first buffer, and then
|
||
|
|
// cut everything it had read down to the size of the last read.
|
||
|
|
// - A signal arriving while a receive waited made it throw "Interrupted
|
||
|
|
// system call", where the receive should simply wait on.
|
||
|
|
// - RecieveSync(bufferSize, buffer) returned recv's -1 or 0 where the other
|
||
|
|
// receives throw: it now returns the count and throws the same way. A
|
||
|
|
// receive of no bytes returns at once: recv's 0 for it isn't a close.
|
||
|
|
// - The *Async receives let a closed peer's exception escape the thread
|
||
|
|
// pool's task, which ended the whole program; it now goes to onError,
|
||
|
|
// and without an onError it's dropped.
|
||
|
|
//
|
||
|
|
// Everything runs over loopback.
|
||
|
|
|
||
|
|
#include <arpa/inet.h>
|
||
|
|
#include <netinet/in.h>
|
||
|
|
#include <pthread.h>
|
||
|
|
#include <signal.h>
|
||
|
|
#include <sys/socket.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
#include <cstdlib>
|
||
|
|
|
||
|
|
import Crafter.Network;
|
||
|
|
import Crafter.Thread;
|
||
|
|
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 connected pair: a ClientTCP and the socket the listener accepted for it.
|
||
|
|
struct Connection {
|
||
|
|
int listener = -1;
|
||
|
|
int peer = -1;
|
||
|
|
std::optional<ClientTCP> client;
|
||
|
|
|
||
|
|
Connection() {
|
||
|
|
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);
|
||
|
|
client.emplace("127.0.0.1", ntohs(address.sin_port));
|
||
|
|
peer = accept(listener, nullptr, nullptr);
|
||
|
|
}
|
||
|
|
~Connection() {
|
||
|
|
if (peer != -1) close(peer);
|
||
|
|
close(listener);
|
||
|
|
}
|
||
|
|
void Send(std::string_view bytes) const {
|
||
|
|
send(peer, bytes.data(), bytes.size(), 0);
|
||
|
|
}
|
||
|
|
void ClosePeer() {
|
||
|
|
close(peer);
|
||
|
|
peer = -1;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
std::string_view Text(const std::vector<char>& bytes) {
|
||
|
|
return std::string_view(bytes.data(), bytes.size());
|
||
|
|
}
|
||
|
|
|
||
|
|
// What a receive threw, by kind.
|
||
|
|
std::string Kind(std::exception_ptr error) {
|
||
|
|
try {
|
||
|
|
std::rethrow_exception(error);
|
||
|
|
} catch (const SocketClosedException&) {
|
||
|
|
return "closed";
|
||
|
|
} catch (const SocketTimeoutException&) {
|
||
|
|
return "timeout";
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
return std::string("other: ") + e.what();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// An async receive's outcome. Shared with the callbacks, so it outlives
|
||
|
|
// whichever thread sets it.
|
||
|
|
struct Outcome {
|
||
|
|
std::mutex mutex;
|
||
|
|
std::condition_variable done;
|
||
|
|
std::optional<std::string> value;
|
||
|
|
|
||
|
|
void Set(std::string what) {
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
value = std::move(what);
|
||
|
|
}
|
||
|
|
done.notify_all();
|
||
|
|
}
|
||
|
|
std::string Wait() {
|
||
|
|
std::unique_lock lock(mutex);
|
||
|
|
done.wait_for(lock, 2s, [&] { return value.has_value(); });
|
||
|
|
return value.value_or("nothing within 2 s");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
void IgnoreSignal(int) {}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
// ── A short read returns only what arrived ───────────────────────────────
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.Send("hello");
|
||
|
|
std::this_thread::sleep_for(20ms);
|
||
|
|
const std::vector<char> received = connection.client->RecieveSync(64);
|
||
|
|
Check(Text(received) == "hello", std::format("RecieveSync(64) returns the 5 bytes that arrived (got {})", received.size()));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Until close: every chunk, in order, nothing more ─────────────────────
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
std::thread sender([&] {
|
||
|
|
connection.Send("abc");
|
||
|
|
std::this_thread::sleep_for(30ms);
|
||
|
|
connection.Send("defgh");
|
||
|
|
std::this_thread::sleep_for(30ms);
|
||
|
|
connection.Send("ij");
|
||
|
|
std::this_thread::sleep_for(30ms);
|
||
|
|
connection.ClosePeer();
|
||
|
|
});
|
||
|
|
const std::vector<char> received = connection.client->RecieveUntilCloseSync();
|
||
|
|
sender.join();
|
||
|
|
Check(Text(received) == "abcdefghij",
|
||
|
|
std::format("RecieveUntilCloseSync returns the three chunks it was sent ({} bytes)", received.size()));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── A signal doesn't break a waiting receive ─────────────────────────────
|
||
|
|
{
|
||
|
|
// No SA_RESTART, so the signal makes recv return EINTR.
|
||
|
|
struct sigaction action {};
|
||
|
|
action.sa_handler = IgnoreSignal;
|
||
|
|
sigemptyset(&action.sa_mask);
|
||
|
|
sigaction(SIGUSR1, &action, nullptr);
|
||
|
|
|
||
|
|
Connection connection;
|
||
|
|
std::string outcome;
|
||
|
|
std::thread reader([&] {
|
||
|
|
try {
|
||
|
|
outcome = Text(connection.client->RecieveUntilFullSync(4));
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
outcome = std::string("threw: ") + e.what();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
std::this_thread::sleep_for(50ms);
|
||
|
|
pthread_kill(reader.native_handle(), SIGUSR1);
|
||
|
|
std::this_thread::sleep_for(50ms);
|
||
|
|
connection.Send("data");
|
||
|
|
reader.join();
|
||
|
|
Check(outcome == "data", std::format("a receive interrupted by a signal waits on for its data ({})", outcome));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Into a buffer: the count, and the same exceptions ────────────────────
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.Send("hello");
|
||
|
|
std::this_thread::sleep_for(20ms);
|
||
|
|
std::array<char, 64> buffer {};
|
||
|
|
const std::uint32_t got = connection.client->RecieveSync(buffer.size(), buffer.data());
|
||
|
|
Check(got == 5 && std::string_view(buffer.data(), got) == "hello", std::format("RecieveSync(64, buffer) returns the 5 bytes that arrived ({})", got));
|
||
|
|
connection.ClosePeer();
|
||
|
|
std::string closed;
|
||
|
|
try {
|
||
|
|
(void)connection.client->RecieveSync(buffer.size(), buffer.data());
|
||
|
|
} catch (...) {
|
||
|
|
closed = Kind(std::current_exception());
|
||
|
|
}
|
||
|
|
Check(closed == "closed", std::format("and throws SocketClosedException once the peer has closed ({})", closed.empty() ? "returned" : closed));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
std::array<char, 16> buffer {};
|
||
|
|
std::string outcome = "returned";
|
||
|
|
try {
|
||
|
|
const bool empty = connection.client->RecieveSync(0).empty() && connection.client->RecieveSync(0, buffer.data()) == 0;
|
||
|
|
if (!empty) outcome = "bytes";
|
||
|
|
} catch (...) {
|
||
|
|
outcome = Kind(std::current_exception());
|
||
|
|
}
|
||
|
|
Check(outcome == "returned", std::format("a receive of no bytes returns nothing on a live connection ({})", outcome));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.client->SetReceiveTimeout(50ms);
|
||
|
|
std::array<char, 16> buffer {};
|
||
|
|
std::string timedOut;
|
||
|
|
try {
|
||
|
|
(void)connection.client->RecieveSync(buffer.size(), buffer.data());
|
||
|
|
} catch (...) {
|
||
|
|
timedOut = Kind(std::current_exception());
|
||
|
|
}
|
||
|
|
Check(timedOut == "timeout", std::format("and SocketTimeoutException when nothing comes in time ({})", timedOut.empty() ? "returned" : timedOut));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Async: data to the callback, failures to onError ─────────────────────
|
||
|
|
ThreadPool::Start(3);
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.Send("hi");
|
||
|
|
auto outcome = std::make_shared<Outcome>();
|
||
|
|
connection.client->RecieveAsync(64, [outcome](std::vector<char> data) { outcome->Set(std::string(Text(data))); },
|
||
|
|
[outcome](std::exception_ptr error) { outcome->Set(Kind(error)); });
|
||
|
|
const std::string got = outcome->Wait();
|
||
|
|
Check(got == "hi", std::format("RecieveAsync hands the data to its callback ({})", got));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.ClosePeer();
|
||
|
|
auto outcome = std::make_shared<Outcome>();
|
||
|
|
connection.client->RecieveAsync(64, [outcome](std::vector<char>) { outcome->Set("data"); },
|
||
|
|
[outcome](std::exception_ptr error) { outcome->Set(Kind(error)); });
|
||
|
|
const std::string got = outcome->Wait();
|
||
|
|
Check(got == "closed", std::format("and a closed peer to onError, the program carrying on ({})", got));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.Send("ab");
|
||
|
|
connection.ClosePeer();
|
||
|
|
auto outcome = std::make_shared<Outcome>();
|
||
|
|
connection.client->RecieveUntilFullAsync(4, [outcome](std::vector<char>) { outcome->Set("data"); },
|
||
|
|
[outcome](std::exception_ptr error) { outcome->Set(Kind(error)); });
|
||
|
|
const std::string got = outcome->Wait();
|
||
|
|
Check(got == "closed", std::format("RecieveUntilFullAsync reports a peer that closes early to onError ({})", got));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.Send("xyz");
|
||
|
|
std::array<char, 16> buffer {};
|
||
|
|
auto outcome = std::make_shared<Outcome>();
|
||
|
|
connection.client->RecieveAsync(buffer.size(), [outcome, &buffer](std::uint32_t count) { outcome->Set(std::string(buffer.data(), count)); },
|
||
|
|
buffer.data(), [outcome](std::exception_ptr error) { outcome->Set(Kind(error)); });
|
||
|
|
const std::string got = outcome->Wait();
|
||
|
|
Check(got == "xyz", std::format("the buffer RecieveAsync hands its callback the count ({})", got));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.ClosePeer();
|
||
|
|
auto outcome = std::make_shared<Outcome>();
|
||
|
|
connection.client->RecieveUntilCloseAsync([outcome](std::vector<char>) { outcome->Set("data"); },
|
||
|
|
[outcome](std::exception_ptr error) { outcome->Set(Kind(error)); });
|
||
|
|
const std::string got = outcome->Wait();
|
||
|
|
Check(got == "closed", std::format("RecieveUntilCloseAsync on a peer that sent nothing goes to onError ({})", got));
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Connection connection;
|
||
|
|
connection.ClosePeer();
|
||
|
|
connection.client->RecieveAsync(64, [](std::vector<char>) {}, {});
|
||
|
|
// Calling the empty onError ended the program from the pool's task.
|
||
|
|
std::this_thread::sleep_for(300ms);
|
||
|
|
Check(true, "without an onError, a closed peer's exception is dropped, the program carrying on");
|
||
|
|
}
|
||
|
|
ThreadPool::Stop();
|
||
|
|
|
||
|
|
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||
|
|
}
|