Repository: sile/erl_dist Branch: main Commit: 77ceda01d05c Files: 20 Total size: 150.1 KB Directory structure: gitextract_hgwcid3u/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples/ │ ├── epmd_cli.rs │ ├── handshake.rs │ ├── recv_msg.rs │ └── send_msg.rs └── src/ ├── channel.rs ├── eetf_ext.rs ├── epmd.rs ├── flags.rs ├── handshake.rs ├── io.rs ├── lib.rs ├── message.rs ├── node.rs └── term.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push] jobs: check: name: Check runs-on: ubuntu-latest strategy: matrix: toolchain: [stable, beta, nightly] steps: - name: Checkout sources uses: actions/checkout@v6 - run: rustup update ${{ matrix.toolchain }} - run: cargo check --all test: name: Test Suite runs-on: ubuntu-latest strategy: matrix: otp: ['26.0', '27.0', '28.0'] toolchain: [stable] steps: - name: Checkout sources uses: actions/checkout@v6 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} - run: rustup update ${{ matrix.toolchain }} - run: cargo test --all lints: name: Lints runs-on: ubuntu-latest strategy: matrix: toolchain: [stable, beta, nightly] steps: - name: Checkout sources uses: actions/checkout@v6 - run: rustup update ${{ matrix.toolchain }} - run: cargo fmt --all -- --check - run: cargo clippy --all -- -D warnings ================================================ FILE: .github/workflows/release.yml ================================================ name: Create GitHub Release and Publish to crates.io on: push: tags: ['v*'] jobs: github-release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 - id: create-release run: gh release create ${{ github.ref_name }} --title "${{ github.ref_name }}" --generate-notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish: runs-on: ubuntu-latest # environment: release # Optional: for enhanced security permissions: id-token: write steps: - uses: actions/checkout@v6 - uses: rust-lang/crates-io-auth-action@v1 id: auth - run: cargo publish env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} ================================================ FILE: .gitignore ================================================ target Cargo.lock ================================================ FILE: Cargo.toml ================================================ [package] name = "erl_dist" version = "0.8.0" authors = ["Takeru Ohta "] description = "Rust Implementation of Erlang Distribution Protocol" homepage = "https://github.com/sile/erl_dist" repository = "https://github.com/sile/erl_dist" readme = "README.md" keywords = ["erlang"] license = "MIT" edition = "2024" [dependencies] eetf = "0.11" futures = "0.3" md5 = "0.8" rand = "0.10" [dev-dependencies] noargs = "0.4.1" nojson = "0.3" smol = "2" ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2016 Takeru Ohta Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ erl_dist ======== [![erl_dist](https://img.shields.io/crates/v/erl_dist.svg)](https://crates.io/crates/erl_dist) [![Documentation](https://docs.rs/erl_dist/badge.svg)](https://docs.rs/erl_dist) [![Actions Status](https://github.com/sile/erl_dist/workflows/CI/badge.svg)](https://github.com/sile/erl_dist/actions) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) Rust Implementation of [Erlang Distribution Protocol](http://erlang.org/doc/apps/erts/erl_dist_protocol.html). The distribution protocol is used to communicate with distributed erlang nodes. Examples --------- Gets a node entry from EPMD: ```rust use erl_dist::epmd::{DEFAULT_EPMD_PORT, EpmdClient}; // Connect to the local EPMD. let connection = TcpStream::connect(("localhost", DEFAULT_EPMD_PORT)).await?; let client = EpmdClient::new(connection); // Get the information of a node. let node_name = "foo"; if let Some(node) = client.get_node(node_name).await? { println!("Found: {:?}", node); } else { println!("Not found"); } ``` Sends a message to an Erlang node: ```rust use erl_dist::LOWEST_DISTRIBUTION_PROTOCOL_VERSION; use erl_dist::node::{Creation, LocalNode}; use erl_dist::handshake::ClientSideHandshake; use erl_dist::term::{Atom, Pid}; use erl_dist::message::{channel, Message}; // Connect to a peer node. let peer_host = "localhost"; let peer_port = 7483; // NOTE: Usually, port number is retrieved from EPMD. let connection = TcpStream::connect((peer_host, peer_port)).await?; // Local node information. let creation = Creation::random(); let local_node = LocalNode::new("foo@localhost".parse()?, creation); // Do handshake. let mut handshake = ClientSideHandshake::new(connection, local_node.clone(), "cookie"); let _status = handshake.execute_send_name(LOWEST_DISTRIBUTION_PROTOCOL_VERSION).await?; let (connection, peer_node) = handshake.execute_rest(true).await?; // Create a channel. let capability_flags = local_node.flags & peer_node.flags; let (mut tx, _) = channel(connection, capability_flags); // Send a message. let from_pid = Pid::new(local_node.name.to_string(), 0, 0, local_node.creation.get()); let to_name = Atom::from("bar"); let msg = Message::reg_send(from_pid, to_name, Atom::from("hello").into()); tx.send(msg).await?; ``` Example commands: - EPMD Client Example: [epmd_cli.rs](examples/epmd_cli.rs) - Client Node Example: [send_msg.rs](examples/send_msg.rs) - Server Node Example: [recv_msg.rs](examples/recv_msg.rs) Related Crates -------------- - [erl_rpc](https://github.com/sile/erl_rpc): Erlang RPC client for Rust - [erldash](https://github.com/sile/erldash): Terminal-based Erlang dashboard ================================================ FILE: examples/epmd_cli.rs ================================================ //! EPMD client example. //! //! # Usage Examples //! //! ```bash //! $ cargo run --example epmd_cli -- --help //! $ cargo run --example epmd_cli names //! $ cargo run --example epmd_cli node_entry foo //! ``` use erl_dist::epmd::{EpmdClient, NodeEntry}; fn main() -> noargs::Result<()> { let mut args = noargs::raw_args(); args.metadata_mut().app_name = "epmd_cli"; args.metadata_mut().app_description = "EPMD client utility"; noargs::HELP_FLAG.take_help(&mut args); let epmd_host: String = noargs::opt("host") .short('h') .default("127.0.0.1") .doc("EPMD host") .take(&mut args) .then(|a| a.value().parse())?; let epmd_port: u16 = noargs::opt("port") .short('p') .default("4369") .doc("EPMD port") .take(&mut args) .then(|a| a.value().parse())?; // Handle subcommands if noargs::cmd("names") .doc("List registered nodes") .take(&mut args) .is_present() { if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let stream = smol::net::TcpStream::connect(format!("{}:{}", epmd_host, epmd_port)).await?; let client = EpmdClient::new(stream); let names = client.get_names().await?; let result = nojson::json(|f| { f.set_indent_size(2); f.set_spacing(true); f.array(|f| { for (name, port) in &names { f.element(nojson::json(|f| { f.object(|f| { f.member("name", name.as_str())?; f.member("port", *port) }) }))?; } Ok(()) }) }); println!("{result}"); Ok::<(), Box>(()) })?; } else if noargs::cmd("dump") .doc("Dump all registered nodes") .take(&mut args) .is_present() { if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let stream = smol::net::TcpStream::connect(format!("{}:{}", epmd_host, epmd_port)).await?; let client = EpmdClient::new(stream); let result = client.dump().await?; println!("{}", result); Ok::<(), Box>(()) })?; } else if noargs::cmd("node_entry") .doc("Get node entry information") .take(&mut args) .is_present() { let node: String = noargs::arg("") .doc("Node name to query") .take(&mut args) .then(|a| a.value().parse())?; if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let stream = smol::net::TcpStream::connect(format!("{}:{}", epmd_host, epmd_port)).await?; let client = EpmdClient::new(stream); let node_info = client.get_node(&node).await?.ok_or("node not found")?; let result = nojson::json(|f| { f.set_indent_size(2); f.set_spacing(true); f.object(|f| { f.member("name", node_info.name.as_str())?; f.member("port", node_info.port)?; f.member( "node_type", format!( "{:?} ({})", node_info.node_type, u8::from(node_info.node_type) ) .as_str(), )?; f.member( "protocol", format!( "{:?} ({})", node_info.protocol, u8::from(node_info.protocol) ) .as_str(), )?; f.member("highest_version", node_info.highest_version)?; f.member("lowest_version", node_info.lowest_version)?; f.member("extra", node_info.extra.as_slice()) }) }); println!("{result}"); Ok::<(), Box>(()) })?; } else if noargs::cmd("kill") .doc("Kill EPMD daemon") .take(&mut args) .is_present() { if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let stream = smol::net::TcpStream::connect(format!("{}:{}", epmd_host, epmd_port)).await?; let client = EpmdClient::new(stream); let result = client.kill().await?; let result = nojson::json(|f| { f.set_indent_size(2); f.set_spacing(true); f.object(|f| f.member("result", result.as_str())) }); println!("{result}"); Ok::<(), Box>(()) })?; } else if noargs::cmd("register") .doc("Register a node with EPMD") .take(&mut args) .is_present() { let name: String = noargs::arg("") .doc("Node name to register") .take(&mut args) .then(|a| a.value().parse())?; let port: u16 = noargs::opt("port") .default("3000") .doc("Port number") .take(&mut args) .then(|a| a.value().parse())?; let hidden: bool = noargs::flag("hidden") .doc("Register as hidden node") .take(&mut args) .is_present(); if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let stream = smol::net::TcpStream::connect(format!("{}:{}", epmd_host, epmd_port)).await?; let client = EpmdClient::new(stream); let node = if hidden { NodeEntry::new_hidden(&name, port) } else { NodeEntry::new(&name, port) }; let (_, creation) = client.register(node).await?; let result = nojson::json(|f| { f.set_indent_size(2); f.set_spacing(true); f.object(|f| f.member("creation", creation.get())) }); println!("{result}"); Ok::<(), Box>(()) })?; } else if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } Ok(()) } ================================================ FILE: examples/handshake.rs ================================================ //! Client side handshake example. //! //! # Usage Examples //! //! ```bash //! $ cargo run --example handshake -- --help //! $ cargo run --example handshake -- --peer foo --self bar@localhost --cookie erlang_cookie //! ``` fn main() -> noargs::Result<()> { let mut args = noargs::raw_args(); args.metadata_mut().app_name = "handshake"; args.metadata_mut().app_description = "Client side handshake example"; noargs::HELP_FLAG.take_help(&mut args); let local_node: erl_dist::node::NodeName = noargs::opt("self") .default("bar@localhost") .doc("Local node name") .take(&mut args) .then(|a| a.value().parse())?; let peer_node: erl_dist::node::NodeName = noargs::opt("peer") .default("foo@localhost") .doc("Peer node name") .take(&mut args) .then(|a| a.value().parse())?; let cookie: String = noargs::opt("cookie") .default("WPKYDIOSJIMJUURLRUHV") .doc("Erlang cookie") .take(&mut args) .then(|a| a.value().parse())?; if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let peer_node_info = { let addr = (peer_node.host(), erl_dist::epmd::DEFAULT_EPMD_PORT); let stream = smol::net::TcpStream::connect(addr).await?; let epmd_client = erl_dist::epmd::EpmdClient::new(stream); epmd_client .get_node(&peer_node.name()) .await? .ok_or("peer node not found")? }; println!("Got peer node info: {:?}", peer_node_info); let dummy_listening_port = 3333; let local_node_entry = erl_dist::epmd::NodeEntry::new_hidden(local_node.name(), dummy_listening_port); let (keepalive_connection, creation) = { let addr = (local_node.host(), erl_dist::epmd::DEFAULT_EPMD_PORT); let stream = smol::net::TcpStream::connect(addr).await?; let epmd_client = erl_dist::epmd::EpmdClient::new(stream); epmd_client.register(local_node_entry).await? }; println!("Registered self node: creation={:?}", creation); let stream = smol::net::TcpStream::connect((peer_node.host(), peer_node_info.port)).await?; let mut handshake = erl_dist::handshake::ClientSideHandshake::new( stream, erl_dist::node::LocalNode::new(local_node.clone(), creation), &cookie, ); let _status = handshake .execute_send_name(erl_dist::LOWEST_DISTRIBUTION_PROTOCOL_VERSION) .await?; let (_, peer_node) = handshake.execute_rest(true).await?; println!("Handshake finished: peer={:?}", peer_node); std::mem::drop(keepalive_connection); Ok::<(), Box>(()) })?; Ok(()) } ================================================ FILE: examples/recv_msg.rs ================================================ //! Server Node Example. //! //! The node registers specified name to the EPMD and waits messages from other (connected) nodes. //! If this node receives a message, it will print the message and discard it. //! //! # Usage Examples //! //! ```bash //! $ cargo run --example recv_msg -- --help //! $ cargo run --example recv_msg -- --local bar@localhost --cookie erlang_cookie //! //! # On another shell //! $ erl -sname foo //! > {bar, bar@localhost} ! hello. //! ``` use futures::stream::StreamExt; fn main() -> noargs::Result<()> { let mut args = noargs::raw_args(); args.metadata_mut().app_name = "recv_msg"; args.metadata_mut().app_description = "Receive messages from Erlang nodes"; noargs::HELP_FLAG.take_help(&mut args); let local_node: erl_dist::node::NodeName = noargs::opt("local") .default("bar@localhost") .doc("Local node name") .take(&mut args) .then(|a| a.value().parse())?; let cookie: String = noargs::opt("cookie") .default("WPKYDIOSJIMJUURLRUHV") .doc("Erlang cookie") .take(&mut args) .then(|a| a.value().parse())?; let published: bool = noargs::flag("published") .doc("Add PUBLISHED distribution flag to the node (otherwise, the node runs as a hidden node)") .take(&mut args) .is_present(); if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let listener = smol::net::TcpListener::bind("0.0.0.0:0").await?; let listening_port = listener.local_addr()?.port(); println!("Listening port: {}", listening_port); let local_node_entry = if published { erl_dist::epmd::NodeEntry::new(local_node.name(), listening_port) } else { erl_dist::epmd::NodeEntry::new_hidden(local_node.name(), listening_port) }; let epmd_addr = (local_node.host(), erl_dist::epmd::DEFAULT_EPMD_PORT); let stream = smol::net::TcpStream::connect(epmd_addr).await?; let epmd_client = erl_dist::epmd::EpmdClient::new(stream); let (keepalive_connection, creation) = epmd_client.register(local_node_entry).await?; println!("Registered self node: creation={:?}", creation); let mut incoming = listener.incoming(); while let Some(stream) = incoming.next().await.transpose()? { let mut local_node = erl_dist::node::LocalNode::new(local_node.clone(), creation); if published { local_node.flags |= erl_dist::DistributionFlags::PUBLISHED; } let cookie = cookie.clone(); smol::spawn(async move { match handle_client(local_node, cookie, stream).await { Ok(()) => { println!("Client disconnected"); } Err(e) => { println!("Error: {}", e); } }; }) .detach(); } std::mem::drop(keepalive_connection); Ok::<(), Box>(()) })?; Ok(()) } async fn handle_client( local_node: erl_dist::node::LocalNode, cookie: String, stream: smol::net::TcpStream, ) -> Result<(), Box> { let mut handshake = erl_dist::handshake::ServerSideHandshake::new(stream, local_node.clone(), &cookie); let status = if handshake.execute_recv_name().await?.is_some() { erl_dist::handshake::HandshakeStatus::Ok } else { // Dynamic name. erl_dist::handshake::HandshakeStatus::Named { name: "generated_name".to_owned(), creation: erl_dist::node::Creation::random(), } }; let (stream, peer_node) = handshake.execute_rest(status).await?; println!("Connected: {:?}", peer_node); let (mut tx, rx) = erl_dist::message::channel(stream, local_node.flags & peer_node.flags); let mut timer = smol::Timer::after(std::time::Duration::from_secs(30)); let mut msg_future = Box::pin(rx.recv_owned()); loop { let result = futures::future::select( msg_future, smol::Timer::after(std::time::Duration::from_secs(10)), ) .await; match result { futures::future::Either::Left((result, _)) => { let (msg, rx) = result?; println!("Recv: {:?}", msg); msg_future = Box::pin(rx.recv_owned()); } futures::future::Either::Right((_, f)) => { msg_future = f; } } if smol::future::poll_once(&mut timer).await.is_some() { tx.send(erl_dist::message::Message::Tick).await?; timer.set_after(std::time::Duration::from_secs(30)); } } } ================================================ FILE: examples/send_msg.rs ================================================ //! Client Node Example. //! //! The node sends a message (atom) to the specified erlang node. //! //! # Usage Examples //! //! ```bash //! $ cargo run --example send_msg -- --help //! $ cargo run --example send_msg -- --peer foo@localhost --destination foo --cookie erlang_cookie -m hello //! ``` fn main() -> noargs::Result<()> { let mut args = noargs::raw_args(); args.metadata_mut().app_name = "send_msg"; args.metadata_mut().app_description = "Send a message to an Erlang node"; noargs::HELP_FLAG.take_help(&mut args); let local_node: erl_dist::node::NodeName = noargs::opt("local") .default("bar@localhost") .doc("Local node name") .take(&mut args) .then(|a| a.value().parse())?; let peer_node: erl_dist::node::NodeName = noargs::opt("peer") .default("foo@localhost") .doc("Peer node name") .take(&mut args) .then(|a| a.value().parse())?; let cookie: String = noargs::opt("cookie") .default("WPKYDIOSJIMJUURLRUHV") .doc("Erlang cookie") .take(&mut args) .then(|a| a.value().parse())?; let destination: String = noargs::opt("destination") .short('d') .default("foo") .doc("Destination process name") .take(&mut args) .then(|a| a.value().parse())?; let message: String = noargs::opt("message") .short('m') .default("hello_world") .doc("Message to send") .take(&mut args) .then(|a| a.value().parse())?; if let Some(help) = args.finish()? { print!("{help}"); return Ok(()); } smol::block_on(async { let peer_node_info = { let addr = (peer_node.host(), erl_dist::epmd::DEFAULT_EPMD_PORT); let stream = smol::net::TcpStream::connect(addr).await?; let epmd_client = erl_dist::epmd::EpmdClient::new(stream); epmd_client .get_node(&peer_node.name()) .await? .ok_or("peer node not found")? }; println!("Got peer node info: {:?}", peer_node_info); let creation = erl_dist::node::Creation::random(); let stream = smol::net::TcpStream::connect((peer_node.host(), peer_node_info.port)).await?; let local_node = erl_dist::node::LocalNode::new(local_node, creation); let mut handshake = erl_dist::handshake::ClientSideHandshake::new(stream, local_node.clone(), &cookie); let _status = handshake .execute_send_name(erl_dist::LOWEST_DISTRIBUTION_PROTOCOL_VERSION) .await?; let (connection, peer_node) = handshake.execute_rest(true).await?; println!("Handshake finished: peer={:?}", peer_node); let (mut tx, _) = erl_dist::message::channel(connection, local_node.flags & peer_node.flags); let pid = eetf::Pid::new(local_node.name.to_string(), 0, 0, creation.get()); let msg = erl_dist::message::Message::reg_send( pid, eetf::Atom::from(destination), eetf::Atom::from(message).into(), ); tx.send(msg).await?; Ok::<(), Box>(()) })?; Ok(()) } ================================================ FILE: src/channel.rs ================================================ use crate::DistributionFlags; #[cfg(doc)] use crate::handshake; use crate::io::Connection; use crate::message::Message; use futures::io::{AsyncRead, AsyncWrite}; /// Makes a channel to send/received messages to/from a connected node. /// /// Please ensure that the [`handshake`] has been completed using the `connection` before creating a channel. /// /// `flags` should be an intersection of distribution flags of both nodes. /// Note that the current implementation doesn't consider the distribution flags. /// /// Note that, to keep the connection established, you need to send `Message::Tick` periodically. /// Please see [the official `net_ticktime` doc](https://www.erlang.org/doc/man/kernel_app.html#net_ticktime) for more details. pub fn channel(connection: T, flags: DistributionFlags) -> (Sender, Receiver) where T: AsyncRead + AsyncWrite + Unpin + Clone, { let _ = flags; (Sender::new(connection.clone()), Receiver::new(connection)) } const TYPE_TAG: u8 = 112; /// Sender of a message channel. #[derive(Debug)] pub struct Sender { connection: Connection, } impl Sender where T: AsyncRead + AsyncWrite + Unpin, { fn new(connection: T) -> Self { Self { connection: Connection::new(connection), } } /// Sends a message. pub async fn send(&mut self, message: Message) -> Result<(), SendError> { if matches!(message, Message::Tick) { self.connection.write_u32(0).await?; } else { let mut buf = Vec::new(); message.write_into(&mut buf)?; self.connection.write_u32(1 + buf.len() as u32).await?; self.connection.write_u8(TYPE_TAG).await?; self.connection.write_all(&buf).await?; self.connection.flush().await?; } Ok(()) } } /// Receiver of a message channel. #[derive(Debug)] pub struct Receiver { connection: Connection, } impl Receiver where T: AsyncRead + AsyncWrite + Unpin, { fn new(connection: T) -> Self { Self { connection: Connection::new(connection), } } /// Receives a message. pub async fn recv(&mut self) -> Result { let size = match self.connection.read_u32().await { Ok(size) => size as usize, Err(e) => { if e.kind() == std::io::ErrorKind::UnexpectedEof { return Err(RecvError::Closed); } else { return Err(e.into()); } } }; if size == 0 { return Ok(Message::Tick); } let tag = self.connection.read_u8().await?; if tag != TYPE_TAG { return Err(RecvError::UnexpectedTypeTag { tag }); } let mut buf = vec![0; size - 1]; self.connection.read_exact(&mut buf).await?; Message::read_from(&mut buf.as_slice()) } /// Receives a message (owned version). pub async fn recv_owned(mut self) -> Result<(Message, Self), RecvError> { let msg = self.recv().await?; Ok((msg, self)) } } /// Possible errors during sending messages. #[derive(Debug)] #[non_exhaustive] #[allow(missing_docs)] pub enum SendError { /// Encode error. Encode(eetf::EncodeError), /// I/O error. Io(std::io::Error), } impl std::fmt::Display for SendError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Encode(error) => write!(f, "{error}"), Self::Io(error) => write!(f, "{error}"), } } } impl std::error::Error for SendError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Encode(error) => Some(error), Self::Io(error) => Some(error), } } } impl From for SendError { fn from(value: std::io::Error) -> Self { Self::Io(value) } } impl From for SendError { fn from(value: eetf::EncodeError) -> Self { Self::Encode(value) } } /// Possible errors during receiving messages. #[derive(Debug)] #[non_exhaustive] #[allow(missing_docs)] pub enum RecvError { /// Connection was closed by the peer. Closed, /// Unsupported distributed operation. UnsupportedOp { op: i32 }, /// Unexpected type tag. UnexpectedTypeTag { tag: u8 }, /// Decode error. Decode(eetf::DecodeError), /// I/O error. Io(std::io::Error), } impl std::fmt::Display for RecvError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Closed => write!(f, "connection was closed by the peer"), Self::UnsupportedOp { op } => write!(f, "unsupported distributed operation {op}"), Self::UnexpectedTypeTag { tag } => { write!(f, "expected type tag {TYPE_TAG} but got {tag}") } Self::Decode(error) => write!(f, "{error}"), Self::Io(error) => write!(f, "{error}"), } } } impl std::error::Error for RecvError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Decode(e) => Some(e), Self::Io(e) => Some(e), _ => None, } } } impl From for RecvError { fn from(value: std::io::Error) -> Self { Self::Io(value) } } impl From for RecvError { fn from(value: eetf::DecodeError) -> Self { Self::Decode(value) } } ================================================ FILE: src/eetf_ext.rs ================================================ use crate::term::{Mfa, PidOrAtom}; use eetf::{Atom, DecodeError, FixInteger, List, Pid, Reference, Term, Tuple}; pub fn nil() -> Term { eetf::List::nil().into() } pub fn check_tuple_len(tuple: &Tuple, n: usize) -> Result<(), DecodeError> { if tuple.elements.len() != n { Err(DecodeError::UnexpectedType { value: tuple.clone().into(), expected: format!("{} elements tuple", n), }) } else { Ok(()) } } pub fn try_from_tagged_tuple3(mut tuple: Tuple) -> Result<(T0, T1), DecodeError> where T0: TryFromTerm, T1: TryFromTerm, { check_tuple_len(&tuple, 3)?; Ok(( T0::try_from_term(std::mem::replace(&mut tuple.elements[1], nil()))?, T1::try_from_term(std::mem::replace(&mut tuple.elements[2], nil()))?, )) } pub fn try_from_tagged_tuple4(mut tuple: Tuple) -> Result<(T0, T1, T2), DecodeError> where T0: TryFromTerm, T1: TryFromTerm, T2: TryFromTerm, { check_tuple_len(&tuple, 4)?; Ok(( T0::try_from_term(std::mem::replace(&mut tuple.elements[1], nil()))?, T1::try_from_term(std::mem::replace(&mut tuple.elements[2], nil()))?, T2::try_from_term(std::mem::replace(&mut tuple.elements[3], nil()))?, )) } pub fn try_from_tagged_tuple5( mut tuple: Tuple, ) -> Result<(T0, T1, T2, T3), DecodeError> where T0: TryFromTerm, T1: TryFromTerm, T2: TryFromTerm, T3: TryFromTerm, { check_tuple_len(&tuple, 5)?; Ok(( T0::try_from_term(std::mem::replace(&mut tuple.elements[1], nil()))?, T1::try_from_term(std::mem::replace(&mut tuple.elements[2], nil()))?, T2::try_from_term(std::mem::replace(&mut tuple.elements[3], nil()))?, T3::try_from_term(std::mem::replace(&mut tuple.elements[4], nil()))?, )) } pub fn try_from_tagged_tuple6( mut tuple: Tuple, ) -> Result<(T0, T1, T2, T3, T4), DecodeError> where T0: TryFromTerm, T1: TryFromTerm, T2: TryFromTerm, T3: TryFromTerm, T4: TryFromTerm, { check_tuple_len(&tuple, 6)?; Ok(( T0::try_from_term(std::mem::replace(&mut tuple.elements[1], nil()))?, T1::try_from_term(std::mem::replace(&mut tuple.elements[2], nil()))?, T2::try_from_term(std::mem::replace(&mut tuple.elements[3], nil()))?, T3::try_from_term(std::mem::replace(&mut tuple.elements[4], nil()))?, T4::try_from_term(std::mem::replace(&mut tuple.elements[5], nil()))?, )) } pub fn try_from_tagged_tuple7( mut tuple: Tuple, ) -> Result<(T0, T1, T2, T3, T4, T5), DecodeError> where T0: TryFromTerm, T1: TryFromTerm, T2: TryFromTerm, T3: TryFromTerm, T4: TryFromTerm, T5: TryFromTerm, { check_tuple_len(&tuple, 7)?; Ok(( T0::try_from_term(std::mem::replace(&mut tuple.elements[1], nil()))?, T1::try_from_term(std::mem::replace(&mut tuple.elements[2], nil()))?, T2::try_from_term(std::mem::replace(&mut tuple.elements[3], nil()))?, T3::try_from_term(std::mem::replace(&mut tuple.elements[4], nil()))?, T4::try_from_term(std::mem::replace(&mut tuple.elements[5], nil()))?, T5::try_from_term(std::mem::replace(&mut tuple.elements[6], nil()))?, )) } pub trait TryFromTerm: Sized { fn try_from_term(term: Term) -> Result; } impl TryFromTerm for Term { fn try_from_term(term: Term) -> Result { Ok(term) } } impl TryFromTerm for Pid { fn try_from_term(term: Term) -> Result { try_from_term(term, "pid") } } impl TryFromTerm for Atom { fn try_from_term(term: Term) -> Result { try_from_term(term, "atom") } } impl TryFromTerm for Reference { fn try_from_term(term: Term) -> Result { try_from_term(term, "reference") } } impl TryFromTerm for FixInteger { fn try_from_term(term: Term) -> Result { try_from_term(term, "integer") } } impl TryFromTerm for List { fn try_from_term(term: Term) -> Result { try_from_term(term, "list") } } impl TryFromTerm for PidOrAtom { fn try_from_term(term: Term) -> Result { term.try_into() .map(Self::Pid) .or_else(|term| term.try_into().map(Self::Atom)) .map_err(|value| DecodeError::UnexpectedType { value, expected: "pid or atom".to_owned(), }) } } impl TryFromTerm for Mfa { fn try_from_term(term: Term) -> Result { let mut tuple = try_from_term(term, "tuple")?; check_tuple_len(&tuple, 3)?; Ok(Self { module: TryFromTerm::try_from_term(std::mem::replace(&mut tuple.elements[0], nil()))?, function: TryFromTerm::try_from_term(std::mem::replace(&mut tuple.elements[1], nil()))?, arity: TryFromTerm::try_from_term(std::mem::replace(&mut tuple.elements[2], nil()))?, }) } } pub fn try_from_term(term: Term, expected: &str) -> Result where Term: TryInto, { term.try_into() .map_err(|value| DecodeError::UnexpectedType { value, expected: expected.to_owned(), }) } ================================================ FILE: src/epmd.rs ================================================ //! EPMD client and other EPMD related components. //! //! "EPMD" stands for "Erlang Port Mapper Daemon" and //! it provides name resolution functionalities for distributed erlang nodes. //! //! See [EPMD Protocol (Erlang Official Doc)](https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html#epmd-protocol) //! for more details. use crate::io::Connection; use crate::node::Creation; #[cfg(doc)] use crate::node::NodeName; use crate::{HIGHEST_DISTRIBUTION_PROTOCOL_VERSION, LOWEST_DISTRIBUTION_PROTOCOL_VERSION}; use futures::io::{AsyncRead, AsyncWrite}; use std::str::FromStr; /// Default EPMD listening port. pub const DEFAULT_EPMD_PORT: u16 = 4369; const TAG_DUMP_REQ: u8 = 100; const TAG_KILL_REQ: u8 = 107; const TAG_NAMES_REQ: u8 = 110; const TAG_ALIVE2_X_RESP: u8 = 118; const TAG_PORT2_RESP: u8 = 119; const TAG_ALIVE2_REQ: u8 = 120; const TAG_ALIVE2_RESP: u8 = 121; const TAG_PORT_PLEASE2_REQ: u8 = 122; /// Entry of a node registered in EPMD. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NodeEntry { /// Node name. /// /// Note that it differs from [`NodeName`] as this name doesn't contain the host part. pub name: String, /// Port number on which this node accepts connection requests. pub port: u16, /// Node type. pub node_type: NodeType, /// Transport protocol to communicate with this node. pub protocol: TransportProtocol, /// Highest distribution protocol version that this node can handle. pub highest_version: u16, /// Lowest distribution protocol version that this node can handle. pub lowest_version: u16, /// Extra field. pub extra: Vec, } impl NodeEntry { /// Makes a [`NodeEntry`] instance for a normal node. pub fn new(name: &str, port: u16) -> Self { Self { name: name.to_owned(), port, node_type: NodeType::Normal, protocol: TransportProtocol::TcpIpV4, highest_version: HIGHEST_DISTRIBUTION_PROTOCOL_VERSION, lowest_version: LOWEST_DISTRIBUTION_PROTOCOL_VERSION, extra: Vec::new(), } } /// Makes a [`NodeEntry`] instance for a hidden node. pub fn new_hidden(name: &str, port: u16) -> Self { Self { name: name.to_owned(), port, node_type: NodeType::Hidden, protocol: TransportProtocol::TcpIpV4, highest_version: HIGHEST_DISTRIBUTION_PROTOCOL_VERSION, lowest_version: LOWEST_DISTRIBUTION_PROTOCOL_VERSION, extra: Vec::new(), } } fn bytes_len(&self) -> usize { 2 + self.name.len() + // name 2 + // port 1 + // node_type 1 + // protocol 2 + // highest_version 2 + // lowest_version 2 + self.extra.len() // extra } } /// Possible errors. #[derive(Debug)] #[non_exhaustive] #[allow(missing_docs)] pub enum EpmdError { /// Unknown response tag. UnknownResponseTag { request: &'static str, tag: u8 }, /// Too long request. TooLongRequest { size: usize }, /// `PORT_PLEASE2_REQ` request failure. GetNodeEntryError { code: u8 }, /// `ALIVE2_REQ` request failure. RegisterNodeError { code: u8 }, /// Malformed `NAMES_RESP` line. MalformedNamesResponse { line: String }, /// I/O error. Io(std::io::Error), } impl std::fmt::Display for EpmdError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UnknownResponseTag { request, tag } => { write!( f, "received an unknown tag {tag} as the response of {request}" ) } Self::TooLongRequest { size } => { write!( f, "request byte size must be less than 0xFFFF, but got {size} bytes" ) } Self::GetNodeEntryError { code } => { write!( f, "EPMD responded an error code {code} against a PORT_PLEASE2_REQ request" ) } Self::RegisterNodeError { code } => { write!( f, "EPMD responded an error code {code} against an ALIVE2_REQ request" ) } Self::MalformedNamesResponse { line } => { write!( f, "found a malformed NAMES_RESP line: expected_format=\"name {{NAME}} at port {{PORT}}\", actual_line={line:?}" ) } Self::Io(error) => write!(f, "{error}"), } } } impl std::error::Error for EpmdError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { if let Self::Io(error) = self { Some(error) } else { None } } } impl From for EpmdError { fn from(value: std::io::Error) -> Self { Self::Io(value) } } /// EPMD client. #[derive(Debug)] pub struct EpmdClient { connection: Connection, } impl EpmdClient where T: AsyncRead + AsyncWrite + Unpin, { /// Makes a new [`EpmdClient`] instance. /// /// `connection` is a connection to communicate with the target EPMD server. pub fn new(connection: T) -> Self { Self { connection: Connection::new(connection), } } /// Registers a node in EPMD. /// /// The connection created to the EPMD must be kept as long as the node is a distributed node. /// When the connection is closed, the node is automatically unregistered from the EPMD. pub async fn register(mut self, node: NodeEntry) -> Result<(T, Creation), EpmdError> { // Request. let size = 1 + node.bytes_len(); let size = u16::try_from(size).map_err(|_| EpmdError::TooLongRequest { size })?; self.connection.write_u16(size).await?; self.connection.write_u8(TAG_ALIVE2_REQ).await?; self.connection.write_u16(node.port).await?; self.connection.write_u8(node.node_type.into()).await?; self.connection.write_u8(node.protocol.into()).await?; self.connection.write_u16(node.highest_version).await?; self.connection.write_u16(node.lowest_version).await?; self.connection.write_u16(node.name.len() as u16).await?; self.connection.write_all(node.name.as_bytes()).await?; self.connection.write_u16(node.extra.len() as u16).await?; self.connection.write_all(&node.extra).await?; self.connection.flush().await?; // Response. match self.connection.read_u8().await? { TAG_ALIVE2_RESP => { match self.connection.read_u8().await? { 0 => {} code => return Err(EpmdError::RegisterNodeError { code }), } let creation = Creation::new(u32::from(self.connection.read_u16().await?)); Ok((self.connection.into_inner(), creation)) } TAG_ALIVE2_X_RESP => { match self.connection.read_u8().await? { 0 => {} code => return Err(EpmdError::RegisterNodeError { code }), } let creation = Creation::new(self.connection.read_u32().await?); Ok((self.connection.into_inner(), creation)) } tag => Err(EpmdError::UnknownResponseTag { request: "ALIVE2_REQ", tag, }), } } /// Gets all registered nodes (name and port pairs) from EPMD. pub async fn get_names(mut self) -> Result, EpmdError> { // Request. self.connection.write_u16(1).await?; // Length self.connection.write_u8(TAG_NAMES_REQ).await?; self.connection.flush().await?; // Response. let _epmd_port = self.connection.read_u32().await?; let node_info_text = self.connection.read_string().await?; node_info_text .split('\n') .filter(|s| !s.is_empty()) .map(|line| NodeNameAndPort::from_str(line).map(|x| (x.name, x.port))) .collect() } /// Queries the node which has the given name to EPMD. /// /// If the node has not been registered in the connected EPMD, this method will return `None`. pub async fn get_node(mut self, node_name: &str) -> Result, EpmdError> { // Request. let size = 1 + node_name.len(); let size = u16::try_from(size).map_err(|_| EpmdError::TooLongRequest { size })?; self.connection.write_u16(size).await?; self.connection.write_u8(TAG_PORT_PLEASE2_REQ).await?; self.connection.write_all(node_name.as_bytes()).await?; self.connection.flush().await?; // Response. let tag = self.connection.read_u8().await?; if tag != TAG_PORT2_RESP { return Err(EpmdError::UnknownResponseTag { request: "NAMES_REQ", tag, }); } match self.connection.read_u8().await? { 0 => {} 1 => { return Ok(None); } code => { return Err(EpmdError::GetNodeEntryError { code }); } } Ok(Some(NodeEntry { port: self.connection.read_u16().await?, node_type: self.connection.read_u8().await?.into(), protocol: self.connection.read_u8().await?.into(), highest_version: self.connection.read_u16().await?, lowest_version: self.connection.read_u16().await?, name: self.connection.read_u16_string().await?, extra: self.connection.read_u16_bytes().await?, })) } /// Kills EPMD. /// /// This request kills the running EPMD. /// It is almost never used. /// /// If EPMD is killed, this method returns `"OK"`. pub async fn kill(mut self) -> Result { // Request. self.connection.write_u16(1).await?; self.connection.write_u8(TAG_KILL_REQ).await?; self.connection.flush().await?; // Response. let result = self.connection.read_string().await?; Ok(result) } /// Dumps all data from EPMD. /// /// This request is not really used, it is to be regarded as a debug feature. /// /// The result value is a string written for each node kept in the connected EPMD. /// /// The format of each entry is /// /// ```shell /// "active name ${NODE_NAME} at port ${PORT}, fd = ${FD}\n" /// ``` /// /// or /// /// ```shell /// "old/unused name ${NODE_NAME} at port ${PORT}, fd = ${FD}\n" /// ``` pub async fn dump(mut self) -> Result { // Request. self.connection.write_u16(1).await?; self.connection.write_u8(TAG_DUMP_REQ).await?; self.connection.flush().await?; // Response. let _epmd_port = self.connection.read_u32().await?; let info = self.connection.read_string().await?; Ok(info) } } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] struct NodeNameAndPort { name: String, port: u16, } impl FromStr for NodeNameAndPort { type Err = EpmdError; fn from_str(s: &str) -> Result { let error = || EpmdError::MalformedNamesResponse { line: s.to_owned() }; if !s.starts_with("name ") { return Err(error()); } let s = &s["name ".len()..]; let pos = s.find(" at port ").ok_or_else(error)?; let name = s[..pos].to_string(); let port = s[pos + " at port ".len()..].parse().map_err(|_| error())?; Ok(Self { name, port }) } } /// Protocol for communicating with a distributed node. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum TransportProtocol { /// TCP/IPv4. TcpIpV4, /// Other protocol. Other(u8), } impl From for TransportProtocol { fn from(v: u8) -> Self { match v { 0 => Self::TcpIpV4, _ => Self::Other(v), } } } impl From for u8 { fn from(v: TransportProtocol) -> Self { match v { TransportProtocol::TcpIpV4 => 0, TransportProtocol::Other(v) => v, } } } /// Type of a distributed node. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum NodeType { /// Hidden node (C-node). Hidden, /// Normal Erlang node. Normal, /// Other node. Other(u8), } impl From for NodeType { fn from(v: u8) -> Self { match v { 72 => Self::Hidden, 77 => Self::Normal, _ => Self::Other(v), } } } impl From for u8 { fn from(v: NodeType) -> Self { match v { NodeType::Hidden => 72, NodeType::Normal => 77, NodeType::Other(v) => v, } } } #[cfg(test)] mod tests { use super::*; #[test] fn epmd_client_works() { let node_name = "epmd_client_works"; smol::block_on(async { let erl_node = crate::tests::TestErlangNode::new(node_name) .await .expect("failed to run a test erlang node"); // Get the information of an existing Erlang node. let node = crate::tests::epmd_client() .await .get_node(node_name) .await .expect("failed to get node"); let node = node.expect("no such node"); assert_eq!(node.name, node_name); // Register a new node. let client = crate::tests::epmd_client().await; let new_node_name = "erl_dist_test_new_node"; let new_node = NodeEntry::new_hidden(new_node_name, 3000); let (stream, _creation) = client .register(new_node) .await .expect("failed to register a new node"); // Get the information of the newly added Erlang node. let node = crate::tests::epmd_client() .await .get_node(new_node_name) .await .expect("failed to get node"); let node = node.expect("no such node"); assert_eq!(node.name, new_node_name); // Deregister the node. std::mem::drop(stream); std::thread::sleep(std::time::Duration::from_millis(100)); let node = crate::tests::epmd_client() .await .get_node(new_node_name) .await .expect("failed to get node"); assert!(node.is_none()); std::mem::drop(erl_node); }); } } ================================================ FILE: src/flags.rs ================================================ /// Distribution flags. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DistributionFlags(u64); impl DistributionFlags { /// The node is to be published and part of the global namespace. pub const PUBLISHED: Self = Self(0x01); /// The node implements an atom cache (obsolete). pub const ATOM_CACHE: Self = Self(0x02); /// The node implements extended (3 × 32 bits) references (required). /// /// [NOTE] This flag is mandatory. If not present, the connection is refused. pub const EXTENDED_REFERENCES: Self = Self(0x04); /// The node implements distributed process monitoring. /// /// [NOTE] This flag will become mandatory in OTP 25. pub const DIST_MONITOR: Self = Self(0x08); /// The node uses separate tag for funs (lambdas) in the distribution protocol. pub const FUN_TAGS: Self = Self(0x10); /// The node implements distributed named process monitoring. pub const DIST_MONITOR_NAME: Self = Self(0x20); /// The (hidden) node implements atom cache (obsolete). pub const HIDDEN_ATOM_CACHE: Self = Self(0x40); /// The node understands new fun tags. /// /// [NOTE] This flag is mandatory. If not present, the connection is refused. pub const NEW_FUN_TAGS: Self = Self(0x80); /// The node can handle extended pids and ports (required). /// /// [NOTE] This flag is mandatory. If not present, the connection is refused. pub const EXTENDED_PIDS_PORTS: Self = Self(0x100); /// This node understands `EXPORT_EXT` tag. /// /// [NOTE] This flag will become mandatory in OTP 25. pub const EXPORT_PTR_TAG: Self = Self(0x200); /// The node understands bit binaries. /// /// [NOTE] This flag will become mandatory in OTP 25. pub const BIT_BINARIES: Self = Self(0x400); /// The node understandss new float format. /// /// [NOTE] This flag will become mandatory in OTP 25. pub const NEW_FLOATS: Self = Self(0x800); /// This node allows unicode characters in I/O operations. pub const UNICODE_IO: Self = Self(0x1000); /// The node implements atom cache in distribution header. /// /// Note that currently `erl_dist` can not handle distribution headers. pub const DIST_HDR_ATOM_CACHE: Self = Self(0x2000); /// The node understands the `SMALL_ATOM_EXT` tag. pub const SMALL_ATOM_TAGS: Self = Self(0x4000); /// The node understands UTF-8 encoded atoms. /// /// [NOTE] This flag is mandatory. If not present, the connection is refused. pub const UTF8_ATOMS: Self = Self(0x10000); /// The node understands maps. /// /// [NOTE] This flag will become mandatory in OTP 25. pub const MAP_TAGS: Self = Self(0x20000); /// The node understands big node creation tags `NEW_PID_EXT`, `NEW_PORT_EXT` and `NEWER_REFERENCE_EXT`. /// /// [NOTE] This flag is mandatory. If not present, the connection is refused. pub const BIG_CREATION: Self = Self(0x40000); /// Use the `SEND_SENDER` control message instead of the `SEND` control message and use the `SEND_SENDER_TT` control message instead of the `SEND_TT` control message. pub const SEND_SENDER: Self = Self(0x80000); /// The node understands any term as the seqtrace label. pub const BIG_SEQTRACE_LABELS: Self = Self(0x100000); /// Use the `PAYLOAD_EXIT`, `PAYLOAD_EXIT_TT`, `PAYLOAD_EXIT2`, `PAYLOAD_EXIT2_TT` and `PAYLOAD_MONITOR_P_EXIT` control messages instead of the non-PAYLOAD variants. pub const EXIT_PAYLOAD: Self = Self(0x400000); /// Use fragmented distribution messages to send large messages. pub const FRAGMENTS: Self = Self(0x800000); /// The node supports the new connection setup handshake (version 6) introduced in OTP 23. pub const HANDSHAKE_23: Self = Self(0x1000000); /// Use the new link protocol. /// /// Unless both nodes have set the `UNLINK_ID` flag, the old link protocol will be used as a fallback. /// /// [NOTE] This flag will become mandatory in OTP 25. pub const UNLINK_ID: Self = Self(0x2000000); /// Set if the `SPAWN_REQUEST`, `SPAWN_REQUEST_TT`, `SPAWN_REPLY`, `SPAWN_REPLY_TT` control messages are supported. pub const SPAWN: Self = Self(1 << 32); /// Dynamic node name. /// /// This is not a capability but rather used as a request from the connecting node /// to receive its node name from the accepting node as part of the handshake. pub const NAME_ME: Self = Self(1 << 33); /// The node accepts a larger amount of data in pids, ports and references (node container types version 4). /// /// In the pid case full 32-bit `ID` and `Serial` fields in `NEW_PID_EXT`, /// in the port case a 64-bit integer in `V4_PORT_EXT`, and in the reference case up to 5 32-bit ID words are /// now accepted in `NEWER_REFERENCE_EXT`. /// Introduced in OTP 24. /// /// [NOTE] This flag will become mandatory in OTP 26. pub const V4_NC: Self = Self(1 << 34); /// The node supports process alias and can by this handle the `ALIAS_SEND` and `ALIAS_SEND_TT` control messages. /// /// Introduced in OTP 24. pub const ALIAS: Self = Self(1 << 35); /// The node supports all capabilities that are mandatory in OTP 25. /// /// Introduced in OTP 25. /// [NOTE] This flag will become mandatory in OTP 27. pub const MANDATORY_25_DIGEST: Self = Self(1 << 36); /// Returns the raw bits value. pub const fn bits(self) -> u64 { self.0 } /// Creates flags from raw bits. pub const fn from_bits_truncate(bits: u64) -> Self { Self(bits) } /// Returns `true` if all flags in `other` are contained within `self`. pub const fn contains(self, other: Self) -> bool { (self.0 & other.0) == other.0 } /// Makes a new [`DistributionFlags`] with the default flags. /// /// This is equivalent to the following code: /// /// ``` /// # use erl_dist::DistributionFlags; /// DistributionFlags::mandatory(); /// ``` pub fn new() -> Self { Self::mandatory() } /// Gets the mandatory flags (in OTP 26). pub fn mandatory() -> Self { Self::EXTENDED_REFERENCES | Self::FUN_TAGS | Self::NEW_FUN_TAGS | Self::EXTENDED_PIDS_PORTS | Self::EXPORT_PTR_TAG | Self::BIT_BINARIES | Self::NEW_FLOATS | Self::UTF8_ATOMS | Self::MAP_TAGS | Self::BIG_CREATION | Self::HANDSHAKE_23 | Self::MANDATORY_25_DIGEST | Self::UNLINK_ID | Self::V4_NC } } impl Default for DistributionFlags { fn default() -> Self { Self::new() } } impl std::ops::BitOr for DistributionFlags { type Output = Self; fn bitor(self, rhs: Self) -> Self { Self(self.0 | rhs.0) } } impl std::ops::BitOrAssign for DistributionFlags { fn bitor_assign(&mut self, rhs: Self) { self.0 |= rhs.0; } } impl std::ops::BitAnd for DistributionFlags { type Output = Self; fn bitand(self, rhs: Self) -> Self { Self(self.0 & rhs.0) } } #[cfg(test)] mod tests { use super::*; #[test] fn flag_bit_values() { assert_eq!(DistributionFlags::PUBLISHED.bits(), 0x01); assert_eq!(DistributionFlags::EXTENDED_REFERENCES.bits(), 0x04); assert_eq!(DistributionFlags::UTF8_ATOMS.bits(), 0x10000); assert_eq!(DistributionFlags::HANDSHAKE_23.bits(), 0x1000000); assert_eq!(DistributionFlags::SPAWN.bits(), 1 << 32); assert_eq!(DistributionFlags::NAME_ME.bits(), 1 << 33); assert_eq!(DistributionFlags::V4_NC.bits(), 1 << 34); assert_eq!(DistributionFlags::ALIAS.bits(), 1 << 35); assert_eq!(DistributionFlags::MANDATORY_25_DIGEST.bits(), 1 << 36); } #[test] fn from_bits_truncate_roundtrip() { let flags = DistributionFlags::mandatory(); let bits = flags.bits(); assert_eq!(DistributionFlags::from_bits_truncate(bits), flags); } #[test] fn from_bits_truncate_preserves_unknown_bits() { let raw = 0xFFFF_FFFF_FFFF_FFFF; assert_eq!(DistributionFlags::from_bits_truncate(raw).bits(), raw); } #[test] fn contains_single_flag() { let flags = DistributionFlags::PUBLISHED | DistributionFlags::UTF8_ATOMS; assert!(flags.contains(DistributionFlags::PUBLISHED)); assert!(flags.contains(DistributionFlags::UTF8_ATOMS)); assert!(!flags.contains(DistributionFlags::SPAWN)); } #[test] fn contains_multiple_flags() { let flags = DistributionFlags::mandatory(); let subset = DistributionFlags::EXTENDED_REFERENCES | DistributionFlags::UTF8_ATOMS; assert!(flags.contains(subset)); } #[test] fn contains_empty_is_always_true() { let empty = DistributionFlags::from_bits_truncate(0); assert!(DistributionFlags::PUBLISHED.contains(empty)); assert!(empty.contains(empty)); } #[test] fn bitor_combines_flags() { let a = DistributionFlags::PUBLISHED; let b = DistributionFlags::UTF8_ATOMS; let combined = a | b; assert_eq!(combined.bits(), 0x01 | 0x10000); assert!(combined.contains(a)); assert!(combined.contains(b)); } #[test] fn bitor_assign_combines_flags() { let mut flags = DistributionFlags::PUBLISHED; flags |= DistributionFlags::NAME_ME; assert!(flags.contains(DistributionFlags::PUBLISHED)); assert!(flags.contains(DistributionFlags::NAME_ME)); } #[test] fn bitand_intersects_flags() { let a = DistributionFlags::PUBLISHED | DistributionFlags::UTF8_ATOMS; let b = DistributionFlags::UTF8_ATOMS | DistributionFlags::SPAWN; let intersection = a & b; assert!(intersection.contains(DistributionFlags::UTF8_ATOMS)); assert!(!intersection.contains(DistributionFlags::PUBLISHED)); assert!(!intersection.contains(DistributionFlags::SPAWN)); } #[test] fn default_equals_mandatory() { assert_eq!(DistributionFlags::default(), DistributionFlags::mandatory()); assert_eq!(DistributionFlags::new(), DistributionFlags::mandatory()); } #[test] fn mandatory_contains_expected_flags() { let m = DistributionFlags::mandatory(); assert!(m.contains(DistributionFlags::EXTENDED_REFERENCES)); assert!(m.contains(DistributionFlags::FUN_TAGS)); assert!(m.contains(DistributionFlags::NEW_FUN_TAGS)); assert!(m.contains(DistributionFlags::EXTENDED_PIDS_PORTS)); assert!(m.contains(DistributionFlags::EXPORT_PTR_TAG)); assert!(m.contains(DistributionFlags::BIT_BINARIES)); assert!(m.contains(DistributionFlags::NEW_FLOATS)); assert!(m.contains(DistributionFlags::UTF8_ATOMS)); assert!(m.contains(DistributionFlags::MAP_TAGS)); assert!(m.contains(DistributionFlags::BIG_CREATION)); assert!(m.contains(DistributionFlags::HANDSHAKE_23)); assert!(m.contains(DistributionFlags::MANDATORY_25_DIGEST)); assert!(m.contains(DistributionFlags::UNLINK_ID)); assert!(m.contains(DistributionFlags::V4_NC)); // Not in mandatory set. assert!(!m.contains(DistributionFlags::PUBLISHED)); assert!(!m.contains(DistributionFlags::NAME_ME)); assert!(!m.contains(DistributionFlags::SPAWN)); assert!(!m.contains(DistributionFlags::ALIAS)); } #[test] fn high_and_low_bits_coexist() { // Verify that flags spanning both u32 halves work correctly together. let flags = DistributionFlags::PUBLISHED | DistributionFlags::SPAWN; // bit 0 + bit 32 assert_eq!(flags.bits(), 0x01 | (1 << 32)); assert!(flags.contains(DistributionFlags::PUBLISHED)); assert!(flags.contains(DistributionFlags::SPAWN)); // Truncating to u32 should lose high bits. let low = flags.bits() as u32; assert_eq!(low, 0x01); } #[test] fn clone_and_copy() { let a = DistributionFlags::mandatory(); let b = a; let c = a.clone(); assert_eq!(a, b); assert_eq!(a, c); } #[test] fn debug_format() { // Just ensure it doesn't panic. let _ = format!("{:?}", DistributionFlags::mandatory()); } #[test] fn hash_consistent() { use std::collections::HashSet; let mut set = HashSet::new(); set.insert(DistributionFlags::PUBLISHED); set.insert(DistributionFlags::PUBLISHED); assert_eq!(set.len(), 1); set.insert(DistributionFlags::UTF8_ATOMS); assert_eq!(set.len(), 2); } } ================================================ FILE: src/handshake.rs ================================================ //! Distribution Handshake. //! //! This handshake is used by an Erlang node for connecting to another one. //! //! See //! [Distribution Handshake (Erlang Official Doc)](https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html#distribution-handshake) //! for more details. use crate::io::Connection; use crate::node::{Creation, LocalNode, NodeName, NodeNameError, PeerNode}; use crate::{DistributionFlags, LOWEST_DISTRIBUTION_PROTOCOL_VERSION}; use futures::io::{AsyncRead, AsyncWrite}; const PROTOCOL_VERSION: u16 = LOWEST_DISTRIBUTION_PROTOCOL_VERSION; const NODE_NAME_VERSION: u16 = 5; /// Client-side handshake. #[derive(Debug)] pub struct ClientSideHandshake { local_node: LocalNode, local_challenge: Challenge, cookie: String, connection: Connection, send_name_status: Option, may_need_complement: bool, } impl ClientSideHandshake where T: AsyncRead + AsyncWrite + Unpin, { /// Makes a new [`ClientSideHandshake`] instance. pub fn new(connection: T, local_node: LocalNode, cookie: &str) -> Self { Self { local_node, local_challenge: Challenge::new(), cookie: cookie.to_owned(), connection: Connection::new(connection), send_name_status: None, may_need_complement: false, } } /// Executes the first part of the handshake protocol. /// /// To complete the handshake, you then need to call [`ClientSideHandshake::execute_rest()`] method /// taking into account the [`HandshakeStatus`] replied from the peer node. pub async fn execute_send_name( &mut self, protocol_version: u16, ) -> Result { self.send_name(protocol_version).await?; let status = self.recv_status().await?; self.send_name_status = Some(status.clone()); Ok(status) } /// Executes the rest part of the handshake protocol. /// /// You must need to have called [`ClientSideHandshake::execute_send_name()`] before this method. /// In a case where [`HandshakeStatus`] of the first part is [`HandshakeStatus::Alive`], /// the `do_continue` argument is used to indicate whether this handshake should continue or not /// (otherwise the argument is ignored). /// /// If the [`HandshakeStatus`] returned is a non-ok status, this method call fails immediately. pub async fn execute_rest( mut self, do_continue: bool, ) -> Result<(T, PeerNode), HandshakeError> { match self.send_name_status { None => { return Err(HandshakeError::PhaseError { current: "ClientSideHandshake::execute_rest()", depends_on: "ClientSideHandshake::execute_send_name()", }); } Some(HandshakeStatus::Nok) => return Err(HandshakeError::OngoingHandshake), Some(HandshakeStatus::NotAllowed) => return Err(HandshakeError::NotAllowed), Some(HandshakeStatus::Alive) => { self.send_status(if do_continue { "true" } else { "false" }) .await?; if !do_continue { return Err(HandshakeError::AlreadyActive); } } _ => {} } let (peer_node, peer_challenge) = self.recv_challenge().await?; if self.may_need_complement && peer_node.creation.is_some() { self.send_complement().await?; } self.send_challenge_reply(peer_challenge).await?; self.recv_challenge_ack().await?; let connection = self.connection.into_inner(); Ok((connection, peer_node)) } async fn send_name(&mut self, protocol_version: u16) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); match protocol_version { PROTOCOL_VERSION => { writer.write_u8(b'N')?; writer.write_u64(self.local_node.flags.bits())?; writer.write_u32(self.local_node.creation.get())?; if self.local_node.flags.contains(DistributionFlags::NAME_ME) { writer.write_u16(self.local_node.name.host().len() as u16)?; writer.write_all(self.local_node.name.host().as_bytes())?; } else { writer.write_u16(self.local_node.name.len() as u16)?; writer.write_all(self.local_node.name.to_string().as_bytes())?; } } value => { return Err(HandshakeError::UnknownProtocolVersion { value }); } } writer.finish().await?; Ok(()) } async fn recv_status(&mut self) -> Result { let mut reader = self.connection.handshake_message_reader().await?; let tag = reader.read_u8().await?; if tag != b's' { return Err(HandshakeError::UnexpectedTag { message: "STATUS", tag, }); } let status = reader.read_bytes().await?; let status = match status.as_slice() { b"ok" => HandshakeStatus::Ok, b"ok_simultaneous" => HandshakeStatus::OkSimultaneous, b"nok" => HandshakeStatus::Nok, b"not_allowed" => HandshakeStatus::NotAllowed, b"alive" => HandshakeStatus::Alive, _ => { if status.starts_with(b"named:") { let bytes = &status["named:".len()..]; if bytes.len() < 2 { return Err(HandshakeError::Io(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "unexpected eof", ))); } let n = u16::from_be_bytes([bytes[0], bytes[1]]) as usize; let bytes = &bytes[2..]; if bytes.len() < n + 4 { return Err(HandshakeError::Io(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "unexpected eof", ))); } let name = std::str::from_utf8(&bytes[..n]) .map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, "invalid UTF-8 in node name", ) })? .to_owned(); let bytes = &bytes[n..]; let node_name: NodeName = name.parse()?; let name = node_name.name().to_owned(); let creation = Creation::new(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])); HandshakeStatus::Named { name, creation } } else { let status = String::from_utf8_lossy(&status).to_string(); return Err(HandshakeError::UnknownStatus { status }); } } }; reader.finish().await?; Ok(status) } async fn send_status(&mut self, status: &str) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); writer.write_u8(b's')?; writer.write_all(status.as_bytes())?; Ok(()) } async fn recv_challenge(&mut self) -> Result<(PeerNode, Challenge), HandshakeError> { let mut reader = self.connection.handshake_message_reader().await?; let (node, challenge) = match reader.read_u8().await? { b'n' => { let version = reader.read_u16().await?; if version != NODE_NAME_VERSION { return Err(HandshakeError::InvalidVersionValue { value: version }); } let flags = DistributionFlags::from_bits_truncate(u64::from(reader.read_u32().await?)); let challenge = Challenge(reader.read_u32().await?); let name = reader.read_string().await?.parse()?; let node = PeerNode { name, flags, creation: None, }; (node, challenge) } b'N' => { let flags = DistributionFlags::from_bits_truncate(reader.read_u64().await?); let challenge = Challenge(reader.read_u32().await?); let creation = Creation::new(reader.read_u32().await?); let name = reader.read_u16_string().await?.parse()?; let node = PeerNode { name, flags, creation: Some(creation), }; (node, challenge) } tag => { return Err(HandshakeError::UnexpectedTag { message: "CHALLENGE", tag, }); } }; reader.finish().await?; Ok((node, challenge)) } async fn send_complement(&mut self) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); writer.write_u8(b'c')?; writer.write_u32((self.local_node.flags.bits() >> 32) as u32)?; writer.write_u32(self.local_node.creation.get())?; writer.finish().await?; Ok(()) } async fn send_challenge_reply( &mut self, peer_challenge: Challenge, ) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); writer.write_u8(b'r')?; writer.write_u32(self.local_challenge.0)?; writer.write_all(&peer_challenge.digest(&self.cookie).0)?; writer.finish().await?; Ok(()) } async fn recv_challenge_ack(&mut self) -> Result<(), HandshakeError> { let mut reader = self.connection.handshake_message_reader().await?; let tag = reader.read_u8().await?; if tag != b'a' { return Err(HandshakeError::UnexpectedTag { message: "CHALLENGE_ACK", tag, }); } let mut digest = [0; 16]; reader.read_exact(&mut digest).await?; if digest != self.local_challenge.digest(&self.cookie).0 { return Err(HandshakeError::CookieMismatch); } reader.finish().await?; Ok(()) } } /// Server-side handshake. #[derive(Debug)] pub struct ServerSideHandshake { local_node: LocalNode, local_challenge: Challenge, cookie: String, connection: Connection, peer_node: Option, } impl ServerSideHandshake where T: AsyncRead + AsyncWrite + Unpin, { /// Makes a new [`ServerSideHandshake`] instance. pub fn new(connection: T, local_node: LocalNode, cookie: &str) -> Self { Self { local_node, local_challenge: Challenge::new(), cookie: cookie.to_owned(), connection: Connection::new(connection), peer_node: None, } } /// Executes the first part of the handshake protocol. /// /// To complete the handshake, you then need to call [`ServerSideHandshake::execute_rest()`] method /// taking into account the [`NodeName`] sent from the peer node. /// /// Note that the return value becomes `None` if the peer requested a dynamic node name. /// In the case, if you want to continue the handshake, you need to use [`HandshakeStatus::Named`] for the reply. pub async fn execute_recv_name(&mut self) -> Result, HandshakeError> { let mut reader = self.connection.handshake_message_reader().await?; let tag = reader.read_u8().await?; let node = match tag { b'n' => { let version = reader.read_u16().await?; if version != NODE_NAME_VERSION { return Err(HandshakeError::InvalidVersionValue { value: version }); } let flags = DistributionFlags::from_bits_truncate(u64::from(reader.read_u32().await?)); let name = reader.read_string().await?.parse()?; PeerNode { name, flags, creation: None, } } b'N' => { let flags = DistributionFlags::from_bits_truncate(reader.read_u64().await?); let creation = Creation::new(reader.read_u32().await?); let name = if flags.contains(DistributionFlags::NAME_ME) { let host = reader.read_u16_string().await?; NodeName::new("nonode", &host)? } else { reader.read_u16_string().await?.parse()? }; PeerNode { name, flags, creation: Some(creation), } } _ => { return Err(HandshakeError::UnexpectedTag { message: "NAME", tag, }); } }; reader.finish().await?; let name = node.name.clone(); let is_dynamic = node.flags.contains(DistributionFlags::NAME_ME); self.peer_node = Some(node); if is_dynamic { Ok(None) } else { Ok(Some(name)) } } /// Executes the rest part of the handshake protocol. /// /// You must need to have called [`ServerSideHandshake::execute_recv_name()`] before this method. /// /// Note that if the [`HandshakeStatus`] is a non-ok status, this method call fails just /// after sending the status to the peer node. pub async fn execute_rest( mut self, status: HandshakeStatus, ) -> Result<(T, PeerNode), HandshakeError> { let (peer_flags, peer_creation) = if let Some(peer) = &self.peer_node { (peer.flags, peer.creation) } else { return Err(HandshakeError::PhaseError { current: "ServerSideHandshake::execute_rest()", depends_on: "ServerSideHandshake::execute_recv_name()", }); }; self.send_status(status).await?; self.send_challenge(peer_flags).await?; if peer_flags.contains(DistributionFlags::HANDSHAKE_23) && peer_creation.is_none() { self.recv_complement().await?; } let peer_challenge = self.recv_challenge_reply().await?; self.send_challenge_ack(peer_challenge).await?; let peer_node = self.peer_node.take().expect("unreachable"); let connection = self.connection.into_inner(); Ok((connection, peer_node)) } async fn send_status(&mut self, status: HandshakeStatus) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); writer.write_u8(b's')?; match &status { HandshakeStatus::Ok => writer.write_all(b"ok")?, HandshakeStatus::OkSimultaneous => writer.write_all(b"ok_simultaneous")?, HandshakeStatus::Nok => writer.write_all(b"nok")?, HandshakeStatus::NotAllowed => writer.write_all(b"not_allowed")?, HandshakeStatus::Alive => writer.write_all(b"alive")?, HandshakeStatus::Named { name, creation } => { let peer_node = self.peer_node.as_mut().expect("unreachable"); let node_name = NodeName::new(name, peer_node.name.host())?; writer.write_all(b"named:")?; writer.write_u16(node_name.len() as u16)?; writer.write_all(node_name.to_string().as_bytes())?; writer.write_u32(creation.get())?; peer_node.name = node_name; peer_node.creation = Some(*creation); self.local_node.flags |= DistributionFlags::NAME_ME; } } writer.finish().await?; match status { HandshakeStatus::Nok => Err(HandshakeError::OngoingHandshake), HandshakeStatus::NotAllowed => Err(HandshakeError::NotAllowed), _ => Ok(()), } } async fn send_challenge( &mut self, peer_flags: DistributionFlags, ) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); if peer_flags.contains(DistributionFlags::HANDSHAKE_23) { writer.write_u8(b'N')?; writer.write_u64(self.local_node.flags.bits())?; writer.write_u32(self.local_challenge.0)?; writer.write_u32(self.local_node.creation.get())?; writer.write_u16(self.local_node.name.len() as u16)?; writer.write_all(self.local_node.name.to_string().as_bytes())?; } else { writer.write_u8(b'n')?; writer.write_u16(5)?; writer.write_u32(self.local_node.flags.bits() as u32)?; writer.write_u32(self.local_challenge.0)?; writer.write_all(self.local_node.name.to_string().as_bytes())?; } writer.finish().await?; Ok(()) } async fn recv_complement(&mut self) -> Result<(), HandshakeError> { let mut reader = self.connection.handshake_message_reader().await?; let tag = reader.read_u8().await?; if tag != b'c' { return Err(HandshakeError::UnexpectedTag { message: "send_complement", tag, }); } let flags_high = DistributionFlags::from_bits_truncate(u64::from(reader.read_u32().await?) << 32); let creation = Creation::new(reader.read_u32().await?); reader.finish().await?; let peer = self.peer_node.as_mut().expect("unreachable"); peer.flags |= flags_high; peer.creation = Some(creation); Ok(()) } async fn recv_challenge_reply(&mut self) -> Result { let mut reader = self.connection.handshake_message_reader().await?; let tag = reader.read_u8().await?; if tag != b'r' { return Err(HandshakeError::UnexpectedTag { message: "challenge_reply", tag, }); } let peer_challenge = Challenge(reader.read_u32().await?); let mut digest = Digest([0; 16]); reader.read_exact(&mut digest.0).await?; reader.finish().await?; if self.local_challenge.digest(&self.cookie) != digest { return Err(HandshakeError::CookieMismatch); } Ok(peer_challenge) } async fn send_challenge_ack( &mut self, peer_challenge: Challenge, ) -> Result<(), HandshakeError> { let mut writer = self.connection.handshake_message_writer(); writer.write_u8(b'a')?; writer.write_all(&peer_challenge.digest(&self.cookie).0)?; writer.finish().await?; Ok(()) } } /// Handshake status. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum HandshakeStatus { /// The handshake will continue. Ok, /// The handshake will continue. /// /// The client-side node is informed that the server-side node has another ongoing connection attempt /// that will be shut down (simultaneous connect where the client-side node's name is /// greater than the server-side node's name, compared literally). OkSimultaneous, /// The handshake will not continue. /// /// The client-side already has an ongoing handshake, which it itself has initiated /// (simultaneous connect where the server-side node's name is greater than the client-side node's). Nok, /// The connection is disallowed for some (unspecified) security reason. NotAllowed, /// A connection to the node is already active. /// /// This either means that node the client-side node is confused or /// that the TCP connection breakdown of a previous node with this name has not yet reached the server-side node. /// /// The client-side node then decides whether to continue or not the handshake process. Alive, /// The handshake willl continue, but the client-side node requested a dynamic node name. Named { /// Dynamic node name that the server-side node generated. /// /// Note that this name doesn't contain the host part. name: String, /// Node creation that the server-side node generated for the client node. creation: Creation, }, } #[derive(Debug, Clone, Copy)] struct Challenge(u32); impl Challenge { fn new() -> Self { Self(rand::random()) } fn digest(self, cookie: &str) -> Digest { Digest(md5::compute(format!("{}{}", cookie, self.0)).0) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Digest([u8; 16]); /// Possible errors during handshake. #[derive(Debug)] #[non_exhaustive] #[allow(missing_docs)] pub enum HandshakeError { /// Peer already has an ongoing handshake with this node. OngoingHandshake, /// Connection is disallowed for some (unspecified) security reason. NotAllowed, /// Connection to the node is already active. AlreadyActive, /// Unknown distribution protocol version. UnknownProtocolVersion { value: u16 }, /// Unknown status. UnknownStatus { status: String }, /// Unexpected tag. UnexpectedTag { message: &'static str, tag: u8 }, /// Cookie mismatch. CookieMismatch, /// Invalid version. InvalidVersionValue { value: u16 }, /// Unexpected handshake phase. PhaseError { current: &'static str, depends_on: &'static str, }, /// Node name error. NodeNameError(NodeNameError), /// I/O error. Io(std::io::Error), } impl std::fmt::Display for HandshakeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::OngoingHandshake => { write!(f, "peer already has an ongoing handshake with this node") } Self::NotAllowed => write!( f, "the connection is disallowed for some (unspecified) security reason" ), Self::AlreadyActive => write!(f, "a connection to the node is already active"), Self::UnknownProtocolVersion { value } => { write!(f, "unknown distribution protocol version {value:?}") } Self::UnknownStatus { status } => { write!(f, "received an unknown status {status:?}") } Self::UnexpectedTag { message, tag } => { write!(f, "received an unexpected tag {tag} for {message:?}") } Self::CookieMismatch => write!(f, "cookie mismatch"), Self::InvalidVersionValue { value } => { write!( f, "the 'version' value of an old 'send_name' message must be {NODE_NAME_VERSION}, but got {value}" ) } Self::PhaseError { current, depends_on, } => { write!( f, "{current:?} was unexpectedly executed before {depends_on:?}" ) } Self::NodeNameError(error) => write!(f, "{error}"), Self::Io(error) => write!(f, "{error}"), } } } impl std::error::Error for HandshakeError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::NodeNameError(error) => Some(error), Self::Io(error) => Some(error), _ => None, } } } impl From for HandshakeError { fn from(value: std::io::Error) -> Self { Self::Io(value) } } impl From for HandshakeError { fn from(value: NodeNameError) -> Self { Self::NodeNameError(value) } } #[cfg(test)] mod tests { use super::*; use futures::StreamExt; #[test] fn client_side_handshake_works() { let peer_name = "client_side_handshake_works"; smol::block_on(async { let erl_node = crate::tests::TestErlangNode::new(peer_name) .await .expect("failed to run a test erlang node"); let peer_entry = crate::tests::epmd_client() .await .get_node(peer_name) .await .expect("failed to get node"); let peer_entry = peer_entry.expect("no such node"); let connection = smol::net::TcpStream::connect(("localhost", peer_entry.port)) .await .expect("failed to connect"); let local_node = LocalNode::new("foo@localhost".parse().unwrap(), Creation::random()); let mut handshake = ClientSideHandshake::new(connection, local_node, crate::tests::COOKIE); let status = handshake .execute_send_name(crate::LOWEST_DISTRIBUTION_PROTOCOL_VERSION) .await .expect("failed to execute send name"); assert_eq!(status, HandshakeStatus::Ok); let (_, peer_node) = handshake .execute_rest(true) .await .expect("failed to execute handshake"); assert_eq!(peer_entry.name, peer_node.name.name()); std::mem::drop(erl_node); }); } #[test] fn server_side_handshake_works() { smol::block_on(async { let listener = smol::net::TcpListener::bind("0.0.0.0:0").await.unwrap(); let listening_port = listener.local_addr().unwrap().port(); let (tx, rx) = futures::channel::oneshot::channel(); let connection = smol::net::TcpStream::connect(("localhost", listening_port)) .await .unwrap(); smol::spawn(async move { let local_node = LocalNode::new("foo@localhost".parse().unwrap(), Creation::random()); let mut handshake = ClientSideHandshake::new(connection, local_node, crate::tests::COOKIE); let _status = handshake .execute_send_name(crate::LOWEST_DISTRIBUTION_PROTOCOL_VERSION) .await .unwrap(); let (connection, _) = handshake.execute_rest(true).await.unwrap(); let _ = tx.send(connection); }) .detach(); let mut incoming = listener.incoming(); if let Some(connection) = incoming.next().await { let local_node = LocalNode::new("bar@localhost".parse().unwrap(), Creation::random()); let mut handshake = ServerSideHandshake::new(connection.unwrap(), local_node, crate::tests::COOKIE); let peer_name = handshake.execute_recv_name().await.unwrap(); assert!(peer_name.is_some()); handshake.execute_rest(HandshakeStatus::Ok).await.unwrap(); } let _ = rx.await; }) } } ================================================ FILE: src/io.rs ================================================ use eetf::{DecodeError, EncodeError, FixInteger, Term, Tuple}; use futures::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _}; use std::io::{Read, Write}; #[derive(Debug)] pub struct Connection { inner: T, } impl Connection where T: AsyncRead + AsyncWrite + Unpin, { pub fn new(inner: T) -> Self { Self { inner } } pub fn into_inner(self) -> T { self.inner } pub fn handshake_message_writer(&mut self) -> HandshakeMessageWriter<'_, T> { HandshakeMessageWriter { connection: self, buf: Vec::new(), } } #[allow(clippy::needless_lifetimes)] pub async fn handshake_message_reader<'a>( &'a mut self, ) -> std::io::Result> { let size = self.read_u16().await? as usize; Ok(HandshakeMessageReader { connection: self, size, }) } pub async fn write_u8(&mut self, v: u8) -> std::io::Result<()> { self.inner.write_all(&[v]).await } pub async fn write_u16(&mut self, v: u16) -> std::io::Result<()> { self.inner.write_all(&v.to_be_bytes()).await } pub async fn write_u32(&mut self, v: u32) -> std::io::Result<()> { self.inner.write_all(&v.to_be_bytes()).await } pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { self.inner.write_all(buf).await } pub async fn flush(&mut self) -> std::io::Result<()> { self.inner.flush().await } pub async fn read_u8(&mut self) -> std::io::Result { let mut buf = [0; 1]; self.inner.read_exact(&mut buf).await?; Ok(buf[0]) } pub async fn read_u16(&mut self) -> std::io::Result { let mut buf = [0; 2]; self.inner.read_exact(&mut buf).await?; Ok(u16::from_be_bytes(buf)) } pub async fn read_u32(&mut self) -> std::io::Result { let mut buf = [0; 4]; self.inner.read_exact(&mut buf).await?; Ok(u32::from_be_bytes(buf)) } pub async fn read_u64(&mut self) -> std::io::Result { let mut buf = [0; 8]; self.inner.read_exact(&mut buf).await?; Ok(u64::from_be_bytes(buf)) } pub async fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { self.inner.read_exact(buf).await } pub async fn read_string(&mut self) -> std::io::Result { let mut buf = String::new(); self.inner.read_to_string(&mut buf).await?; Ok(buf) } pub async fn read_stringn(&mut self, size: usize) -> std::io::Result { let mut buf = vec![0; size]; self.inner.read_exact(&mut buf).await?; String::from_utf8(buf).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, "stream did not contain valid UTF-8", ) }) } pub async fn read_u16_bytes(&mut self) -> std::io::Result> { let mut buf = vec![0; usize::from(self.read_u16().await?)]; self.inner.read_exact(&mut buf).await?; Ok(buf) } pub async fn read_u16_string(&mut self) -> std::io::Result { let buf = self.read_u16_bytes().await?; String::from_utf8(buf).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, "stream did not contain valid UTF-8", ) }) } } #[derive(Debug)] pub struct HandshakeMessageWriter<'a, T> { connection: &'a mut Connection, buf: Vec, } impl HandshakeMessageWriter<'_, T> where T: AsyncRead + AsyncWrite + Unpin, { pub async fn finish(self) -> std::io::Result<()> { if self.buf.len() > u16::MAX as usize { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!( "too large bytes: expected less then {}, but got {} bytes", u16::MAX as usize + 1, self.buf.len() ), )); } self.connection.write_u16(self.buf.len() as u16).await?; self.connection.write_all(&self.buf).await?; self.connection.flush().await?; Ok(()) } pub fn write_u8(&mut self, v: u8) -> std::io::Result<()> { self.buf.push(v); Ok(()) } pub fn write_u16(&mut self, v: u16) -> std::io::Result<()> { self.buf.extend_from_slice(&v.to_be_bytes()); Ok(()) } pub fn write_u32(&mut self, v: u32) -> std::io::Result<()> { self.buf.extend_from_slice(&v.to_be_bytes()); Ok(()) } pub fn write_u64(&mut self, v: u64) -> std::io::Result<()> { self.buf.extend_from_slice(&v.to_be_bytes()); Ok(()) } pub fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> { self.buf.extend_from_slice(bytes); Ok(()) } } #[derive(Debug)] pub struct HandshakeMessageReader<'a, T> { connection: &'a mut Connection, size: usize, } impl HandshakeMessageReader<'_, T> where T: AsyncRead + AsyncWrite + Unpin, { pub async fn read_u8(&mut self) -> std::io::Result { self.size = self.size.checked_sub(1).ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof") })?; self.connection.read_u8().await } pub async fn read_u16(&mut self) -> std::io::Result { self.size = self.size.checked_sub(2).ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof") })?; self.connection.read_u16().await } pub async fn read_u32(&mut self) -> std::io::Result { self.size = self.size.checked_sub(4).ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof") })?; self.connection.read_u32().await } pub async fn read_u64(&mut self) -> std::io::Result { self.size = self.size.checked_sub(8).ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof") })?; self.connection.read_u64().await } pub async fn read_string(&mut self) -> std::io::Result { let n = self.size; self.size = 0; self.connection.read_stringn(n).await } pub async fn read_bytes(&mut self) -> std::io::Result> { let n = self.size; let mut buf = vec![0; n]; self.read_exact(&mut buf).await?; Ok(buf) } pub async fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { let n = buf.len(); self.size = self.size.checked_sub(n).ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof") })?; self.connection.read_exact(buf).await } pub async fn read_u16_string(&mut self) -> std::io::Result { let n = self.read_u16().await? as usize; self.size = self.size.checked_sub(n).ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof") })?; self.connection.read_stringn(n).await } pub async fn consume_remaining_bytes(&mut self) -> std::io::Result<()> { let mut buf = vec![0; self.size]; self.size = 0; self.connection.read_exact(&mut buf).await?; Ok(()) } pub async fn finish(mut self) -> std::io::Result<()> { self.consume_remaining_bytes().await } } pub trait ReadTermExt: Read { fn read_tuple(&mut self) -> Result { let term = self.read_term()?; term.try_into() .map_err(|value| DecodeError::UnexpectedType { value, expected: "Tuple".to_owned(), }) } fn read_term(&mut self) -> Result { Term::decode(self) } } impl ReadTermExt for T {} pub trait WriteTermExt: Write { fn write_tagged_tuple1(&mut self, tag: i32) -> Result<(), EncodeError> { let tuple = Tuple { elements: vec![Term::from(FixInteger { value: tag })], }; self.write_term(tuple) } fn write_tagged_tuple3( &mut self, tag: i32, term0: T0, term1: T1, ) -> Result<(), EncodeError> where Term: From, Term: From, { let tuple = Tuple { elements: vec![ Term::from(FixInteger { value: tag }), Term::from(term0), Term::from(term1), ], }; self.write_term(tuple) } fn write_tagged_tuple4( &mut self, tag: i32, term0: T0, term1: T1, term2: T2, ) -> Result<(), EncodeError> where Term: From, Term: From, Term: From, { let tuple = Tuple { elements: vec![ Term::from(FixInteger { value: tag }), Term::from(term0), Term::from(term1), Term::from(term2), ], }; self.write_term(tuple) } fn write_tagged_tuple5( &mut self, tag: i32, term0: T0, term1: T1, term2: T2, term3: T3, ) -> Result<(), EncodeError> where Term: From, Term: From, Term: From, Term: From, { let tuple = Tuple { elements: vec![ Term::from(FixInteger { value: tag }), Term::from(term0), Term::from(term1), Term::from(term2), Term::from(term3), ], }; self.write_term(tuple) } fn write_tagged_tuple6( &mut self, tag: i32, term0: T0, term1: T1, term2: T2, term3: T3, term4: T4, ) -> Result<(), EncodeError> where Term: From, Term: From, Term: From, Term: From, Term: From, { let tuple = Tuple { elements: vec![ Term::from(FixInteger { value: tag }), Term::from(term0), Term::from(term1), Term::from(term2), Term::from(term3), Term::from(term4), ], }; self.write_term(tuple) } #[allow(clippy::too_many_arguments)] fn write_tagged_tuple7( &mut self, tag: i32, term0: T0, term1: T1, term2: T2, term3: T3, term4: T4, term5: T5, ) -> Result<(), EncodeError> where Term: From, Term: From, Term: From, Term: From, Term: From, Term: From, { let tuple = Tuple { elements: vec![ Term::from(FixInteger { value: tag }), Term::from(term0), Term::from(term1), Term::from(term2), Term::from(term3), Term::from(term4), Term::from(term5), ], }; self.write_term(tuple) } fn write_term(&mut self, term: T) -> Result<(), EncodeError> where Term: From, { Term::from(term).encode(self) } } impl WriteTermExt for T {} ================================================ FILE: src/lib.rs ================================================ //! Rust Implementation of Erlang Distribution Protocol. //! //! Distribution protocol is used to communicate with distributed erlang nodes. //! //! Reference: [Distribution Protocol](http://erlang.org/doc/apps/erts/erl_dist_protocol.html) //! //! # Examples //! //! Gets a node entry from EPMD: //! ```no_run //! # use smol::net::TcpStream; //! use erl_dist::epmd::{DEFAULT_EPMD_PORT, EpmdClient}; //! //! # fn main() -> Result<(), Box> { //! # smol::block_on(async { //! // Connect to the local EPMD. //! let connection = TcpStream::connect(("localhost", DEFAULT_EPMD_PORT)).await?; //! let client = EpmdClient::new(connection); //! //! // Get the information of a node. //! let node_name = "foo"; //! if let Some(node) = client.get_node(node_name).await? { //! println!("Found: {:?}", node); //! } else { //! println!("Not found"); //! } //! # Ok(()) //! # }) //! # } //! ``` //! //! Sends a message to an Erlang node: //! ```no_run //! # use smol::net::TcpStream; //! use erl_dist::LOWEST_DISTRIBUTION_PROTOCOL_VERSION; //! use erl_dist::node::{Creation, LocalNode}; //! use erl_dist::handshake::ClientSideHandshake; //! use erl_dist::term::{Atom, Pid}; //! use erl_dist::message::{channel, Message}; //! //! # fn main() -> Result<(), Box> { //! # smol::block_on(async { //! // Connect to a peer node. //! let peer_host = "localhost"; //! let peer_port = 7483; // NOTE: Usually, port number is retrieved from EPMD. //! let connection = TcpStream::connect((peer_host, peer_port)).await?; //! //! // Local node information. //! let creation = Creation::random(); //! let local_node = LocalNode::new("foo@localhost".parse()?, creation); //! //! // Do handshake. //! let mut handshake = ClientSideHandshake::new(connection, local_node.clone(), "cookie"); //! let _status = handshake.execute_send_name(LOWEST_DISTRIBUTION_PROTOCOL_VERSION).await?; //! let (connection, peer_node) = handshake.execute_rest(true).await?; //! //! // Create a channel. //! let capability_flags = local_node.flags & peer_node.flags; //! let (mut tx, _) = channel(connection, capability_flags); //! //! // Send a message. //! let from_pid = Pid::new(local_node.name.to_string(), 0, 0, local_node.creation.get()); //! let to_name = Atom::from("bar"); //! let msg = Message::reg_send(from_pid, to_name, Atom::from("hello").into()); //! tx.send(msg).await?; //! # Ok(()) //! # }) //! # } //! ``` //! //! Example commands: //! - EPMD Client Example: [send_msg.rs](https://github.com/sile/erl_dist/blob/master/examples/epmd_cli.rs) //! - Client Node Example: [send_msg.rs](https://github.com/sile/erl_dist/blob/master/examples/send_msg.rs) //! - Server Node Example: [recv_msg.rs](https://github.com/sile/erl_dist/blob/master/examples/recv_msg.rs) #![warn(missing_docs)] pub mod epmd; pub mod handshake; pub mod message; pub mod node; pub mod term; mod channel; mod eetf_ext; mod flags; mod io; pub use self::flags::DistributionFlags; /// The lowest distribution protocol version this crate can handle. pub const LOWEST_DISTRIBUTION_PROTOCOL_VERSION: u16 = 6; /// The highest distribution protocol version this crate can handle. pub const HIGHEST_DISTRIBUTION_PROTOCOL_VERSION: u16 = 6; #[cfg(test)] mod tests { use std::process::{Child, Command}; type BoxError = Box; pub const COOKIE: &str = "test-cookie"; #[derive(Debug)] pub struct TestErlangNode { child: Child, } impl TestErlangNode { pub async fn new(name: &str) -> Result { let child = Command::new("erl") .args(&["-sname", name, "-noshell", "-setcookie", COOKIE]) .spawn()?; let start = std::time::Instant::now(); loop { if let Ok(client) = try_epmd_client().await { if client.get_node(name).await?.is_some() { break; } } std::thread::sleep(std::time::Duration::from_millis(500)); if start.elapsed() > std::time::Duration::from_secs(10) { break; } } Ok(Self { child }) } } impl Drop for TestErlangNode { fn drop(&mut self) { let _ = self.child.kill(); } } pub async fn try_epmd_client() -> Result, BoxError> { let client = smol::net::TcpStream::connect(("127.0.0.1", crate::epmd::DEFAULT_EPMD_PORT)) .await .map(crate::epmd::EpmdClient::new)?; Ok(client) } pub async fn epmd_client() -> crate::epmd::EpmdClient { try_epmd_client().await.unwrap() } } ================================================ FILE: src/message.rs ================================================ //! Messages passed between distributed nodes, and channels for those messages. //! //! Reference: [Protocol between Connected Nodes](https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html#protocol-between-connected-nodes) //! //! Note that distribution headers are not supported in the current version. #[cfg(doc)] use crate::DistributionFlags; use crate::eetf_ext; use crate::io::{ReadTermExt, WriteTermExt}; use crate::term::{Atom, FixInteger, List, Mfa, Pid, PidOrAtom, Reference, Term, Tuple}; use eetf::{DecodeError, EncodeError}; use std::io::{Read, Write}; pub use crate::channel::{Receiver, RecvError, SendError, Sender, channel}; trait DistributionMessage: Sized { const OP: i32; fn write_into(self, writer: &mut W) -> Result<(), EncodeError>; fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result; } /// This signal is sent by `from_pid` in order to create a link between `from_pid` and `to_pid`. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Link { pub from_pid: Pid, pub to_pid: Pid, } impl DistributionMessage for Link { const OP: i32 = 1; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.to_pid)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; Ok(Self { from_pid, to_pid }) } } /// This signal is used to send a message to `to_pid`. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Send { pub to_pid: Pid, pub message: Term, } impl DistributionMessage for Send { const OP: i32 = 2; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, Tuple::nil(), self.to_pid)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (_, to_pid): (Term, _) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { to_pid, message }) } } /// This signal is sent when a link has been broken. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Exit { pub from_pid: Pid, pub to_pid: Pid, pub reason: Term, } impl DistributionMessage for Exit { const OP: i32 = 3; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_pid, self.reason)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, reason) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; Ok(Self { from_pid, to_pid, reason, }) } } /// **DEPRECATED** This signal is sent by `from_pid` in order to remove a link between `from_pid` and `to_pid`. /// /// This signal has been deprecated and will not be supported in OTP 26. /// For more information see the documentation of the [new link protocol](https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html#new_link_protocol). #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Unlink { pub from_pid: Pid, pub to_pid: Pid, } impl DistributionMessage for Unlink { const OP: i32 = 4; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.to_pid)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; Ok(Self { from_pid, to_pid }) } } /// Node link. #[derive(Debug, Clone, PartialEq)] pub struct NodeLink; impl DistributionMessage for NodeLink { const OP: i32 = 5; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple1(Self::OP)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { eetf_ext::check_tuple_len(&ctrl_msg, 1)?; Ok(Self {}) } } /// This signal is used to send a message to a named process. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct RegSend { pub from_pid: Pid, pub to_name: Atom, pub message: Term, } impl DistributionMessage for RegSend { const OP: i32 = 6; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, Tuple::nil(), self.to_name)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, _, to_name): (_, Term, _) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { from_pid, to_name, message, }) } } /// Group leader. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct GroupLeader { pub from_pid: Pid, pub to_pid: Pid, } impl DistributionMessage for GroupLeader { const OP: i32 = 7; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.to_pid)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; Ok(Self { from_pid, to_pid }) } } /// This signal is sent by a call to the `erlang:exit/2` bif. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Exit2 { pub from_pid: Pid, pub to_pid: Pid, pub reason: Term, } impl DistributionMessage for Exit2 { const OP: i32 = 8; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_pid, self.reason)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, reason) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; Ok(Self { from_pid, to_pid, reason, }) } } /// [`Send`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SendTt { pub to_pid: Pid, pub trace_token: Term, pub message: Term, } impl DistributionMessage for SendTt { const OP: i32 = 12; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, Tuple::nil(), self.to_pid, self.trace_token)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (_, trace_token, to_pid): (Term, _, _) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { to_pid, trace_token, message, }) } } /// [`Exit`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct ExitTt { pub from_pid: Pid, pub to_pid: Pid, pub trace_token: Term, pub reason: Term, } impl DistributionMessage for ExitTt { const OP: i32 = 13; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple5( Self::OP, self.from_pid, self.to_pid, self.trace_token, self.reason, )?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, trace_token, reason) = eetf_ext::try_from_tagged_tuple5(ctrl_msg)?; Ok(Self { from_pid, to_pid, trace_token, reason, }) } } /// [`RegSend`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct RegSendTt { pub from_pid: Pid, pub to_name: Atom, pub trace_token: Term, pub message: Term, } impl DistributionMessage for RegSendTt { const OP: i32 = 16; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple5( Self::OP, self.from_pid, Tuple::nil(), self.to_name, self.trace_token, )?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, _, to_name, trace_token): (_, Term, _, _) = eetf_ext::try_from_tagged_tuple5(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { from_pid, to_name, trace_token, message, }) } } /// [`Exit2`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Exit2Tt { pub from_pid: Pid, pub to_pid: Pid, pub trace_token: Term, pub reason: Term, } impl DistributionMessage for Exit2Tt { const OP: i32 = 18; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple5( Self::OP, self.from_pid, self.to_pid, self.trace_token, self.reason, )?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, trace_token, reason) = eetf_ext::try_from_tagged_tuple5(ctrl_msg)?; Ok(Self { from_pid, to_pid, trace_token, reason, }) } } /// `from_pid` = monitoring process and `to_proc` = monitored process pid or name (atom). #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct MonitorP { pub from_pid: Pid, pub to_proc: PidOrAtom, pub reference: Reference, } impl DistributionMessage for MonitorP { const OP: i32 = 19; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_proc, self.reference)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_proc, reference) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; Ok(Self { from_pid, to_proc, reference, }) } } /// `from_pid` = monitoring process and `to_proc` = monitored process pid or name (atom). /// /// We include `from_pid` just in case we want to trace this. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct DemonitorP { pub from_pid: Pid, pub to_proc: PidOrAtom, pub reference: Reference, } impl DistributionMessage for DemonitorP { const OP: i32 = 20; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_proc, self.reference)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_proc, reference) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; Ok(Self { from_pid, to_proc, reference, }) } } /// `from_proc` = monitored process pid or name (atom), `to_pid` = monitoring process, and `reason` = exit reason for the monitored process. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct MonitorPExit { pub from_proc: PidOrAtom, pub to_pid: Pid, pub reference: Reference, pub reason: Term, } impl DistributionMessage for MonitorPExit { const OP: i32 = 21; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple5( Self::OP, self.from_proc, self.to_pid, self.reference, self.reason, )?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_proc, to_pid, reference, reason) = eetf_ext::try_from_tagged_tuple5(ctrl_msg)?; Ok(Self { from_proc, to_pid, reference, reason, }) } } /// This control message replaces the [`Send`] control message and will be sent when the distribution flag `DistributionFlags::SEND_SENDER` has been negotiated in the connection setup handshake. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SendSender { pub from_pid: Pid, pub to_pid: Pid, pub message: Term, } impl DistributionMessage for SendSender { const OP: i32 = 22; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.to_pid)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { from_pid, to_pid, message, }) } } /// [`SendSender`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SendSenderTt { pub from_pid: Pid, pub to_pid: Pid, pub trace_token: Term, pub message: Term, } impl DistributionMessage for SendSenderTt { const OP: i32 = 23; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_pid, self.trace_token)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, trace_token) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { from_pid, to_pid, trace_token, message, }) } } /// This control message replaces the [`Exit`] control message and will be sent when the distribution flag [`DistributionFlags::EXIT_PAYLOAD`] has been negotiated in the connection setup handshake. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct PayloadExit { pub from_pid: Pid, pub to_pid: Pid, pub reason: Term, } impl DistributionMessage for PayloadExit { const OP: i32 = 24; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.to_pid)?; writer.write_term(self.reason)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; let reason = reader.read_term()?; Ok(Self { from_pid, to_pid, reason, }) } } /// [`PayloadExit`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct PayloadExitTt { pub from_pid: Pid, pub to_pid: Pid, pub trace_token: Term, pub reason: Term, } impl DistributionMessage for PayloadExitTt { const OP: i32 = 25; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_pid, self.trace_token)?; writer.write_term(self.reason)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, trace_token) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let reason = reader.read_term()?; Ok(Self { from_pid, to_pid, trace_token, reason, }) } } /// This control message replaces the [`Exit2`] control message and will be sent when the distribution flag [`DistributionFlags::EXIT_PAYLOAD`] has been negotiated in the connection setup handshake. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct PayloadExit2 { pub from_pid: Pid, pub to_pid: Pid, pub reason: Term, } impl DistributionMessage for PayloadExit2 { const OP: i32 = 26; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.to_pid)?; writer.write_term(self.reason)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; let reason = reader.read_term()?; Ok(Self { from_pid, to_pid, reason, }) } } /// [`PayloadExit2`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct PayloadExit2Tt { pub from_pid: Pid, pub to_pid: Pid, pub trace_token: Term, pub reason: Term, } impl DistributionMessage for PayloadExit2Tt { const OP: i32 = 27; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.to_pid, self.trace_token)?; writer.write_term(self.reason)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, to_pid, trace_token) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let reason = reader.read_term()?; Ok(Self { from_pid, to_pid, trace_token, reason, }) } } /// This control message replaces the [`MonitorPExit`] control message and will be sent when the distribution flag [`DistributionFlags::EXIT_PAYLOAD`] has been negotiated in the connection setup handshake. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct PayloadMonitorPExit { pub from_proc: PidOrAtom, pub to_pid: Pid, pub reference: Reference, pub reason: Term, } impl DistributionMessage for PayloadMonitorPExit { const OP: i32 = 28; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_proc, self.to_pid, self.reference)?; writer.write_term(self.reason)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_proc, to_pid, reference) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let reason = reader.read_term()?; Ok(Self { from_proc, to_pid, reference, reason, }) } } /// This signal is sent by the `spawn_request()` BIF. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SpawnRequest { pub req_id: Reference, pub from_pid: Pid, pub group_leader: Pid, pub mfa: Mfa, pub opt_list: List, pub arg_list: List, } impl DistributionMessage for SpawnRequest { const OP: i32 = 29; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple6( Self::OP, self.req_id, self.from_pid, self.group_leader, self.mfa, self.opt_list, )?; writer.write_term(self.arg_list)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (req_id, from_pid, group_leader, mfa, opt_list) = eetf_ext::try_from_tagged_tuple6(ctrl_msg)?; let arg_list = eetf_ext::try_from_term(reader.read_term()?, "list")?; Ok(Self { req_id, from_pid, group_leader, mfa, opt_list, arg_list, }) } } /// [`SpawnRequest`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SpawnRequestTt { pub req_id: Reference, pub from_pid: Pid, pub group_leader: Pid, pub mfa: Mfa, pub opt_list: List, pub trace_token: Term, pub arg_list: List, } impl DistributionMessage for SpawnRequestTt { const OP: i32 = 30; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple7( Self::OP, self.req_id, self.from_pid, self.group_leader, self.mfa, self.opt_list, self.trace_token, )?; writer.write_term(self.arg_list)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (req_id, from_pid, group_leader, mfa, opt_list, trace_token) = eetf_ext::try_from_tagged_tuple7(ctrl_msg)?; let arg_list = eetf_ext::try_from_term(reader.read_term()?, "list")?; Ok(Self { req_id, from_pid, group_leader, mfa, opt_list, trace_token, arg_list, }) } } /// This signal is sent as a reply to a process previously sending a [`SpawnRequest`] signal. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SpawnReply { pub req_id: Reference, pub to_pid: Pid, pub flags: FixInteger, pub result: PidOrAtom, } impl DistributionMessage for SpawnReply { const OP: i32 = 31; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple5(Self::OP, self.req_id, self.to_pid, self.flags, self.result)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (req_id, to_pid, flags, result) = eetf_ext::try_from_tagged_tuple5(ctrl_msg)?; Ok(Self { req_id, to_pid, flags, result, }) } } /// [`SpawnReply`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct SpawnReplyTt { pub req_id: Reference, pub to_pid: Pid, pub flags: FixInteger, pub result: PidOrAtom, pub trace_token: Term, } impl DistributionMessage for SpawnReplyTt { const OP: i32 = 32; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple6( Self::OP, self.req_id, self.to_pid, self.flags, self.result, self.trace_token, )?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (req_id, to_pid, flags, result, trace_token) = eetf_ext::try_from_tagged_tuple6(ctrl_msg)?; Ok(Self { req_id, to_pid, flags, result, trace_token, }) } } /// This signal is sent by `from_pid` in order to remove a link between `from_pid` and `to_pid`. /// /// This unlink signal replaces the [`Unlink`] signal. /// This signal is only passed when the [new link protocol](https://www.erlang.org/doc/apps/erts/erl_dist_protocol.html#new_link_protocol) has been negotiated using the [`DistributionFlags::UNLINK_ID`] distribution flag. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct UnlinkId { pub id: Term, pub from_pid: Pid, pub to_pid: Pid, } impl DistributionMessage for UnlinkId { const OP: i32 = 35; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.id, self.from_pid, self.to_pid)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (id, from_pid, to_pid) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; Ok(Self { id, from_pid, to_pid, }) } } /// An unlink acknowledgement signal. /// /// This signal is sent as an acknowledgement of the reception of an [`UnlinkId`] signal. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct UnlinkIdAck { pub id: Term, pub from_pid: Pid, pub to_pid: Pid, } impl DistributionMessage for UnlinkIdAck { const OP: i32 = 36; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.id, self.from_pid, self.to_pid)?; Ok(()) } fn read_from(_reader: &mut R, ctrl_msg: Tuple) -> Result { let (id, from_pid, to_pid) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; Ok(Self { id, from_pid, to_pid, }) } } /// This control message is used when sending the message Message to the process identified by the process alias `alias`. /// /// Nodes that can handle this control message sets the distribution flag [`DistributionFlags::ALIAS`] in the connection setup handshake. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct AliasSend { pub from_pid: Pid, pub alias: Reference, pub message: Term, } impl DistributionMessage for AliasSend { const OP: i32 = 33; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple3(Self::OP, self.from_pid, self.alias)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, alias) = eetf_ext::try_from_tagged_tuple3(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { from_pid, alias, message, }) } } /// [`AliasSend`] with a trace token. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct AliasSendTt { pub from_pid: Pid, pub alias: Reference, pub trace_token: Term, pub message: Term, } impl DistributionMessage for AliasSendTt { const OP: i32 = 34; fn write_into(self, writer: &mut W) -> Result<(), EncodeError> { writer.write_tagged_tuple4(Self::OP, self.from_pid, self.alias, self.trace_token)?; writer.write_term(self.message)?; Ok(()) } fn read_from(reader: &mut R, ctrl_msg: Tuple) -> Result { let (from_pid, alias, trace_token) = eetf_ext::try_from_tagged_tuple4(ctrl_msg)?; let message = reader.read_term()?; Ok(Self { from_pid, alias, trace_token, message, }) } } /// Message. #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub enum Message { Link(Link), Send(Send), Exit(Exit), Unlink(Unlink), // deprecated NodeLink(NodeLink), RegSend(RegSend), GroupLeader(GroupLeader), Exit2(Exit2), SendTt(SendTt), ExitTt(ExitTt), RegSendTt(RegSendTt), Exit2Tt(Exit2Tt), MonitorP(MonitorP), DemonitorP(DemonitorP), MonitorPExit(MonitorPExit), SendSender(SendSender), SendSenderTt(SendSenderTt), PayloadExit(PayloadExit), PayloadExitTt(PayloadExitTt), PayloadExit2(PayloadExit2), PayloadExit2Tt(PayloadExit2Tt), PayloadMonitorPExit(PayloadMonitorPExit), SpawnRequest(SpawnRequest), SpawnRequestTt(SpawnRequestTt), SpawnReply(SpawnReply), SpawnReplyTt(SpawnReplyTt), UnlinkId(UnlinkId), UnlinkIdAck(UnlinkIdAck), AliasSend(AliasSend), AliasSendTt(AliasSendTt), /// Tick message used for keeping alive a connection. /// /// See also: [`net_ticktime` parameter](https://www.erlang.org/doc/man/kernel_app.html#net_ticktime) Tick, } impl Message { /// Makes a [`Link`] message. pub fn link(from_pid: Pid, to_pid: Pid) -> Self { Self::Link(Link { from_pid, to_pid }) } /// Makes a [`Send`] message. pub fn send(to_pid: Pid, message: Term) -> Self { Self::Send(Send { to_pid, message }) } /// Makes a [`Exit`] message. pub fn exit(from_pid: Pid, to_pid: Pid, reason: Term) -> Self { Self::Exit(Exit { from_pid, to_pid, reason, }) } /// Makes a [`Unlink`] message. pub fn unlink(from_pid: Pid, to_pid: Pid) -> Self { Self::Unlink(Unlink { from_pid, to_pid }) } /// Makes a [`NodeLink`] message. pub fn node_link() -> Self { Self::NodeLink(NodeLink) } /// Makes a [`RegSend`] message. pub fn reg_send(from_pid: Pid, to_name: Atom, message: Term) -> Self { Self::RegSend(RegSend { from_pid, to_name, message, }) } /// Makes a [`GroupLeader`] message. pub fn group_leader(from_pid: Pid, to_pid: Pid) -> Self { Self::GroupLeader(GroupLeader { from_pid, to_pid }) } /// Makes a [`Exit2`] message. pub fn exit2(from_pid: Pid, to_pid: Pid, reason: Term) -> Self { Self::Exit2(Exit2 { from_pid, to_pid, reason, }) } /// Makes a [`SendTt`] message. pub fn send_tt(to_pid: Pid, message: Term, trace_token: Term) -> Self { Self::SendTt(SendTt { to_pid, trace_token, message, }) } /// Makes a [`ExitTt`] message. pub fn exit_tt(from_pid: Pid, to_pid: Pid, reason: Term, trace_token: Term) -> Self { Self::ExitTt(ExitTt { from_pid, to_pid, trace_token, reason, }) } /// Makes a [`RegSendTt`] message. pub fn reg_send_tt(from_pid: Pid, to_name: Atom, message: Term, trace_token: Term) -> Self { Self::RegSendTt(RegSendTt { from_pid, to_name, trace_token, message, }) } /// Makes a [`Exit2Tt`] message. pub fn exit2_tt(from_pid: Pid, to_pid: Pid, reason: Term, trace_token: Term) -> Self { Self::Exit2Tt(Exit2Tt { from_pid, to_pid, trace_token, reason, }) } /// Makes as [`MonitorP`] message. pub fn monitor_p(from_pid: Pid, to_proc: PidOrAtom, reference: Reference) -> Self { Self::MonitorP(MonitorP { from_pid, to_proc, reference, }) } /// Makes as [`DemonitorP`] message. pub fn demonitor_p(from_pid: Pid, to_proc: PidOrAtom, reference: Reference) -> Self { Self::DemonitorP(DemonitorP { from_pid, to_proc, reference, }) } /// Makes as [`MonitorPExit`] message. pub fn monitor_p_exit( from_proc: PidOrAtom, to_pid: Pid, reference: Reference, reason: Term, ) -> Self { Self::MonitorPExit(MonitorPExit { from_proc, to_pid, reference, reason, }) } /// Makes a [`SendSender`] message. pub fn send_sender(from_pid: Pid, to_pid: Pid, message: Term) -> Self { Self::SendSender(SendSender { from_pid, to_pid, message, }) } /// Makes a [`SendSenderTt`] message. pub fn send_sender_tt(from_pid: Pid, to_pid: Pid, message: Term, trace_token: Term) -> Self { Self::SendSenderTt(SendSenderTt { from_pid, to_pid, message, trace_token, }) } /// Makes a [`PayloadExit`] message. pub fn payload_exit(from_pid: Pid, to_pid: Pid, reason: Term) -> Self { Self::PayloadExit(PayloadExit { from_pid, to_pid, reason, }) } /// Makes a [`PayloadExitTt`] message. pub fn payload_exit_tt(from_pid: Pid, to_pid: Pid, reason: Term, trace_token: Term) -> Self { Self::PayloadExitTt(PayloadExitTt { from_pid, to_pid, reason, trace_token, }) } /// Makes a [`PayloadExit2`] message. pub fn payload_exit2(from_pid: Pid, to_pid: Pid, reason: Term) -> Self { Self::PayloadExit2(PayloadExit2 { from_pid, to_pid, reason, }) } /// Makes a [`PayloadExit2Tt`] message. pub fn payload_exit2_tt(from_pid: Pid, to_pid: Pid, reason: Term, trace_token: Term) -> Self { Self::PayloadExit2Tt(PayloadExit2Tt { from_pid, to_pid, reason, trace_token, }) } /// Makes a [`PayloadMonitorPExit`] message. pub fn payload_monitor_p_exit( from_proc: PidOrAtom, to_pid: Pid, reference: Reference, reason: Term, ) -> Self { Self::PayloadMonitorPExit(PayloadMonitorPExit { from_proc, to_pid, reference, reason, }) } /// Makes a [`SpawnRequest`] message. pub fn spawn_request( req_id: Reference, from_pid: Pid, group_leader: Pid, mfa: Mfa, opt_list: List, arg_list: List, ) -> Self { Self::SpawnRequest(SpawnRequest { req_id, from_pid, group_leader, mfa, opt_list, arg_list, }) } /// Makes a [`SpawnRequestTt`] message. pub fn spawn_request_tt( req_id: Reference, from_pid: Pid, group_leader: Pid, mfa: Mfa, opt_list: List, arg_list: List, trace_token: Term, ) -> Self { Self::SpawnRequestTt(SpawnRequestTt { req_id, from_pid, group_leader, mfa, opt_list, arg_list, trace_token, }) } /// Makes a [`SpawnReply`] message. pub fn spawn_reply( req_id: Reference, to_pid: Pid, flags: FixInteger, result: PidOrAtom, ) -> Self { Self::SpawnReply(SpawnReply { req_id, to_pid, flags, result, }) } /// Makes a [`SpawnReplyTt`] message. pub fn spawn_reply_tt( req_id: Reference, to_pid: Pid, flags: FixInteger, result: PidOrAtom, trace_token: Term, ) -> Self { Self::SpawnReplyTt(SpawnReplyTt { req_id, to_pid, flags, result, trace_token, }) } /// Makes a [`UnlinkId`] message. pub fn unlink_id(id: Term, from_pid: Pid, to_pid: Pid) -> Self { Self::UnlinkId(UnlinkId { id, from_pid, to_pid, }) } /// Makes a [`UnlinkIdAck`] message. pub fn unlink_id_ack(id: Term, from_pid: Pid, to_pid: Pid) -> Self { Self::UnlinkIdAck(UnlinkIdAck { id, from_pid, to_pid, }) } /// Makes a [`AliasSend`] message. pub fn alias_send(from_pid: Pid, alias: Reference, message: Term) -> Self { Self::AliasSend(AliasSend { from_pid, alias, message, }) } /// Makes a [`AliasSendTt`] message. pub fn alias_send_tt( from_pid: Pid, alias: Reference, message: Term, trace_token: Term, ) -> Self { Self::AliasSendTt(AliasSendTt { from_pid, alias, message, trace_token, }) } /// Serialize the [Message] into a byte buffer pub fn write_into(self, writer: &mut W) -> Result<(), crate::channel::SendError> { match self { Self::Link(x) => x.write_into(writer)?, Self::Send(x) => x.write_into(writer)?, Self::Exit(x) => x.write_into(writer)?, Self::Unlink(x) => x.write_into(writer)?, Self::NodeLink(x) => x.write_into(writer)?, Self::RegSend(x) => x.write_into(writer)?, Self::GroupLeader(x) => x.write_into(writer)?, Self::Exit2(x) => x.write_into(writer)?, Self::SendTt(x) => x.write_into(writer)?, Self::ExitTt(x) => x.write_into(writer)?, Self::RegSendTt(x) => x.write_into(writer)?, Self::Exit2Tt(x) => x.write_into(writer)?, Self::MonitorP(x) => x.write_into(writer)?, Self::DemonitorP(x) => x.write_into(writer)?, Self::MonitorPExit(x) => x.write_into(writer)?, Self::SendSender(x) => x.write_into(writer)?, Self::SendSenderTt(x) => x.write_into(writer)?, Self::PayloadExit(x) => x.write_into(writer)?, Self::PayloadExitTt(x) => x.write_into(writer)?, Self::PayloadExit2(x) => x.write_into(writer)?, Self::PayloadExit2Tt(x) => x.write_into(writer)?, Self::PayloadMonitorPExit(x) => x.write_into(writer)?, Self::SpawnRequest(x) => x.write_into(writer)?, Self::SpawnRequestTt(x) => x.write_into(writer)?, Self::SpawnReply(x) => x.write_into(writer)?, Self::SpawnReplyTt(x) => x.write_into(writer)?, Self::UnlinkId(x) => x.write_into(writer)?, Self::UnlinkIdAck(x) => x.write_into(writer)?, Self::AliasSend(x) => x.write_into(writer)?, Self::AliasSendTt(x) => x.write_into(writer)?, Self::Tick => unreachable!(), } Ok(()) } /// Deserialize a given byte buffer into a [Message]. Returns [Err(_)] if the message /// is malformed pub fn read_from(reader: &mut R) -> Result { let mut ctrl_msg = reader.read_tuple()?; if ctrl_msg.elements.is_empty() { return Err(DecodeError::UnexpectedType { value: ctrl_msg.into(), expected: "non empty tuple".to_owned(), } .into()); } let op: FixInteger = eetf_ext::try_from_term( std::mem::replace(&mut ctrl_msg.elements[0], eetf_ext::nil()), "integer", )?; let msg = match op.value { Link::OP => Link::read_from(reader, ctrl_msg).map(Self::Link)?, Send::OP => Send::read_from(reader, ctrl_msg).map(Self::Send)?, Exit::OP => Exit::read_from(reader, ctrl_msg).map(Self::Exit)?, Unlink::OP => Unlink::read_from(reader, ctrl_msg).map(Self::Unlink)?, NodeLink::OP => NodeLink::read_from(reader, ctrl_msg).map(Self::NodeLink)?, RegSend::OP => RegSend::read_from(reader, ctrl_msg).map(Self::RegSend)?, GroupLeader::OP => GroupLeader::read_from(reader, ctrl_msg).map(Self::GroupLeader)?, Exit2::OP => Exit2::read_from(reader, ctrl_msg).map(Self::Exit2)?, SendTt::OP => SendTt::read_from(reader, ctrl_msg).map(Self::SendTt)?, ExitTt::OP => ExitTt::read_from(reader, ctrl_msg).map(Self::ExitTt)?, RegSendTt::OP => RegSendTt::read_from(reader, ctrl_msg).map(Self::RegSendTt)?, Exit2Tt::OP => Exit2Tt::read_from(reader, ctrl_msg).map(Self::Exit2Tt)?, MonitorP::OP => MonitorP::read_from(reader, ctrl_msg).map(Self::MonitorP)?, DemonitorP::OP => DemonitorP::read_from(reader, ctrl_msg).map(Self::DemonitorP)?, MonitorPExit::OP => { MonitorPExit::read_from(reader, ctrl_msg).map(Self::MonitorPExit)? } SendSender::OP => SendSender::read_from(reader, ctrl_msg).map(Self::SendSender)?, SendSenderTt::OP => { SendSenderTt::read_from(reader, ctrl_msg).map(Self::SendSenderTt)? } PayloadExit::OP => PayloadExit::read_from(reader, ctrl_msg).map(Self::PayloadExit)?, PayloadExitTt::OP => { PayloadExitTt::read_from(reader, ctrl_msg).map(Self::PayloadExitTt)? } PayloadExit2::OP => { PayloadExit2::read_from(reader, ctrl_msg).map(Self::PayloadExit2)? } PayloadExit2Tt::OP => { PayloadExit2Tt::read_from(reader, ctrl_msg).map(Self::PayloadExit2Tt)? } PayloadMonitorPExit::OP => { PayloadMonitorPExit::read_from(reader, ctrl_msg).map(Self::PayloadMonitorPExit)? } SpawnRequest::OP => { SpawnRequest::read_from(reader, ctrl_msg).map(Self::SpawnRequest)? } SpawnRequestTt::OP => { SpawnRequestTt::read_from(reader, ctrl_msg).map(Self::SpawnRequestTt)? } SpawnReply::OP => SpawnReply::read_from(reader, ctrl_msg).map(Self::SpawnReply)?, SpawnReplyTt::OP => { SpawnReplyTt::read_from(reader, ctrl_msg).map(Self::SpawnReplyTt)? } UnlinkId::OP => UnlinkId::read_from(reader, ctrl_msg).map(Self::UnlinkId)?, UnlinkIdAck::OP => UnlinkIdAck::read_from(reader, ctrl_msg).map(Self::UnlinkIdAck)?, AliasSend::OP => AliasSend::read_from(reader, ctrl_msg).map(Self::AliasSend)?, AliasSendTt::OP => AliasSendTt::read_from(reader, ctrl_msg).map(Self::AliasSendTt)?, op => return Err(crate::channel::RecvError::UnsupportedOp { op }), }; Ok(msg) } } ================================================ FILE: src/node.rs ================================================ //! Node related components. use crate::DistributionFlags; /// Local node information. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct LocalNode { /// Node name. pub name: NodeName, /// Distribution flags. pub flags: DistributionFlags, /// Incarnation identifier. pub creation: Creation, } impl LocalNode { /// Makes a new [`LocalNode`] instance with the default distribution flags. pub fn new(name: NodeName, creation: Creation) -> Self { Self { name, flags: Default::default(), creation, } } } /// Peer node information. /// /// This is similar to [`LocalNode`] but the `creation` field can be `None` as older nodes may not provide that information during the handshake. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PeerNode { /// Node name. pub name: NodeName, /// Distribution flags. pub flags: DistributionFlags, /// Incarnation identifier. pub creation: Option, } /// Errors that can occur while parsing node names. #[derive(Debug)] #[non_exhaustive] #[allow(missing_docs)] pub enum NodeNameError { /// Node name length must be less than 256. TooLongName { size: usize }, /// Name part of a node name is empty. EmptyName, /// Host part of a node name is empty. EmptyHost, /// Node name must contain an '@' character. MissingAtmark, } impl std::fmt::Display for NodeNameError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::TooLongName { size } => { write!( f, "node name length must be less than 256, but got {size} characters" ) } Self::EmptyName => write!(f, "the name part of a node name is empty"), Self::EmptyHost => write!(f, "the host part of a node name is empty"), Self::MissingAtmark => write!(f, "node name must contain an '@' character"), } } } impl std::error::Error for NodeNameError {} /// Full node name with the format "{NAME}@{HOST}". #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NodeName { name: String, host: String, } impl NodeName { /// Makes a new [`NodeName`] instance. pub fn new(name: &str, host: &str) -> Result { let size = name.len() + 1 + host.len(); if size > 255 { Err(NodeNameError::TooLongName { size }) } else if name.is_empty() { Err(NodeNameError::EmptyName) } else if host.is_empty() { Err(NodeNameError::EmptyHost) } else { Ok(Self { name: name.to_owned(), host: host.to_owned(), }) } } /// Returns the name part. pub fn name(&self) -> &str { &self.name } /// Returns the host part. pub fn host(&self) -> &str { &self.host } /// Returns the name length. /// /// Note that the result will never be less than `3`. #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { self.name.len() + 1 + self.host.len() } } impl std::str::FromStr for NodeName { type Err = NodeNameError; fn from_str(s: &str) -> Result { let mut tokens = s.splitn(2, '@'); if let (Some(name), Some(host)) = (tokens.next(), tokens.next()) { Self::new(name, host) } else { Err(NodeNameError::MissingAtmark) } } } impl std::fmt::Display for NodeName { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}@{}", self.name, self.host) } } /// Incarnation identifier of a node. /// /// [`Creation`] is used by the node to create its pids, ports and references. /// If the node restarts, the value of [`Creation`] will be changed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Creation(u32); impl Creation { /// Makes a new [`Creation`] instance. pub const fn new(n: u32) -> Self { Self(n) } /// Makes a new [`Creation`] instance having a random value. pub fn random() -> Self { Self(rand::random()) } /// Gets the value. pub const fn get(self) -> u32 { self.0 } } ================================================ FILE: src/term.rs ================================================ //! Erlang terms. pub use eetf::{ Atom, BigInteger, Binary, BitBinary, ExternalFun, FixInteger, Float, ImproperList, InternalFun, List, Map, Pid, Port, Reference, Term, Tuple, }; /// [`Pid`] or [`Atom`] #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub enum PidOrAtom { Pid(Pid), Atom(Atom), } impl From for Term { fn from(v: PidOrAtom) -> Self { match v { PidOrAtom::Pid(v) => v.into(), PidOrAtom::Atom(v) => v.into(), } } } /// { [`Atom`], [`Atom`], [`FixInteger`] } #[derive(Debug, Clone, PartialEq)] #[allow(missing_docs)] pub struct Mfa { pub module: Atom, pub function: Atom, pub arity: FixInteger, } impl From for Term { fn from(v: Mfa) -> Self { Tuple::from(vec![v.module.into(), v.function.into(), v.arity.into()]).into() } }