pymobiledevice3 4.27.7__py3-none-any.whl → 5.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pymobiledevice3 might be problematic. Click here for more details.

@@ -27,7 +27,6 @@ coloredlogs.install(level=logging.INFO)
27
27
 
28
28
  logging.getLogger('quic').disabled = True
29
29
  logging.getLogger('asyncio').disabled = True
30
- logging.getLogger('zeroconf').disabled = True
31
30
  logging.getLogger('parso.cache').disabled = True
32
31
  logging.getLogger('parso.cache.pickle').disabled = True
33
32
  logging.getLogger('parso.python.diff').disabled = True
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '4.27.7'
32
- __version_tuple__ = version_tuple = (4, 27, 7)
31
+ __version__ = version = '5.0.1'
32
+ __version_tuple__ = version_tuple = (5, 0, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,145 +1,402 @@
1
+ # Async, dependency-light mDNS browser returning dataclasses with per-address interface names.
2
+ # Works for any DNS-SD type, e.g. "_remoted._tcp.local."
3
+ # - Uses ifaddr (optional) to map IPs -> local interfaces; otherwise iface will be None.
4
+
1
5
  import asyncio
2
- import dataclasses
3
- from asyncio import CancelledError
4
- from socket import AF_INET, AF_INET6, inet_ntop
5
- from typing import Optional
6
+ import ipaddress
7
+ import socket
8
+ import struct
9
+ import sys
10
+ from collections import defaultdict
11
+ from dataclasses import dataclass, field
12
+ from typing import Dict, List, Optional, Set, Tuple
6
13
 
7
- from ifaddr import get_adapters
8
- from zeroconf import IPVersion, ServiceListener, ServiceStateChange, Zeroconf
9
- from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
14
+ import ifaddr # pip install ifaddr
10
15
 
11
16
  from pymobiledevice3.osu.os_utils import get_os_utils
12
17
 
13
- REMOTEPAIRING_SERVICE_NAMES = ['_remotepairing._tcp.local.']
14
- REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAMES = ['_remotepairing-manual-pairing._tcp.local.']
15
- MOBDEV2_SERVICE_NAMES = ['_apple-mobdev2._tcp.local.']
16
- REMOTED_SERVICE_NAMES = ['_remoted._tcp.local.']
18
+ REMOTEPAIRING_SERVICE_NAME = '_remotepairing._tcp.local.'
19
+ REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAME = '_remotepairing-manual-pairing._tcp.local.'
20
+ MOBDEV2_SERVICE_NAME = '_apple-mobdev2._tcp.local.'
21
+ REMOTED_SERVICE_NAME = '_remoted._tcp.local.'
17
22
  OSUTILS = get_os_utils()
18
23
  DEFAULT_BONJOUR_TIMEOUT = OSUTILS.bonjour_timeout
19
24
 
25
+ MDNS_PORT = 5353
26
+ MDNS_MCAST_V4 = "224.0.0.251"
27
+ MDNS_MCAST_V6 = "ff02::fb"
20
28
 
21
- @dataclasses.dataclass
22
- class BonjourAnswer:
23
- name: str
24
- properties: dict[bytes, bytes]
25
- ips: list[str]
26
- port: int
27
-
28
-
29
- class BonjourListener(ServiceListener):
30
- def __init__(self, ip: str):
31
- super().__init__()
32
- self.name: Optional[str] = None
33
- self.properties: dict[bytes, bytes] = {}
34
- self.ip = ip
35
- self.port: Optional[int] = None
36
- self.addresses: list[str] = []
37
- self.queue: asyncio.Queue = asyncio.Queue()
38
- self.querying_task: Optional[asyncio.Task] = asyncio.create_task(self.query_addresses())
39
-
40
- def async_on_service_state_change(
41
- self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None:
42
- self.queue.put_nowait((zeroconf, service_type, name, state_change))
43
-
44
- async def query_addresses(self) -> None:
45
- zeroconf, service_type, name, state_change = await self.queue.get()
46
- self.name = name
47
- service_info = AsyncServiceInfo(service_type, name)
48
- await service_info.async_request(zeroconf, 3000)
49
- ipv4 = [inet_ntop(AF_INET, address.packed) for address in
50
- service_info.ip_addresses_by_version(IPVersion.V4Only)]
51
- ipv6 = []
52
- if '%' in self.ip:
53
- ipv6 = [inet_ntop(AF_INET6, address.packed) + '%' + self.ip.split('%')[1] for address in
54
- service_info.ip_addresses_by_version(IPVersion.V6Only)]
55
- self.addresses = ipv4 + ipv6
56
- self.properties = service_info.properties
57
- self.port = service_info.port
58
-
59
- async def close(self) -> None:
60
- self.querying_task.cancel()
61
- try:
62
- await self.querying_task
63
- except asyncio.CancelledError:
64
- pass
29
+ QTYPE_A = 1
30
+ QTYPE_PTR = 12
31
+ QTYPE_TXT = 16
32
+ QTYPE_AAAA = 28
33
+ QTYPE_SRV = 33
65
34
 
35
+ CLASS_IN = 0x0001
36
+ CLASS_QU = 0x8000 # unicast-response bit (we use multicast queries)
66
37
 
67
- @dataclasses.dataclass
68
- class BonjourQuery:
69
- zc: AsyncZeroconf
70
- service_browser: AsyncServiceBrowser
71
- listener: BonjourListener
72
38
 
39
+ # ---------------- Dataclasses ----------------
73
40
 
74
- def query_bonjour(service_names: list[str], ip: str) -> BonjourQuery:
75
- aiozc = AsyncZeroconf(interfaces=[ip])
76
- listener = BonjourListener(ip)
77
- service_browser = AsyncServiceBrowser(aiozc.zeroconf, service_names,
78
- handlers=[listener.async_on_service_state_change])
79
- return BonjourQuery(aiozc, service_browser, listener)
41
+ # --- Dataclass decorator shim (adds slots only on 3.10+)
42
+ def dataclass_compat(*d_args, **d_kwargs):
43
+ if sys.version_info < (3, 10):
44
+ d_kwargs.pop("slots", None) # ignore on 3.9
45
+ return dataclass(*d_args, **d_kwargs)
80
46
 
81
47
 
82
- async def browse(service_names: list[str], ips: list[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) \
83
- -> list[BonjourAnswer]:
84
- bonjour_queries = [query_bonjour(service_names, adapter) for adapter in ips]
85
- answers = []
86
- try:
87
- await asyncio.sleep(timeout)
88
- except CancelledError:
89
- for bonjour_query in bonjour_queries:
90
- await bonjour_query.listener.close()
91
- await bonjour_query.service_browser.async_cancel()
92
- await bonjour_query.zc.async_close()
93
- raise
94
- for bonjour_query in bonjour_queries:
95
- if bonjour_query.listener.addresses:
96
- answer = BonjourAnswer(
97
- bonjour_query.listener.name, bonjour_query.listener.properties, bonjour_query.listener.addresses,
98
- bonjour_query.listener.port)
99
- if answer not in answers:
100
- answers.append(answer)
101
- await bonjour_query.listener.close()
102
- await bonjour_query.service_browser.async_cancel()
103
- await bonjour_query.zc.async_close()
104
- return answers
105
-
106
-
107
- async def browse_ipv6(service_names: list[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
108
- return await browse(service_names, OSUTILS.get_ipv6_ips(), timeout=timeout)
109
-
110
-
111
- def get_ipv4_addresses() -> list[str]:
112
- ips = []
113
- for adapter in get_adapters():
114
- if adapter.nice_name.startswith('tun'):
115
- # skip browsing on already established tunnels
48
+ @dataclass_compat(slots=True)
49
+ class Address:
50
+ ip: str
51
+ iface: Optional[str] # local interface name (e.g., "en0"), or None if unknown
52
+
53
+ @property
54
+ def full_ip(self) -> str:
55
+ if self.iface and self.ip.lower().startswith("fe80:"):
56
+ return f"{self.ip}%{self.iface}"
57
+ return self.ip
58
+
59
+
60
+ @dataclass_compat(slots=True)
61
+ class ServiceInstance:
62
+ instance: str # "<Instance Name>._type._proto.local."
63
+ host: Optional[str] # "host.local" (without trailing dot), or None if unresolved
64
+ port: Optional[int] # SRV port
65
+ addresses: List[Address] = field(default_factory=list) # IPs with interface names
66
+ properties: Dict[str, str] = field(default_factory=dict) # TXT key/values
67
+
68
+
69
+ # ---------------- DNS helpers ----------------
70
+
71
+
72
+ def encode_name(name: str) -> bytes:
73
+ name = name.rstrip(".")
74
+ out = bytearray()
75
+ for label in name.split(".") if name else []:
76
+ b = label.encode("utf-8")
77
+ if len(b) > 63:
78
+ raise ValueError("label too long")
79
+ out.append(len(b))
80
+ out += b
81
+ out.append(0)
82
+ return bytes(out)
83
+
84
+
85
+ def decode_name(data: bytes, off: int) -> Tuple[str, int]:
86
+ labels = []
87
+ jumped = False
88
+ orig_end = off
89
+ for _ in range(128): # loop guard
90
+ if off >= len(data):
91
+ break
92
+ length = data[off]
93
+ if length == 0:
94
+ off += 1
95
+ break
96
+ if (length & 0xC0) == 0xC0:
97
+ if off + 1 >= len(data):
98
+ raise ValueError("truncated name pointer")
99
+ ptr = ((length & 0x3F) << 8) | data[off + 1]
100
+ if ptr >= len(data):
101
+ raise ValueError("bad name pointer")
102
+ if not jumped:
103
+ orig_end = off + 2
104
+ off = ptr
105
+ jumped = True
116
106
  continue
117
- for ip in adapter.ips:
118
- if ip.ip == '127.0.0.1':
119
- continue
120
- if not ip.is_IPv4:
107
+ off += 1
108
+ end = off + length
109
+ if end > len(data):
110
+ raise ValueError("truncated label")
111
+ labels.append(data[off:end].decode("utf-8", errors="replace"))
112
+ off = end
113
+ return ".".join(labels) + ".", (orig_end if jumped else off)
114
+
115
+
116
+ def build_query(name: str, qtype: int, unicast: bool = False) -> bytes:
117
+ hdr = struct.pack("!HHHHHH", 0, 0, 1, 0, 0, 0) # TXID=0, flags=0, 1 question
118
+ qclass = CLASS_IN | (CLASS_QU if unicast else 0)
119
+ return hdr + encode_name(name) + struct.pack("!HH", qtype, qclass)
120
+
121
+
122
+ def parse_rr(data: bytes, off: int):
123
+ name, off = decode_name(data, off)
124
+ if off + 10 > len(data):
125
+ raise ValueError("truncated RR header")
126
+ rtype, rclass, ttl, rdlen = struct.unpack("!HHIH", data[off: off + 10])
127
+ off += 10
128
+ rdata = data[off: off + rdlen]
129
+ off += rdlen
130
+
131
+ rr = {"name": name, "type": rtype, "class": rclass & 0x7FFF, "ttl": ttl}
132
+ if rtype == QTYPE_PTR:
133
+ target, _ = decode_name(data, off - rdlen)
134
+ rr["ptrdname"] = target
135
+ elif rtype == QTYPE_SRV and rdlen >= 6:
136
+ priority, weight, port = struct.unpack("!HHH", rdata[:6])
137
+ target, _ = decode_name(data, off - rdlen + 6)
138
+ rr.update(
139
+ {"priority": priority, "weight": weight, "port": port, "target": target}
140
+ )
141
+ elif rtype == QTYPE_TXT:
142
+ kv = {}
143
+ i = 0
144
+ while i < rdlen:
145
+ b = rdata[i]
146
+ i += 1
147
+ seg = rdata[i: i + b]
148
+ i += b
149
+ if not seg:
121
150
  continue
122
- ips.append(ip.ip)
123
- return ips
151
+ if b"=" in seg:
152
+ k, v = seg.split(b"=", 1)
153
+ kv[k.decode()] = v.decode(errors="replace")
154
+ else:
155
+ kv[seg.decode()] = ""
156
+ rr["txt"] = kv
157
+ elif rtype == QTYPE_A and rdlen == 4:
158
+ rr["address"] = socket.inet_ntop(socket.AF_INET, rdata)
159
+ elif rtype == QTYPE_AAAA and rdlen == 16:
160
+ rr["address"] = socket.inet_ntop(socket.AF_INET6, rdata)
161
+ else:
162
+ rr["raw"] = rdata
163
+ return rr, off
164
+
165
+
166
+ def parse_mdns_message(data: bytes):
167
+ if len(data) < 12:
168
+ return []
169
+ _, _, qd, an, ns, ar = struct.unpack("!HHHHHH", data[:12])
170
+ off = 12
171
+ for _ in range(qd):
172
+ _, off = decode_name(data, off)
173
+ off += 4
174
+ rrs = []
175
+ for _ in range(an + ns + ar):
176
+ rr, off = parse_rr(data, off)
177
+ rrs.append(rr)
178
+ return rrs
179
+
180
+
181
+ # ---------------- Interface mapping helpers ----------------
182
+
183
+
184
+ class _Adapters:
185
+ def __init__(self):
186
+ self.adapters = ifaddr.get_adapters() if ifaddr is not None else []
187
+
188
+ def pick_iface_for_ip(
189
+ self, ip_str: str, family: int, v6_scopeid: Optional[int]
190
+ ) -> Optional[str]:
191
+ # Prefer scope id for IPv6 link-local
192
+ if family == socket.AF_INET6 and ip_str.lower().startswith("fe80:"):
193
+ if v6_scopeid:
194
+ try:
195
+ return socket.if_indextoname(v6_scopeid)
196
+ except OSError:
197
+ pass
198
+
199
+ # Otherwise, try to match destination ip to local subnet via ifaddr
200
+ if not self.adapters:
201
+ return None
202
+ ip = ipaddress.ip_address(ip_str)
203
+ best = (None, -1) # (name, prefix_len)
204
+ for ad in self.adapters:
205
+ for ipn in ad.ips:
206
+ if isinstance(ipn.ip, str):
207
+ # IPv4
208
+ fam = socket.AF_INET
209
+ ipn_ip = ipn.ip
210
+ else:
211
+ # IPv6
212
+ fam = socket.AF_INET6
213
+ ipn_ip = ipn.ip[0]
214
+ if fam != family:
215
+ continue
216
+ net = ipaddress.ip_network(
217
+ f"{ipn_ip}/{ipn.network_prefix}", strict=False
218
+ )
219
+ if ip in net and ipn.network_prefix > best[1]:
220
+ best = (ad.nice_name or ad.name, ipn.network_prefix)
221
+ return best[0]
222
+
124
223
 
224
+ # ---------------- async sockets ----------------
225
+
226
+
227
+ class _DatagramProtocol(asyncio.DatagramProtocol):
228
+ def __init__(self, queue: asyncio.Queue):
229
+ self.queue = queue
230
+
231
+ def datagram_received(self, data, addr):
232
+ # addr: IPv4 -> (host, port); IPv6 -> (host, port, flowinfo, scopeid)
233
+ self.queue.put_nowait((data, addr))
234
+
235
+
236
+ async def _bind_ipv4(queue: asyncio.Queue):
237
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
238
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
239
+ if hasattr(socket, "SO_REUSEPORT"):
240
+ try:
241
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
242
+ except OSError:
243
+ pass
244
+ s.bind(("0.0.0.0", MDNS_PORT))
245
+ try:
246
+ mreq = struct.pack(
247
+ "=4s4s", socket.inet_aton(MDNS_MCAST_V4), socket.inet_aton("0.0.0.0")
248
+ )
249
+ s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
250
+ except OSError:
251
+ pass
252
+ transport, _ = await asyncio.get_running_loop().create_datagram_endpoint(
253
+ lambda: _DatagramProtocol(queue), sock=s
254
+ )
255
+ return transport, s
256
+
257
+
258
+ async def _bind_ipv6_all_ifaces(queue: asyncio.Queue):
259
+ s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
260
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
261
+ if hasattr(socket, "SO_REUSEPORT"):
262
+ try:
263
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
264
+ except OSError:
265
+ pass
266
+ s.bind(("::", MDNS_PORT))
267
+ grp = socket.inet_pton(socket.AF_INET6, MDNS_MCAST_V6)
268
+ for ifindex, _ in socket.if_nameindex():
269
+ mreq6 = grp + struct.pack("@I", ifindex)
270
+ try:
271
+ s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq6)
272
+ except OSError:
273
+ continue
274
+ transport, _ = await asyncio.get_running_loop().create_datagram_endpoint(
275
+ lambda: _DatagramProtocol(queue), sock=s
276
+ )
277
+ return transport, s
278
+
279
+
280
+ async def _open_mdns_sockets():
281
+ queue = asyncio.Queue()
282
+ transports: List[Tuple[asyncio.BaseTransport, socket.socket]] = []
283
+ t4, s4 = await _bind_ipv4(queue)
284
+ transports.append((t4, s4))
285
+ t6, s6 = await _bind_ipv6_all_ifaces(queue)
286
+ transports.append((t6, s6))
287
+ if not transports:
288
+ raise RuntimeError("Failed to open mDNS sockets (UDP/5353)")
289
+ return transports, queue
290
+
291
+
292
+ async def _send_query_all(transports, pkt: bytes):
293
+ for transport, sock in transports:
294
+ if sock.family == socket.AF_INET:
295
+ transport.sendto(pkt, (MDNS_MCAST_V4, MDNS_PORT))
296
+ else:
297
+ # Send once per iface index for better reachability
298
+ for ifindex, _ in socket.if_nameindex():
299
+ transport.sendto(pkt, (MDNS_MCAST_V6, MDNS_PORT, 0, ifindex))
300
+
301
+
302
+ # ---------------- Public API ----------------
303
+
304
+
305
+ async def browse_service(
306
+ service_type: str, timeout: float = 4.0
307
+ ) -> List[ServiceInstance]:
308
+ """
309
+ Discover a DNS-SD/mDNS service type (e.g. "_remoted._tcp.local.") on the local network.
310
+
311
+ Returns: List[ServiceInstance] with Address(ip, iface) entries.
312
+ """
313
+ if not service_type.endswith("."):
314
+ service_type += "."
315
+
316
+ transports, queue = await _open_mdns_sockets()
317
+ adapters = _Adapters()
318
+
319
+ ptr_targets: Set[str] = set()
320
+ srv_map: Dict[str, Dict] = {}
321
+ txt_map: Dict[str, Dict] = {}
322
+ # host -> list[(ip, iface)]
323
+ host_addrs: Dict[str, List[Address]] = defaultdict(list)
324
+
325
+ def _record_addr(rr_name: str, ip_str: str, pkt_addr):
326
+ # Determine family and possible scopeid from the packet that delivered this RR
327
+ family = socket.AF_INET6 if ":" in ip_str else socket.AF_INET
328
+ scopeid = None
329
+ if isinstance(pkt_addr, tuple) and len(pkt_addr) == 4: # IPv6 remote tuple
330
+ scopeid = pkt_addr[3]
331
+ iface = adapters.pick_iface_for_ip(ip_str, family, scopeid)
332
+ # avoid duplicates for the same host/ip
333
+ existing = host_addrs[rr_name]
334
+ if not any(a.ip == ip_str for a in existing):
335
+ existing.append(Address(ip=ip_str, iface=iface))
336
+
337
+ try:
338
+ await _send_query_all(
339
+ transports, build_query(service_type, QTYPE_PTR, unicast=False)
340
+ )
341
+ loop = asyncio.get_running_loop()
342
+ end = loop.time() + timeout
343
+ while loop.time() < end:
344
+ try:
345
+ data, pkt_addr = await asyncio.wait_for(
346
+ queue.get(), timeout=end - loop.time()
347
+ )
348
+ except asyncio.TimeoutError:
349
+ break
350
+ for rr in parse_mdns_message(data):
351
+ t = rr.get("type")
352
+ if t == QTYPE_PTR and rr.get("name") == service_type:
353
+ ptr_targets.add(rr.get("ptrdname"))
354
+ elif t == QTYPE_SRV:
355
+ srv_map[rr["name"]] = {
356
+ "target": rr.get("target"),
357
+ "port": rr.get("port"),
358
+ }
359
+ elif t == QTYPE_TXT:
360
+ txt_map[rr["name"]] = rr.get("txt", {})
361
+ elif t == QTYPE_A and rr.get("address"):
362
+ _record_addr(rr["name"], rr["address"], pkt_addr)
363
+ elif t == QTYPE_AAAA and rr.get("address"):
364
+ _record_addr(rr["name"], rr["address"], pkt_addr)
365
+ finally:
366
+ for transport, _ in transports:
367
+ transport.close()
125
368
 
126
- async def browse_ipv4(service_names: list[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
127
- return await browse(service_names, get_ipv4_addresses(), timeout=timeout)
369
+ # Assemble dataclasses
370
+ results: List[ServiceInstance] = []
371
+ for inst in sorted(ptr_targets):
372
+ srv = srv_map.get(inst, {})
373
+ target = srv.get("target")
374
+ host = (target[:-1] if target and target.endswith(".") else target) or None
375
+ addrs = host_addrs.get(target, []) if target else []
376
+ props = txt_map.get(inst, {})
377
+ results.append(
378
+ ServiceInstance(
379
+ instance=inst,
380
+ host=host,
381
+ port=srv.get("port"),
382
+ addresses=addrs,
383
+ properties=props,
384
+ )
385
+ )
386
+ return results
128
387
 
129
388
 
130
- async def browse_remoted(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
131
- return await browse_ipv6(REMOTED_SERVICE_NAMES, timeout=timeout)
389
+ async def browse_remoted(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
390
+ return await browse_service(REMOTED_SERVICE_NAME, timeout=timeout)
132
391
 
133
392
 
134
- async def browse_mobdev2(timeout: float = DEFAULT_BONJOUR_TIMEOUT, ips: Optional[list[str]] = None) -> list[BonjourAnswer]:
135
- if ips is None:
136
- ips = get_ipv4_addresses() + OSUTILS.get_ipv6_ips()
137
- return await browse(MOBDEV2_SERVICE_NAMES, ips, timeout=timeout)
393
+ async def browse_mobdev2(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
394
+ return await browse_service(MOBDEV2_SERVICE_NAME, timeout=timeout)
138
395
 
139
396
 
140
- async def browse_remotepairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
141
- return await browse_ipv4(REMOTEPAIRING_SERVICE_NAMES, timeout=timeout)
397
+ async def browse_remotepairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
398
+ return await browse_service(REMOTEPAIRING_SERVICE_NAME, timeout=timeout)
142
399
 
143
400
 
144
- async def browse_remotepairing_manual_pairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
145
- return await browse_ipv4(REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAMES, timeout=timeout)
401
+ async def browse_remotepairing_manual_pairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
402
+ return await browse_service(REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAME, timeout=timeout)
@@ -41,8 +41,8 @@ def cli_mobdev2(timeout: float, pair_records: Optional[str]) -> None:
41
41
  async def cli_remotepairing_task(timeout: float) -> None:
42
42
  output = []
43
43
  for answer in await browse_remotepairing(timeout=timeout):
44
- for ip in answer.ips:
45
- output.append({'hostname': ip, 'port': answer.port})
44
+ for address in answer.addresses:
45
+ output.append({'hostname': address.full_ip, 'port': answer.port})
46
46
  print_json(output)
47
47
 
48
48
 
@@ -56,8 +56,8 @@ def cli_remotepairing(timeout: float) -> None:
56
56
  async def cli_remotepairing_manual_pairing_task(timeout: float) -> None:
57
57
  output = []
58
58
  for answer in await browse_remotepairing_manual_pairing(timeout=timeout):
59
- for ip in answer.ips:
60
- output.append({'hostname': ip, 'port': answer.port, 'name': answer.properties[b'name'].decode()})
59
+ for address in answer.addresses:
60
+ output.append({'hostname': address.full_ip, 'port': answer.port, 'name': answer.properties[b'name'].decode()})
61
61
  print_json(output)
62
62
 
63
63
 
@@ -161,6 +161,12 @@ def lockdown_wifi_connections(service_provider: LockdownClient, state):
161
161
  service_provider.enable_wifi_connections = state == 'on'
162
162
 
163
163
 
164
+ async def async_cli_start_tunnel(
165
+ service_provider: LockdownServiceProvider, script_mode: bool) -> None:
166
+ await tunnel_task(await CoreDeviceTunnelProxy.create(service_provider),
167
+ script_mode=script_mode, secrets=None, protocol=TunnelProtocol.TCP)
168
+
169
+
164
170
  @lockdown_group.command('start-tunnel', cls=Command)
165
171
  @click.option('--script-mode', is_flag=True,
166
172
  help='Show only HOST and port number to allow easy parsing from external shell scripts')
@@ -168,8 +174,7 @@ def lockdown_wifi_connections(service_provider: LockdownClient, state):
168
174
  def cli_start_tunnel(
169
175
  service_provider: LockdownServiceProvider, script_mode: bool) -> None:
170
176
  """ start tunnel """
171
- service = CoreDeviceTunnelProxy(service_provider)
172
- asyncio.run(tunnel_task(service, script_mode=script_mode, secrets=None, protocol=TunnelProtocol.TCP), debug=True)
177
+ asyncio.run(async_cli_start_tunnel(service_provider, script_mode), debug=True)
173
178
 
174
179
 
175
180
  @lockdown_group.command('assistive-touch', cls=Command)
@@ -220,9 +220,10 @@ async def start_remote_pair_task(device_name: str) -> None:
220
220
  if device_name is not None and current_device_name != device_name:
221
221
  continue
222
222
 
223
- for ip in answer.ips:
224
- devices.append(RemotePairingManualPairingDevice(ip=ip, port=answer.port, device_name=current_device_name,
225
- identifier=answer.properties[b'identifier'].decode()))
223
+ for address in answer.addresses:
224
+ devices.append(
225
+ RemotePairingManualPairingDevice(ip=address.full_ip, port=answer.port, device_name=current_device_name,
226
+ identifier=answer.properties[b'identifier'].decode()))
226
227
 
227
228
  if len(devices) > 0:
228
229
  device = prompt_device_list(devices)
@@ -848,7 +848,7 @@ def create_using_remote(service: ServiceConnection, identifier: str = None, labe
848
848
 
849
849
  async def get_mobdev2_lockdowns(
850
850
  udid: Optional[str] = None, pair_records: Optional[Path] = None, only_paired: bool = False,
851
- timeout: float = DEFAULT_BONJOUR_TIMEOUT, ips: Optional[list[str]] = None) \
851
+ timeout: float = DEFAULT_BONJOUR_TIMEOUT) \
852
852
  -> AsyncIterable[tuple[str, TcpLockdownClient]]:
853
853
  records = {}
854
854
  if pair_records is None:
@@ -863,26 +863,21 @@ async def get_mobdev2_lockdowns(
863
863
  record = plistlib.loads(file.read_bytes())
864
864
  records[record['WiFiMACAddress']] = record
865
865
 
866
- iterated_ips = set()
867
- for answer in await browse_mobdev2(timeout=timeout, ips=ips):
868
- if '@' not in answer.name:
866
+ for answer in await browse_mobdev2(timeout=timeout):
867
+ if '@' not in answer.instance:
869
868
  continue
870
- wifi_mac_address = answer.name.split('@', 1)[0]
869
+ wifi_mac_address = answer.instance.split('@', 1)[0]
871
870
  record = records.get(wifi_mac_address)
872
871
 
873
872
  if only_paired and record is None:
874
873
  continue
875
874
 
876
- for ip in answer.ips:
877
- if ip in iterated_ips:
878
- # skip ips we already iterated over, possibly from previous queries
879
- continue
880
- iterated_ips.add(ip)
875
+ for address in answer.addresses:
881
876
  try:
882
- lockdown = create_using_tcp(hostname=ip, autopair=False, pair_record=record)
877
+ lockdown = create_using_tcp(hostname=address.full_ip, autopair=False, pair_record=record)
883
878
  except Exception:
884
879
  continue
885
880
  if only_paired and not lockdown.paired:
886
881
  lockdown.close()
887
882
  continue
888
- yield ip, lockdown
883
+ yield address.full_ip, lockdown