pymobiledevice3 4.27.7__py3-none-any.whl → 5.0.0__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.0'
32
+ __version_tuple__ = version_tuple = (5, 0, 0)
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
@@ -190,7 +190,7 @@ class RemotePairingTunnel(ABC):
190
190
  self._tun_read_task = asyncio.create_task(self.tun_read_task(), name=f'tun-read-{address}')
191
191
 
192
192
  async def stop_tunnel(self) -> None:
193
- self._logger.debug('stopping tunnel')
193
+ self._logger.debug(f'[{asyncio.current_task().get_name()}] stopping tunnel')
194
194
  self._tun_read_task.cancel()
195
195
  with suppress(CancelledError):
196
196
  await self._tun_read_task
@@ -223,6 +223,9 @@ class RemotePairingQuicTunnel(RemotePairingTunnel, QuicConnectionProtocol):
223
223
  self._quic.send_datagram_frame(packet)
224
224
  self.transmit()
225
225
 
226
+ # Allow other tasks to run
227
+ await asyncio.sleep(0)
228
+
226
229
  async def request_tunnel_establish(self) -> dict:
227
230
  stream_id = self._quic.get_next_available_stream_id()
228
231
  # pad the data with random data to force the MTU size correctly
@@ -939,17 +942,21 @@ class RemotePairingManualPairingService(RemotePairingTunnelService):
939
942
  class CoreDeviceTunnelProxy(StartTcpTunnel):
940
943
  SERVICE_NAME = 'com.apple.internal.devicecompute.CoreDeviceProxy'
941
944
 
942
- def __init__(self, lockdown: LockdownServiceProvider) -> None:
943
- self._lockdown = lockdown
944
- self._service: Optional[ServiceConnection] = None
945
+ @classmethod
946
+ async def create(cls, lockdown: LockdownServiceProvider) -> 'CoreDeviceTunnelProxy':
947
+ return cls(await lockdown.aio_start_lockdown_service(cls.SERVICE_NAME), lockdown.udid)
948
+
949
+ def __init__(self, service: ServiceConnection, remote_identifier: str) -> None:
950
+ self._service: ServiceConnection = service
951
+ self._remote_identifier: str = remote_identifier
945
952
 
946
953
  @property
947
954
  def remote_identifier(self) -> str:
948
- return self._lockdown.udid
955
+ return self._remote_identifier
949
956
 
950
957
  @asynccontextmanager
951
958
  async def start_tcp_tunnel(self) -> AsyncGenerator['TunnelResult', None]:
952
- self._service = await self._lockdown.aio_start_lockdown_service(self.SERVICE_NAME)
959
+ assert self._service is not None, 'service must be connected first'
953
960
  tunnel = RemotePairingTcpTunnel(self._service.reader, self._service.writer)
954
961
  handshake_response = await tunnel.request_tunnel_establish()
