OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / tools / swarm_probe.cpp

171 lines · cpp ref:
// swarm-probe — the smallest thing that uses hyperswarm-peer end to end.
//
//   swarm-probe --topic WORDS [--name WHO] [--seconds N] [--bootstrap host:port]
//
// Join a topic, open a Protomux channel on every peer that appears, say hello,
// and exit 0 the moment a hello comes back. Nothing else: no Unix socket, no
// grab table, no Blender, no Godot.
//
// ⛔ IT EXISTS BECAUSE EVERY GATE THIS REPO HAS RUNS ON A TESTNET. Both
// cross-machine suites pass `--bootstrap` at a local testnet, so the one
// configuration the products actually use — two machines, the PUBLIC DHT — has
// no gate at all, and that is where the failures have lived (hyper-games #68
// bug A, and a whole evening of "joined but never connected"). The exit code is
// the point: 0 means a message crossed between two processes, and nothing else
// counts as the swarm working.
//
// ⚠ Hearing YOURSELF does not count. A probe that connects to its own announce
// would report success on one machine with no network at all, so the hello
// carries a name and a peer's hello must carry a different one.

#include "../src/udx_transport.h"
#include "../src/protomux.h"
#include "../src/noise_ik.h"

#include <sodium.h>

#include <atomic>
#include <cstdio>
#include <cstring>
#include <csignal>
#include <mutex>
#include <string>
#include <unistd.h>
#include <vector>



static const char *PROBE_PROTOCOL = "hyperswarm-peer/probe/v1";

// ⛔ NOT atoi(). One call to it links __isoc23_strtol and raises this binary's
// glibc floor from 2.34 to 2.38 — measured: the probe built fine here and would
// not load on a 22.04 machine at all ("GLIBC_2.38 not found"), which is the same
// trap the ship-build note warns about, reached by one line of argument parsing.
static int parse_int(const char *s, int fallback) {
	if (s == nullptr || *s == 0) return fallback;
	int v = 0;
	for (const char *p = s; *p; p++) {
		if (*p < '0' || *p > '9') return fallback;
		v = v * 10 + (*p - '0');
	}
	return v;
}

static std::atomic<bool> g_stop(false);
static void on_signal(int) { g_stop = true; }

struct Probe {
	UdxTransport t;
	std::vector<uint8_t> topic;
	std::string name;

	std::mutex m;
	std::vector<ProtomuxChannel *> channels;
	std::atomic<bool> heard{false};
	std::string heard_from;

	void on_mux(int32_t, Protomux *mux) {
		if (!mux) return;
		ProtomuxChannel *ch = mux->create_channel(PROBE_PROTOCOL, topic);
		if (ch == nullptr) return; // already open on this mux
		ch->add_message([this](const uint8_t *data, size_t len, ProtomuxChannel *) {
			std::string who((const char *)data, len);
			// ⚠ Our own hello echoed back is not evidence of a peer.
			if (who == name) return;
			{
				std::lock_guard<std::mutex> lk(m);
				heard_from = who;
			}
			heard = true;
		});
		ch->on_open = [this, ch](const uint8_t *, size_t) {
			ch->send(0, (const uint8_t *)name.data(), name.size());
		};
		{
			std::lock_guard<std::mutex> lk(m);
			channels.push_back(ch);
		}
		ch->open();
	}

	// Say hello again on every pulse: the channel may have opened before the
	// other side registered its handler, and a hello is 8 bytes.
	void greet() {
		std::lock_guard<std::mutex> lk(m);
		for (auto *ch : channels) ch->send(0, (const uint8_t *)name.data(), name.size());
	}

	int peers() {
		std::lock_guard<std::mutex> lk(m);
		return (int)channels.size();
	}
};

int main(int argc, char **argv) {
	const char *seed = nullptr;
	const char *bootstrap = nullptr;
	std::string name = "probe-" + std::to_string(getpid());
	int seconds = 90;

	for (int i = 1; i < argc; i++) {
		if (!strcmp(argv[i], "--topic") && i + 1 < argc) seed = argv[++i];
		else if (!strcmp(argv[i], "--bootstrap") && i + 1 < argc) bootstrap = argv[++i];
		else if (!strcmp(argv[i], "--name") && i + 1 < argc) name = argv[++i];
		else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = parse_int(argv[++i], 90);
	}
	if (seed == nullptr) {
		fprintf(stderr, "usage: swarm-probe --topic WORDS [--name WHO] [--seconds N] [--bootstrap host:port]\n");
		return 2;
	}
	if (sodium_init() < 0) return 2;

	Probe p;
	p.name = name;
	p.topic.resize(32);
	crypto_generichash(p.topic.data(), 32, (const uint8_t *)seed, strlen(seed), nullptr, 0);

	signal(SIGINT, on_signal);
	signal(SIGTERM, on_signal);

	p.t.set_mux_callbacks(
			[&p](int32_t id, Protomux *mux) { p.on_mux(id, mux); },
			[](int32_t) {});

	Ed25519KeyPair kp = NoiseIK::generate_keypair();
	const char *bs[1] = { bootstrap };
	p.t.start_dht(kp, bs, 1);

	printf("[probe] %s on topic %s\n", name.c_str(), seed);
	fflush(stdout);

	// ⛔ THE JOIN IS DEFERRED. start_dht() returns before the UV loop's async
	// handle exists, and joining into that segfaults inside uv_async_send with
	// no output at all. is_dht_active() is the signal it is safe.
	bool joined = false;
	int ticks = 0;
	while (!g_stop && ticks < seconds * 5 && !p.heard) {
		if (!joined && p.t.is_dht_active()) {
			p.t.swarm_join(p.topic.data(), true, true);
			joined = true;
		}
		if ((ticks % 25) == 0) {
			printf("[probe] dht=%s peers=%d heard=%d\n",
				   p.t.is_dht_bootstrapped() ? "up" : (p.t.is_dht_active() ? "connecting" : "starting"),
				   p.peers(), p.heard ? 1 : 0);
			fflush(stdout);
			p.greet();
		}
		usleep(200 * 1000);
		ticks++;
	}

	if (p.heard) {
		printf("[probe] HEARD %s — the swarm works\n", p.heard_from.c_str());
		fflush(stdout);
		return 0;
	}
	printf("[probe] NOTHING HEARD after %ds (peers=%d)\n", seconds, p.peers());
	fflush(stdout);
	return 1;
}