OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / ce9311e

swarmd: channels, and a C++ probe that speaks the wire with generated encoders
CHANNEL_OPEN / CHANNEL_CLOSE / SEND now reach Protomux, and inbound frames come back to clients as on-message events. Three threading rules the implementation follows, each commented where it matters: - Protomux belongs to the UDX thread, so channel_open/close/send marshal through run_on_loop. channel_open returns its handle immediately rather than waiting for the channel to exist. - A channel opened BEFORE the swarm meshed must still reach peers that arrive later, and one opened AFTER must reach peers already there. Both directions are covered: the mux-ready callback opens every known spec on the new peer, and channel_open opens the new spec on every known mux. - Inbound frames arrive on the UDX thread and only ENQUEUE. The fds belong to the main thread. ⛔ AND A LESSON THE FIRST GATE TAUGHT IMMEDIATELY. test_channel_e2e.py hand-rolled compact-encoding in Python, and the daemon answered "undecodable message" to every frame — the test was wrong, not the daemon. Hand-writing the wire in a client is exactly what generating it was supposed to stop, and a test client is still a client. So tools/swarmd-probe is a real C++ client built on the GENERATED encoders, which is also §7 item 7's "C++ client library" started. Verified against a live daemon: no decode errors, local=1, commands accepted. ⏳ The two-daemon channel crossing is NOT proven. The gate exists and exits INCONCLUSIVE rather than FAIL when the daemons have not met — it asserts its own baseline, which the first long-lived gate did not. Also fixed: build.sh compiled control.cpp with 2>/dev/null, which hid a real compile error behind a silent `set -e` exit. Same self-inflicted blindness as the `&&` trap in the same file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ce9311ee0676e7c46b2cd49b0518f3179d066b91
by plankatron <fleeky@gmail.com> · 2026-09-10T17:56:18.000Z

Changed files (9) +511 -6