955
962
  tunnel.start_tunnel(handshake_response['clientParameters']['address'],
@@ -1078,13 +1085,14 @@ async def get_remote_pairing_tunnel_services(
1078
1085
  udid: Optional[str] = None) -> list[RemotePairingTunnelService]:
1079
1086
  result = []
1080
1087
  for answer in await browse_remotepairing(timeout=bonjour_timeout):
1081
- for ip in answer.ips:
1088
+ for address in answer.addresses:
1082
1089
  for identifier in iter_remote_paired_identifiers():
1083
1090
  if udid is not None and identifier != udid:
1084
1091
  continue
1085
1092
  conn = None
1086
1093
  try:
1087
- conn = await create_core_device_tunnel_service_using_remotepairing(identifier, ip, answer.port)
1094
+ conn = await create_core_device_tunnel_service_using_remotepairing(
1095
+ identifier, address.full_ip, answer.port)
1088
1096
  result.append(conn)
1089
1097
  break
1090
1098
  except ConnectionAbortedError:
@@ -17,8 +17,8 @@ async def get_rsds(bonjour_timeout: float = DEFAULT_BONJOUR_TIMEOUT, udid: Optio
17
17
  result = []
18
18
  with stop_remoted():
19
19
  for answer in await browse_remoted(timeout=bonjour_timeout):
20
- for ip in answer.ips:
21
- rsd = RemoteServiceDiscoveryService((ip, RSD_PORT))
20
+ for address in answer.addresses:
21
+ rsd = RemoteServiceDiscoveryService((address.full_ip, RSD_PORT))
22
22
  try:
23
23
  await rsd.connect()
24
24
  except ConnectionRefusedError:
@@ -7,10 +7,13 @@ import signal
7
7
  import traceback
8
8
  import warnings
9
9
  from contextlib import asynccontextmanager, suppress
10
+ from ssl import SSLEOFError
10
11
  from typing import Optional, Union
11
12
 
12
13
  import construct
13
14
 
15
+ from pymobiledevice3.bonjour import browse_remoted
16
+
14
17
  with warnings.catch_warnings():
15
18
  # Ignore: "Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater."
16
19
  warnings.simplefilter('ignore', category=UserWarning)
@@ -22,9 +25,9 @@ from fastapi import FastAPI
22
25
  from packaging.version import Version
23
26
 
24
27
  from pymobiledevice3 import usbmux
25
- from pymobiledevice3.bonjour import REMOTED_SERVICE_NAMES, browse
26
28
  from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionFailedToUsbmuxdError, DeviceNotFoundError, \
27
- GetProhibitedError, IncorrectModeError, InvalidServiceError, LockdownError, MuxException, PairingError
29
+ GetProhibitedError, IncorrectModeError, InvalidServiceError, LockdownError, MuxException, PairingError, \
30
+ StreamClosedError
28
31
  from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
29
32
  from pymobiledevice3.osu.os_utils import get_os_utils
30
33
  from pymobiledevice3.remote.common import TunnelProtocol
@@ -42,7 +45,7 @@ REATTEMPT_INTERVAL = 5
42
45
  REATTEMPT_COUNT = 5
43
46
 
44
47
  REMOTEPAIRING_INTERVAL = 5
45
- MOVDEV2_INTERVAL = 5
48
+ MOBDEV2_INTERVAL = 5
46
49
 
47
50
  USBMUX_INTERVAL = 2
48
51
  OSUTILS = get_os_utils()
@@ -91,50 +94,75 @@ class TunneldCore:
91
94
 
92
95
  @asyncio_print_traceback
93
96
  async def monitor_usb_task(self) -> None:
94
- previous_ips = []
95
- while True:
96
- current_ips = OSUTILS.get_ipv6_ips()
97
- added = [ip for ip in current_ips if ip not in previous_ips]
98
- removed = [ip for ip in previous_ips if ip not in current_ips]
99
-
100
- previous_ips = current_ips
101
-
102
- logger.debug(f'added interfaces: {added}')
103
- logger.debug(f'removed interfaces: {removed}')
104
-
105
- for ip in removed:
106
- if ip in self.tunnel_tasks:
107
- self.tunnel_tasks[ip].task.cancel()
108
- await self.tunnel_tasks[ip].task
109
-
110
- for ip in added:
111
- self.tunnel_tasks[ip] = TunnelTask(
112
- task=asyncio.create_task(self.handle_new_potential_usb_cdc_ncm_interface_task(ip),
113
- name=f'handle-new-potential-usb-cdc-ncm-interface-task-{ip}'))
114
-
115
- # wait before re-iterating
116
- await asyncio.sleep(1)
97
+ try:
98
+ previous_ips = []
99
+ while True:
100
+ current_ips = OSUTILS.get_ipv6_ips()
101
+ added = [ip for ip in current_ips if ip not in previous_ips]
102
+ removed = [ip for ip in previous_ips if ip not in current_ips]
103
+
104
+ previous_ips = current_ips
105
+
106
+ # logger.debug(f'added interfaces: {added}')
107
+ # logger.debug(f'removed interfaces: {removed}')
108
+
109
+ for ip in removed:
110
+ if ip in self.tunnel_tasks:
111
+ self.tunnel_tasks[ip].task.cancel()
112
+ with suppress(asyncio.CancelledError):
113
+ await self.tunnel_tasks[ip].task
114
+
115
+ if added:
116
+ # A new interface was attached
117
+ for answer in await browse_remoted():
118
+ for address in answer.addresses:
119
+ if address.iface.startswith('utun'):
120
+ # Skip already established tunnels
121
+ continue
122
+ if address.full_ip in self.tunnel_tasks.keys():
123
+ # Skip already established tunnels
124
+ continue
125
+ self.tunnel_tasks[address.full_ip] = TunnelTask(
126
+ task=asyncio.create_task(
127
+ self.handle_new_potential_usb_cdc_ncm_interface_task(address.full_ip),
128
+ name=f'handle-new-potential-usb-cdc-ncm-interface-task-{address.full_ip}'))
129
+
130
+ # wait before re-iterating
131
+ await asyncio.sleep(1)
132
+ except asyncio.CancelledError:
133
+ pass
117
134
 
118
135
  @asyncio_print_traceback
119
136
  async def monitor_wifi_task(self) -> None:
120
137
  try:
121
138
  while True:
122
- for service in await get_remote_pairing_tunnel_services():
123
- if service.hostname in self.tunnel_tasks:
124
- # skip tunnel if already exists for this ip
125
- await service.close()
126
- continue
127
- if self.tunnel_exists_for_udid(service.remote_identifier):
128
- # skip tunnel if already exists for this udid
129
- await service.close()
130
- continue
131
- self.tunnel_tasks[service.hostname] = TunnelTask(
132
- task=asyncio.create_task(self.start_tunnel_task(service.hostname, service),
133
- name=f'start-tunnel-task-wifi-{service.hostname}'),
134
- udid=service.remote_identifier
135
- )
139
+ try:
140
+ remote_pairing_tunnel_services = await get_remote_pairing_tunnel_services()
141
+ for service in remote_pairing_tunnel_services:
142
+ if service.hostname in self.tunnel_tasks:
143
+ # skip tunnel if already exists for this ip
144
+ await service.close()
145
+ continue
146
+ if self.tunnel_exists_for_udid(service.remote_identifier):
147
+ # skip tunnel if already exists for this udid
148
+ await service.close()
149
+ continue
150
+ self.tunnel_tasks[service.hostname] = TunnelTask(
151
+ task=asyncio.create_task(self.start_tunnel_task(service.hostname, service),
152
+ name=f'start-tunnel-task-wifi-{service.hostname}'),
153
+ udid=service.remote_identifier
154
+ )
155
+ except asyncio.exceptions.IncompleteReadError:
156
+ continue
157
+ except asyncio.CancelledError:
158
+ # Raise and cancel gracefully
159
+ raise
160
+ except Exception:
161
+ logger.error(f'Got exception from {asyncio.current_task().get_name()}: {traceback.format_exc()}')
162
+ continue
136
163
  await asyncio.sleep(REMOTEPAIRING_INTERVAL)
137
164
  except asyncio.CancelledError:
165
+ # Cancel gracefully
138
166
  pass
139
167
 
140
168
  @asyncio_print_traceback
@@ -145,11 +173,20 @@ class TunneldCore:
145
173
  for mux_device in usbmux.list_devices():
146
174
  task_identifier = f'usbmux-{mux_device.serial}-{mux_device.connection_type}'
147
175
  if self.tunnel_exists_for_udid(mux_device.serial):
176
+ # Skip if already established a tunnel for this udid
148
177
  continue
178
+ if task_identifier in self.tunnel_tasks:
179
+ # Skip if already trying to establish a tunnel for this device
180
+ continue
181
+ service = None
149
182
  try:
150
- service = CoreDeviceTunnelProxy(create_using_usbmux(mux_device.serial))
183
+ with create_using_usbmux(mux_device.serial) as lockdown:
184
+ service = await CoreDeviceTunnelProxy.create(lockdown)
151
185
  except (MuxException, InvalidServiceError, GetProhibitedError, construct.core.StreamError,
152
- ConnectionAbortedError, DeviceNotFoundError, LockdownError, IncorrectModeError):
186
+ ConnectionAbortedError, DeviceNotFoundError, LockdownError, IncorrectModeError,
187
+ SSLEOFError):
188
+ if service is not None:
189
+ await service.close()
153
190
  continue
154
191
  self.tunnel_tasks[task_identifier] = TunnelTask(
155
192
  udid=mux_device.serial,
@@ -172,22 +209,26 @@ class TunneldCore:
172
209
  try:
173
210
  while True:
174
211
  async for ip, lockdown in get_mobdev2_lockdowns(only_paired=True):
175
- if self.tunnel_exists_for_udid(lockdown.udid):
176
- # skip tunnel if already exists for this udid
177
- continue
178
- task_identifier = f'mobdev2-{lockdown.udid}-{ip}'
179
- try:
180
- tunnel_service = CoreDeviceTunnelProxy(lockdown)
181
- except InvalidServiceError:
182
- logger.warning(f'[{task_identifier}] failed to start CoreDeviceTunnelProxy - skipping')
183
- lockdown.close()
184
- continue
212
+ with lockdown:
213
+ udid = lockdown.udid
214
+ task_identifier = f'mobdev2-{udid}-{ip}'
215
+ if self.tunnel_exists_for_udid(udid):
216
+ # Skip tunnel if already exists for this udid
217
+ continue
218
+ if task_identifier in self.tunnel_tasks:
219
+ # Skip if already trying to establish a tunnel for this device
220
+ continue
221
+ try:
222
+ tunnel_service = await CoreDeviceTunnelProxy.create(lockdown)
223
+ except InvalidServiceError:
224
+ logger.warning(f'[{task_identifier}] failed to start CoreDeviceTunnelProxy - skipping')
225
+ continue
185
226
  self.tunnel_tasks[task_identifier] = TunnelTask(
186
227
  task=asyncio.create_task(self.start_tunnel_task(task_identifier, tunnel_service),
187
228
  name=f'start-tunnel-task-{task_identifier}'),
188
- udid=lockdown.udid
229
+ udid=udid
189
230
  )
190
- await asyncio.sleep(MOVDEV2_INTERVAL)
231
+ await asyncio.sleep(MOBDEV2_INTERVAL)
191
232
  except asyncio.CancelledError:
192
233
  pass
193
234
 
@@ -219,8 +260,8 @@ class TunneldCore:
219
260
  else:
220
261
  bailed_out = True
221
262
  logger.debug(
222
- f'not establishing tunnel from {asyncio.current_task().get_name()} '
223
- f'since there is already an active one for same udid')
263
+ f'[{asyncio.current_task().get_name()}] Not establishing tunnel since there is already an '
264
+ f'active one for same udid')
224
265
  except asyncio.CancelledError:
225
266
  pass
226
267
  except (asyncio.exceptions.IncompleteReadError, TimeoutError, OSError, ConnectionResetError, StreamError,
@@ -254,30 +295,28 @@ class TunneldCore:
254
295
  async def handle_new_potential_usb_cdc_ncm_interface_task(self, ip: str) -> None:
255
296
  rsd = None
256
297
  try:
257
- answers = None
258
- for i in range(REATTEMPT_COUNT):
259
- answers = await browse(REMOTED_SERVICE_NAMES, [ip])
260
- if answers:
261
- break
262
- logger.debug(f'No addresses found for: {ip}')
263
- await asyncio.sleep(REATTEMPT_INTERVAL)
264
-
265
- if not answers:
266
- raise asyncio.CancelledError()
267
-
268
- peer_address = answers[0].ips[0]
269
-
270
298
  # establish an untrusted RSD handshake
271
- rsd = RemoteServiceDiscoveryService((peer_address, RSD_PORT))
299
+ rsd = RemoteServiceDiscoveryService((ip, RSD_PORT))
272
300
 
273
301
  with stop_remoted():
274
- try:
275
- await rsd.connect()
276
- except (ConnectionRefusedError, TimeoutError):
277
- raise asyncio.CancelledError()
302
+ first_time = True
303
+ retry = False
304
+ while retry or first_time:
305
+ retry = False
306
+ try:
307
+ await rsd.connect()
308
+ except StreamClosedError:
309
+ # Could be on first try because of remoted race
310
+ if first_time:
311
+ retry = True
312
+ except (ConnectionRefusedError, TimeoutError, OSError):
313
+ raise asyncio.CancelledError()
314
+ finally:
315
+ first_time = False
278
316
 
279
317
  if (self.protocol == TunnelProtocol.QUIC) and (Version(rsd.product_version) < Version('17.0.0')):
280
318
  await rsd.close()
319
+ rsd = None
281
320
  raise asyncio.CancelledError()
282
321
 
283
322
  await asyncio.create_task(
@@ -299,7 +338,7 @@ class TunneldCore:
299
338
  pass
300
339
 
301
340
  if ip in self.tunnel_tasks:
302
- # in case the tunnel was removed just now
341
+ # In case the tunnel was removed just now
303
342
  self.tunnel_tasks.pop(ip)
304
343
 
305
344
  async def close(self) -> None:
@@ -325,7 +364,7 @@ class TunneldCore:
325
364
  """ Cancel active tunnels """
326
365
  for tunnel_ip in self.get_tunnels_ips().get(udid, []):
327
366
  self.tunnel_tasks.pop(tunnel_ip).task.cancel()
328
- logger.info(f'canceling tunnel {tunnel_ip}')
367
+ logger.info(f'Canceling tunnel {tunnel_ip}')
329
368
 
330
369
  def clear(self) -> None:
331
370
  """ Clear active tunnels """
@@ -348,7 +387,6 @@ class TunneldRunner:
348
387
  wifi_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True):
349
388
  @asynccontextmanager
350
389
  async def lifespan(app: FastAPI):
351
- logging.getLogger('zeroconf').disabled = True
352
390
  self._tunneld_core.start()
353
391
  yield
354
392
  logger.info('Closing tunneld tasks...')
@@ -427,7 +465,8 @@ class TunneldRunner:
427
465
  if not created_task and connection_type in ('usbmux', None):
428
466
  task_identifier = f'usbmux-{udid}'
429
467
  try:
430
- service = CoreDeviceTunnelProxy(create_using_usbmux(udid))
468
+ with create_using_usbmux(udid) as lockdown:
469
+ service = await CoreDeviceTunnelProxy.create(lockdown)
431
470
  task = asyncio.create_task(
432
471
  self._tunneld_core.start_tunnel_task(task_identifier, service, protocol=TunnelProtocol.TCP,
433
472
  queue=queue),
@@ -469,7 +508,7 @@ class TunneldRunner:
469
508
  }}))
