-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, JMM}) ) -> {ok, nil} | {error, JMM}. 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 => <>, module => <<"esdee"/utf8>>, function => <<"constant_ip"/utf8>>, line => 375, value => _assert_fail, start => 11506, 'end' => 11543, pattern_start => 11517, pattern_end => 11523}) 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(JLT), fun((udp_message()) -> JLT) ) -> gleam@erlang@process:selector(JLT). select_processed_udp_messages(Selector, Mapper) -> toss:select_udp_messages( Selector, fun(Message) -> Mapper(classify_message(Message)) end ).