OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / swarmd / tools / swarmd_probe.cpp

209 lines · cpp ref:
// 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;
}