Node wraps EPMD lookup + handshake + Channel into a single client
object. Constructor eagerly opens the dist connection; call() runs the
synchronous $gen_call protocol against a registered name on the peer:
caller -> {'$gen_call', {FromPid, Ref}, Request} (REG_SEND)
server -> {Ref, Reply} (SEND)
Synthesized FromPid and a Node-lifetime Ref counter route replies back
to us; mismatched Ref or unexpected control op raises CallProtocolError.
Reply timeout raises CallTimeout (also covers Erlang's silent-drop case
when the registered name doesn't exist).
Tests against an erl peer running a $gen_call-aware loop:
- {ping, X} -> {pong, X}
- {add, A, B} -> {ok, A + B}
- five sequential calls with monotonically increasing Refs
- server error response surfaces as Python tuple
- slow responder triggers CallTimeout
- unknown registered name surfaces as CallTimeout
- ref uniqueness across 100 synthesized refs
111 tests green across 10 consecutive runs, lint clean.
Channel wraps the post-handshake socket and carries 4-byte length-
prefixed distribution messages: pass-through byte ('p') + ETF control
tuple + optional payload term.
API surface:
send_raw / recv_raw -- raw 4-byte framed bytes, empty == net_tick
send_tick -- send keepalive frame
send_control / recv_message -- structured control + payload
send_reg_send -- helper for the REG_SEND case (FromPid,
registered name, payload)
recv_message() transparently skips inbound ticks; callers wanting tick
awareness use recv_raw().
etf.decode_term(data, offset) exposed as a streaming decoder so the
channel can read control + payload back-to-back from one frame body.
Tests:
- pure encode/decode round-trips
- socketpair tests for framing, ticks, helper signatures
- live end-to-end against an erl node with a registered echo process:
EPMD -> handshake -> REG_SEND -> recv reply, payload matches
- boot script writes a /tmp ready-flag after registering `echo`;
fixture waits for both EPMD registration AND the flag to dodge
the race where EPMD registers the node before -eval runs
101 tests green, lint clean.
handshake(sock, our_name=..., cookie=...) drives the OTP 23+ dance over
a 2-byte length-prefixed frame stream:
send_name (N) client -> server
recv_status (s) server -> client
recv_challenge (N) server -> client
challenge_reply (r) client -> server
challenge_ack (a) server -> client
Cookie digest formula md5(cookie ++ integer_to_list(challenge)) was
cross-checked against erlang:md5/1 output as a test reference.
Distribution flags in erldistpy/flags.py advertise the minimum useful
set: extended refs/pids, new fun tags, utf8 atoms, maps, big creation,
v6 handshake, unlink id, v4 node containers.
86 tests green: frame builders + parsers as pure functions, digest
reference, full live handshake against `erl -sname -setcookie`, and a
wrong-cookie rejection test.
Newer SHA-256 digest (DFLAG_MANDATORY_25_DIGEST) deferred until a peer
requires it.