Skip to content

Commit 439f9eb

Browse files
committed
Merge pull request #1465 from pguyot/w02/add-epmd
Add erlang-based epmd server 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 6cdbb3d + 6ac823e commit 439f9eb

File tree

4 files changed

+270
-2
lines changed

4 files changed

+270
-2
lines changed

.github/workflows/run-tests-with-beam.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ jobs:
142142
working-directory: build
143143
run: |
144144
export PATH="${{ matrix.path_prefix }}$PATH"
145-
erl -pa tests/libs/estdlib/ -pa tests/libs/estdlib/beams/ -pa libs/etest/src/beams -s tests -s init stop -noshell
145+
erl -pa tests/libs/estdlib/ -pa tests/libs/estdlib/beams/ -pa libs/etest/src/beams -pa libs/eavmlib/src/beams -s tests -s init stop -noshell
146146
147147
# Test
148148
- name: "Run tests/libs/etest/test_eunit with OTP eunit"

libs/eavmlib/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
avm_pubsub
2929
console
3030
emscripten
31+
epmd
3132
esp
3233
esp_adc
3334
gpio

libs/eavmlib/src/epmd.erl

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2025 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+
-module(epmd).
22+
23+
-behaviour(gen_server).
24+
25+
-export([start_link/1]).
26+
27+
% gen_server API
28+
-export([
29+
init/1,
30+
handle_call/3,
31+
handle_cast/2,
32+
handle_info/2,
33+
terminate/2
34+
]).
35+
36+
-define(NAMES_REQ, 110).
37+
-define(ALIVE2_X_RESP, 118).
38+
-define(PORT2_RESP, 119).
39+
-define(ALIVE2_REQ, 120).
40+
-define(ALIVE2_RESP, 121).
41+
-define(PORT_PLEASE2_REQ, 122).
42+
43+
-define(EPMD_DEFAULT_PORT, 4369).
44+
-type epmd_config_option() :: {port, non_neg_integer()}.
45+
-type epmd_config() :: [epmd_config_option()].
46+
47+
-spec start_link(Config :: epmd_config()) -> {ok, pid()} | {error, Reason :: term()}.
48+
start_link(Config) ->
49+
gen_server:start_link({local, ?MODULE}, ?MODULE, Config, []).
50+
51+
%%
52+
%% gen_server callbacks
53+
%%
54+
55+
-record(state, {
56+
socket :: any(),
57+
port :: non_neg_integer(),
58+
accept_handle :: undefined | reference(),
59+
recv_selects :: [tuple()],
60+
clients :: [{binary(), non_neg_integer(), reference(), binary()}],
61+
creation :: non_neg_integer()
62+
}).
63+
64+
%% @hidden
65+
init(Config) ->
66+
Port = proplists:get_value(port, Config, ?EPMD_DEFAULT_PORT),
67+
{ok, Socket} = socket:open(inet, stream, tcp),
68+
ok = socket:setopt(Socket, {socket, reuseaddr}, true),
69+
ok = socket:bind(Socket, #{
70+
family => inet,
71+
port => Port,
72+
addr => {0, 0, 0, 0}
73+
}),
74+
ok = socket:listen(Socket),
75+
RandCreation = 42,
76+
State0 = #state{
77+
socket = Socket, port = Port, recv_selects = [], clients = [], creation = RandCreation
78+
},
79+
State1 = accept(State0),
80+
{ok, State1}.
81+
82+
%% @hidden
83+
handle_call(_Msg, _From, State) ->
84+
{noreply, State}.
85+
86+
%% @hidden
87+
handle_cast(_Msg, State) ->
88+
{noreply, State}.
89+
90+
%% @hidden
91+
handle_info(
92+
{'$socket', _Socket, abort, {Ref, closed}},
93+
#state{clients = Clients0, recv_selects = RecvSelects0} = State
94+
) ->
95+
Clients1 = lists:keydelete(Ref, 3, Clients0),
96+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
97+
{noreply, State#state{clients = Clients1, recv_selects = RecvSelects1}};
98+
handle_info({'$socket', Socket, select, Ref}, #state{socket = Socket, accept_handle = Ref} = State) ->
99+
NewState = accept(State),
100+
{noreply, NewState};
101+
handle_info(
102+
{'$socket', Socket, select, Ref},
103+
#state{clients = Clients0, recv_selects = RecvSelects0} = State
104+
) ->
105+
NewState =
106+
case lists:keyfind(Ref, 1, RecvSelects0) of
107+
{Ref, client} ->
108+
socket:close(Socket),
109+
Clients1 = lists:keydelete(Ref, 3, Clients0),
110+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
111+
State#state{clients = Clients1, recv_selects = RecvSelects1};
112+
{Ref, req_size} ->
113+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
114+
client_socket_recv_req_size(Socket, State#state{recv_selects = RecvSelects1});
115+
{Ref, req, Size} ->
116+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
117+
client_socket_recv_req(Socket, Size, State#state{recv_selects = RecvSelects1});
118+
false ->
119+
State
120+
end,
121+
{noreply, NewState}.
122+
123+
%% @hidden
124+
terminate(_Reason, _State) ->
125+
ok.
126+
127+
accept(#state{socket = Socket} = State) ->
128+
case socket:accept(Socket, nowait) of
129+
{select, {select_info, accept, Ref}} ->
130+
State#state{accept_handle = Ref};
131+
{ok, ClientSocket} ->
132+
State1 = client_socket_recv_req_size(ClientSocket, State),
133+
accept(State1)
134+
end.
135+
136+
client_socket_recv_req_size(Socket, #state{recv_selects = RecvSelects} = State) ->
137+
case socket:recv(Socket, 2, nowait) of
138+
{select, {select_info, recv, Ref}} ->
139+
State#state{recv_selects = [{Ref, req_size} | RecvSelects]};
140+
{ok, <<Size:16>>} ->
141+
client_socket_recv_req(Socket, Size, State)
142+
end.
143+
144+
client_socket_recv_req(Socket, Size, #state{recv_selects = RecvSelects} = State) ->
145+
case socket:recv(Socket, Size, nowait) of
146+
{select, {select_info, recv, Ref}} ->
147+
State#state{recv_selects = [{Ref, req, Size} | RecvSelects]};
148+
{ok, Data} ->
149+
process_req(Data, Socket, State)
150+
end.
151+
152+
process_req(
153+
<<?ALIVE2_REQ, Port:16, NodeType, Protocol, HighestVersion:16, LowestVersion:16, NameLen:16,
154+
Name:NameLen/binary, ExtraLen:16, ExtraData:ExtraLen/binary>>,
155+
Socket,
156+
#state{clients = Clients, recv_selects = RecvSelects, creation = Creation} = State
157+
) ->
158+
case socket:recv(Socket, 1, nowait) of
159+
{select, {select_info, recv, Ref}} ->
160+
socket:send(Socket, <<?ALIVE2_X_RESP, 0, Creation:32>>),
161+
State#state{
162+
recv_selects = [{Ref, client} | RecvSelects],
163+
clients = [
164+
{Name, Port, Ref,
165+
<<Port:16, NodeType, Protocol, HighestVersion:16, LowestVersion:16,
166+
NameLen:16, Name:NameLen/binary, ExtraLen:16,
167+
ExtraData:ExtraLen/binary>>}
168+
| Clients
169+
],
170+
creation = (Creation + 1) rem 16#ffffffff
171+
};
172+
{ok, <<_Byte>>} ->
173+
socket:close(Socket),
174+
State;
175+
{error, closed} ->
176+
State
177+
end;
178+
process_req(<<?PORT_PLEASE2_REQ, Name/binary>>, Socket, #state{clients = Clients} = State) ->
179+
case lists:keyfind(Name, 1, Clients) of
180+
false ->
181+
ok = socket:send(Socket, <<?PORT2_RESP, 1>>);
182+
{Name, _Port, _Ref, Data} ->
183+
ok = socket:send(Socket, <<?PORT2_RESP, 0, Data/binary>>)
184+
end,
185+
socket:close(Socket),
186+
State;
187+
process_req(<<?NAMES_REQ>>, Socket, #state{clients = Clients, port = Port} = State) ->
188+
ok = socket:send(Socket, <<Port:32>>),
189+
lists:foreach(
190+
fun({NodeName, NodePort, _Ref, _Data}) ->
191+
Line = iolist_to_binary(io_lib:format("name ~ts at port ~p~n", [NodeName, NodePort])),
192+
ok = socket:send(Socket, Line)
193+
end,
194+
Clients
195+
),
196+
socket:close(Socket),
197+
State.

tests/libs/estdlib/test_epmd.erl

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,37 @@
2323
-export([test/0]).
2424

2525
test() ->
26+
% AtomVM's epmd only runs on AtomVM and OTP 24+
27+
CanRunEpmd =
28+
case erlang:system_info(machine) of
29+
"ATOM" ->
30+
true;
31+
"BEAM" ->
32+
OTPRelease = erlang:system_info(otp_release),
33+
OTPRelease >= "24"
34+
end,
35+
if
36+
CanRunEpmd ->
37+
case stop_epmd() of
38+
ok ->
39+
{ok, Pid} = epmd:start_link([]),
40+
ok = test_client(),
41+
ok = test_two_clients(),
42+
MonitorRef = monitor(process, Pid),
43+
unlink(Pid),
44+
exit(Pid, shutdown),
45+
ok =
46+
receive
47+
{'DOWN', MonitorRef, process, Pid, shutdown} -> ok
48+
after 5000 -> timeout
49+
end,
50+
ok;
51+
{error, not_found} ->
52+
ok
53+
end;
54+
true ->
55+
ok
56+
end,
2657
case start_epmd() of
2758
ok ->
2859
ok = test_client(),
@@ -58,11 +89,50 @@ ensure_epmd("ATOM") ->
5889
ok = atomvm:posix_close(Fd),
5990
ok.
6091

92+
stop_epmd("BEAM") ->
93+
case os:cmd("epmd -kill") of
94+
"Killed\n" ->
95+
timer:sleep(500),
96+
ok;
97+
"epmd: Cannot connect to local epmd\n" ->
98+
ok;
99+
"Killing not allowed - " ->
100+
{error, not_allowed}
101+
end;
102+
stop_epmd("ATOM") ->
103+
{ok, _, Fd} = atomvm:subprocess("/bin/sh", ["sh", "-c", "epmd -kill 2>&1"], undefined, [stdout]),
104+
Result =
105+
case atomvm:posix_read(Fd, 200) of
106+
eof ->
107+
{error, eof};
108+
{ok, <<"Killed\n">>} ->
109+
timer:sleep(500),
110+
ok;
111+
{ok, <<"epmd: Cannot connect to local epmd\n">>} ->
112+
ok;
113+
{ok, <<"Killing not allowed - ", _/binary>>} ->
114+
{error, not_allowed}
115+
end,
116+
ok = atomvm:posix_close(Fd),
117+
Result.
118+
61119
start_epmd() ->
62120
Platform = erlang:system_info(machine),
63121
case has_epmd(Platform) of
64122
true ->
65-
ok = ensure_epmd(Platform);
123+
ok = ensure_epmd(Platform),
124+
% on CI, epmd may be slow to accept connections
125+
timer:sleep(500),
126+
ok;
127+
false ->
128+
{error, not_found}
129+
end.
130+
131+
stop_epmd() ->
132+
Platform = erlang:system_info(machine),
133+
case has_epmd(Platform) of
134+
true ->
135+
stop_epmd(Platform);
66136
false ->
67137
{error, not_found}
68138
end.

0 commit comments

Comments
 (0)