1use std::collections::HashMap;
2#[cfg(feature = "mdns")]
3use std::time::{Duration, Instant};
4
5use crate::mux::MuxClient;
6use tokio_stream::Stream;
7
8use crate::error::CoreError;
9
10#[derive(Debug, Clone, serde::Serialize)]
12pub struct DeviceInfo {
13 pub udid: String,
14 pub device_id: u32,
15 pub connection_type: String,
16 pub product_id: u16,
17}
18
19impl DeviceInfo {
20 pub(crate) fn from_mux(d: crate::mux::MuxDevice) -> Self {
21 Self {
22 udid: d.serial_number,
23 device_id: d.device_id,
24 connection_type: d.connection_type,
25 product_id: d.product_id,
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
32pub enum DeviceEvent {
33 Attached(DeviceInfo),
34 Detached { udid: String, device_id: u32 },
35}
36
37pub async fn list_devices() -> Result<Vec<DeviceInfo>, CoreError> {
39 let mut mux = MuxClient::connect().await?;
40 let devices = mux.list_devices().await?;
41 Ok(devices.into_iter().map(DeviceInfo::from_mux).collect())
42}
43
44pub async fn watch_devices() -> Result<impl Stream<Item = Result<DeviceEvent, CoreError>>, CoreError>
46{
47 use tokio_stream::StreamExt;
48
49 let events = crate::mux::listener::listen_events().await?;
50 let attached_devices = list_devices().await?;
51
52 Ok(async_stream::stream! {
53 let mut mapper = DeviceEventMapper::with_attached_devices(attached_devices);
54 tokio::pin!(events);
55
56 while let Some(event) = events.next().await {
57 match event {
58 Ok(event) => {
59 if let Some(mapped) = mapper.map(event) {
60 yield Ok(mapped);
61 }
62 }
63 Err(err) => yield Err(CoreError::from(err)),
64 }
65 }
66 })
67}
68
69#[cfg(feature = "mdns")]
77pub async fn discover_mdns() -> Result<impl Stream<Item = MdnsDevice>, CoreError> {
78 use mdns_sd::{ServiceDaemon, ServiceEvent};
79
80 let mdns = ServiceDaemon::new().map_err(|e| CoreError::Other(format!("mDNS daemon: {e}")))?;
81
82 let service_type = "_remoted._tcp.local.";
83 let receiver = mdns
84 .browse(service_type)
85 .map_err(|e| CoreError::Other(format!("mDNS browse: {e}")))?;
86
87 let stream = async_stream::stream! {
89 loop {
90 match receiver.recv_async().await {
91 Ok(ServiceEvent::ServiceResolved(info)) => {
92 for addr in info.get_addresses() {
94 if let std::net::IpAddr::V6(v6) = addr {
95 let port = info.get_port();
96 let props = info.get_properties();
97 let udid = props.get("UniqueDeviceID")
98 .map(|v| v.val_str().to_string())
99 .unwrap_or_default();
100 yield MdnsDevice {
101 ipv6: *v6,
102 rsd_port: port,
103 udid,
104 name: info.get_fullname().to_string(),
105 };
106 }
107 }
108 }
109 Ok(_) => continue,
110 Err(_) => break,
111 }
112 }
113 };
114
115 Ok(stream)
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119#[cfg(feature = "mdns")]
120pub struct BonjourService {
121 pub instance: String,
122 pub port: u16,
123 pub addresses: Vec<String>,
124 pub properties: HashMap<String, String>,
125}
126
127#[cfg(feature = "mdns")]
128pub async fn browse_mobdev2(timeout: Duration) -> Result<Vec<BonjourService>, CoreError> {
129 browse_bonjour_service("_apple-mobdev2._tcp.local.", timeout).await
130}
131
132#[cfg(feature = "mdns")]
133pub async fn browse_remotepairing(timeout: Duration) -> Result<Vec<BonjourService>, CoreError> {
134 browse_bonjour_service("_remotepairing._tcp.local.", timeout).await
135}
136
137pub fn mobdev2_wifi_mac(instance: &str) -> Option<&str> {
138 instance.split_once('@').map(|(mac, _)| mac)
139}
140
141#[derive(Debug, Clone)]
143#[cfg(feature = "mdns")]
144pub struct MdnsDevice {
145 pub ipv6: std::net::Ipv6Addr,
147 pub rsd_port: u16,
149 pub udid: String,
151 pub name: String,
153}
154
155#[cfg(feature = "mdns")]
156async fn browse_bonjour_service(
157 service_type: &str,
158 timeout: Duration,
159) -> Result<Vec<BonjourService>, CoreError> {
160 use mdns_sd::{ServiceDaemon, ServiceEvent};
161
162 let mdns = ServiceDaemon::new().map_err(|e| CoreError::Other(format!("mDNS daemon: {e}")))?;
163 let receiver = mdns
164 .browse(service_type)
165 .map_err(|e| CoreError::Other(format!("mDNS browse: {e}")))?;
166
167 let deadline = Instant::now() + timeout;
168 let mut services = HashMap::<String, BonjourService>::new();
169
170 loop {
171 let remaining = deadline.saturating_duration_since(Instant::now());
172 if remaining.is_zero() {
173 break;
174 }
175
176 match tokio::time::timeout(remaining, receiver.recv_async()).await {
177 Ok(Ok(ServiceEvent::ServiceResolved(info))) => {
178 let instance = info.get_fullname().to_string();
179 let entry = services
180 .entry(instance.clone())
181 .or_insert_with(|| BonjourService {
182 instance,
183 port: info.get_port(),
184 addresses: Vec::new(),
185 properties: info
186 .get_properties()
187 .iter()
188 .map(|property| {
189 (property.key().to_string(), property.val_str().to_string())
190 })
191 .collect(),
192 });
193
194 entry.port = info.get_port();
195 for address in info.get_addresses() {
196 let full = address.to_string();
197 if !entry.addresses.contains(&full) {
198 entry.addresses.push(full);
199 }
200 }
201 }
202 Ok(Ok(_)) => {}
203 Ok(Err(_)) | Err(_) => break,
204 }
205 }
206
207 Ok(services.into_values().collect())
208}
209
210#[derive(Default)]
211struct DeviceEventMapper {
212 attached_devices: HashMap<u32, DeviceInfo>,
213}
214
215impl DeviceEventMapper {
216 fn with_attached_devices(attached_devices: Vec<DeviceInfo>) -> Self {
217 let attached_devices = attached_devices
218 .into_iter()
219 .map(|device| (device.device_id, device))
220 .collect();
221 Self { attached_devices }
222 }
223
224 fn map(&mut self, event: crate::mux::MuxEvent) -> Option<DeviceEvent> {
225 match event {
226 crate::mux::MuxEvent::Attached(device) => {
227 let info = DeviceInfo::from_mux(device);
228 self.attached_devices.insert(info.device_id, info.clone());
229 Some(DeviceEvent::Attached(info))
230 }
231 crate::mux::MuxEvent::Detached { device_id } => {
232 let udid = self
233 .attached_devices
234 .remove(&device_id)
235 .map(|device| device.udid)
236 .unwrap_or_default();
237 Some(DeviceEvent::Detached { udid, device_id })
238 }
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn mapper_preserves_attached_device_details() {
249 let mut mapper = DeviceEventMapper::default();
250 let event = mapper
251 .map(crate::mux::MuxEvent::Attached(crate::mux::MuxDevice {
252 device_id: 2,
253 serial_number: "00008150-000A584C0E62401C".into(),
254 connection_type: "USB".into(),
255 product_id: 0,
256 }))
257 .expect("attached event should map");
258
259 match event {
260 DeviceEvent::Attached(device) => {
261 assert_eq!(device.udid, "00008150-000A584C0E62401C");
262 assert_eq!(device.device_id, 2);
263 assert_eq!(device.connection_type, "USB");
264 assert_eq!(device.product_id, 0);
265 }
266 DeviceEvent::Detached { .. } => panic!("expected attached event"),
267 }
268 }
269
270 #[test]
271 fn mapper_rehydrates_udid_for_detached_device() {
272 let mut mapper = DeviceEventMapper::default();
273 mapper.map(crate::mux::MuxEvent::Attached(crate::mux::MuxDevice {
274 device_id: 7,
275 serial_number: "detaching-udid".into(),
276 connection_type: "USB".into(),
277 product_id: 0,
278 }));
279
280 let event = mapper
281 .map(crate::mux::MuxEvent::Detached { device_id: 7 })
282 .expect("detached event should map");
283
284 assert!(matches!(
285 event,
286 DeviceEvent::Detached {
287 udid,
288 device_id: 7
289 } if udid == "detaching-udid"
290 ));
291 }
292
293 #[test]
294 fn mapper_emits_empty_udid_when_detach_arrives_without_prior_attach() {
295 let mut mapper = DeviceEventMapper::default();
296 let event = mapper
297 .map(crate::mux::MuxEvent::Detached { device_id: 99 })
298 .expect("detached event should still map");
299
300 assert!(matches!(
301 event,
302 DeviceEvent::Detached {
303 udid,
304 device_id: 99
305 } if udid.is_empty()
306 ));
307 }
308
309 #[test]
310 fn mapper_uses_seeded_devices_for_initial_detach_events() {
311 let mut mapper = DeviceEventMapper::with_attached_devices(vec![DeviceInfo {
312 udid: "seeded-udid".into(),
313 device_id: 42,
314 connection_type: "USB".into(),
315 product_id: 0,
316 }]);
317
318 let event = mapper
319 .map(crate::mux::MuxEvent::Detached { device_id: 42 })
320 .expect("detached event should still map");
321
322 assert!(matches!(
323 event,
324 DeviceEvent::Detached {
325 udid,
326 device_id: 42
327 } if udid == "seeded-udid"
328 ));
329 }
330
331 #[test]
332 fn extracts_wifi_mac_from_mobdev2_instance() {
333 let mac = mobdev2_wifi_mac(
334 "34:10:be:1b:a6:4c@fe80::3610:beff:fe1b:a64c-supportsRP-24._apple-mobdev2._tcp.local.",
335 )
336 .expect("mobdev2 instance should contain Wi-Fi MAC");
337
338 assert_eq!(mac, "34:10:be:1b:a6:4c");
339 }
340
341 #[test]
342 fn rejects_non_mobdev2_instance_without_wifi_mac() {
343 assert!(mobdev2_wifi_mac("_apple-mobdev2._tcp.local.").is_none());
344 }
345}