470
509
 
471
510
  if not created_task:
472
- return fastapi.Response(status_code=501, content=json.dumps({'error': 'task not not created'}))
511
+ return fastapi.Response(status_code=501, content=json.dumps({'error': 'task not created'}))
473
512
 
474
513
  tunnel: Optional[TunnelResult] = await queue.get()
475
514
  if tunnel is not None:
pymobiledevice3/usbmux.py CHANGED
@@ -174,15 +174,17 @@ class MuxConnection:
174
174
  # first attempt to connect with possibly the wrong version header (plist protocol)
175
175
  sock = MuxConnection.create_usbmux_socket(usbmux_address=usbmux_address)
176
176
 
177
- message = usbmuxd_request.build({
178
- 'header': {'version': usbmuxd_version.PLIST, 'message': usbmuxd_msgtype.PLIST, 'tag': 1},
179
- 'data': plistlib.dumps({'MessageType': 'ReadBUID'})
180
- })
181
- sock.send(message)
182
- response = usbmuxd_response.parse_stream(sock)
183
-
184
- # if we sent a bad request, we should re-create the socket in the correct version this time
185
- sock.close()
177
+ try:
178
+ message = usbmuxd_request.build({
179
+ 'header': {'version': usbmuxd_version.PLIST, 'message': usbmuxd_msgtype.PLIST, 'tag': 1},
180
+ 'data': plistlib.dumps({'MessageType': 'ReadBUID'})
181
+ })
182
+ sock.send(message)
183
+ response = usbmuxd_response.parse_stream(sock)
184
+
185
+ finally:
186
+ # If we sent a bad request, we should re-create the socket in the correct version this time
187
+ sock.close()
186
188
  sock = MuxConnection.create_usbmux_socket(usbmux_address=usbmux_address)
