Packages

Gleam DNS-SD discovery on Erlang

Current section

Files

Jump to
esdee src esdee.erl
Raw

src/esdee.erl

-module(esdee).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/esdee.gleam").
-export([new/0, using_port/2, with_max_data_size/2, use_address_families/2, use_ipv4/2, use_ipv6/2, describe_setup_error/1, close_sockets/1, receive_next_datagram_as_message/1, broadcast_service_question/2, set_up_sockets/1, parse_sd_update/1, classify_message/1, select_processed_udp_messages/2]).
-export_type([service_description/0, options/0, service_discvery_update/0, udp_message/0, socket_configuration/0, sockets/0, socket_setup_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Provides the base functionality for DNS-SD discovery\n"
" and `Selector`-based functions to listen to discovery messages.\n"
" For an actor-based discovery client, see `discoverer`.\n"
).
-type service_description() :: {service_description,
binary(),
binary(),
binary(),
integer(),
integer(),
integer(),
list(binary()),
glip:ip_address()}.
-opaque options() :: {options,
integer(),
gleam@set:set(glip:address_family()),
integer()}.
-type service_discvery_update() :: {service_type_discovered, binary()} |
{service_discovered, service_description()}.
-type udp_message() :: {dns_sd_message, service_discvery_update()} |
{other_udp_message, toss:udp_message()}.
-type socket_configuration() :: {socket_configuration,
toss:socket(),
glip:ip_address()}.
-opaque sockets() :: {sockets, list(socket_configuration()), integer()}.
-type socket_setup_error() :: no_address_family_enabled |
{failed_to_open_socket, glip:address_family()} |
failed_to_join_multicast_group |
set_active_mode_failed.
-file("src/esdee.gleam", 55).
?DOC(
" Create default options for DNS-SD discovery.\n"
" Can be used either with a full-fledged actor implementation\n"
" which can be found in the `discoverer` module,\n"
" or used with the `set_up_sockets` function.\n"
).
-spec new() -> options().
new() ->
Address_families = begin
_pipe = gleam@set:new(),
gleam@set:insert(_pipe, ipv4)
end,
{options, 8192, Address_families, 5353}.
-file("src/esdee.gleam", 62).
?DOC(false).
-spec using_port(options(), integer()) -> options().
using_port(Options, Port) ->
{options, erlang:element(2, Options), erlang:element(3, Options), Port}.
-file("src/esdee.gleam", 68).
?DOC(
" Configures the maximum data size when receiving UDP datagrams.\n"
" Affects UDP performance, 8 KiB by default.\n"
).
-spec with_max_data_size(options(), integer()) -> options().
with_max_data_size(Options, Max_data_size) ->
{options,
Max_data_size,
erlang:element(3, Options),
erlang:element(4, Options)}.
-file("src/esdee.gleam", 73).
?DOC(" Sets the used address families explicitly\n").
-spec use_address_families(options(), list(glip:address_family())) -> options().
use_address_families(Options, Families) ->
{options,
erlang:element(2, Options),
gleam@set:from_list(Families),
erlang:element(4, Options)}.
-file("src/esdee.gleam", 90).
-spec set_address_family(options(), glip:address_family(), boolean()) -> options().
set_address_family(Options, Family, Enabled) ->
Address_families = begin
_pipe = erlang:element(3, Options),
case Enabled of
false ->
fun(_capture) -> gleam@set:delete(_capture, Family) end;
true ->
fun(_capture@1) -> gleam@set:insert(_capture@1, Family) end
end(_pipe)
end,
{options,
erlang:element(2, Options),
Address_families,
erlang:element(4, Options)}.
-file("src/esdee.gleam", 81).
?DOC(" Sets whether IPv4 will be used. True by default.\n").
-spec use_ipv4(options(), boolean()) -> options().
use_ipv4(Options, Enabled) ->
set_address_family(Options, ipv4, Enabled).
-file("src/esdee.gleam", 86).
?DOC(" Sets whether IPv6 will be used. False by default.\n").
-spec use_ipv6(options(), boolean()) -> options().
use_ipv6(Options, Enabled) ->
set_address_family(Options, ipv6, Enabled).
-file("src/esdee.gleam", 181).
-spec find_ptr(list(esdee@internal@dns:resource_record())) -> {ok,
{binary(), binary()}} |
{error, nil}.
find_ptr(Records) ->
gleam@list:find_map(Records, fun(Record) -> case Record of
{ptr_record, Service_type, Instance_name} ->
{ok, {Service_type, Instance_name}};
_ ->
{error, nil}
end end).
-file("src/esdee.gleam", 191).
-spec description_from_records(
list(esdee@internal@dns:resource_record()),
binary(),
binary()
) -> {ok, service_description()} | {error, nil}.
description_from_records(Records, Service_type, Instance_name) ->
Try_find = fun(With, Apply) ->
gleam@result:'try'(gleam@list:find_map(Records, With), Apply)
end,
Try_find(fun(Record) -> case Record of
{srv_record, _, Priority, Weight, Port, Target_name} ->
{ok, {Priority, Weight, Port, Target_name}};
_ ->
{error, nil}
end end, fun(_use0) ->
{Priority@1, Weight@1, Port@1, Target_name@1} = _use0,
Try_find(fun(Record@1) -> case Record@1 of
{a_record, _, Ip} ->
{ok, Ip};
{aaaa_record, _, Ip} ->
{ok, Ip};
_ ->
{error, nil}
end end, fun(Ip@1) ->
Txt_values = gleam@list:flat_map(
Records,
fun(Record@2) -> case Record@2 of
{txt_record, _, Values} ->
Values;
_ ->
[]
end end
),
{ok,
{service_description,
Service_type,
Instance_name,
Target_name@1,
Priority@1,
Weight@1,
Port@1,
Txt_values,
Ip@1}}
end)
end).
-file("src/esdee.gleam", 258).
?DOC(" Converts a setup error value to a description string.\n").
-spec describe_setup_error(socket_setup_error()) -> binary().
describe_setup_error(Error) ->
case Error of
failed_to_join_multicast_group ->
<<"Failed to join (IPv4) multicast group"/utf8>>;
no_address_family_enabled ->
<<"No address family enabled in options"/utf8>>;
set_active_mode_failed ->
<<"Setting socket(s) to active mode failed"/utf8>>;
{failed_to_open_socket, Family} ->
<<<<"Failed to open IPv"/utf8, (case Family of
ipv4 ->
<<"4"/utf8>>;
ipv6 ->
<<"6"/utf8>>
end)/binary>>/binary, " socket"/utf8>>
end.
-file("src/esdee.gleam", 348).
?DOC(" Closes all the sockets in the socket collection.\n").
-spec close_sockets(sockets()) -> nil.
close_sockets(Sockets) ->
gleam@list:each(
erlang:element(2, Sockets),
fun(Socket) -> toss:close(erlang:element(2, Socket)) end
).
-file("src/esdee.gleam", 363).
-spec for_each_socket(
sockets(),
fun((socket_configuration()) -> {ok, nil} | {error, GNZ})
) -> {ok, nil} | {error, GNZ}.
for_each_socket(Sockets, Do) ->
_pipe = erlang:element(2, Sockets),
_pipe@1 = gleam@list:map(_pipe, Do),
_pipe@2 = gleam@result:all(_pipe@1),
gleam@result:replace(_pipe@2, nil).
-file("src/esdee.gleam", 340).
?DOC(
" Sets the underlying sockets to receive the next datagram as a message.\n"
" See `toss.receive_next_datagram_as_message` for details.\n"
).
-spec receive_next_datagram_as_message(sockets()) -> {ok, nil} |
{error, toss:error()}.
receive_next_datagram_as_message(Sockets) ->
for_each_socket(
Sockets,
fun(Config) ->
toss:receive_next_datagram_as_message(erlang:element(2, Config))
end
).
-file("src/esdee.gleam", 354).
?DOC(" Broadcasts the DNS-SD question for the given service type.\n").
-spec broadcast_service_question(sockets(), binary()) -> {ok, nil} |
{error, toss:error()}.
broadcast_service_question(Sockets, Service_type) ->
Data = esdee_ffi:encode_question(Service_type),
for_each_socket(
Sockets,
fun(Config) ->
toss_ffi:send(
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(3, Sockets),
Data
)
end
).
-file("src/esdee.gleam", 374).
?DOC(" Expects an IP to be valid, DON'T use for dynamic strings.\n").
-spec constant_ip(binary()) -> glip:ip_address().
constant_ip(Ip) ->
Ip@2 = case glip_ffi:parse_ip(Ip) of
{ok, Ip@1} -> Ip@1;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Did the IP standard change?"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"esdee"/utf8>>,
function => <<"constant_ip"/utf8>>,
line => 375,
value => _assert_fail,
start => 11498,
'end' => 11535,
pattern_start => 11509,
pattern_end => 11515})
end,
Ip@2.
-file("src/esdee.gleam", 291).
-spec open_socket(glip:address_family(), integer()) -> {ok,
socket_configuration()} |
{error, socket_setup_error()}.
open_socket(Family, Port) ->
gleam@result:'try'(case Family of
ipv4 ->
Broadcast_ip = constant_ip(<<"224.0.0.251"/utf8>>),
gleam@result:'try'(
begin
_pipe = toss:new(Port),
_pipe@1 = toss:use_ipv4(_pipe),
_pipe@2 = toss:reuse_address(_pipe@1),
_pipe@3 = toss:using_interface(_pipe@2, Broadcast_ip),
_pipe@4 = toss:open(_pipe@3),
gleam@result:replace_error(
_pipe@4,
{failed_to_open_socket, Family}
)
end,
fun(Socket) ->
Local_addr = constant_ip(<<"0.0.0.0"/utf8>>),
gleam@result:'try'(
begin
_pipe@5 = toss:join_multicast_group(
Socket,
Broadcast_ip,
Local_addr
),
gleam@result:replace_error(
_pipe@5,
failed_to_join_multicast_group
)
end,
fun(_) -> {ok, {Socket, Broadcast_ip}} end
)
end
);
ipv6 ->
Broadcast_ip@1 = constant_ip(<<"ff02::fb"/utf8>>),
gleam@result:'try'(
begin
_pipe@6 = toss:new(Port),
_pipe@7 = toss:use_ipv6(_pipe@6),
_pipe@8 = toss:reuse_address(_pipe@7),
_pipe@9 = toss:open(_pipe@8),
gleam@result:replace_error(
_pipe@9,
{failed_to_open_socket, Family}
)
end,
fun(Socket@1) -> {ok, {Socket@1, Broadcast_ip@1}} end
)
end, fun(_use0) ->
{Socket@2, Broadcast_ip@2} = _use0,
gleam@result:'try'(
begin
_pipe@10 = toss:receive_next_datagram_as_message(Socket@2),
gleam@result:replace_error(_pipe@10, set_active_mode_failed)
end,
fun(_) ->
{ok, {socket_configuration, Socket@2, Broadcast_ip@2}}
end
)
end).
-file("src/esdee.gleam", 275).
?DOC(" Opens and sets up the service discovery UDP socket(s).\n").
-spec set_up_sockets(options()) -> {ok, sockets()} |
{error, socket_setup_error()}.
set_up_sockets(Options) ->
gleam@result:'try'(case gleam@set:is_empty(erlang:element(3, Options)) of
false ->
{ok, nil};
true ->
{error, no_address_family_enabled}
end, fun(_) ->
gleam@result:'try'(
begin
_pipe = erlang:element(3, Options),
_pipe@1 = gleam@set:to_list(_pipe),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(_capture) ->
open_socket(_capture, erlang:element(4, Options))
end
),
gleam@result:all(_pipe@2)
end,
fun(Sockets) ->
{ok, {sockets, Sockets, erlang:element(4, Options)}}
end
)
end).
-file("src/esdee.gleam", 158).
?DOC(
" Parses the contents of a UDP datagram into a DNS-SD update,\n"
" or returns an error, if the data was not a DNS-SD update, or had incomplete data.\n"
" If you don't want to use `toss` for IO, \n"
" this function can be used directly to parse DNS-SD data.\n"
" (Note: if you have a use case for this, \n"
" and would prefer it to be in a separate package,\n"
" please open an issue on GitHub!)\n"
).
-spec parse_sd_update(bitstring()) -> {ok, service_discvery_update()} |
{error, nil}.
parse_sd_update(Data) ->
gleam@result:'try'(
begin
_pipe = esdee_ffi:decode_records(Data),
gleam@result:replace_error(_pipe, nil)
end,
fun(Records) ->
gleam@result:'try'(
find_ptr(Records),
fun(_use0) ->
{Ptr_from, Ptr_to} = _use0,
gleam@bool:lazy_guard(
Ptr_from =:= <<"_services._dns-sd._udp.local"/utf8>>,
fun() -> {ok, {service_type_discovered, Ptr_to}} end,
fun() ->
gleam@result:'try'(
description_from_records(
Records,
Ptr_from,
Ptr_to
),
fun(Description) ->
{ok, {service_discovered, Description}}
end
)
end
)
end
)
end
).
-file("src/esdee.gleam", 139).
?DOC(" Classifies a UDP message to determine if it is related to DNS-SD.\n").
-spec classify_message(toss:udp_message()) -> udp_message().
classify_message(Message) ->
case Message of
{datagram, _, _, _, Data} = Datagram ->
case parse_sd_update(Data) of
{error, _} ->
{other_udp_message, Datagram};
{ok, Update} ->
{dns_sd_message, Update}
end;
Other ->
{other_udp_message, Other}
end.
-file("src/esdee.gleam", 130).
?DOC(
" Configure a selector to receive messages from UDP sockets,\n"
" pre-processing them to separate DNS-SD messages from other messages.\n"
" You will also need to call\n"
" [`receive_next_datagram_as_message`](#receive_next_datagram_as_message)\n"
" to use the selector successfully - once initially,\n"
" and again after receiving each message.\n"
"\n"
" Note that this will receive messages from all `gen_udp` sockets that the process controls,\n"
" rather than any specific one.\n"
" If you wish to only handle messages from one socket then use one process per socket.\n"
).
-spec select_processed_udp_messages(
gleam@erlang@process:selector(GNG),
fun((udp_message()) -> GNG)
) -> gleam@erlang@process:selector(GNG).
select_processed_udp_messages(Selector, Mapper) ->
toss:select_udp_messages(
Selector,
fun(Message) -> Mapper(classify_message(Message)) end
).