// swarmd-probe — the smallest real client, and now a demonstration that the
// client library carries one.
//
// swarmd-probe --socket <path> --topic-hex <64 hex> [--protocol <name>]
// [--send <text>] [--seconds N]
// [--serve-file <path>] [--fetch-hex <64hex> --dest <path>]
//
// ⛔ IT USED TO BE 250 LINES OF WIRE, AND TWO OF THEM WERE WRONG. It framed,
// encoded, decoded and matched replies by hand; its send branch claimed any
// reply with an id at or above 4, which swept up the content calls and reported
// a fetch's answer as a send result. All of that is now swarmd::Client's, which
// is the entire argument for having a client library: a program that wants to
// use the daemon should not also be implementing it.
//
// What remains here is what a probe is actually for — say what happened, in
// words another program can read.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <time.h>
#include <unistd.h>
#include "client.h"
static bool hex32(const char *s, uint8_t *out) {
if (s == nullptr || strlen(s) != 64) return false;
for (int i = 0; i < 32; i++) {
char b[3] = { s[i * 2], s[i * 2 + 1], 0 };
out[i] = (uint8_t)strtoul(b, nullptr, 16);
}
return true;
}
static std::string to_hex(const uint8_t *h) {
static const char *d = "0123456789abcdef";
std::string out(64, '0');
for (int i = 0; i < 32; i++) { out[i * 2] = d[h[i] >> 4]; out[i * 2 + 1] = d[h[i] & 15]; }
return out;
}
int main(int argc, char **argv) {
const char *sock = nullptr, *topic_hex = nullptr, *proto = "swarmd-probe/v1";
const char *send_text = nullptr, *serve_file = nullptr;
const char *fetch_hex = nullptr, *dest = nullptr;
bool watch_peers = false;
int seconds = 30;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--socket") && i + 1 < argc) sock = argv[++i];
else if (!strcmp(argv[i], "--topic-hex") && i + 1 < argc) topic_hex = argv[++i];
else if (!strcmp(argv[i], "--protocol") && i + 1 < argc) proto = argv[++i];
else if (!strcmp(argv[i], "--send") && i + 1 < argc) send_text = argv[++i];
else if (!strcmp(argv[i], "--serve-file") && i + 1 < argc) serve_file = argv[++i];
else if (!strcmp(argv[i], "--fetch-hex") && i + 1 < argc) fetch_hex = argv[++i];
else if (!strcmp(argv[i], "--dest") && i + 1 < argc) dest = argv[++i];
else if (!strcmp(argv[i], "--watch-peers")) watch_peers = true;
else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = atoi(argv[++i]);
}
uint8_t topic[32];
if (!sock || !hex32(topic_hex, topic)) {
fprintf(stderr, "usage: swarmd-probe --socket <p> --topic-hex <64hex> "
"[--protocol n] [--send text] [--serve-file p] "
"[--fetch-hex h --dest p] [--seconds N]\n");
return 2;
}
swarmd::Client c;
bool received = false, fetched = false, failed = false;
std::string received_text, fetched_path, fail_reason;
c.on.on_message = [&](uint64_t, uint64_t, uint64_t, uint64_t,
const uint8_t *bytes, size_t len) {
received_text.assign((const char *)bytes, len);
received = true;
};
c.on.on_content_done = [&](const uint8_t *, const std::string &path) {
fetched_path = path;
fetched = true;
};
c.on.on_content_failed = [&](const uint8_t *, const std::string &why) {
fail_reason = why;
failed = true;
};
// Gate D5 reads these: a peer going away must be reported when the channel
// closes, not when something times out.
if (watch_peers) {
c.on.on_peer_up = [](uint64_t peer, const uint8_t *) {
printf("PEER-UP %llu\n", (unsigned long long)peer);
fflush(stdout);
};
c.on.on_peer_down = [](uint64_t peer) {
printf("PEER-DOWN %llu\n", (unsigned long long)peer);
fflush(stdout);
};
}
c.on.on_disconnect = [&](const std::string &why) {
fprintf(stderr, "swarmd went away: %s\n", why.c_str());
};
if (!c.connect(sock, "probe")) {
fprintf(stderr, "cannot reach %s: %s\n", sock, c.last_error().c_str());
return 2;
}
if (!c.join(topic)) { fprintf(stderr, "join failed: %s\n", c.last_error().c_str()); return 2; }
const uint64_t ch = c.open_channel(proto, {}, 2);
if (ch == 0) { fprintf(stderr, "channel failed: %s\n", c.last_error().c_str()); return 2; }
printf("joined topic, opened %s\n", proto);
{
// D7's other half: what the daemon says about itself, from a client
// rather than from its own log line.
swarmd::Status st;
if (c.status(st)) {
printf("STATUS dht_state=%llu bootstrapped=%d peers=%llu\n",
(unsigned long long)st.dht_state, st.bootstrapped ? 1 : 0,
(unsigned long long)st.peers_connected);
}
}
fflush(stdout);
// ---- content ----
// A serve is two calls: ask the DAEMON for the file's overlay hash, then
// register it. Asking the daemon rather than computing it here is the point —
// a hash from anywhere else is one the overlay will reject.
if (serve_file) {
uint8_t h[32];
if (!c.content_hash(serve_file, h)) {
fprintf(stderr, "cannot hash %s: %s\n", serve_file, c.last_error().c_str());
return 2;
}
if (!c.content_serve(h, serve_file)) {
fprintf(stderr, "serve refused: %s\n", c.last_error().c_str());
return 2;
}
printf("SERVING %s %s\n", to_hex(h).c_str(), serve_file);
fflush(stdout);
}
if (fetch_hex && dest) {
uint8_t h[32];
if (!hex32(fetch_hex, h)) { fprintf(stderr, "bad --fetch-hex\n"); return 2; }
if (!c.content_fetch(h, dest)) {
fprintf(stderr, "fetch refused: %s\n", c.last_error().c_str());
return 2;
}
printf("fetching %s -> %s\n", fetch_hex, dest);
fflush(stdout);
}
// ⛔ OFFER THE FRAME UNTIL IT IS TAKEN, do not send on a timer. A frame
// offered before the remote opens the channel is discarded by protomux, and
// the daemon now says so — `accepted` is the trigger, not the clock.
const time_t deadline = time(nullptr) + seconds;
bool sent = false;
int attempts = 0;
time_t last_try = 0;
while (time(nullptr) < deadline && !received && !fetched && !failed) {
if (watch_peers && !send_text && !fetch_hex) {
// A watcher has nothing of its own to finish; it runs the window out.
}
if (send_text && !sent && time(nullptr) != last_try) {
last_try = time(nullptr);
attempts++;
if (c.send(ch, 0, 0, (const uint8_t *)send_text, strlen(send_text))) {
sent = true;
printf("sent (accepted after %d attempt%s): %s\n",
attempts, attempts == 1 ? "" : "s", send_text);
fflush(stdout);
}
}
if (!c.poll(200)) break; // the daemon closed
}
if (received) {
printf("RECEIVED: %s\n", received_text.c_str());
return 0;
}
if (fetched) {
// §6: bulk bytes never cross this socket. This is the number that says
// so — everything the daemon told us, for a file of any size.
printf("SOCKET-BYTES %llu\n", (unsigned long long)c.bytes_in());
printf("FETCHED %s\n", fetched_path.c_str());
return 0;
}
if (failed) {
printf("FETCH FAILED: %s\n", fail_reason.c_str());
return 1;
}
if (fetch_hex) { printf("the fetch never completed inside the window\n"); return 1; }
if (serve_file) {
// A holder answers requests; it has nothing to wait for. Reaching the
// end of the window is what success looks like.
printf("served for %ds\n", seconds);
return 0;
}
if (send_text) {
printf(sent ? "sent, never answered\n"
: "the daemon never accepted the frame — no channel was open "
"on any peer\n");
return sent ? 0 : 1;
}
printf("no message received\n");
return 1;
}