187
189
 
188
190
  if response.header.version == usbmuxd_version.BINARY:
@@ -414,9 +416,11 @@ def create_mux(usbmux_address: Optional[str] = None) -> MuxConnection:
414
416
 
415
417
  def list_devices(usbmux_address: Optional[str] = None) -> list[MuxDevice]:
416
418
  mux = create_mux(usbmux_address=usbmux_address)
417
- mux.get_device_list(0.1)
418
- devices = mux.devices
419
- mux.close()
419
+ try:
420
+ mux.get_device_list(0.1)
421
+ devices = mux.devices
422
+ finally:
423
+ mux.close()
420
424
  return devices
421
425
 
422
426
 
pymobiledevice3/utils.py CHANGED
@@ -45,7 +45,7 @@ def asyncio_print_traceback(f: Callable):
45
45
  async def wrapper(*args, **kwargs):
46
46
  try:
47
47
  return await f(*args, **kwargs)
48
- except Exception as e: # noqa: E72
48
+ except (Exception, RuntimeError) as e: # noqa: E72
49
49
  if not isinstance(e, asyncio.CancelledError):
50
50
  traceback.print_exc()
51
51
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymobiledevice3
3
- Version: 4.27.7
3
+ Version: 5.0.0
4
4
  Summary: Pure python3 implementation for working with iDevices (iPhone, etc...)
