OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / 50dc99a

swarmd-ctl: control a running daemon, instead of hunting for its process
⛔ `pkill -f` IS NOT A CONTROL PLANE, and this is not a style objection. 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, with the hazard documented verbatim in a memory file that had already been read. It is also indiscriminate: two daemons on two topics share every substring worth matching on. So the daemon names its own process. Both swarmd and swarmd-session write a pidfile beside their socket and remove it on a clean exit, and `stop` signals THAT pid. Whatever socket you can reach, socket + ".pid" is the process behind it. ⛔ AND A TOPIC WAS A START-UP ARGUMENT AND NOTHING ELSE, so changing session meant killing a process — which is exactly why killing processes became the interface. `join` and `leave` work on a daemon that is already running. The swarm-join and swarm-leave RPCs have existed since the control plane was written; nothing ever exposed them. swarmd-ctl --socket <path> status | join <words> | leave <words> | stop swarmd-ctl --socket <path> watch [--seconds N] The words are hashed to a topic the same way session-daemon and swarmd-session hash them, so what you type here is what you type in Blender. For swarmd-session, the control socket is the legacy socket + ".swarmd". Gate: test/test_ctl.py, 12/12 — including that stop reports the pid it signalled, that the daemon actually exits, that the pidfile goes with it, and that stop on a socket with no pidfile fails with a sentence rather than guessing. No regressions: control 10/10, backpressure 7/7, send_truth 7/7, client 30/30. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50dc99a1581637764f1a6bfbe9666e28447f5a7f
by plankatron <fleeky@gmail.com> · 2026-09-11T00:02:40.000Z

Changed files (7) +320 -2

