OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / swarmd / client / client.h

159 lines · c ref:
#ifndef SWARMD_CLIENT_H
#define SWARMD_CLIENT_H

// A C++ client for swarmd. docs/SWARMD-DESIGN.md §7 item 7, §9 step 5.
//
// "Any program in C++" is the goal, and until now the only clients were a
// Python test harness and one 250-line tool. Both hand-assembled frames.
//
// ⛔ NOTHING HERE IS HAND-WRITTEN WIRE, and that is the entire reason this file
// exists rather than a page of documentation telling each consumer how to do it
// themselves. Two clients in this repo have already been wrong that way — the
// first channel gate hand-rolled compact-encoding in Python and the daemon
// answered "undecodable message" to every frame, and swarmd-probe gave its sends
// an id range that overlapped the content calls and reported a fetch reply as a
// send result. Both are the same mistake: a client is a wire implementation, and
// there should be ONE.
//
// SHAPE. Setup calls block for their reply; everything the daemon says on its
// own arrives through callbacks during poll(). That split is not arbitrary:
// channel_open has to return a handle before you can send on it, while a peer
// arriving has nobody waiting for it.
//
// ⛔ AND A BLOCKING CALL STILL DISPATCHES EVENTS. Frames do not queue politely
// behind the reply you asked for: a peer can come up, or a 100 MB fetch can
// finish, while you are waiting for the answer to channel_open. Anything read on
// the way to a reply is delivered, not dropped and not deferred.
//
// THREADING. One Client belongs to one thread. It owns a socket and a buffer and
// guards neither.

#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <string>
#include <vector>

namespace swarmd {

// What the daemon says on its own. Every one of these arrives inside poll(), or
// inside a blocking call that read it on the way to a reply.
struct ClientCallbacks {
	// A frame from a peer, on a channel this client opened.
	std::function<void(uint64_t channel, uint64_t slot, uint64_t origin_kind,
					   uint64_t origin_id, const uint8_t *bytes, size_t len)> on_message;
	std::function<void(uint64_t peer, const uint8_t pubkey[32])> on_peer_up;
	std::function<void(uint64_t peer)> on_peer_down;
	// The channel has drained and will accept writes again.
	std::function<void(uint64_t channel)> on_drain;

	std::function<void(const uint8_t hash[32], uint64_t received, uint64_t total)> on_content_progress;
	std::function<void(const uint8_t hash[32], const std::string &path)> on_content_done;
	std::function<void(const uint8_t hash[32], const std::string &reason)> on_content_failed;

	// The daemon went away, or sent something this client could not read. After
	// this the Client is closed; connect() again to resume.
	std::function<void(const std::string &reason)> on_disconnect;
};

struct Status {
	uint64_t dht_state = 0;      // 0 starting, 1 connecting, 2 up
	bool bootstrapped = false;
	uint64_t peers_known = 0;
	uint64_t peers_connected = 0;
	uint64_t connecting = 0;
	uint64_t queued = 0;
	uint64_t local_clients = 0;
};

class Client {
public:
	Client() = default;
	~Client();
	Client(const Client &) = delete;
	Client &operator=(const Client &) = delete;

	ClientCallbacks on;

	// Dial the daemon and say HELLO. `name` and `app_identity` are what the
	// daemon records for this client; app_identity is what echo suppression and
	// membership will key on, so a program that has one should pass it.
	bool connect(const std::string &socket_path, const std::string &name,
				 const uint8_t app_identity[32] = nullptr, int timeout_ms = 5000);
	void close();
	bool connected() const { return _fd >= 0; }

	uint64_t client_id() const { return _client_id; }
	// Every byte the daemon has written to us. This is the number that says
	// bulk content did NOT come over the socket (design §6), so it belongs to
	// the client rather than to whoever remembers to count.
	uint64_t bytes_in() const { return _bytes_in; }
	const uint8_t *daemon_pubkey() const { return _daemon_pubkey; }
	const std::string &last_error() const { return _err; }

	// Serve/receive on a topic. Both flags default true, which is what a peer
	// that wants to meet anyone on the topic wants.
	bool join(const uint8_t topic[32], bool server = true, bool client = true,
			  int timeout_ms = 5000);
	bool leave(const uint8_t topic[32], int timeout_ms = 5000);

	// Returns a channel handle, or 0. `slots` is how many message types the
	// channel carries.
	uint64_t open_channel(const std::string &protocol,
						  const std::vector<uint8_t> &id = {},
						  uint32_t slots = 1, int timeout_ms = 5000);
	bool close_channel(uint64_t channel, int timeout_ms = 5000);

	// ⛔ `accepted` FALSE MEANS THE FRAME WAS NOT TAKEN — retry, do not assume.
	// The daemon refuses when the handle is unknown or the channel is open on no
	// peer, which is every moment before the remote opens its side. `writable`
	// says whether more will be accepted right now; on false, wait for on_drain.
	// `target` 0 means every peer holding this channel.
	bool send(uint64_t channel, uint32_t slot, uint64_t target,
			  const uint8_t *bytes, size_t len, bool *writable = nullptr,
			  int timeout_ms = 5000);

	bool status(Status &out, int timeout_ms = 5000);

	// ---- content ----
	// The overlay's hash of a file the DAEMON can read — blake2b over the
	// hypercore leaf construction. A hash from anywhere else is one the overlay
	// will reject, which is why this asks the daemon rather than computing it.
	bool content_hash(const std::string &path, uint8_t out_hash[32], int timeout_ms = 30000);
	bool content_serve(const uint8_t hash[32], const std::string &path, int timeout_ms = 5000);
	// Returns once the fetch has STARTED. Watch on_content_done / _failed.
	bool content_fetch(const uint8_t hash[32], const std::string &dest_path, int timeout_ms = 5000);
	bool content_cancel(const uint8_t hash[32], bool discard, int timeout_ms = 5000);

	// Read whatever is there and dispatch it. Returns false once disconnected.
	// `timeout_ms` 0 polls without waiting.
	bool poll(int timeout_ms);

private:
	// Send one encoded request. Takes ownership of `buf` exactly as the
	// generated encoders hand it over: heap-allocated, caller frees.
	bool _send_frame(uint8_t *buf, size_t len);
	// Pump until the response to `id` lands, dispatching events on the way.
	// `body`/`body_len` point into `_reply` and are valid until the next pump.
	bool _await(uint64_t id, int timeout_ms, const uint8_t **body, size_t *body_len);
	bool _read_some(int timeout_ms);
	// Consume whole frames from `_in`. Returns true if the awaited id landed.
	bool _drain_frames(uint64_t await_id, const uint8_t **body, size_t *body_len);
	void _dispatch_event(const void *msg);
	void _fail(const std::string &why);

	int _fd = -1;
	uint64_t _next_id = 1;
	uint64_t _client_id = 0;
	uint8_t _daemon_pubkey[32] = { 0 };
	std::string _err;
	uint64_t _bytes_in = 0;
	std::vector<uint8_t> _in;      // partial inbound
	std::vector<uint8_t> _reply;   // the frame a blocking call is returning
};

} // namespace swarmd

#endif