5
5
  Author-email: doronz88 <doron88@gmail.com>, matan <matan1008@gmail.com>
6
6
  Maintainer-email: doronz88 <doron88@gmail.com>, matan <matan1008@gmail.com>
@@ -47,7 +47,6 @@ Requires-Dist: nest_asyncio>=1.5.5
47
47
  Requires-Dist: Pillow
48
48
  Requires-Dist: inquirer3>=0.6.0
49
49
  Requires-Dist: ipsw_parser>=1.3.4
50
- Requires-Dist: zeroconf>=0.132.2
51
50
  Requires-Dist: ifaddr
52
51
  Requires-Dist: hyperframe
53
52
  Requires-Dist: srptools
@@ -7,35 +7,35 @@ misc/remotexpc_sniffer.py,sha256=EThsKN0Vbs-mnLKCDXeooqg0MdpSkjwhAZHZwvhI458,797
7
7
  misc/understanding_idevice_protocol_layers.md,sha256=8tEqRXWOUPoxOJLZVh7C7H9JGCh2sQ3B5UH8_AymaQc,18805
8
8
  misc/usbmux_sniff.sh,sha256=iWtbucOEQ9_UEFXk9x-2VNt48Jg5zrPsnUbZ_LfZxwA,212
9
9
  pymobiledevice3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- pymobiledevice3/__main__.py,sha256=1nv18QRgR_FDq5rE95uhdZGOQ6xkPzNDrcMzQQs8ZZ4,11697