swarmd/README.md +20 -0
……@@ -22,6 +22,25 @@ encodings the same way, with `schema/build.js` and the resulting
2222- **hyperschema-c** — structs + `preencode/encode/decode/destroy(compact_state_t*)`
2323- **hrpc-c** — typed sans-io RPC over the bare-rpc wire (unary + event handlers)
2424
25## Controlling a running daemon
26
27 swarmd-ctl --socket /tmp/s.sock status
28 swarmd-ctl --socket /tmp/s.sock join "some words"
29 swarmd-ctl --socket /tmp/s.sock leave "some words"
30 swarmd-ctl --socket /tmp/s.sock stop
31
32⛔ `pkill -f` IS NOT A CONTROL PLANE. A pattern that matches a daemon's argv also
33matches the shell that typed it — that killed an ssh session's own remote shell
34twice in this project. `stop` signals the pid in the pidfile the daemon writes
35beside its socket: one process, named by itself.
36
37⛔ And a topic used to be a START-UP ARGUMENT, so changing session meant killing
38a process — which is why killing processes was the interface. join/leave work on
39a daemon that is already running; the RPCs existed all along and nothing exposed
40them.
41
42For swarmd-session, the control socket is the legacy socket + `.swarmd`.
43
2544## Using it from C++
2645
2746 #include "client/client.h"
……@@ -64,6 +83,7 @@ wrong in ways the daemon could not distinguish from a bad daemon.
6483 channels Protomux channel per peer, SEND/on-message D1/D4 PASS
6584 content liboverlay: hash/serve/fetch/cancel D6 PASS
6685 client/ swarmd::Client — the ONE wire implementation 30/30
86 swarmd-ctl inspect/retarget/stop a RUNNING daemon 12/12
6787 tools/ swarmd-probe — built on it, in 181 lines
6888
6989 ./swarmd --socket /tmp/s.sock --storage ~/.swarmd # public DHT
swarmd/build.sh +3 -0
……@@ -122,6 +122,9 @@ echo "Building tools..."
122122"$CXX" -std=c++17 -O2 $INC -o tools/swarmd-probe tools/swarmd_probe.cpp \
123123 "$OBJ/client.o" "$OBJ/swarmd_schema.o" "$OBJ/swarmd_hrpc.o" $CLIBS
124124
125"$CXX" -std=c++17 -O2 $INC -o tools/swarmd-ctl tools/swarmd_ctl.cpp \
126 "$OBJ/client.o" "$OBJ/swarmd_schema.o" "$OBJ/swarmd_hrpc.o" $CLIBS -lsodium
127
125128# swarmd-session: session-daemon's socket, swarmd's insides. Links the daemon
126129# too, because it runs one.
127130"$CXX" -std=c++17 -O2 $INC -o tools/swarmd-session tools/session_shim.cpp \
swarmd/src/main.cpp +22 -1
……@@ -12,8 +12,27 @@
1212
1313#include <sodium.h>
1414
15#include <string>
16#include <unistd.h>
17
1518#include "daemon.h"
1619
20// ⛔ A DAEMON MUST NAME ITS OWN PROCESS. Without this the only way to stop one
21// is to match its command line, and a pattern that matches a daemon's argv also
22// matches the shell that typed it — `pkill -f` killed an ssh session's own
23// remote shell twice in this project. The pidfile sits beside the socket, so
24// whoever can reach the socket can stop exactly the process behind it.
25static void write_pidfile(const std::string &socket_path) {
26 FILE *f = fopen((socket_path + ".pid").c_str(), "w");
27 if (!f) return; // diagnostics, never a blocker
28 fprintf(f, "%ld\n", (long)getpid());
29 fclose(f);
30}
31
32static void remove_pidfile(const std::string &socket_path) {
33 ::unlink((socket_path + ".pid").c_str());
34}
35
1736static volatile sig_atomic_t g_stop = 0;
1837static void on_signal(int) { g_stop = 1; }
1938
……@@ -45,7 +64,8 @@ int main(int argc, char **argv) {
4564 fprintf(stderr, "failed to start (socket %s)\n", sock);
4665 return 1;
4766 }
48 printf("[swarmd] listening on %s\n", sock);
67 write_pidfile(sock);
68 printf("[swarmd] listening on %s (pid %ld)\n", sock, (long)getpid());
4969 fflush(stdout);
5070
5171 // ⛔ SAY WHETHER THE SWARM IS REACHABLE, not just whether we started.
……@@ -71,5 +91,6 @@ int main(int argc, char **argv) {
7191
7292 printf("[swarmd] stopping\n");
7393 d.stop();
94 remove_pidfile(sock);
7495 return 0;
7596}
swarmd/test/test_ctl.py +114 -0
……@@ -0,0 +1,114 @@
1#!/usr/bin/env python3
2"""Gate: a running daemon can be inspected, retargeted and stopped — by NAME.
3
4 python3 -B test/test_ctl.py [--bootstrap host:port]
5
6⛔ WHY THIS IS A GATE AND NOT A CONVENIENCE. Before swarmd-ctl the only way to
7stop a daemon was to match its command line, and a pattern that matches a
8daemon's argv also matches the SHELL that typed it: `pkill -f "session-daemon
9--topic mountain"` over ssh killed the ssh session's own remote shell, twice in
10this project — once in the session that wrote this file. It is also
11indiscriminate, since two daemons on two topics share every substring worth
12matching on.
13
14And the topic was a START-UP ARGUMENT and nothing else, so changing session meant
15killing a process. That is why killing processes was the interface. The
16swarm-join / swarm-leave RPCs had existed since the control plane was written;
17nothing exposed them.
18"""
19
20import os, subprocess, sys, tempfile, time
21
22HERE = os.path.dirname(os.path.realpath(__file__))
23ROOT = os.path.dirname(HERE)
24SWARMD = os.path.join(ROOT, "swarmd")
25CTL = os.path.join(ROOT, "tools", "swarmd-ctl")
26
27passed = failed = 0
28
29
30def ck(name, cond, detail=""):
31 global passed, failed
32 if cond:
33 passed += 1
34 print(" PASS: %-52s %s" % (name, detail))
35 else:
36 failed += 1
37 print(" FAIL: %-52s %s" % (name, detail))
38
39
40def ctl(sock, *args):
41 r = subprocess.run([CTL, "--socket", sock] + list(args),
42 capture_output=True, text=True, timeout=30)
43 return r.returncode, (r.stdout + r.stderr).strip()
44
45
46def main():
47 bootstrap = (sys.argv[sys.argv.index("--bootstrap") + 1]
48 if "--bootstrap" in sys.argv else None)
49 for p in (SWARMD, CTL):
50 if not os.path.isfile(p):
51 print("missing %s — run ./build.sh" % p)
52 return 2
53
54 tmp = tempfile.mkdtemp(prefix="swarmd-ctl-")
55 sock = os.path.join(tmp, "d.sock")
56 argv = [SWARMD, "--socket", sock]
57 if bootstrap:
58 argv += ["--bootstrap", bootstrap]
59 log = open(os.path.join(tmp, "d.log"), "wb")
60 proc = subprocess.Popen(argv, stdout=log, stderr=subprocess.STDOUT)
61
62 try:
63 deadline = time.time() + 30
64 while time.time() < deadline and not os.path.exists(sock):
65 time.sleep(0.2)
66 ck("the daemon bound its socket", os.path.exists(sock))
67
68 pidfile = sock + ".pid"
69 ck("it wrote a pidfile beside the socket", os.path.exists(pidfile))
70 pid = int(open(pidfile).read().strip())
71 ck("the pidfile names the daemon's own process", pid == proc.pid,
72 "%d" % pid)
73
74 rc, out = ctl(sock, "status")
75 ck("status answers", rc == 0 and "dht=" in out, out)
76 ck("and counts the ctl client that asked", "local=1" in out, out)
77
78 # ⛔ THE POINT: a topic can be changed on a daemon that is ALREADY RUNNING.
79 rc, out = ctl(sock, "join", "ctl-gate-topic")
80 ck("join reaches a running daemon", rc == 0 and "joined" in out, out)
81 rc, out = ctl(sock, "leave", "ctl-gate-topic")
82 ck("leave does too", rc == 0 and "left" in out, out)
83
84 rc, out = ctl(sock, "join")
85 ck("join with no topic is refused, not guessed", rc != 0)
86
87 # ---- stop, by pid, from the daemon's own file ----
88 rc, out = ctl(sock, "stop")
89 ck("stop reports the pid it signalled", rc == 0 and str(pid) in out, out)
90 try:
91 proc.wait(timeout=15)
92 gone = True
93 except subprocess.TimeoutExpired:
94 gone = False
95 ck("the daemon actually exited", gone)
96 ck("and took its pidfile with it", not os.path.exists(pidfile))
97
98 # ---- and it refuses what it cannot name ----
99 rc, out = ctl(os.path.join(tmp, "nothing.sock"), "stop")
100 ck("stop on a socket with no pidfile fails clearly",
101 rc != 0 and "pidfile" in out, out)
102 finally:
103 if proc.poll() is None:
104 proc.terminate()
105 try: proc.wait(timeout=5)
106 except subprocess.TimeoutExpired: proc.kill()
107 log.close()
108 print("(logs: %s)" % tmp)
109
110 print("\n%d/%d passed" % (passed, passed + failed))
111 return 1 if failed else 0
112
113
114sys.exit(main())
swarmd/tools/session_shim.cpp +23 -1
……@@ -40,6 +40,22 @@
4040
4141static const char *SESSION_PROTOCOL = "blender-bridge-session/v1";
4242
43// ⛔ A DAEMON MUST NAME ITS OWN PROCESS. Without this the only way to stop one
44// is to match its command line, and a pattern that matches a daemon's argv also
45// matches the shell that typed it — `pkill -f` killed an ssh session's own
46// remote shell twice in this project. The pidfile sits beside the socket, so
47// whoever can reach the socket can stop exactly the process behind it.
48static void write_pidfile(const std::string &socket_path) {
49 FILE *f = fopen((socket_path + ".pid").c_str(), "w");
50 if (!f) return; // diagnostics, never a blocker
51 fprintf(f, "%ld\n", (long)getpid());
52 fclose(f);
53}
54
55static void remove_pidfile(const std::string &socket_path) {
56 ::unlink((socket_path + ".pid").c_str());
57}
58
4359static volatile sig_atomic_t g_stop = 0;
4460static void on_signal(int) { g_stop = 1; }
4561
……@@ -139,7 +155,12 @@ int main(int argc, char **argv) {
139155 if (client.send(channel, 0, 0, copy.data(), copy.size())) to_peers++;
140156 };
141157
142 printf("[swarmd-session] listening on %s (swarmd at %s)\n", sock, swarmd_sock.c_str());
158 // The pidfile goes with the CONTROL socket, because that is the one
159 // swarmd-ctl is pointed at: whatever socket you can reach, socket + ".pid"
160 // names the process behind it.
161 write_pidfile(swarmd_sock);
162 printf("[swarmd-session] listening on %s (swarmd at %s, pid %ld)\n",
163 sock, swarmd_sock.c_str(), (long)getpid());
143164 fflush(stdout);
144165
145166 // ⛔ SAY WHETHER THE SWARM IS REACHABLE, not just whether we started. "Bound"
……@@ -176,5 +197,6 @@ int main(int argc, char **argv) {
176197 daemon.stop();
177198 ::unlink(sock);
178199 ::unlink(swarmd_sock.c_str());
200 remove_pidfile(swarmd_sock);
179201 return 0;
180202}
swarmd/tools/swarmd-ctl +0 -0
(no content changes — binary or rename)
swarmd/tools/swarmd_ctl.cpp +138 -0
……@@ -0,0 +1,138 @@
1// swarmd-ctl — talk to a running daemon instead of hunting for its process.
2//
3// swarmd-ctl --socket <path> status
4// swarmd-ctl --socket <path> join <words>
5// swarmd-ctl --socket <path> leave <words>
6// swarmd-ctl --socket <path> stop
7// swarmd-ctl --socket <path> watch [--seconds N]
8//
9// ⛔ THIS EXISTS BECAUSE `pkill -f` IS NOT A CONTROL PLANE. A pattern that
10// matches a daemon's argv also matches the SHELL that typed it: `pkill -f
11// "session-daemon --topic mountain"` run over ssh killed the ssh session's own
12// remote shell — twice in this project's history, once in the session that wrote
13// this file. It is also indiscriminate: two daemons on two topics share every
14// substring worth matching on.
15//
16// `stop` reads the pidfile the daemon writes beside its socket and signals THAT
17// pid. One process, named by the daemon itself, never a pattern.
18//
19// ⛔ JOIN AND LEAVE WORK ON A RUNNING DAEMON. The topic was a start-up argument
20// and nothing else, so changing session meant killing a process — which is why
21// killing processes was the interface. The RPCs have existed since the control
22// plane was written; nothing exposed them.
23//
24// The topic is BLAKE2b(words), the same derivation session-daemon and
25// swarmd-session use, so the words you type here are the words you type in
26// Blender.
27
28#include <cstdio>
29#include <cstdlib>
30#include <cstring>
31#include <string>
32
33#include <csignal>
34#include <unistd.h>
35
36#include <sodium.h>
37
38#include "client.h"
39
40static void usage() {
41 fprintf(stderr,
42 "usage: swarmd-ctl --socket <path> <command>\n"
43 " status what the daemon says about itself\n"
44 " join <words> join a topic, on a daemon that is already running\n"
45 " leave <words> leave one\n"
46 " stop terminate it by the pid in its pidfile\n"
47 " watch [--seconds N] status, repeatedly\n");
48}
49
50static void topic_of(const char *words, uint8_t out[32]) {
51 crypto_generichash(out, 32, (const uint8_t *)words, strlen(words), nullptr, 0);
52}
53
54static int do_stop(const std::string &sock) {
55 // ⛔ BY PID, FROM THE DAEMON'S OWN PIDFILE. Never by pattern.
56 const std::string pidfile = sock + ".pid";
57 FILE *f = fopen(pidfile.c_str(), "r");
58 if (!f) {
59 fprintf(stderr, "no pidfile at %s — is this a swarmd socket?\n", pidfile.c_str());
60 return 1;
61 }
62 long pid = 0;
63 const int got = fscanf(f, "%ld", &pid);
64 fclose(f);
65 if (got != 1 || pid <= 0) {
66 fprintf(stderr, "%s does not contain a pid\n", pidfile.c_str());
67 return 1;
68 }
69 if (kill((pid_t)pid, SIGTERM) != 0) {
70 fprintf(stderr, "could not signal %ld (already gone?)\n", pid);
71 return 1;
72 }
73 printf("stopped %ld\n", pid);
74 return 0;
75}
76
77static void print_status(const swarmd::Status &st, int local) {
78 const char *dht = st.dht_state == 2 ? "up" : (st.dht_state == 1 ? "connecting" : "starting");
79 printf("dht=%s peers=%llu known=%llu connecting=%llu queued=%llu local=%d\n",
80 dht, (unsigned long long)st.peers_connected,
81 (unsigned long long)st.peers_known, (unsigned long long)st.connecting,
82 (unsigned long long)st.queued, local);
83 fflush(stdout);
84}
85
86int main(int argc, char **argv) {
87 if (sodium_init() < 0) { fprintf(stderr, "sodium_init failed\n"); return 1; }
88
89 const char *sock = nullptr;
90 int seconds = 0;
91 std::string cmd, words;
92 for (int i = 1; i < argc; i++) {
93 if (!strcmp(argv[i], "--socket") && i + 1 < argc) sock = argv[++i];
94 else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = atoi(argv[++i]);
95 else if (cmd.empty()) cmd = argv[i];
96 else if (words.empty()) words = argv[i];
97 }
98 if (!sock || cmd.empty()) { usage(); return 2; }
99
100 if (cmd == "stop") return do_stop(sock);
101
102 swarmd::Client c;
103 if (!c.connect(sock, "swarmd-ctl")) {
104 fprintf(stderr, "%s\n", c.last_error().c_str());
105 return 1;
106 }
107
108 if (cmd == "status" || cmd == "watch") {
109 const int rounds = (cmd == "watch") ? (seconds > 0 ? seconds : 3600) : 1;
110 for (int i = 0; i < rounds; i++) {
111 swarmd::Status st;
112 if (!c.status(st)) { fprintf(stderr, "%s\n", c.last_error().c_str()); return 1; }
113 print_status(st, (int)st.local_clients);
114 if (cmd == "status") break;
115 if (!c.poll(1000)) break;
116 }
117 return 0;
118 }
119
120 if (cmd == "join" || cmd == "leave") {
121 if (words.empty()) { usage(); return 2; }
122 uint8_t topic[32];
123 topic_of(words.c_str(), topic);
124 const bool ok = (cmd == "join") ? c.join(topic) : c.leave(topic);
125 if (!ok) { fprintf(stderr, "%s\n", c.last_error().c_str()); return 1; }
126 // ⚠ Say what was done to WHICH topic: the words are hashed, and two
127 // similar names are indistinguishable in a log otherwise.
128 char hex[17];
129 for (int i = 0; i < 8; i++) sprintf(hex + i * 2, "%02x", topic[i]);
130 hex[16] = 0;
131 printf("%s %s (topic %s…)\n", cmd == "join" ? "joined" : "left",
132 words.c_str(), hex);
133 return 0;
134 }
135
136 usage();
137 return 2;
138}