Current section
Files
Jump to
Current section
Files
src/epmdless_client.erl
-module(epmdless_client).
-behaviour(gen_server).
%% @doc
%% Module which used as a callback which passed to erlang vm via `-epmd_module` attribute
%% @end
%% epmd callbacks
-export([start_link/0,
register_node/3,
address_please/3,
port_please/2,
listen_port_please/2,
names/1]).
-export([init/1,
handle_call/3,
handle_cast/2]).
%% functions for interacting with the map of nodes to ports
-export([add_node/2,
add_node/4,
remove_node/1,
list_nodes/0]).
%% auxiliary funcs
-export([get_info/0]).
-include_lib("kernel/include/inet.hrl").
-record(state, {
dist_port :: inet:port_number(),
nodes = #{} :: map()
}).
-ifdef(OTP_VERSION).
-if(?OTP_VERSION < 23).
-define(ERL_DIST_VER, 5). % OTP-22 or (much) older
-else.
-define(ERL_DIST_VER, 6). % OTP-23 (or maybe newer?)
-endif.
-else.
-define(ERL_DIST_VER, 5). % OTP-22 or (much) older
-endif.
start_link() ->
%% new option that allows users to not have to use epmdless_proto_dist
%% instead only set ERL_DIST_PORT
Port = case os:getenv("ERL_DIST_PORT") of
false ->
0;
PortString ->
list_to_integer(PortString)
end,
gen_server:start_link({local, ?MODULE}, ?MODULE, [Port], []).
-spec register_node(Name, Port, Family) -> {ok, CreationId} when
Name :: atom(),
Port :: inet:port_number(),
Family :: atom(),
CreationId :: 1..3.
register_node(_Name, Port, _Family) ->
gen_server:call(?MODULE, {port, Port}, infinity),
{ok, rand:uniform(3)}.
-spec port_please(Name, Host) -> {port, Port, Version} | noport when
Name :: atom(),
Host :: inet:hostname() | inet:ip_address(),
Port :: inet:port_number(),
Version :: 5 | 6.
%% @doc
%% request port of node `Name`
%% @end
port_please(Name, Host) ->
case gen_server:call(?MODULE, {port_please, Name, Host}, infinity) of
{ok, Port} ->
{port, Port, ?ERL_DIST_VER};
{error, noport} ->
case os:getenv("EPMDLESS_REMSH_PORT") of
false ->
%% new option that allows users to not have to use epmdless_proto_dist
%% instead only set ERL_DIST_PORT
case os:getenv("ERL_DIST_PORT") of
false ->
noport;
PortString ->
{port, list_to_integer(PortString), ?ERL_DIST_VER}
end;
RemotePort ->
{port, list_to_integer(RemotePort), ?ERL_DIST_VER}
end
end.
-spec address_please(Name, Host, AddressFamily) -> Success | {error, term()} when
Name :: atom(),
Host :: string() | inet:ip_address(),
AddressFamily :: inet | inet6 | local,
Port :: non_neg_integer(),
Version :: non_neg_integer(),
Success :: {ok, inet:ip_address(), Port, Version}.
%% @doc Resolves the Host to an IP address of a remote node.
address_please(Name, Host, AddressFamily) ->
{ok, Address} = inet:getaddr(Host, AddressFamily),
case port_please(Name, Address) of
{port, Port, Version} ->
{ok, Address, Port, Version};
noport ->
{error, noport}
end.
%% @doc Returns the port the local node should listen to when accepting new distribution requests.
listen_port_please(Name, Host) ->
gen_server:call(?MODULE, {listen_port, Name, Host}, infinity).
-spec add_node(Node, Port) -> ok when
Node :: atom(),
Port :: inet:port_number().
add_node(Node, Port) ->
case split_node(Node) of
{ok, {NodeName, Host, IP}} ->
add_node(NodeName, Host, IP, Port);
{error, Reason} ->
{error, Reason}
end.
-spec add_node(NodeName, Host, IP, Port) -> ok when
NodeName :: string(),
Host :: inet:hostname() | inet:ip_address(),
IP :: inet:ip_address(),
Port :: inet:port_number().
add_node(NodeName, Host, IP, Port) ->
ok = gen_server:call(?MODULE, {add_node, NodeName, Host, IP, Port}, infinity).
-spec list_nodes() -> [{Node, {Host, Port}}] when
Node :: {atom(), inet:ip_address()},
Host :: inet:hostname() | inet:ip_address(),
Port :: {inet:port_number()}.
list_nodes() ->
Nodes = gen_server:call(?MODULE, list_nodes, infinity),
maps:to_list(Nodes).
-spec remove_node(Node) -> ok when
Node :: atom().
remove_node(Node) ->
case split_node(Node) of
{ok, {NodeName, _Host, IP}} ->
ok = gen_server:call(?MODULE, {remove_node, NodeName, IP}, infinity);
{error, Reason} ->
{error, Reason}
end.
%% @doc
%% List the Erlang nodes on a certain host. We don't need that
%% @end
names(_Hostname) ->
{error, address}.
-spec get_info() -> Info when
Info :: [{dist_port, inet:port_number()}].
get_info() ->
gen_server:call(?MODULE, get_info, infinity).
init([Port]) ->
{ok, #state{dist_port=Port}}.
handle_call(list_nodes, _From, State) ->
{reply, State#state.nodes, State};
handle_call({add_node, NodeName, Host, IP, Port}, _From, State) ->
{reply, ok, State#state{nodes=maps:put({NodeName, IP}, {Host, Port}, State#state.nodes)}};
handle_call({remove_node, NodeName, IP}, _From, State) ->
{reply, ok, State#state{nodes=maps:remove({NodeName, IP}, State#state.nodes)}};
handle_call({port_please, Node, IP}, _From, State) ->
Reply = case maps:find({Node, IP}, State#state.nodes) of
error ->
{error, noport};
{ok, {_H, P}} ->
{ok, P}
end,
{reply, Reply, State};
handle_call({port, DistPort}, _From, State) ->
{reply, ok, State#state{dist_port=DistPort}};
handle_call({listen_port, _Name, _Host}, _From, State=#state{dist_port=DistPort}) ->
{reply, {ok, DistPort}, State};
handle_call(get_info, _From, State=#state{dist_port=DistPort}) ->
{reply, [{dist_port, DistPort}], State};
handle_call(_Msg, _From, State) ->
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
%%
%% TODO: support node's with @ using inet_db:gethostname() and inet_db:res_option(domain)
split_node(Name) ->
case lists:splitwith(fun(C) -> C =/= $@ end, atom_to_list(Name)) of
{NodeName, [$@ | Host]} ->
%% TODO: support ipv6
case inet:getaddr(Host, inet) of
{ok, IP} ->
{ok, {NodeName, Host, IP}};
{error, Reason} ->
{error, Reason}
end;
_ ->
{error, {bad_name, Name}}
end.