-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, host_please/1, port_please/2, names/1]). %% gen server callbacks -export([init/1, handle_info/2, handle_cast/2, handle_call/3, terminate/2, code_change/3]). %% db funcs -export([add_node/2, add_node/3, remove_node/1, list_nodes/0]). %% auxiliary funcs -export([get_info/0]). -record(state, { dist_port :: inet:port_number(), nodes = #{} :: map() }). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -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 host_please(Node) -> {host, Host} | nohost when Node :: atom(), Host :: inet:hostname() | inet:ip_address(). host_please(Node) -> case gen_server:call(?MODULE, {host_please, Node}, infinity) of {error, nohost} -> nohost; {ok, Host} -> {host, Host} end. -spec port_please(Name, Host) -> {port, Port, Version} | noport when Name :: atom(), Host :: inet:hostname(), Port :: inet:port_number(), Version :: 5. %% @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, 5}; {error, noport} -> case os:getenv("EPMDLESS_REMSH_PORT") of false -> noport; RemotePort -> {port, list_to_integer(RemotePort), 5} end end. -spec add_node(Node, Port) -> ok when Node :: atom(), Port :: inet:port_number(). add_node(Node, Port) -> Host = case string:tokens(atom_to_list(Node), "@") of [_Node, H] -> H; [H] -> H end, add_node(Node, Host, Port). -spec add_node(Node, Host, Port) -> ok when Node :: atom(), Host :: inet:hostname() | inet:ip_address(), Port :: inet:port_number(). add_node(Node, Host, Port) -> ok = gen_server:call(?MODULE, {add_node, Node, Host, Port}, infinity). -spec list_nodes() -> [{Node, Port}] when Node :: atom(), 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) -> ok = gen_server:call(?MODULE, {remove_node, Node}, infinity). %% @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([]) -> {ok, #state{}}. handle_info(_Msg, State) -> {noreply, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_call(list_nodes, _From, State) -> {reply, State#state.nodes, State}; handle_call({add_node, Node, Host, Port}, _From, State) -> {reply, ok, State#state{nodes = maps:put(Node, {Host, Port}, State#state.nodes)}}; handle_call({remove_node, Node}, _From, State) -> {reply, ok, State#state{nodes = maps:remove(Node, State#state.nodes)}}; handle_call({host_please, Node}, _From, State) -> Reply = case maps:find(Node, State#state.nodes) of error -> {error, nohost}; {ok, {H, _P}} -> {ok, H} end, {reply, Reply, State}; handle_call({port_please, Node, Host}, _From, State) -> Reply = case maps:find(node_host_to_name(Node, Host), 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(get_info, _From, State = #state{dist_port = DistPort}) -> {reply, [{dist_port, DistPort}], State}; handle_call(_Msg, _From, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_Old, State, _Extra) -> {ok, State}. %% internal funcs -spec node_host_to_name(Node, Host) -> Name when Node :: atom(), Host :: inet:hostname(), Name :: atom(). node_host_to_name(Node, Host) -> list_to_atom(lists:flatten(io_lib:format("~s@~s", [Node, Host]))).