Packages

Bonjour/Zeroconf DNS Service Discovery for the Nerves platform

Current section

Files

Jump to
nerves_dnssd src dnssd_server.erl
Raw

src/dnssd_server.erl

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2011 Andrew Tunnell-Jones. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%% @private
-module(dnssd_server).
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([stop/1, enumerate/1, browse/2, resolve/3, register/6, query_record/2, results/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {subs = [], ops = []}).
-record(sub, {ref, mon_ref, pid, op_id}).
-record(op, {id, type, arg, pid}).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc Stop the operation
%% @spec stop(Ref) -> ok | {error, Error}
%% @end
%%--------------------------------------------------------------------
stop(Ref) when is_reference(Ref) ->
gen_server:call(?SERVER, {stop, Ref}).
%%--------------------------------------------------------------------
%% @private
%% @doc Enumerate browse or registration domains
%% @spec enumerate(browse | reg) -> {ok, Ref, Results} | {error, Error}
%% @end
%%--------------------------------------------------------------------
enumerate(Type) when Type =:= reg orelse Type =:= browse ->
gen_server:call(?SERVER, {start, {enumerate, Type}}).
%%--------------------------------------------------------------------
%% @private
%% @doc Browse for instances of a service
%% @spec browse(Type, Domain) -> {ok, Ref, Results} | {error, Error}
%% @end
%%--------------------------------------------------------------------
browse(Type, Domain) when is_binary(Type) andalso is_binary(Domain) ->
gen_server:call(?SERVER, {start, {browse, Type, Domain}}).
%%--------------------------------------------------------------------
%% @private
%% @doc Resolve a service instance
%% @spec resolve(Name, Type, Domain) -> {ok, Ref, Results} | {error, Error}
%% @end
%%--------------------------------------------------------------------
resolve(Name, Type, Domain)
when is_binary(Name) andalso is_binary(Type) andalso is_binary(Domain) ->
gen_server:call(?SERVER, {start, {resolve, Name, Type, Domain}}).
%%--------------------------------------------------------------------
%% @private
%% @doc Register a service instance
%% @spec register(Name, Type, Domain, Host, Port, Txt) ->
%% {ok, Ref, Results} | {error, Error}
%% @end
%%--------------------------------------------------------------------
register(Name, Type, Domain, Host, Port, Txt)
when is_binary(Name), is_binary(Type), is_binary(Domain), is_binary(Host),
is_integer(Port), is_binary(Txt) ->
Req = {start, {register, Name, Type, Domain, Host, Port, Txt}},
gen_server:call(?SERVER, Req).
%%--------------------------------------------------------------------
%% @private
%% @doc Query for a specific DNS record type
%% @end
%%--------------------------------------------------------------------
query_record(Domain, RType) ->
Req = {start, {query_record, Domain, RType}},
gen_server:call(?SERVER, Req).
results(Ref) when is_reference(Ref) ->
Req = {results, Ref},
gen_server:call(?SERVER, Req).
%%--------------------------------------------------------------------
%% @private
%% @doc Starts the server
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([]) ->
process_flag(trap_exit, true),
{ok, #state{}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%% {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call({stop, Ref}, _From, #state{subs = Subs, ops = Ops} = State)
when is_reference(Ref) ->
case lists:keytake(Ref, #sub.ref, Subs) of
{value, #sub{ref = Ref,
mon_ref = MonRef,
op_id = OpId}, NewSubs} ->
erlang:demonitor(MonRef),
case lists:keymember(OpId, #sub.op_id, NewSubs) of
true ->
NewState = State#state{subs = NewSubs},
{reply, ok, NewState};
false ->
case lists:keytake(OpId, #op.id, Ops) of
{value, #op{id = OpId, pid = OpPid}, NewOps} ->
dnssd_drv:stop(OpPid),
NewState = State#state{subs = NewSubs,
ops = NewOps},
{reply, ok, NewState};
false ->
NewState = State#state{subs = NewSubs},
{reply, ok, NewState}
end
end;
false ->
Reply = {error, unknown_ref},
{reply, Reply, State}
end;
handle_call({start, Op}, {ClientPid, _Tag} = Client,
#state{subs = Subs, ops = Ops} = State) ->
Type = element(1, Op),
OpId = erlang:phash2(Op),
Ref = make_ref(),
MonRef = erlang:monitor(process, ClientPid),
NewSub = #sub{ref = Ref, mon_ref = MonRef, pid = ClientPid, op_id = OpId},
NewSubs = [NewSub|Subs],
case lists:keyfind(OpId, #op.id, Ops) of
#op{id = OpId, type = Type, pid = OpPid} ->
{ok, Results} = dnssd_drv:results(OpPid),
Reply = {ok, Ref},
NewState = State#state{subs = NewSubs},
gen_server:reply(Client, Reply),
[ ClientPid ! {dnssd, Ref, case Type of
resolve -> {Type, Result};
Type -> {Type, add, Result}
end}
|| Result <- Results ],
{noreply, NewState};
false ->
case dnssd_drv:start_link(Op) of
{ok, OpPid} ->
Reply = {ok, Ref},
NewOp = #op{id = OpId,
type = Type,
arg = Op,
pid = OpPid},
NewOps = [NewOp|Ops],
NewState = State#state{subs = NewSubs, ops = NewOps},
{reply, Reply, NewState};
{error, _Error} = Reply ->
true = erlang:demonitor(MonRef),
{reply, Reply, State}
end
end;
handle_call({results, Ref}, _From, #state{subs = Subs, ops = Ops} = State) ->
Reply = case lists:keyfind(Ref, #sub.ref, Subs) of
#sub{op_id = OpId} ->
#op{pid = OpPid} = lists:keyfind(OpId, #op.id, Ops),
dnssd_drv:results(OpPid);
_ ->
{error, unknown_ref}
end,
{reply, Reply, State};
handle_call(Request, _From, State) ->
error_logger:info_msg(?MODULE_STRING " ~p ignoring call: ~p~n",
[ self(), Request]),
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast(Msg, State) ->
error_logger:info_msg(?MODULE_STRING " ~p ignoring cast: ~p~n",
[ self(), Msg]),
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info({dnssd_drv, DrvPid, Message},
#state{subs = Subs, ops = Ops} = State) when is_pid(DrvPid) ->
case lists:keyfind(DrvPid, #op.pid, Ops) of
#op{id = OpId} ->
[ SubPid ! {dnssd, Ref, Message}
|| #sub{pid = SubPid, ref = Ref, op_id = SubOpId} <- Subs,
SubOpId =:= OpId ];
false ->
error_logger:error_msg(
?MODULE_STRING " ~p discarded dnssd_drv message from unknown pid "
"~p. Message: ~p~n", [self(), DrvPid, Message]
)
end,
{noreply, State};
handle_info({'DOWN', MonRef, process, Pid, _Reason},
#state{subs = Subs, ops = Ops} = State) ->
case lists:keytake(MonRef, #sub.mon_ref, Subs) of
{value, #sub{mon_ref = MonRef,
pid = Pid,
op_id = OpId}, NewSubs} ->
case lists:keymember(OpId, #sub.op_id, NewSubs) of
true ->
NewState = State#state{subs = NewSubs},
{noreply, NewState};
false ->
case lists:keytake(OpId, #op.id, Ops) of
{value, #op{id = OpId, pid = OpPid}, NewOps} ->
dnssd_drv:stop(OpPid),
NewState = State#state{subs = NewSubs,
ops = NewOps},
{noreply, NewState};
false ->
NewState = State#state{subs = NewSubs},
{noreply, NewState}
end
end;
_ ->
{noreply, State}
end;
handle_info({'EXIT', Pid, Reason}, #state{subs = Subs, ops = Ops} = State) ->
if Reason =/= normal ->
error_logger:error_msg(
?MODULE_STRING " ~p notified of non-normal exit of ~p:~n~p~n",
[ self(), Pid, Reason]
);
true -> ok end,
case lists:keytake(Pid, #op.pid, Ops) of
{value, #op{id = OpId, pid = Pid}, NewOps} ->
case lists:partition(fun(#sub{op_id = SubOpId}) ->
SubOpId =:= OpId
end, Subs) of
{[], Subs} ->
%% No clients affected
NewState = State#state{ops = NewOps},
{noreply, NewState};
{DefunctSubs, NewSubs} ->
%% There was active clients - shouldn't happen
%% regardless of Reason so tell clients there
%% was a crash
[ SubPid ! {dnssd, Ref, crash}
|| #sub{pid = SubPid,
ref = Ref,
op_id = SubOpId} <- DefunctSubs,
SubOpId =:= OpId ],
NewState = State#state{subs = NewSubs, ops = NewOps},
{noreply, NewState}
end;
_ ->
{noreply, State}
end;
handle_info(Info, State) ->
error_logger:error_msg(?MODULE_STRING " ~p discarded message: ~p~n",
[ self(), Info]),
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================