| … | … | @@ -22,6 +22,25 @@ encodings the same way, with `schema/build.js` and the resulting |
| 22 | 22 | - **hyperschema-c** — structs + `preencode/encode/decode/destroy(compact_state_t*)` |
| 23 | 23 | - **hrpc-c** — typed sans-io RPC over the bare-rpc wire (unary + event handlers) |
| 24 | 24 | |
| 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 | |
| 33 | matches the shell that typed it — that killed an ssh session's own remote shell | |
| 34 | twice in this project. `stop` signals the pid in the pidfile the daemon writes | |
| 35 | beside its socket: one process, named by itself. | |
| 36 | ||
| 37 | ⛔ And a topic used to be a START-UP ARGUMENT, so changing session meant killing | |
| 38 | a process — which is why killing processes was the interface. join/leave work on | |
| 39 | a daemon that is already running; the RPCs existed all along and nothing exposed | |
| 40 | them. | |
| 41 | ||
| 42 | For swarmd-session, the control socket is the legacy socket + `.swarmd`. | |
| 43 | ||
| 25 | 44 | ## Using it from C++ |
| 26 | 45 | |
| 27 | 46 | #include "client/client.h" |
| … | … | @@ -64,6 +83,7 @@ wrong in ways the daemon could not distinguish from a bad daemon. |
| 64 | 83 | channels Protomux channel per peer, SEND/on-message D1/D4 PASS |
| 65 | 84 | content liboverlay: hash/serve/fetch/cancel D6 PASS |
| 66 | 85 | client/ swarmd::Client — the ONE wire implementation 30/30 |
| 86 | swarmd-ctl inspect/retarget/stop a RUNNING daemon 12/12 | |
| 67 | 87 | tools/ swarmd-probe — built on it, in 181 lines |
| 68 | 88 | |
| 69 | 89 | ./swarmd --socket /tmp/s.sock --storage ~/.swarmd # public DHT |
| … | … | @@ -122,6 +122,9 @@ echo "Building tools..." |
| 122 | 122 | "$CXX" -std=c++17 -O2 $INC -o tools/swarmd-probe tools/swarmd_probe.cpp \ |
| 123 | 123 | "$OBJ/client.o" "$OBJ/swarmd_schema.o" "$OBJ/swarmd_hrpc.o" $CLIBS |
| 124 | 124 | |
| 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 | ||
| 125 | 128 | # swarmd-session: session-daemon's socket, swarmd's insides. Links the daemon |
| 126 | 129 | # too, because it runs one. |
| 127 | 130 | "$CXX" -std=c++17 -O2 $INC -o tools/swarmd-session tools/session_shim.cpp \ |
| … | … | @@ -12,8 +12,27 @@ |
| 12 | 12 | |
| 13 | 13 | #include <sodium.h> |
| 14 | 14 | |
| 15 | #include <string> | |
| 16 | #include <unistd.h> | |
| 17 | ||
| 15 | 18 | #include "daemon.h" |
| 16 | 19 | |
| 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. | |
| 25 | static 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 | ||
| 32 | static void remove_pidfile(const std::string &socket_path) { | |
| 33 | ::unlink((socket_path + ".pid").c_str()); | |
| 34 | } | |
| 35 | ||
| 17 | 36 | static volatile sig_atomic_t g_stop = 0; |
| 18 | 37 | static void on_signal(int) { g_stop = 1; } |
| 19 | 38 | |
| … | … | @@ -45,7 +64,8 @@ int main(int argc, char **argv) { |
| 45 | 64 | fprintf(stderr, "failed to start (socket %s)\n", sock); |
| 46 | 65 | return 1; |
| 47 | 66 | } |
| 48 | printf("[swarmd] listening on %s\n", sock); | |
| 67 | write_pidfile(sock); | |
| 68 | printf("[swarmd] listening on %s (pid %ld)\n", sock, (long)getpid()); | |
| 49 | 69 | fflush(stdout); |
| 50 | 70 | |
| 51 | 71 | // ⛔ SAY WHETHER THE SWARM IS REACHABLE, not just whether we started. |
| … | … | @@ -71,5 +91,6 @@ int main(int argc, char **argv) { |
| 71 | 91 | |
| 72 | 92 | printf("[swarmd] stopping\n"); |
| 73 | 93 | d.stop(); |
| 94 | remove_pidfile(sock); | |
| 74 | 95 | return 0; |
| 75 | 96 | } |
| … | … | @@ -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 | |
| 7 | stop a daemon was to match its command line, and a pattern that matches a | |
| 8 | daemon'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 | |
| 10 | this project — once in the session that wrote this file. It is also | |
| 11 | indiscriminate, since two daemons on two topics share every substring worth | |
| 12 | matching on. | |
| 13 | ||
| 14 | And the topic was a START-UP ARGUMENT and nothing else, so changing session meant | |
| 15 | killing a process. That is why killing processes was the interface. The | |
| 16 | swarm-join / swarm-leave RPCs had existed since the control plane was written; | |
| 17 | nothing exposed them. | |
| 18 | """ | |
| 19 | ||
| 20 | import os, subprocess, sys, tempfile, time | |
| 21 | ||
| 22 | HERE = os.path.dirname(os.path.realpath(__file__)) | |
| 23 | ROOT = os.path.dirname(HERE) | |
| 24 | SWARMD = os.path.join(ROOT, "swarmd") | |
| 25 | CTL = os.path.join(ROOT, "tools", "swarmd-ctl") | |
| 26 | ||
| 27 | passed = failed = 0 | |
| 28 | ||
| 29 | ||
| 30 | def 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 | ||
| 40 | def 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 | ||
| 46 | def 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 | ||
| 114 | sys.exit(main()) |
| … | … | @@ -40,6 +40,22 @@ |
| 40 | 40 | |
| 41 | 41 | static const char *SESSION_PROTOCOL = "blender-bridge-session/v1"; |
| 42 | 42 | |
| 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. | |
| 48 | static 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 | ||
| 55 | static void remove_pidfile(const std::string &socket_path) { | |
| 56 | ::unlink((socket_path + ".pid").c_str()); | |
| 57 | } | |
| 58 | ||
| 43 | 59 | static volatile sig_atomic_t g_stop = 0; |
| 44 | 60 | static void on_signal(int) { g_stop = 1; } |
| 45 | 61 | |
| … | … | @@ -139,7 +155,12 @@ int main(int argc, char **argv) { |
| 139 | 155 | if (client.send(channel, 0, 0, copy.data(), copy.size())) to_peers++; |
| 140 | 156 | }; |
| 141 | 157 | |
| 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()); | |
| 143 | 164 | fflush(stdout); |
| 144 | 165 | |
| 145 | 166 | // ⛔ SAY WHETHER THE SWARM IS REACHABLE, not just whether we started. "Bound" |
| … | … | @@ -176,5 +197,6 @@ int main(int argc, char **argv) { |
| 176 | 197 | daemon.stop(); |
| 177 | 198 | ::unlink(sock); |
| 178 | 199 | ::unlink(swarmd_sock.c_str()); |
| 200 | remove_pidfile(swarmd_sock); | |
| 179 | 201 | return 0; |
| 180 | 202 | } |
| … | … | @@ -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 | ||
| 40 | static 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 | ||
| 50 | static 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 | ||
| 54 | static 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 | ||
| 77 | static 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 | ||
| 86 | int 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 | } |