Skip to content

Commit dfbac09

Browse files
committed
Add erlang-based epmd server
Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent 1b64b00 commit dfbac09

File tree

4 files changed

+256
-2
lines changed

4 files changed

+256
-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: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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:setopt(Socket, {socket, linger}, #{onoff => true, linger => 0}),
70+
ok = socket:bind(Socket, #{
71+
family => inet,
72+
port => Port,
73+
addr => {0, 0, 0, 0}
74+
}),
75+
ok = socket:listen(Socket),
76+
RandCreation = 42,
77+
State0 = #state{
78+
socket = Socket, port = Port, recv_selects = [], clients = [], creation = RandCreation
79+
},
80+
State1 = accept(State0),
81+
{ok, State1}.
82+
83+
%% @hidden
84+
handle_call(_Msg, _From, State) ->
85+
{noreply, State}.
86+
87+
%% @hidden
88+
handle_cast(_Msg, State) ->
89+
{noreply, State}.
90+
91+
%% @hidden
92+
handle_info(
93+
{'$socket', _Socket, abort, {Ref, closed}},
94+
#state{clients = Clients0, recv_selects = RecvSelects0} = State
95+
) ->
96+
Clients1 = lists:keydelete(Ref, 3, Clients0),
97+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
98+
{noreply, State#state{clients = Clients1, recv_selects = RecvSelects1}};
99+
handle_info({'$socket', Socket, select, Ref}, #state{socket = Socket, accept_handle = Ref} = State) ->
100+
NewState = accept(State),
101+
{noreply, NewState};
102+
handle_info(
103+
{'$socket', Socket, select, Ref},
104+
#state{clients = Clients0, recv_selects = RecvSelects0} = State
105+
) ->
106+
NewState =
107+
case lists:keyfind(Ref, 1, RecvSelects0) of
108+
{Ref, client} ->
109+
socket:close(Socket),
110+
Clients1 = lists:keydelete(Ref, 3, Clients0),
111+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
112+
State#state{clients = Clients1, recv_selects = RecvSelects1};
113+
{Ref, req_size} ->
114+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
115+
client_socket_recv_req_size(Socket, State#state{recv_selects = RecvSelects1});
116+
{Ref, req, Size} ->
117+
RecvSelects1 = lists:keydelete(Ref, 1, RecvSelects0),
118+
client_socket_recv_req(Socket, Size, State#state{recv_selects = RecvSelects1});
119+
false ->
120+
State
121+
end,
122+
{noreply, NewState}.
123+
124+
%% @hidden
125+
terminate(_Reason, _State) ->
126+
ok.
127+
128+
accept(#state{socket = Socket} = State) ->
129+
case socket:accept(Socket, nowait) of
130+
{select, {select_info, accept, Ref}} ->
131+
State#state{accept_handle = Ref};
132+
{ok, ClientSocket} ->
133+
State1 = client_socket_recv_req_size(ClientSocket, State),
134+
accept(State1)
135+
end.
136+
137+
client_socket_recv_req_size(Socket, #state{recv_selects = RecvSelects} = State) ->
138+
case socket:recv(Socket, 2, nowait) of
139+
{select, {select_info, recv, Ref}} ->
140+
State#state{recv_selects = [{Ref, req_size} | RecvSelects]};
141+
{ok, <<Size:16>>} ->
142+
client_socket_recv_req(Socket, Size, State)
143+
end.
144+
145+
client_socket_recv_req(Socket, Size, #state{recv_selects = RecvSelects} = State) ->
146+
case socket:recv(Socket, Size, nowait) of
147+
{select, {select_info, recv, Ref}} ->
148+
State#state{recv_selects = [{Ref, req, Size} | RecvSelects]};
149+
{ok, Data} ->
150+
process_req(Data, Socket, State)
151+
end.
152+
153+
process_req(
154+
<<?ALIVE2_REQ, Port:16, NodeType, Protocol, HighestVersion:16, LowestVersion:16, NameLen:16,
155+
Name:NameLen/binary, ExtraLen:16, ExtraData:ExtraLen/binary>>,
156+
Socket,
157+
#state{clients = Clients, recv_selects = RecvSelects, creation = Creation} = State
158+
) ->
159+
case socket:recv(Socket, 1, nowait) of
160+
{select, {select_info, recv, Ref}} ->
161+
socket:send(Socket, <<?ALIVE2_X_RESP, 0, Creation:32>>),
162+
State#state{
163+
recv_selects = [{Ref, client} | RecvSelects],
164+
clients = [
165+
{Name, Port, Ref,
166+
<<Port:16, NodeType, Protocol, HighestVersion:16, LowestVersion:16,
167+
NameLen:16, Name:NameLen/binary, ExtraLen:16,
168+
ExtraData:ExtraLen/binary>>}
169+
| Clients
170+
],
171+
creation = (Creation + 1) rem 16#ffffffff
172+
};
173+
{ok, <<_Byte>>} ->
174+
socket:close(Socket),
175+
State;
176+
{error, closed} ->
177+
State
178+
end;
179+
process_req(<<?PORT_PLEASE2_REQ, Name/binary>>, Socket, #state{clients = Clients} = State) ->
180+
case lists:keyfind(Name, 1, Clients) of
181+
false ->
182+
ok = socket:send(Socket, <<?PORT2_RESP, 1>>);
183+
{Name, _Port, _Ref, Data} ->
184+
ok = socket:send(Socket, <<?PORT2_RESP, 0, Data/binary>>)
185+
end,
186+
socket:close(Socket),
187+
State;
188+
process_req(<<?NAMES_REQ>>, Socket, #state{clients = Clients, port = Port} = State) ->
189+
ok = socket:send(Socket, <<Port:32>>),
190+
lists:foreach(
191+
fun({NodeName, NodePort, _Ref, _Data}) ->
192+
Line = iolist_to_binary(io_lib:format("name ~ts at port ~p~n", [NodeName, NodePort])),
193+
ok = socket:send(Socket, Line)
194+
end,
195+
Clients
196+
),
197+
socket:close(Socket),
198+
State.

tests/libs/estdlib/test_epmd.erl

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

2525
test() ->
26+
case stop_epmd() of
27+
ok ->
28+
{ok, Pid} = epmd:start_link([]),
29+
ok = test_client(),
30+
ok = test_two_clients(),
31+
MonitorRef = monitor(process, Pid),
32+
unlink(Pid),
33+
exit(Pid, shutdown),
34+
ok =
35+
receive
36+
{'DOWN', MonitorRef, process, Pid, shutdown} -> ok
37+
after 5000 -> timeout
38+
end,
39+
ok;
40+
{error, not_found} ->
41+
ok
42+
end,
2643
case start_epmd() of
2744
ok ->
2845
ok = test_client(),
@@ -56,11 +73,49 @@ ensure_epmd("ATOM") ->
5673
ok = atomvm:posix_close(Fd),
5774
ok.
5875

76+
stop_epmd("BEAM") ->
77+
case os:cmd("epmd -kill") of
78+
"Killed\n" ->
79+
timer:sleep(500),
80+
ok;
81+
"epmd: Cannot connect to local epmd\n" ->
82+
ok;
83+
"Killing not allowed - " ->
84+
{error, not_allowed}
85+
end;
86+
stop_epmd("ATOM") ->
87+
{ok, _, Fd} = atomvm:subprocess("/bin/sh", ["sh", "-c", "epmd -kill 2>&1"], [], [stdout]),
88+
Result =
89+
case atomvm:posix_read(Fd, 200) of
90+
eof ->
91+
{error, eof};
92+
{ok, <<"Killed\n">>} ->
93+
timer:sleep(500),
94+
ok;
95+
{ok, <<"epmd: Cannot connect to local epmd\n">>} ->
96+
ok;
97+
{ok, <<"Killing not allowed - ", _/binary>>} ->
98+
{error, not_allowed}
99+
end,
100+
ok = atomvm:posix_close(Fd),
101+
Result.
102+
59103
start_epmd() ->
60104
Platform = erlang:system_info(machine),
61105
case has_epmd(Platform) of
62106
true ->
63-
ok = ensure_epmd(Platform);
107+
ok = ensure_epmd(Platform),
108+
timer:sleep(500),
109+
ok;
110+
false ->
111+
{error, not_found}
112+
end.
113+
114+
stop_epmd() ->
115+
Platform = erlang:system_info(machine),
116+
case has_epmd(Platform) of
117+
true ->
118+
stop_epmd(Platform);
64119
false ->
65120
{error, not_found}
66121
end.

0 commit comments

Comments
 (0)