Skip to content

Commit ffa26d1

Browse files
committed
Merge pull request atomvm#1419 from pguyot/w52/add-erl_epmd
Add `erl_epmd` These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
2 parents 1f576a9 + 21117df commit ffa26d1

File tree

6 files changed

+466
-0
lines changed

6 files changed

+466
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Added `supervisor:terminate_child/2`, `supervisor:restart_child/2` and `supervisor:delete_child/2`
2424
- Added `atomvm:subprocess/4` to perform pipe/fork/execve on POSIX platforms
2525
- Added `externalterm_to_term_with_roots` to efficiently preserve roots when allocating memory for external terms.
26+
- Added `erl_epmd` client implementation to epmd using `socket` module
2627

2728
### Changed
2829

libs/estdlib/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ set(ERLANG_MODULES
2828
calendar
2929
code
3030
crypto
31+
erl_epmd
3132
erts_debug
3233
ets
3334
gen_event

libs/estdlib/src/erl_epmd.erl

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2024 Paul Guyot <pguyot@kallisys.net>
5+
%
6+
% Licensed under the Apache License, Version 2.0 (the "License");
7+
% you may not use this file except in compliance with the License.
8+
% You may obtain a copy of the License at
9+
%
10+
% http://www.apache.org/licenses/LICENSE-2.0
11+
%
12+
% Unless required by applicable law or agreed to in writing, software
13+
% distributed under the License is distributed on an "AS IS" BASIS,
14+
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
% See the License for the specific language governing permissions and
16+
% limitations under the License.
17+
%
18+
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
19+
%
20+
21+
%%-----------------------------------------------------------------------------
22+
%% @doc An implementation of the Erlang/OTP erl_epmd interface.
23+
%%
24+
%% This module implements a strict subset of the Erlang/OTP erl_epmd
25+
%% interface.
26+
%% @end
27+
%%-----------------------------------------------------------------------------
28+
-module(erl_epmd).
29+
30+
% API
31+
-export([
32+
start_link/0,
33+
stop/0,
34+
port_please/2,
35+
register_node/2,
36+
names/1
37+
]).
38+
39+
% gen_server
40+
-behaviour(gen_server).
41+
-export([
42+
init/1,
43+
handle_call/3,
44+
handle_cast/2,
45+
handle_info/2,
46+
terminate/2,
47+
code_change/3
48+
]).
49+
50+
-record(state, {socket = undefined}).
51+
52+
-define(EPMD_PORT, 4369).
53+
-define(TIMEOUT, 5000).
54+
55+
-define(NAMES_REQ, 110).
56+
-define(ALIVE2_X_RESP, 118).
57+
-define(PORT2_RESP, 119).
58+
-define(ALIVE2_REQ, 120).
59+
-define(ALIVE2_RESP, 121).
60+
-define(PORT_PLEASE2_REQ, 122).
61+
62+
-define(TCP_INET4_PROTOCOL, 0).
63+
-define(ERLANG_NODE_TYPE, 77).
64+
-define(VERSION, 6).
65+
66+
-record(receive_port2_resp, {
67+
port_no :: non_neg_integer(),
68+
highest_version :: non_neg_integer(),
69+
lowest_version :: non_neg_integer()
70+
}).
71+
72+
-record(alive2_resp, {
73+
creation :: non_neg_integer()
74+
}).
75+
76+
%% @doc Start EPMD client
77+
-spec start_link() -> {ok, pid()}.
78+
start_link() ->
79+
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
80+
81+
%% @doc Stop EPMD client-
82+
-spec stop() -> ok.
83+
stop() ->
84+
gen_server:call(?MODULE, stop, infinity).
85+
86+
%% @param Name name of the node to get the port of
87+
%% @param Host host of the node to get the port of
88+
%% @doc Get the port and version of a node on a given host.
89+
%% This function will connect to epmd on the host.
90+
-spec port_please(Name :: iodata(), Host :: inet:hostname() | inet:ip_address()) ->
91+
{port, inet:port_number(), non_neg_integer()} | noport.
92+
port_please(Name, Host) ->
93+
case inet:getaddr(Host, inet) of
94+
{ok, IP} ->
95+
{ok, Socket} = socket:open(inet, stream, tcp),
96+
case socket:connect(Socket, #{addr => IP, port => ?EPMD_PORT, family => inet}) of
97+
ok ->
98+
NameBin = iolist_to_binary(Name),
99+
Result =
100+
case send_request(Socket, <<?PORT_PLEASE2_REQ, NameBin/binary>>) of
101+
{ok, #receive_port2_resp{
102+
port_no = PortNo,
103+
highest_version = HighVersion,
104+
lowest_version = LowVersion
105+
}} when HighVersion >= ?VERSION andalso LowVersion =< ?VERSION ->
106+
{port, PortNo, ?VERSION};
107+
{ok, #receive_port2_resp{port_no = PortNo}} ->
108+
{port, PortNo, 0};
109+
{ok, _Unexpected} ->
110+
noport;
111+
{error, _} ->
112+
noport
113+
end,
114+
ok = socket:close(Socket),
115+
Result;
116+
{error, _} ->
117+
noport
118+
end;
119+
{error, _} ->
120+
noport
121+
end.
122+
123+
%% @param Host the host to connect to
124+
%% @return a list of names and ports of registered nodes
125+
%% @doc Get the names and ports of all registered nodes
126+
%% This function will connect to epmd on localhost.
127+
-spec names(Host :: inet:hostname() | inet:ip_address()) ->
128+
{ok, [{string(), inet:port_number()}]} | {error, any()}.
129+
names(Host) ->
130+
case inet:getaddr(Host, inet) of
131+
{ok, IP} ->
132+
{ok, Socket} = socket:open(inet, stream, tcp),
133+
case socket:connect(Socket, #{addr => IP, port => ?EPMD_PORT, family => inet}) of
134+
ok ->
135+
Result =
136+
case socket:send(Socket, <<1:16, ?NAMES_REQ>>) of
137+
ok ->
138+
case socket:recv(Socket, 4, ?TIMEOUT) of
139+
{ok, <<?EPMD_PORT:32>>} ->
140+
receive_names_loop(Socket, <<>>, []);
141+
{ok, Unexpected} ->
142+
{error, {unexpected, Unexpected}};
143+
{error, _} = ErrRecv ->
144+
ErrRecv
145+
end;
146+
{error, _} = ErrSend ->
147+
ErrSend
148+
end,
149+
ok = socket:close(Socket),
150+
Result;
151+
{error, _} = ErrConnect ->
152+
ErrConnect
153+
end;
154+
{error, _} = ErrGetAddr ->
155+
ErrGetAddr
156+
end.
157+
158+
receive_names_loop(Socket, AccBuffer, AccL) ->
159+
case binary:split(AccBuffer, <<"\n">>) of
160+
[AccBuffer] ->
161+
case socket:recv(Socket, 0, ?TIMEOUT) of
162+
{error, closed} when AccBuffer =:= <<>> -> {ok, lists:reverse(AccL)};
163+
{error, _} = ErrT -> ErrT;
164+
{ok, Data} -> receive_names_loop(Socket, <<Data/binary, AccBuffer/binary>>, AccL)
165+
end;
166+
[<<"name ", RestLine/binary>>, RestBuffer] ->
167+
case binary:split(RestLine, <<" at port ">>) of
168+
[NameBin, PortBin] ->
169+
try binary_to_integer(PortBin) of
170+
Port ->
171+
receive_names_loop(Socket, RestBuffer, [
172+
{binary_to_list(NameBin), Port} | AccL
173+
])
174+
catch
175+
error:badarg ->
176+
{error, {unexpected, <<"name ", RestLine/binary>>}}
177+
end;
178+
[_] ->
179+
{error, {unexpected, <<"name ", RestLine/binary>>}}
180+
end;
181+
[UnexpectedLine, _RestBuffer] ->
182+
{error, {unexpected, UnexpectedLine}}
183+
end.
184+
185+
%% @param Name name to register
186+
%% @param Port port to register
187+
%% @doc Register to local epmd and get a creation number
188+
-spec register_node(Name :: iodata(), Port :: inet:port_number()) ->
189+
{ok, non_neg_integer()} | {error, any()}.
190+
register_node(Name, Port) ->
191+
gen_server:call(?MODULE, {register_node, Name, Port}, infinity).
192+
193+
%% @hidden
194+
init([]) ->
195+
State = #state{},
196+
{ok, State}.
197+
198+
%% @hidden
199+
handle_call({register_node, _Name, _Port}, _From, #state{socket = Socket} = State) when
200+
Socket =/= undefined
201+
->
202+
{reply, {error, already_registered}, State};
203+
handle_call({register_node, Name, Port}, _From, #state{} = State) ->
204+
{ok, Socket} = socket:open(inet, stream, tcp),
205+
case socket:connect(Socket, #{addr => {127, 0, 0, 1}, port => ?EPMD_PORT, family => inet}) of
206+
ok ->
207+
NameBin = iolist_to_binary(Name),
208+
NameLen = byte_size(NameBin),
209+
Packet =
210+
<<?ALIVE2_REQ, Port:16, ?ERLANG_NODE_TYPE, ?TCP_INET4_PROTOCOL, ?VERSION:16,
211+
?VERSION:16, NameLen:16, NameBin/binary, 0:16>>,
212+
case send_request(Socket, Packet) of
213+
{ok, #alive2_resp{creation = Creation}} ->
214+
{reply, {ok, Creation}, State#state{socket = Socket}};
215+
{error, _} = RequestErr ->
216+
socket:close(Socket),
217+
{reply, RequestErr, State}
218+
end;
219+
{error, _} = ConnectErr ->
220+
socket:close(Socket),
221+
{reply, ConnectErr, State}
222+
end;
223+
handle_call(stop, _From, State) ->
224+
{stop, shutdown, ok, State}.
225+
226+
%% @hidden
227+
handle_cast(_Message, State) ->
228+
{noreply, State}.
229+
230+
%% @hidden
231+
handle_info(_Message, State) ->
232+
{noreply, State}.
233+
234+
%% @hidden
235+
terminate(_Reason, #state{socket = Socket}) ->
236+
case Socket of
237+
undefined -> ok;
238+
_ -> socket:close(Socket)
239+
end,
240+
ok.
241+
242+
%% @hidden
243+
code_change(_OldVsn, State, _Extra) ->
244+
{ok, State}.
245+
246+
send_request(Socket, Request) ->
247+
RequestSize = byte_size(Request),
248+
case socket:send(Socket, <<(RequestSize):16, Request/binary>>) of
249+
ok ->
250+
case socket:recv(Socket, 1, ?TIMEOUT) of
251+
{ok, <<?PORT2_RESP>>} ->
252+
receive_port2_resp(Socket);
253+
{ok, <<?ALIVE2_X_RESP>>} ->
254+
receive_alive2_x_resp(Socket);
255+
{ok, <<?ALIVE2_RESP>>} ->
256+
receive_alive2_resp(Socket);
257+
{error, _} = ErrorRecv2 ->
258+
ErrorRecv2
259+
end;
260+
{error, _} = ErrorSend ->
261+
ErrorSend
262+
end.
263+
264+
receive_port2_resp(Socket) ->
265+
case socket:recv(Socket, 1, ?TIMEOUT) of
266+
{ok, <<0>>} ->
267+
case socket:recv(Socket, 10, ?TIMEOUT) of
268+
{ok,
269+
<<PortNo:16, _NodeType, _Protocol, HighestVersion:16, LowestVersion:16,
270+
NameLen:16>>} ->
271+
case socket:recv(Socket, NameLen + 2, ?TIMEOUT) of
272+
{ok, <<_Name:NameLen/binary, ExtraLen:16>>} ->
273+
case ExtraLen of
274+
0 ->
275+
{ok, #receive_port2_resp{
276+
port_no = PortNo,
277+
highest_version = HighestVersion,
278+
lowest_version = LowestVersion
279+
}};
280+
N ->
281+
case socket:recv(Socket, N, ?TIMEOUT) of
282+
{ok, _ExtraData} ->
283+
{ok, #receive_port2_resp{
284+
port_no = PortNo,
285+
highest_version = HighestVersion,
286+
lowest_version = LowestVersion
287+
}};
288+
{error, _} = ErrT1 ->
289+
ErrT1
290+
end
291+
end;
292+
{error, _} = ErrT2 ->
293+
ErrT2
294+
end;
295+
{error, _} = ErrT3 ->
296+
ErrT3
297+
end;
298+
{ok, <<N>>} ->
299+
{error, N};
300+
{error, _} = ErrT4 ->
301+
ErrT4
302+
end.
303+
304+
receive_alive2_x_resp(Socket) ->
305+
case socket:recv(Socket, 5, ?TIMEOUT) of
306+
{ok, <<0, Creation:32>>} -> {ok, #alive2_resp{creation = Creation}};
307+
{ok, <<Err, _:32>>} -> {error, Err};
308+
{error, _} = ErrT -> ErrT
309+
end.
310+
311+
receive_alive2_resp(Socket) ->
312+
case socket:recv(Socket, 3, ?TIMEOUT) of
313+
{ok, <<0, Creation:16>>} -> {ok, #alive2_resp{creation = Creation}};
314+
{ok, <<Err, _:16>>} -> {error, Err};
315+
{error, _} = ErrT -> ErrT
316+
end.

tests/libs/estdlib/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(ERLANG_MODULES
2626
test_apply
2727
test_binary
2828
test_calendar
29+
test_epmd
2930
test_gen_event
3031
test_gen_server
3132
test_gen_statem

0 commit comments

Comments
 (0)