11
- pymobiledevice3/_version.py,sha256=o71ypd5ta4I52mZXxyMHxfYh6TBRnN5IHKzT1nOt2gI,706
12
- pymobiledevice3/bonjour.py,sha256=-Q_TLBGJ6qW3CX_DgBcz-CXfWSwxWVQ2L64hk6PxnDY,5631
10
+ pymobiledevice3/__main__.py,sha256=viUbhGzaoDi18zu5crX33PMws4qNBjBTwnaK5rAaObY,11651
11
+ pymobiledevice3/_version.py,sha256=cF99FZpN-aTdrzTHYCxKJsYbLYzvaOLuf8fkq4yEuRk,704
12
+ pymobiledevice3/bonjour.py,sha256=_f5RQs9uLDjDFsXMvykQs8vFoGJXiw6xfth86qewEGA,13719
13
13
  pymobiledevice3/ca.py,sha256=mTvWdSjTZw6Eb-22-IZ323GyA1G6CXYmdPedImTjm3A,10542
14
14
  pymobiledevice3/common.py,sha256=-PG6oaUkNFlB3jb7E0finMrX8wqhkS-cuTAfmLvZUmc,329
15
15
  pymobiledevice3/exceptions.py,sha256=VqWB6WWoMrXt8GDdKqRHeJ1otP-eZIThoHERswXWqpw,10347
16
16
  pymobiledevice3/irecv.py,sha256=FoEln1_zHkAiNcEctB5bStfhKNgniOSg7lg9xcX1U2Q,10596
17
17
  pymobiledevice3/irecv_devices.py,sha256=BG30ecXSChxdyYCCGIrIO0sVWT31hbKymB78nZWVfWc,38506
18
- pymobiledevice3/lockdown.py,sha256=xejqmSLhJsvM-F4rs4InxtVVtSYYSN3VJXnxd-ijspI,38814
18
+ pymobiledevice3/lockdown.py,sha256=jVrw--ifD8ewGwp5fZVYdoQDuaUiSckZ7fnB8dDtc58,38615
19
19
  pymobiledevice3/lockdown_service_provider.py,sha256=l5N72tiuI-2uowk8wu6B7qkjY2UmqQsnhdJqvJy3I8A,1744
20
20
  pymobiledevice3/pair_records.py,sha256=Tr28mlBWPXvOF7vdKBDOuw1rCRwm6RViDTGbikfP77I,6034
21
21
  pymobiledevice3/service_connection.py,sha256=_-PTLFr3krtwEBNHEKXCd_2eOGwMpbsfPbB8AX2uN-g,14861
22
22
  pymobiledevice3/tcp_forwarder.py,sha256=TVtIHn4hFlNIMEYXW9nwdSEhLfHaEHf4jkMsfJXLrTA,8906
23
- pymobiledevice3/usbmux.py,sha256=CvJ_NgH77wcfF7ZAQuLGHTIYkuWvhXPYZNQNR7-Jf8A,16820
24
- pymobiledevice3/utils.py,sha256=ybli_l8JIG2usFiToAsVYe0Ymg3q0bcpKqmYUF_wpi8,2179
23
+ pymobiledevice3/usbmux.py,sha256=NSgcgEbaFxGqyyYuZm4SgLx1DPdkP1oeGRLZ8bEkXes,16916
24
+ pymobiledevice3/utils.py,sha256=X3hU3wf_REUPRS-XtLgGgI2pxIwDGZP0RpZvZf7hOBY,2195
25
25
  pymobiledevice3/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  pymobiledevice3/cli/activation.py,sha256=mF64abX7d0bbyALVC-f_9rjc1DuB6mukP3Zwg9Hoj1Y,1321
