OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / swarmd / tools / swarmd_ctl.cpp

139 lines · cpp ref:
// swarmd-ctl — talk to a running daemon instead of hunting for its process.
//
//   swarmd-ctl --socket <path> status
//   swarmd-ctl --socket <path> join  <words>
//   swarmd-ctl --socket <path> leave <words>
//   swarmd-ctl --socket <path> stop
//   swarmd-ctl --socket <path> watch [--seconds N]
//
// ⛔ THIS EXISTS BECAUSE `pkill -f` IS NOT A CONTROL PLANE. A pattern that
// matches a daemon's argv also matches the SHELL that typed it: `pkill -f
// "session-daemon --topic mountain"` run over ssh killed the ssh session's own
// remote shell — twice in this project's history, once in the session that wrote
// this file. It is also indiscriminate: two daemons on two topics share every
// substring worth matching on.
//
// `stop` reads the pidfile the daemon writes beside its socket and signals THAT
// pid. One process, named by the daemon itself, never a pattern.
//
// ⛔ JOIN AND LEAVE WORK ON A RUNNING DAEMON. The topic was a start-up argument
// and nothing else, so changing session meant killing a process — which is why
// killing processes was the interface. The RPCs have existed since the control
// plane was written; nothing exposed them.
//
// The topic is BLAKE2b(words), the same derivation session-daemon and
// swarmd-session use, so the words you type here are the words you type in
// Blender.

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>

#include <csignal>
#include <unistd.h>

#include <sodium.h>

#include "client.h"

static void usage() {
	fprintf(stderr,
			"usage: swarmd-ctl --socket <path> <command>\n"
			"  status            what the daemon says about itself\n"
			"  join  <words>     join a topic, on a daemon that is already running\n"
			"  leave <words>     leave one\n"
			"  stop              terminate it by the pid in its pidfile\n"
			"  watch [--seconds N]   status, repeatedly\n");
}

static void topic_of(const char *words, uint8_t out[32]) {
	crypto_generichash(out, 32, (const uint8_t *)words, strlen(words), nullptr, 0);
}

static int do_stop(const std::string &sock) {
	// ⛔ BY PID, FROM THE DAEMON'S OWN PIDFILE. Never by pattern.
	const std::string pidfile = sock + ".pid";
	FILE *f = fopen(pidfile.c_str(), "r");
	if (!f) {
		fprintf(stderr, "no pidfile at %s — is this a swarmd socket?\n", pidfile.c_str());
		return 1;
	}
	long pid = 0;
	const int got = fscanf(f, "%ld", &pid);
	fclose(f);
	if (got != 1 || pid <= 0) {
		fprintf(stderr, "%s does not contain a pid\n", pidfile.c_str());
		return 1;
	}
	if (kill((pid_t)pid, SIGTERM) != 0) {
		fprintf(stderr, "could not signal %ld (already gone?)\n", pid);
		return 1;
	}
	printf("stopped %ld\n", pid);
	return 0;
}

static void print_status(const swarmd::Status &st, int local) {
	const char *dht = st.dht_state == 2 ? "up" : (st.dht_state == 1 ? "connecting" : "starting");
	printf("dht=%s peers=%llu known=%llu connecting=%llu queued=%llu local=%d\n",
		   dht, (unsigned long long)st.peers_connected,
		   (unsigned long long)st.peers_known, (unsigned long long)st.connecting,
		   (unsigned long long)st.queued, local);
	fflush(stdout);
}

int main(int argc, char **argv) {
	if (sodium_init() < 0) { fprintf(stderr, "sodium_init failed\n"); return 1; }

	const char *sock = nullptr;
	int seconds = 0;
	std::string cmd, words;
	for (int i = 1; i < argc; i++) {
		if (!strcmp(argv[i], "--socket") && i + 1 < argc) sock = argv[++i];
		else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = atoi(argv[++i]);
		else if (cmd.empty()) cmd = argv[i];
		else if (words.empty()) words = argv[i];
	}
	if (!sock || cmd.empty()) { usage(); return 2; }

	if (cmd == "stop") return do_stop(sock);

	swarmd::Client c;
	if (!c.connect(sock, "swarmd-ctl")) {
		fprintf(stderr, "%s\n", c.last_error().c_str());
		return 1;
	}

	if (cmd == "status" || cmd == "watch") {
		const int rounds = (cmd == "watch") ? (seconds > 0 ? seconds : 3600) : 1;
		for (int i = 0; i < rounds; i++) {
			swarmd::Status st;
			if (!c.status(st)) { fprintf(stderr, "%s\n", c.last_error().c_str()); return 1; }
			print_status(st, (int)st.local_clients);
			if (cmd == "status") break;
			if (!c.poll(1000)) break;
		}
		return 0;
	}

	if (cmd == "join" || cmd == "leave") {
		if (words.empty()) { usage(); return 2; }
		uint8_t topic[32];
		topic_of(words.c_str(), topic);
		const bool ok = (cmd == "join") ? c.join(topic) : c.leave(topic);
		if (!ok) { fprintf(stderr, "%s\n", c.last_error().c_str()); return 1; }
		// ⚠ Say what was done to WHICH topic: the words are hashed, and two
		// similar names are indistinguishable in a log otherwise.
		char hex[17];
		for (int i = 0; i < 8; i++) sprintf(hex + i * 2, "%02x", topic[i]);
		hex[16] = 0;
		printf("%s %s (topic %s…)\n", cmd == "join" ? "joined" : "left",
			   words.c_str(), hex);
		return 0;
	}

	usage();
	return 2;
}