Skip to content

Commit 639f9bc

Browse files
committed
Add some more logic, not final API, just trying it out
1 parent 02de3f3 commit 639f9bc

File tree

7 files changed

+196
-10
lines changed

7 files changed

+196
-10
lines changed

dev/esdee_dev.gleam

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import esdee
2+
3+
pub fn main() {
4+
let assert Ok(discovery) =
5+
esdee.new()
6+
|> esdee.start()
7+
8+
//let assert Ok(_) = esdee.discover(discovery, "_googlecast._tcp.local")
9+
let assert Ok(_) = esdee.discover(discovery, "_services._dns-sd._udp.local")
10+
recieve_forever(discovery)
11+
}
12+
13+
fn recieve_forever(discovery) {
14+
echo esdee.receive_next(discovery, 10_000)
15+
recieve_forever(discovery)
16+
}

gleam.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ version = "1.0.0"
1515
[dependencies]
1616
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
1717
gleam_erlang = ">= 1.3.0 and < 2.0.0"
18+
toss = { path = "../toss" }
1819

1920
[dev-dependencies]
2021
gleeunit = ">= 1.0.0 and < 2.0.0"
21-

manifest.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ packages = [
55
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
66
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
77
{ name = "gleeunit", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "CD701726CBCE5588B375D157B4391CFD0F2F134CD12D9B6998A395484DE05C58" },
8+
{ name = "toss", version = "1.0.0-rc2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], source = "local", path = "../toss" },
89
]
910

1011
[requirements]
1112
gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
1213
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
1314
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
15+
toss = { path = "../toss" }

src/esdee.gleam

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,176 @@
1-
import gleam/option
1+
import esdee/internal/dns.{type ResourceRecord}
2+
import gleam/list
3+
import gleam/option.{type Option, None, Some}
4+
import gleam/result
5+
import toss
26

7+
const mds_port = 5353
8+
9+
/// Describes a service fully discovered via DNS-SD.
310
pub type ServiceDescription {
411
ServiceDescription(
12+
/// The service type string, e.g. _googlecast._tcp.local
513
service_type: String,
14+
/// The unique instance name for a peer providing the service, e.g.
15+
/// SHIELD-Android-TV-9693d58e3537dddb118b7b7d17f9c1c2._googlecast._tcp.local
616
instance_name: String,
17+
/// The target host name, which can be used to actually connect to the service, e.g.
18+
/// 9693d58e-3537-dddb-118b-7b7d17f9c1c2.local
719
target_name: String,
20+
/// The priority of the target host, lower value means more preferred.
21+
/// Originates from the SRV record.
822
priority: Int,
23+
/// A relative weight for records with the same priority,
24+
/// higher value means higher chance of getting picked.
25+
/// Originates from the SRV record.
926
weight: Int,
27+
/// The port the service is served on.
1028
port: Int,
29+
/// Any TXT records that the service advertises (can be empty).
1130
txt_values: List(String),
12-
ipv4: option.Option(#(Int, Int, Int, Int)),
13-
ipv6: option.Option(#(Int, Int, Int, Int, Int, Int, Int, Int)),
31+
/// The IPv4 address, if advertised.
32+
ipv4: Option(#(Int, Int, Int, Int)),
33+
/// The IPv6 address, if advertised.
34+
ipv6: Option(#(Int, Int, Int, Int, Int, Int, Int, Int)),
1435
)
1536
}
37+
38+
// For future options, e.g. IPv6
39+
pub opaque type Options {
40+
Options(max_data_size: Int, broadcast_ip: toss.IpAddress)
41+
}
42+
43+
pub opaque type ServiceDiscovery {
44+
ServiceDiscovery(options: Options, socket: toss.Socket)
45+
}
46+
47+
pub fn new() -> Options {
48+
Options(max_data_size: 4096, broadcast_ip: toss.Ipv4Address(224, 0, 0, 251))
49+
}
50+
51+
pub type StartError {
52+
CouldNotOpenSocket
53+
CouldNotJoinMulticast
54+
}
55+
56+
pub fn start(options: Options) -> Result(ServiceDiscovery, StartError) {
57+
use socket <- result.try(
58+
toss.new(mds_port)
59+
|> toss.use_ipv4()
60+
|> toss.reuse_address()
61+
|> toss.using_interface(options.broadcast_ip)
62+
|> toss.open
63+
|> result.replace_error(CouldNotOpenSocket),
64+
)
65+
66+
let local_addr = toss.Ipv4Address(0, 0, 0, 0)
67+
use _ <- result.try(
68+
toss.join_multicast_group(socket, options.broadcast_ip, local_addr)
69+
|> result.replace_error(CouldNotJoinMulticast),
70+
)
71+
72+
Ok(ServiceDiscovery(options, socket))
73+
}
74+
75+
pub fn discover(
76+
discovery: ServiceDiscovery,
77+
service: String,
78+
) -> Result(Nil, Nil) {
79+
let data = dns.encode_question(service)
80+
toss.send_to(discovery.socket, discovery.options.broadcast_ip, mds_port, data)
81+
|> result.replace_error(Nil)
82+
}
83+
84+
pub type DiscoveryError {
85+
ReceiveTimeout
86+
ReceiveError
87+
NotAnAnswer
88+
InvalidDnsData
89+
InsufficientData
90+
}
91+
92+
pub fn receive_next(
93+
discovery: ServiceDiscovery,
94+
timeout_ms: Int,
95+
) -> Result(ServiceDescription, DiscoveryError) {
96+
use #(_, _, data) <- result.try(
97+
toss.receive(discovery.socket, discovery.options.max_data_size, timeout_ms)
98+
|> result.map_error(fn(e) {
99+
case e {
100+
toss.Timeout -> ReceiveTimeout
101+
_ -> ReceiveError
102+
}
103+
}),
104+
)
105+
106+
use records <- result.try(
107+
dns.decode_records(data)
108+
|> result.map_error(fn(e) {
109+
case e {
110+
dns.InvalidData -> InvalidDnsData
111+
dns.NotAnAnswer -> NotAnAnswer
112+
}
113+
}),
114+
)
115+
116+
description_from_records(records) |> result.replace_error(InsufficientData)
117+
}
118+
119+
fn description_from_records(
120+
records: List(ResourceRecord),
121+
) -> Result(ServiceDescription, Nil) {
122+
echo records
123+
124+
let try_find = fn(with: fn(ResourceRecord) -> Result(a, Nil), apply) {
125+
result.try(list.find_map(records, with), apply)
126+
}
127+
128+
use #(service_type, instance_name) <- try_find(fn(record) {
129+
case record {
130+
dns.PtrRecord(service_type:, instance_name:) ->
131+
Ok(#(service_type, instance_name))
132+
_ -> Error(Nil)
133+
}
134+
})
135+
136+
use #(priority, weight, port, target_name) <- try_find(fn(record) {
137+
case record {
138+
dns.SrvRecord(priority:, weight:, port:, target_name:, ..) ->
139+
Ok(#(priority, weight, port, target_name))
140+
_ -> Error(Nil)
141+
}
142+
})
143+
144+
let #(ipv4, ipv6) =
145+
list.fold(records, #(None, None), fn(ips, record) {
146+
case record {
147+
dns.ARecord(ip:, ..) -> #(Some(ip), ips.1)
148+
dns.AaaaRecord(ip:, ..) -> #(ips.0, Some(ip))
149+
_ -> ips
150+
}
151+
})
152+
153+
let txt_values =
154+
list.flat_map(records, fn(record) {
155+
case record {
156+
dns.TxtRecord(values:, ..) -> values
157+
_ -> []
158+
}
159+
})
160+
161+
case option.is_some(ipv4) || option.is_some(ipv6) {
162+
True ->
163+
Ok(ServiceDescription(
164+
service_type:,
165+
instance_name:,
166+
target_name:,
167+
priority:,
168+
weight:,
169+
port:,
170+
txt_values:,
171+
ipv4:,
172+
ipv6:,
173+
))
174+
False -> Error(Nil)
175+
}
176+
}

src/esdee/internal/dns.gleam

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ pub type ResourceRecord {
1919
AaaaRecord(target_name: String, ip: #(Int, Int, Int, Int, Int, Int, Int, Int))
2020
}
2121

22+
pub type DecodeError {
23+
InvalidData
24+
NotAnAnswer
25+
}
26+
2227
/// Decode the resource records out of a binary DNS record.
2328
/// We don't currently care about the header or other entries.
2429
@external(erlang, "esdee_ffi", "decode_records")
25-
pub fn decode_records(data: BitArray) -> Result(List(ResourceRecord), Nil)
30+
pub fn decode_records(
31+
data: BitArray,
32+
) -> Result(List(ResourceRecord), DecodeError)

src/esdee_ffi.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ decode_records(Bits) ->
2323
Resources = lists:filtermap(fun map_resource/1, Record#dns_rec.arlist),
2424
{ok, Answers ++ Resources};
2525
_ ->
26-
{ok, []}
26+
{error, not_an_answer}
2727
end;
2828
_ ->
29-
{error, nil}
29+
{error, invalid_data}
3030
end.
3131

3232
map_answer(Answer) ->

test/esdee/internal/dns_test.gleam

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ pub fn encode_question_test() {
4949
}
5050

5151
pub fn decode_records_fail_test() {
52-
assert dns.decode_records(<<"Not valid":utf8>>) == Error(Nil)
52+
assert dns.decode_records(<<"Not valid":utf8>>) == Error(dns.InvalidData)
5353
}
5454

55-
pub fn decode_records_skip_question_test() {
56-
assert dns.decode_records(query_bits) == Ok([])
55+
pub fn decode_records_question_test() {
56+
assert dns.decode_records(query_bits) == Error(dns.NotAnAnswer)
5757
}
5858

5959
pub fn decode_records_test() {

0 commit comments

Comments
 (0)