27
27
  pymobiledevice3/cli/afc.py,sha256=z-qnBVUPA4uOnXADkYVyRJxeibRDFF3j5LejHt_6UW4,2129
28
28
  pymobiledevice3/cli/amfi.py,sha256=6hlqKrKOFj0secUnLQ8grDDnnh3fRsO6x_vo40oy22w,963
29
29
  pymobiledevice3/cli/apps.py,sha256=LH75A1gDRGP0nWO4QFcOUDg0EdphkGuYnWJgIQHrIBg,3859
30
30
  pymobiledevice3/cli/backup.py,sha256=SyHojiRRguxdkAPMz_Rp_9-zJNeuOtmpa0imdPN12-4,6691
31
- pymobiledevice3/cli/bonjour.py,sha256=qWFH05BZ-FlnSilVP0PMzPUidfBs5LPdepMhPyIliFE,2806
31
+ pymobiledevice3/cli/bonjour.py,sha256=X5W-5rPLX3xAwEeQJLPB_iOhdHcOL9ePkrm3xx4-Qic,2854
32
32
  pymobiledevice3/cli/cli_common.py,sha256=lQFhkTwPfi1UYFkMiDc-jrId2s2yHwzF5lFTK0dXM_s,12945
33
33
  pymobiledevice3/cli/companion_proxy.py,sha256=ey0X3moJ49zVJoNCpRMMHmf9fBZfdqimhz2VCA35oII,581
34
34
  pymobiledevice3/cli/completions.py,sha256=t8oryezQTcWDno_E2Cch7o1f-qURVL9M1Z4o6uLA_kM,1722
35
35
  pymobiledevice3/cli/crash.py,sha256=m1vs0_KUy4cxu8vHYjn7olay8oPQGTFZqMCHspnGpVs,3181
36
36
  pymobiledevice3/cli/developer.py,sha256=qSGvUZPVCwJZQaE9jF2vk-0Fp3x_2-wIlFjy-QoM5cI,61471
37
37
  pymobiledevice3/cli/diagnostics.py,sha256=VDWr41ryIZcpuQp47nQSzCiSuIILExqGSrwFizXCIkI,3641
38
- pymobiledevice3/cli/lockdown.py,sha256=498SgKdIC_YaoEjVtQQvo8bvv8GDeUTY8yWh4isy_qc,6941
38
+ pymobiledevice3/cli/lockdown.py,sha256=AV_7snLEkc9mbwWVlWg1Ki0tWQMtPHReziun_lxjNvY,7133
39
39
  pymobiledevice3/cli/mounter.py,sha256=AnNneNF_kW7XnBMe4V5cvlbLYd_mAP4tuB3PXLQpeiA,7724
40
40
  pymobiledevice3/cli/notification.py,sha256=vqn8uPslr7A9HiZ4yrs7YKF2VLS7Nk4G7ab5ELljpVQ,1962
41
41
  pymobiledevice3/cli/pcap.py,sha256=KzFxXWFRYWNOEJE1XAuMF2cG8I4k5wFVcMRhSdY4GQg,2188
@@ -43,7 +43,7 @@ pymobiledevice3/cli/power_assertion.py,sha256=aTlesowRyrbd9JXebEZe9SomTkDkZaAXIO
43
43
  pymobiledevice3/cli/processes.py,sha256=XNJe2KaacP7c-1NtR_HF6Gd5rByyj3vpyyT_xEntIbA,1102
44
44
  pymobiledevice3/cli/profile.py,sha256=WT8hgYOmkOHUlWtEz-BoBelCerT70TpwBBzJC8wRLmY,7939
45
45
  pymobiledevice3/cli/provision.py,sha256=yWabJrieISrBfFo7vCFIAM8xXLG-8_9qRe1igkTHIA0,1967
46
- pymobiledevice3/cli/remote.py,sha256=2XFa0x1EwPoRpndizM70QrKgPPj4BELpnoN1VZZUB5Q,11969
46
+ pymobiledevice3/cli/remote.py,sha256=n_PbPnUqnQRYxVG-y8rERmWu__64OY0mDx12mX62pRg,11999
47
47
  pymobiledevice3/cli/restore.py,sha256=vg3yjKOjsONUUzPp-XHIjNGMV8qCfptJrz6eJzG2diY,8172
48
48
  pymobiledevice3/cli/springboard.py,sha256=pYMqnD0zN_ETIASPqxBohi54F2HMC9jCILW4epVaaIk,3140
