// Gate: the C++ client library, against a real Daemon over a real socket.
//
// docs/SWARMD-DESIGN.md §9 step 5. Until now the wire had two client
// implementations — a Python harness and swarmd-probe — and both were wrong at
// least once in ways the daemon could not distinguish from a bad daemon. This
// asserts the one that replaces them.
//
// ⛔ NO NETWORK. The daemon is pointed at a bootstrap that answers nothing, so
// it never has a peer. Everything here is about the LOCAL contract: the calls,
// their replies, and the events. Peer-to-peer behaviour is
// test_channel_e2e.py's and test_content_e2e.py's job, on the real DHT.
//
// The daemon runs on its own thread because that is how a daemon runs. A client
// that blocks for a reply has nobody to pump the thing it is waiting for
// otherwise, and pretending otherwise is what a single-threaded gate would be
// asserting instead.
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <chrono>
#include <thread>
#include <vector>
#include <unistd.h>
#include <sys/stat.h>
#include <cerrno>
#include "client.h"
#include "daemon.h"
static int checks = 0, failures = 0;
static void check(bool c, const char *what) {
checks++;
if (c) { printf(" PASS: %s\n", what); return; }
failures++;
printf(" FAIL: %s\n", what);
}
int main() {
char sock[128], file[128];
snprintf(sock, sizeof(sock), "/tmp/swarmd-client-%d.sock", (int)getpid());
snprintf(file, sizeof(file), "/tmp/swarmd-client-%d.bin", (int)getpid());
// A file for the content calls to name. Small: this gate is about the
// contract, not about throughput.
{
FILE *f = fopen(file, "wb");
if (!f) { printf("cannot write %s\n", file); return 1; }
std::vector<uint8_t> bytes(64 * 1024);
for (size_t i = 0; i < bytes.size(); i++) bytes[i] = (uint8_t)(i * 31 + 7);
fwrite(bytes.data(), 1, bytes.size(), f);
fclose(f);
}
char store[128];
snprintf(store, sizeof(store), "/tmp/swarmd-client-store-%d", (int)getpid());
if (mkdir(store, 0755) != 0 && errno != EEXIST) { printf("cannot make %s\n", store); return 1; }
swarmd::Daemon d;
if (!d.start(sock, "127.0.0.1:1", store)) { printf("daemon start failed\n"); return 1; }
std::atomic<bool> stop(false);
std::thread pump([&d, &stop]() { while (!stop.load()) d.poll(5); });
int rc = 0;
{
swarmd::Client c;
// Events the daemon raises on its own.
int content_failed = 0;
std::string last_fail;
int failed_during_status = 0;
bool counting = false;
c.on.on_content_failed = [&](const uint8_t *, const std::string &why) {
content_failed++;
last_fail = why;
if (counting) failed_during_status++;
};
int disconnects = 0;
c.on.on_disconnect = [&](const std::string &) { disconnects++; };
uint8_t identity[32];
memset(identity, 0x5c, 32);
check(c.connect(sock, "gate", identity), "connect() reached the daemon and said HELLO");
check(c.connected(), "and reports itself connected");
// ⛔ THESE TWO ARE THE POINT OF HELLO. §5 says the reply carries
// {client_id, daemon_pubkey}; §7 item 4 says client ids exist so a client
// can suppress its own echo. A client that cannot learn its own id cannot
// do that, and a zeroed daemon key tells it nothing about who it reached.
check(c.client_id() != 0, "HELLO answered with this client's OWN id");
{
static const uint8_t zero[32] = { 0 };
check(memcmp(c.daemon_pubkey(), zero, 32) != 0,
"and with the daemon's real public key, not 32 zeroes");
}
uint8_t topic[32];
memset(topic, 0x3a, 32);
check(c.join(topic), "join() was accepted");
const uint64_t ch = c.open_channel("client-gate/v1", {}, 2);
check(ch != 0, "open_channel() returned a handle");
// The send-truth contract, now through the library: with no peer there
// is nowhere to put the frame, and saying so is the whole point.
bool writable = true;
const char *payload = "nowhere-to-go";
check(!c.send(ch, 0, 0, (const uint8_t *)payload, strlen(payload), &writable),
"send() on a channel no peer has open is REFUSED, not silently dropped");
check(!writable, "and the channel is not reported writable");
// ⛔ GATE D7, the half that can be asserted without a network: STATUS must
// say the DHT is NOT up when it is not. #72's panel said Connected while
// the daemon was gone, and a status that only ever reports health is
// worse than no status — it is a reason to stop looking. This daemon is
// pointed at a bootstrap that answers nothing, so `up` here would be a
// lie with nothing behind it.
swarmd::Status st;
check(c.status(st), "status() answered");
check(st.local_clients >= 1, "and counts this client");
check(st.peers_connected == 0, "and reports no peers, which is the truth here");
check(!st.bootstrapped, "D7: it does NOT claim to be bootstrapped against a dead bootstrap");
check(st.dht_state != 2, "D7: and does not report the DHT up");
// ---- content ----
uint8_t hash[32], hash2[32];
check(c.content_hash(file, hash), "content_hash() hashed a real file");
check(c.content_hash(file, hash2) && memcmp(hash, hash2, 32) == 0,
"and the same bytes hash the same way twice");
{
static const uint8_t zero[32] = { 0 };
check(memcmp(hash, zero, 32) != 0, "and the hash is not all zeroes");
}
check(!c.content_hash("/nonexistent/path/at/all", hash2),
"hashing a file that is not there FAILS rather than returning zeroes");
check(c.content_serve(hash, file), "content_serve() registered the file");
// Cancelling a fetch nobody started is refused by liboverlay, and the
// daemon reports that refusal as a FAILED event rather than swallowing it.
uint8_t missing[32];
memset(missing, 0x77, 32);
check(c.content_cancel(missing, false),
"content_cancel() was accepted by the daemon (the RPC, not the outcome)");
// ⛔ THE INTERLEAVE — and the wait is what MAKES it one, not a patch over
// a flake.
//
// The cancel's ok reply is written by the control handler synchronously;
// the FAILED event is produced later, on the UDX loop, and broadcast by a
// subsequent Daemon::poll. Asserting straight after the cancel asserts a
// race: status() is answered just as fast, and the first version of this
// gate failed against a client that was working. Waiting until the event
// is known to be sitting in our socket is what establishes the condition
// the assertion is about — a frame queued AHEAD of the reply a blocking
// call is waiting for. A client that drops what it reads on the way to
// its own answer loses it, invisibly: no error, just an event that never
// fires.
{
const uint64_t deadline = 3000;
uint64_t waited = 0;
while (content_failed == 0 && waited < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
waited += 20;
// Deliberately NOT polling the client: the point is to leave the
// frame unread so the next blocking call has to handle it.
}
// It is in our socket now, unread, because nothing has read it.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
counting = true;
check(c.status(st), "a second status() answered");
counting = false;
check(content_failed >= 1, "the daemon reported the refused cancel at all");
check(failed_during_status >= 1,
"an event queued AHEAD of a reply was delivered by the blocking call "
"that read past it, not dropped");
check(last_fail.find("no such fetch") != std::string::npos,
"and it is the event the daemon actually sent");
check(c.close_channel(ch), "close_channel() was accepted");
check(disconnects == 0, "nothing in that sequence looked like a disconnect");
// ---- what a closed client does ----
c.close();
check(!c.connected(), "close() closes");
check(!c.join(topic), "and a call on a closed client fails rather than crashing");
check(c.last_error() == "not connected", "with an error that says why");
// ---- a daemon that is not there ----
swarmd::Client absent;
check(!absent.connect("/tmp/swarmd-does-not-exist.sock", "gate"),
"connecting to a socket with no daemon fails");
check(absent.last_error().find("no daemon at") != std::string::npos,
"and says so");
rc = failures ? 1 : 0;
}
stop.store(true);
pump.join();
d.stop();
unlink(file);
printf("\n%d/%d passed\n", checks - failures, checks);
return rc;
}