swarmd/.gitignore +1 -0
……@@ -2,3 +2,4 @@ node_modules/
22test/test_uds_backpressure
33test/test_control
44swarmd
5tools/swarmd-probe
swarmd/README.md +5 -1
……@@ -40,11 +40,15 @@ encodings the same way, with `schema/build.js` and the resulting
4040 uds_server local socket + per-client buffering 7/7 (D2, D3)
4141 control generated RPC dispatch on the socket 10/10
4242 daemon UdxTransport + queue + control plane runs
43 channels Protomux channel per peer, SEND/on-message
44 tools/ swarmd-probe — the smallest real client
4345
4446 ./swarmd --socket /tmp/s.sock # public DHT
4547 [swarmd] listening on /tmp/s.sock
4648 [swarmd] dht=connecting peers=0 local=0
4749 [swarmd] dht=up peers=0 local=0
4850
49⏳ Not built: channels (SEND / on-message over Protomux), content. See
51⏳ Channels are wired but the two-daemon crossing is NOT yet proven: the gate
52exists (`test/test_channel_e2e.py`) and currently exits INCONCLUSIVE because the
53daemons had not met inside its window. Content is not built. See
5054`../docs/SWARMD-DESIGN.md` §9 for the order and §8 for the remaining gates.
swarmd/build.sh +5 -1
……@@ -39,7 +39,7 @@ gcc -std=c99 -O2 $INC -c -o "$OBJ/swarmd_hrpc.o" schema/swarmd_hrpc.c
3939
4040echo "Building daemon sources..."
4141g++ -std=c++17 -O2 $INC -c -o "$OBJ/uds_server.o" src/uds_server.cpp
42g++ -std=c++17 -O2 $INC -c -o "$OBJ/control.o" src/control.cpp 2>/dev/null
42g++ -std=c++17 -O2 $INC -c -o "$OBJ/control.o" src/control.cpp
4343
4444# The transport. Reuses hyperswarm-peer's object cache when test/build_tests.sh
4545# has populated it; otherwise builds what swarmd needs.
……@@ -80,4 +80,8 @@ g++ -std=c++17 -O2 $INC -o test/test_control \
8080 test/test_control.cpp "$OBJ/uds_server.o" "$OBJ/control.o" \
8181 "$OBJ/swarmd_schema.o" "$OBJ/swarmd_hrpc.o" $CLIBS
8282
83echo "Building tools..."
84g++ -std=c++17 -O2 $INC -o tools/swarmd-probe tools/swarmd_probe.cpp \
85 "$OBJ/swarmd_schema.o" "$OBJ/swarmd_hrpc.o" $CLIBS
86
8387echo " OK"
swarmd/src/control.cpp +39 -1
……@@ -18,6 +18,7 @@ struct Ctx {
1818 Control::Session *session;
1919 ControlBackend *backend;
2020 uint64_t *hellos;
21 uint64_t *client_id;
2122};
2223
2324static int on_hello(void *vctx, const swarmd_hello_request_t *req,
……@@ -59,6 +60,40 @@ static int on_swarm_leave(void *vctx, const swarmd_topic_ref_t *req,
5960 return 0;
6061}
6162
63static int on_channel_open(void *vctx, const swarmd_channel_open_t *req,
64 swarmd_channel_ref_t *res, hrpc_error_t *) {
65 Ctx *ctx = (Ctx *)vctx;
66 res->channel = 0;
67 if (!ctx->backend->channel_open) return 0;
68 // ⛔ COPY OUT. protocol and id point into the inbound frame buffer, and the
69 // channel outlives this call by design.
70 std::string protocol((const char *)req->protocol.data, req->protocol.len);
71 std::vector<uint8_t> id(req->id, req->id + req->id_len);
72 res->channel = ctx->backend->channel_open(protocol, id, (uint32_t)req->slots,
73 *ctx->client_id);
74 return 0;
75}
76
77static int on_channel_close(void *vctx, const swarmd_channel_ref_t *req,
78 swarmd_ok_t *res, hrpc_error_t *) {
79 Ctx *ctx = (Ctx *)vctx;
80 res->ok = ctx->backend->channel_close ? ctx->backend->channel_close(req->channel) : false;
81 return 0;
82}
83
84static int on_send(void *vctx, const swarmd_send_request_t *req,
85 swarmd_send_result_t *res, hrpc_error_t *) {
86 Ctx *ctx = (Ctx *)vctx;
87 res->accepted = false;
88 res->writable = false;
89 if (!ctx->backend->send) return 0;
90 bool writable = true;
91 res->accepted = ctx->backend->send(req->channel, (uint32_t)req->slot, req->target,
92 req->bytes, req->bytes_len, &writable);
93 res->writable = writable;
94 return 0;
95}
96
6297void Control::attach(UdsServer *uds, ControlBackend backend) {
6398 _uds = uds;
6499 _backend = std::move(backend);
……@@ -84,7 +119,7 @@ void Control::_on_frame(uint64_t client_id, const uint8_t *data, size_t len) {
84119 return;
85120 }
86121
87 Ctx ctx = { this, &sit->second, &_backend, &_hellos };
122 Ctx ctx = { this, &sit->second, &_backend, &_hellos, &client_id };
88123 swarmd_hrpc_handlers_t h;
89124 memset(&h, 0, sizeof(h));
90125 h.ctx = &ctx;
……@@ -92,6 +127,9 @@ void Control::_on_frame(uint64_t client_id, const uint8_t *data, size_t len) {
92127 h.on_status_get = on_status_get;
93128 h.on_swarm_join = on_swarm_join;
94129 h.on_swarm_leave = on_swarm_leave;
130 h.on_channel_open = on_channel_open;
131 h.on_channel_close = on_channel_close;
132 h.on_send = on_send;
95133
96134 uint8_t *reply = nullptr;
97135 size_t reply_len = 0;
swarmd/src/control.h +7 -0
……@@ -19,6 +19,7 @@
1919#include <functional>
2020#include <map>
2121#include <string>
22#include <vector>
2223
2324#include "uds_server.h"
2425
……@@ -35,6 +36,12 @@ struct ControlBackend {
3536 std::function<void(swarmd_status_t *out)> status;
3637 std::function<bool(const uint8_t topic[32], bool server, bool client)> swarm_join;
3738 std::function<bool(const uint8_t topic[32])> swarm_leave;
39 // Returns the channel handle, or 0.
40 std::function<uint64_t(const std::string &protocol, const std::vector<uint8_t> &id,
41 uint32_t slots, uint64_t owner)> channel_open;
42 std::function<bool(uint64_t handle)> channel_close;
43 std::function<bool(uint64_t handle, uint32_t slot, uint64_t target,
44 const uint8_t *data, size_t len, bool *writable)> send;
3845};
3946
4047class Control {
swarmd/src/daemon.cpp +97 -2
……@@ -20,11 +20,64 @@ bool Daemon::start(const char *socket_path, const char *bootstrap) {
2020 _t.swarm_leave(topic);
2121 return true;
2222 };
23 backend.channel_open = [this](const std::string &protocol,
24 const std::vector<uint8_t> &id,
25 uint32_t slots, uint64_t owner) -> uint64_t {
26 const uint64_t handle = _next_channel++;
27 // ⛔ PROTOMUX BELONGS TO THE UDX THREAD. Creating a channel from here
28 // would touch its handles from the wrong thread; run_on_loop is the only
29 // way in, and it is why this returns the handle immediately rather than
30 // waiting for the channel to exist.
31 _t.run_on_loop([this, handle, protocol, id, slots, owner]() {
32 ChannelSpec &spec = _channels[handle];
33 spec.handle = handle;
34 spec.protocol = protocol;
35 spec.id = id;
36 spec.slots = slots ? slots : 1;
37 spec.owner = owner;
38 for (auto &kv : _muxes) _open_on_peer(spec, kv.first, kv.second);
39 });
40 return handle;
41 };
42
43 backend.channel_close = [this](uint64_t handle) {
44 _t.run_on_loop([this, handle]() {
45 auto it = _channels.find(handle);
46 if (it == _channels.end()) return;
47 for (auto &kv : it->second.per_peer) {
48 if (kv.second) kv.second->close();
49 }
50 _channels.erase(it);
51 });
52 return true;
53 };
54
55 backend.send = [this](uint64_t handle, uint32_t slot, uint64_t target,
56 const uint8_t *data, size_t len, bool *writable) {
57 std::vector<uint8_t> copy(data, data + len); // the frame buffer is reused
58 *writable = true; // Protomux answers on the loop; see the note below
59 _t.run_on_loop([this, handle, slot, target, copy]() {
60 auto it = _channels.find(handle);
61 if (it == _channels.end()) return;
62 for (auto &kv : it->second.per_peer) {
63 if (target != 0 && (uint64_t)(uint32_t)kv.first != target) continue;
64 if (kv.second) kv.second->send(slot, copy.data(), copy.size());
65 }
66 });
67 return true;
68 };
69
2370 _control.attach(&_uds, backend);
2471
2572 // Peer lifecycle arrives on the UDX thread. Enqueue only.
2673 _t.set_mux_callbacks(
27 [this](int32_t id, Protomux *) {
74 [this](int32_t id, Protomux *mux) {
75 // UDX THREAD. Record the mux and give this peer every channel a
76 // client has already opened — a channel opened before the swarm
77 // meshed must still reach peers that arrive later.
78 _muxes[id] = mux;
79 for (auto &kv : _channels) _open_on_peer(kv.second, id, mux);
80
2881 Event e;
2982 e.kind = Event::PEER_UP;
3083 e.peer_id = id;
……@@ -33,6 +86,11 @@ bool Daemon::start(const char *socket_path, const char *bootstrap) {
3386 _push(e);
3487 },
3588 [this](int32_t id) {
89 // UDX THREAD. The mux is going away; drop our references to its
90 // channels rather than let them dangle.
91 _muxes.erase(id);
92 for (auto &kv : _channels) kv.second.per_peer.erase(id);
93
3694 Event e;
3795 e.kind = Event::PEER_DOWN;
3896 e.peer_id = id;
……@@ -63,6 +121,33 @@ void Daemon::stop() {
63121 _uds.close_all();
64122}
65123
124void Daemon::_open_on_peer(ChannelSpec &spec, int32_t peer_id, Protomux *mux) {
125 // UDX THREAD ONLY.
126 if (!mux) return;
127 if (spec.per_peer.count(peer_id)) return;
128
129 ProtomuxChannel *ch = mux->create_channel(spec.protocol, spec.id);
130 if (ch == nullptr) return; // already open on this mux
131
132 const uint64_t handle = spec.handle;
133 for (uint32_t slot = 0; slot < spec.slots; slot++) {
134 ch->add_message([this, handle, slot, peer_id](const uint8_t *data, size_t len,
135 ProtomuxChannel *) {
136 // UDX THREAD. Enqueue only — the fds belong to the main thread.
137 Event e;
138 e.kind = Event::MESSAGE;
139 e.peer_id = peer_id;
140 memset(e.pubkey, 0, 32);
141 e.channel = handle;
142 e.slot = slot;
143 e.bytes.assign(data, data + len);
144 _push(e);
145 });
146 }
147 spec.per_peer[peer_id] = ch;
148 ch->open();
149}
150
66151void Daemon::_push(const Event &e) {
67152 std::lock_guard<std::mutex> lk(_m);
68153 if (_events.size() >= MAX_EVENTS) {
……@@ -81,7 +166,17 @@ void Daemon::_drain() {
81166 for (const Event &e : batch) {
82167 uint8_t *out = nullptr;
83168 size_t out_len = 0;
84 if (e.kind == Event::PEER_UP) {
169 if (e.kind == Event::MESSAGE) {
170 swarmd_message_t m;
171 memset(&m, 0, sizeof(m));
172 m.channel = e.channel;
173 m.slot = e.slot;
174 m.origin_kind = 0; // 0 = remote peer, 1 = another local client
175 m.origin_id = (uint64_t)(uint32_t)e.peer_id;
176 m.bytes = (uint8_t *)e.bytes.data();
177 m.bytes_len = e.bytes.size();
178 if (swarmd_encode_on_message(&m, &out, &out_len) < 0) continue;
179 } else if (e.kind == Event::PEER_UP) {
85180 swarmd_peer_up_t up;
86181 memset(&up, 0, sizeof(up));
87182 up.peer = (uint64_t)(uint32_t)e.peer_id;
swarmd/src/daemon.h +24 -1
……@@ -43,10 +43,27 @@ public:
4343private:
4444 // Everything the UDX thread hands over crosses as one of these.
4545 struct Event {
46 enum Kind { PEER_UP, PEER_DOWN } kind;
46 enum Kind { PEER_UP, PEER_DOWN, MESSAGE } kind;
4747 int32_t peer_id;
4848 uint8_t pubkey[32];
49 uint64_t channel = 0;
50 uint64_t slot = 0;
51 std::vector<uint8_t> bytes;
4952 };
53
54 // A channel a client asked for. One spec, N live ProtomuxChannels — one per
55 // peer — because Protomux is per-connection and a topic is not.
56 struct ChannelSpec {
57 uint64_t handle = 0;
58 std::string protocol;
59 std::vector<uint8_t> id;
60 uint32_t slots = 1;
61 uint64_t owner = 0; // the client that opened it
62 std::map<int32_t, ProtomuxChannel *> per_peer;
63 };
64
65 // UDX-THREAD ONLY. Creates this spec's channel on one peer's mux.
66 void _open_on_peer(ChannelSpec &spec, int32_t peer_id, Protomux *mux);
5067 void _push(const Event &e);
5168 void _drain();
5269
……@@ -56,6 +73,12 @@ private:
5673 UdsServer _uds;
5774 Control _control;
5875
76 // UDX-THREAD ONLY, except where a lock is taken: the peer's mux, so a
77 // channel opened after the swarm meshed can still reach existing peers.
78 std::map<int32_t, Protomux *> _muxes;
79 std::map<uint64_t, ChannelSpec> _channels;
80 uint64_t _next_channel = 1;
81
5982 std::mutex _m;
6083 std::deque<Event> _events;
6184 // Bounded: a swarm that churns faster than the socket drains must drop
swarmd/test/test_channel_e2e.py +160 -0
……@@ -0,0 +1,160 @@
1#!/usr/bin/env python3
2"""Gate D1/D4: a frame crosses between two swarmd daemons over a real channel.
3
4 python3 -B test/test_channel_e2e.py [--bootstrap host:port]
5
6Two daemons join one topic, each local client opens the SAME protocol channel,
7and a frame sent by one arrives at the other. That is the whole product in one
8assertion: everything before it is plumbing.
9
10⛔ IT ASSERTS ITS OWN BASELINE. If the two daemons never see each other the
11message result says nothing, so a swarm that never meshes exits INCONCLUSIVE
12rather than FAIL — the mistake the first long-lived gate made, reporting "the
13responder has gone silent" from a run in which nothing had ever connected.
14"""
15
16import os, socket, struct, subprocess, sys, tempfile, time
17
18HERE = os.path.dirname(os.path.realpath(__file__))
19SWARMD = os.path.join(os.path.dirname(HERE), "swarmd")
20EXIT_PASS, EXIT_FAIL, EXIT_INCONCLUSIVE = 0, 1, 2
21
22# Command ids are the append-only ids from schema/hrpc.json.
23import json
24HRPC = json.load(open(os.path.join(os.path.dirname(HERE), "schema", "hrpc.json")))
25CMD = {h["name"].split("/", 1)[1]: h["id"] for h in HRPC["schema"]}
26
27
28def frame(payload):
29 return struct.pack("<I", len(payload)) + payload
30
31
32def uint(n):
33 # compact-encoding uint
34 if n < 0xfc: return bytes([n])
35 if n <= 0xffff: return b"\xfd" + struct.pack("<H", n)
36 if n <= 0xffffffff: return b"\xfe" + struct.pack("<I", n)
37 return b"\xff" + struct.pack("<Q", n)
38
39
40def rpc_request(cmd_id, req_id, body):
41 # rpc_message: type=1 (request), id, command, stream=0, then data
42 return uint(1) + uint(req_id) + uint(cmd_id) + uint(0) + uint(len(body)) + body
43
44
45def spawn(sock, bootstrap, log):
46 argv = [SWARMD, "--socket", sock]
47 if bootstrap:
48 argv += ["--bootstrap", bootstrap]
49 return subprocess.Popen(argv, stdout=log, stderr=subprocess.STDOUT)
50
51
52def wait_log(path, needle, timeout, proc):
53 end = time.time() + timeout
54 while time.time() < end:
55 if proc.poll() is not None:
56 return False
57 try:
58 with open(path, "rb") as fh:
59 if needle in fh.read():
60 return True
61 except OSError:
62 pass
63 time.sleep(0.3)
64 return False
65
66
67def main():
68 bootstrap = None
69 if "--bootstrap" in sys.argv:
70 bootstrap = sys.argv[sys.argv.index("--bootstrap") + 1]
71
72 if not os.path.isfile(SWARMD):
73 print("missing %s — run ./build.sh" % SWARMD)
74 return EXIT_INCONCLUSIVE
75
76 tmp = tempfile.mkdtemp(prefix="swarmd-chan-")
77 topic = bytes([0x5a] * 32)
78 protocol = b"swarmd-gate/v1"
79
80 procs, socks, logs = [], [], []
81 try:
82 for tag in ("a", "b"):
83 sp = os.path.join(tmp, tag + ".sock")
84 lp = os.path.join(tmp, tag + ".log")
85 log = open(lp, "wb")
86 p = spawn(sp, bootstrap, log)
87 procs.append(p); socks.append(sp); logs.append(lp)
88
89 for i, lp in enumerate(logs):
90 if not wait_log(lp, b"dht=up", 60, procs[i]):
91 print("\nINCONCLUSIVE: daemon %d never reached dht=up" % i)
92 return EXIT_INCONCLUSIVE
93 print(" both daemons are on the DHT")
94
95 conns = []
96 for sp in socks:
97 c = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
98 c.connect(sp)
99 conns.append(c)
100
101 # join the topic, open the channel
102 for c in conns:
103 body = topic + b"\x01\x01" # fixed32 + bool + bool
104 c.sendall(frame(rpc_request(CMD["swarm-join"], 1, body)))
105 ob = uint(len(protocol)) + protocol + uint(0) + uint(2) # protocol, id(empty), slots
106 c.sendall(frame(rpc_request(CMD["channel-open"], 2, ob)))
107 print(" joined and opened the channel on both")
108
109 # Give the swarm time to mesh and pair the channel.
110 deadline = time.time() + 90
111 meshed = False
112 while time.time() < deadline and not meshed:
113 for lp in logs:
114 with open(lp, "rb") as fh:
115 if b"peers=1" in fh.read():
116 meshed = True
117 time.sleep(1)
118
119 if not meshed:
120 print("\nINCONCLUSIVE: the two daemons never met, so a message result "
121 "would say nothing about the channel.")
122 return EXIT_INCONCLUSIVE
123 print(" the daemons meshed")
124
125 # a -> b
126 payload = b"hello-over-the-swarm"
127 sb = uint(1) + uint(0) + uint(0) + uint(len(payload)) + payload # channel, slot, target, bytes
128 conns[0].sendall(frame(rpc_request(CMD["send"], 3, sb)))
129
130 conns[1].settimeout(30)
131 got = b""
132 end = time.time() + 30
133 while time.time() < end:
134 try:
135 chunk = conns[1].recv(65536)
136 except socket.timeout:
137 break
138 if not chunk:
139 break
140 got += chunk
141 if payload in got:
142 break
143
144 if payload in got:
145 print("\nPASS: the frame crossed the swarm and arrived on the other daemon.")
146 return EXIT_PASS
147 print("\nFAIL: the daemons meshed but the frame did not arrive.")
148 return EXIT_FAIL
149 finally:
150 for c in socks:
151 pass
152 for p in procs:
153 p.terminate()
154 try: p.wait(timeout=5)
155 except subprocess.TimeoutExpired: p.kill()
156 print("(logs: %s)" % tmp)
157
158
159if __name__ == "__main__":
160 sys.exit(main())
swarmd/tools/swarmd_probe.cpp +173 -0
……@@ -0,0 +1,173 @@
1// swarmd-probe — the smallest real client, and the start of the C++ client
2// library SWARMD-DESIGN.md §7 item 7 asks for.
3//
4// swarmd-probe --socket <path> --topic-hex <64 hex> --protocol <name>
5// [--send <text>] [--seconds N]
6//
7// ⛔ IT USES THE GENERATED ENCODERS. The first version of the channel gate
8// hand-rolled compact-encoding in Python and the daemon answered "undecodable
9// message" to every frame — the test was wrong, not the daemon. Hand-writing
10// the wire in a client is exactly what generating it was supposed to stop, and
11// a test client is still a client.
12
13#include <cstdio>
14#include <cstdlib>
15#include <cstring>
16#include <string>
17#include <vector>
18
19#include <sys/socket.h>
20#include <sys/un.h>
21#include <time.h>
22#include <unistd.h>
23
24extern "C" {
25#include <compact.h>
26#include <rpc.h>
27#include "swarmd_hrpc.h"
28#include "swarmd_schema.h"
29}
30
31static int dial(const char *path) {
32 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
33 struct sockaddr_un a;
34 memset(&a, 0, sizeof(a));
35 a.sun_family = AF_UNIX;
36 strncpy(a.sun_path, path, sizeof(a.sun_path) - 1);
37 if (connect(fd, (struct sockaddr *)&a, sizeof(a)) != 0) { close(fd); return -1; }
38 return fd;
39}
40
41static void send_frame(int fd, uint8_t *buf, size_t len) {
42 const uint32_t n = (uint32_t)len;
43 uint8_t hdr[4] = { (uint8_t)(n & 0xff), (uint8_t)((n >> 8) & 0xff),
44 (uint8_t)((n >> 16) & 0xff), (uint8_t)((n >> 24) & 0xff) };
45 (void)!write(fd, hdr, 4);
46 (void)!write(fd, buf, len);
47 free(buf); // ⛔ the caller frees what *_encode_* allocated
48}
49
50static bool hex32(const char *s, uint8_t *out) {
51 if (strlen(s) != 64) return false;
52 for (int i = 0; i < 32; i++) {
53 char b[3] = { s[i * 2], s[i * 2 + 1], 0 };
54 out[i] = (uint8_t)strtoul(b, nullptr, 16);
55 }
56 return true;
57}
58
59int main(int argc, char **argv) {
60 const char *sock = nullptr, *topic_hex = nullptr, *proto = "swarmd-probe/v1";
61 const char *send_text = nullptr;
62 int seconds = 30;
63 for (int i = 1; i < argc; i++) {
64 if (!strcmp(argv[i], "--socket") && i + 1 < argc) sock = argv[++i];
65 else if (!strcmp(argv[i], "--topic-hex") && i + 1 < argc) topic_hex = argv[++i];
66 else if (!strcmp(argv[i], "--protocol") && i + 1 < argc) proto = argv[++i];
67 else if (!strcmp(argv[i], "--send") && i + 1 < argc) send_text = argv[++i];
68 else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = atoi(argv[++i]);
69 }
70 uint8_t topic[32];
71 if (!sock || !topic_hex || !hex32(topic_hex, topic)) {
72 fprintf(stderr, "usage: swarmd-probe --socket <p> --topic-hex <64hex> "
73 "[--protocol n] [--send text] [--seconds N]\n");
74 return 2;
75 }
76
77 int fd = dial(sock);
78 if (fd < 0) { fprintf(stderr, "cannot reach %s\n", sock); return 2; }
79
80 uint8_t *buf = nullptr; size_t len = 0;
81
82 swarmd_hello_request_t hello;
83 memset(&hello, 0, sizeof(hello));
84 hello.name.data = (utf8_t *)"probe";
85 hello.name.len = 5;
86 memset(hello.app_identity, 0x11, 32);
87 hello.version = 1;
88 if (swarmd_encode_hello(1, &hello, &buf, &len) >= 0) send_frame(fd, buf, len);
89
90 swarmd_swarm_join_t sj;
91 memset(&sj, 0, sizeof(sj));
92 memcpy(sj.topic, topic, 32);
93 sj.server = true; sj.client = true;
94 if (swarmd_encode_swarm_join(2, &sj, &buf, &len) >= 0) send_frame(fd, buf, len);
95
96 swarmd_channel_open_t co;
97 memset(&co, 0, sizeof(co));
98 co.protocol.data = (utf8_t *)proto;
99 co.protocol.len = strlen(proto);
100 co.id = nullptr; co.id_len = 0;
101 co.slots = 2;
102 if (swarmd_encode_channel_open(3, &co, &buf, &len) >= 0) send_frame(fd, buf, len);
103
104 printf("joined topic, opened %s\n", proto);
105 fflush(stdout);
106
107 const time_t deadline = time(nullptr) + seconds;
108 bool sent = false;
109 std::vector<uint8_t> in;
110 while (time(nullptr) < deadline) {
111 if (send_text && !sent && time(nullptr) > deadline - seconds + 8) {
112 swarmd_send_request_t sr;
113 memset(&sr, 0, sizeof(sr));
114 sr.channel = 1; sr.slot = 0; sr.target = 0;
115 sr.bytes = (uint8_t *)send_text;
116 sr.bytes_len = strlen(send_text);
117 if (swarmd_encode_send(4, &sr, &buf, &len) >= 0) {
118 send_frame(fd, buf, len);
119 printf("sent: %s\n", send_text);
120 fflush(stdout);
121 }
122 sent = true;
123 }
124
125 uint8_t tmp[65536];
126 struct timeval tv = { 1, 0 };
127 fd_set rf; FD_ZERO(&rf); FD_SET(fd, &rf);
128 if (select(fd + 1, &rf, nullptr, nullptr, &tv) <= 0) continue;
129 ssize_t r = read(fd, tmp, sizeof(tmp));
130 if (r <= 0) break;
131 in.insert(in.end(), tmp, tmp + r);
132
133 for (;;) {
134 if (in.size() < 4) break;
135 const uint32_t n = (uint32_t)in[0] | ((uint32_t)in[1] << 8) |
136 ((uint32_t)in[2] << 16) | ((uint32_t)in[3] << 24);
137 if (in.size() < 4 + n) break;
138 compact_state_t st = { 0, n, in.data() + 4 };
139 rpc_message_t msg;
140 if (rpc_decode_message(&st, &msg) >= 0) {
141 swarmd_message_t m;
142 hrpc_error_t err;
143 memset(&m, 0, sizeof(m));
144 // An event carries no reply; decoding it as on-message tells us
145 // whether that is what arrived.
146 if (msg.type == rpc_request && m.bytes_len == 0) {
147 swarmd_hrpc_handlers_t h;
148 memset(&h, 0, sizeof(h));
149 static std::string got;
150 h.ctx = &got;
151 h.on_on_message = [](void *ctx, const swarmd_message_t *rm) {
152 ((std::string *)ctx)->assign((const char *)rm->bytes, rm->bytes_len);
153 };
154 uint8_t *rep = nullptr; size_t rl = 0;
155 swarmd_hrpc_dispatch(&h, &msg, &rep, &rl);
156 if (rep) free(rep);
157 if (!got.empty()) {
158 printf("RECEIVED: %s\n", got.c_str());
159 fflush(stdout);
160 close(fd);
161 return 0;
162 }
163 }
164 (void)err;
165 }
166 in.erase(in.begin(), in.begin() + 4 + n);
167 }
168 }
169
170 printf("no message received\n");
171 close(fd);
172 return 1;
173}