49
49
  pymobiledevice3/cli/syslog.py,sha256=JfLhjyVAeRx16VC4BsAu308rABjcIQX8DB7vbVKGiic,7462
@@ -60,8 +60,8 @@ pymobiledevice3/remote/module_imports.py,sha256=DwExSL1r4kkFIWmXiQpqPo-cGl4duYd3
60
60
  pymobiledevice3/remote/remote_service.py,sha256=fCyzm4oT_WEorAXVHVLYnIOyTOuMGhX69Co3HkUdRYY,867
61
61
  pymobiledevice3/remote/remote_service_discovery.py,sha256=iqPE1PiDDB2ISK-ThuUPEiSU9ETZ-FGTcANhb6MrWmo,7156
62
62
  pymobiledevice3/remote/remotexpc.py,sha256=KbFHaH4D3RnaATve6kaIpJMHNF8H-kdhbRbEbxFmO6w,8082
63
- pymobiledevice3/remote/tunnel_service.py,sha256=zVg9t7Z_zzK4Rav8Xn_UEanIXTzBvcjEsd_-bBYs1CE,46451
64
- pymobiledevice3/remote/utils.py,sha256=BgUODRwkET5lyloZxJ-PrVZqTvyOlBuJ-MM7OCSqZ9g,2506
63
+ pymobiledevice3/remote/tunnel_service.py,sha256=LhPCduH4mNU8sACGI3aJeXpH0UjPfvsjm0FGA4QIV_4,46830
64
+ pymobiledevice3/remote/utils.py,sha256=PV9tICVY6-L7eoqRF1yNmOGxdC2kp9h0kBqeoIAo4pA,2530
65
65
  pymobiledevice3/remote/xpc_message.py,sha256=-nVbf88ZN4ZNxLg6cOq4FfeKXYAoVRKnwGdfe7s-sZE,9336
66
66
  pymobiledevice3/remote/core_device/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
67
  pymobiledevice3/remote/core_device/app_service.py,sha256=x_K-3IA4NWG-skyffKyeGrS7slAbn3MsVLhdY62FSh0,5066
@@ -164,10 +164,10 @@ pymobiledevice3/services/web_protocol/session_protocol.py,sha256=7dJkFyivu554K6I
164
164
  pymobiledevice3/services/web_protocol/switch_to.py,sha256=hDddJUEePbRN-8xlllOeGhnYvE4NEnd8JJIlosLMB9c,2880
165
165
  pymobiledevice3/tunneld/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
166
  pymobiledevice3/tunneld/api.py,sha256=EfGKXEWhsMSB__menPmRmL9R6dpazVJDUy7B3pn05MM,2357
167
- pymobiledevice3/tunneld/server.py,sha256=L_98QatvVuyiXexoHF5rA0V56wC84VLeKhLyiFWwHrc,22960
168
- pymobiledevice3-4.27.7.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
169
- pymobiledevice3-4.27.7.dist-info/METADATA,sha256=SeVtOrCTF3zAP9RVUQWVMVUsbhFVazGwlJutdprYp08,17450
170
- pymobiledevice3-4.27.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
171
- pymobiledevice3-4.27.7.dist-info/entry_points.txt,sha256=jJMlOanHlVwUxcY__JwvKeWPrvBJr_wJyEq4oHIZNKE,66
172
- pymobiledevice3-4.27.7.dist-info/top_level.txt,sha256=MjZoRqcWPOh5banG-BbDOnKEfsS3kCxqV9cv-nzyg2Q,21
173
- pymobiledevice3-4.27.7.dist-info/RECORD,,
167
+ pymobiledevice3/tunneld/server.py,sha256=fbwnKrm4d84MpopWKIWoQF1_ZEUB4vONKmHih6qce_U,25206
168
+ pymobiledevice3-5.0.0.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
169
+ pymobiledevice3-5.0.0.dist-info/METADATA,sha256=kQKKC-FV5BNQqQUIOctTPVLa6wSnLgZXD06kbC9QLiI,17416
170
+ pymobiledevice3-5.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
171
+ pymobiledevice3-5.0.0.dist-info/entry_points.txt,sha256=jJMlOanHlVwUxcY__JwvKeWPrvBJr_wJyEq4oHIZNKE,66
172
+ pymobiledevice3-5.0.0.dist-info/top_level.txt,sha256=MjZoRqcWPOh5banG-BbDOnKEfsS3kCxqV9cv-nzyg2Q,21
173
+ pymobiledevice3-5.0.0.dist-info/RECORD,,