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