pymobiledevice3 6.2.0__py3-none-any.whl → 7.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.
Files changed (61) hide show
  1. pymobiledevice3/__main__.py +136 -44
  2. pymobiledevice3/_version.py +2 -2
  3. pymobiledevice3/bonjour.py +19 -20
  4. pymobiledevice3/cli/activation.py +24 -22
  5. pymobiledevice3/cli/afc.py +49 -41
  6. pymobiledevice3/cli/amfi.py +13 -18
  7. pymobiledevice3/cli/apps.py +71 -65
  8. pymobiledevice3/cli/backup.py +134 -93
  9. pymobiledevice3/cli/bonjour.py +31 -29
  10. pymobiledevice3/cli/cli_common.py +179 -232
  11. pymobiledevice3/cli/companion_proxy.py +12 -12
  12. pymobiledevice3/cli/crash.py +95 -52
  13. pymobiledevice3/cli/developer/__init__.py +62 -0
  14. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  15. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  16. pymobiledevice3/cli/developer/arbitration.py +50 -0
  17. pymobiledevice3/cli/developer/condition.py +33 -0
  18. pymobiledevice3/cli/developer/core_device.py +294 -0
  19. pymobiledevice3/cli/developer/debugserver.py +244 -0
  20. pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
  21. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  22. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  23. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  25. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  26. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  27. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  28. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  29. pymobiledevice3/cli/idam.py +18 -22
  30. pymobiledevice3/cli/lockdown.py +70 -75
  31. pymobiledevice3/cli/mounter.py +99 -57
  32. pymobiledevice3/cli/notification.py +38 -26
  33. pymobiledevice3/cli/pcap.py +36 -20
  34. pymobiledevice3/cli/power_assertion.py +15 -16
  35. pymobiledevice3/cli/processes.py +11 -17
  36. pymobiledevice3/cli/profile.py +120 -75
  37. pymobiledevice3/cli/provision.py +27 -26
  38. pymobiledevice3/cli/remote.py +108 -99
  39. pymobiledevice3/cli/restore.py +134 -129
  40. pymobiledevice3/cli/springboard.py +50 -50
  41. pymobiledevice3/cli/syslog.py +138 -74
  42. pymobiledevice3/cli/usbmux.py +66 -27
  43. pymobiledevice3/cli/version.py +2 -5
  44. pymobiledevice3/cli/webinspector.py +149 -103
  45. pymobiledevice3/remote/remote_service_discovery.py +11 -10
  46. pymobiledevice3/restore/device.py +28 -4
  47. pymobiledevice3/service_connection.py +1 -1
  48. pymobiledevice3/services/mobilebackup2.py +4 -1
  49. pymobiledevice3/services/screenshot.py +2 -2
  50. pymobiledevice3/services/web_protocol/automation_session.py +4 -2
  51. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  52. pymobiledevice3/services/web_protocol/element.py +3 -3
  53. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
  54. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
  55. pymobiledevice3/cli/completions.py +0 -50
  56. pymobiledevice3/cli/developer.py +0 -1645
  57. pymobiledevice3/cli/diagnostics.py +0 -110
  58. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
  59. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
  60. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
  61. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,1645 +0,0 @@
1
- import asyncio
2
- import contextlib
3
- import json
4
- import logging
5
- import os
6
- import plistlib
7
- import posixpath
8
- import shlex
9
- import signal
10
- import struct
11
- import subprocess
12
- import sys
13
- import time
14
- from collections import namedtuple
15
- from dataclasses import asdict
16
- from datetime import datetime, timezone
17
- from pathlib import Path
18
- from typing import IO, Optional
19
-
20
- import click
21
- from click.exceptions import MissingParameter, UsageError
22
- from ipsw_parser.dsc import create_device_support_layout, get_device_support_path
23
- from packaging.version import Version
24
- from plumbum import local
25
- from pykdebugparser.pykdebugparser import PyKdebugParser
26
-
27
- import pymobiledevice3
28
- from pymobiledevice3.cli.cli_common import (
29
- BASED_INT,
30
- Command,
31
- RSDCommand,
32
- default_json_encoder,
33
- print_json,
34
- user_requested_colored_output,
35
- )
36
- from pymobiledevice3.exceptions import (
37
- DeviceAlreadyInUseError,
38
- DvtDirListError,
39
- ExtractingStackshotError,
40
- RSDRequiredError,
41
- UnrecognizedSelectorError,
42
- )
43
- from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
44
- from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
45
- from pymobiledevice3.osu.os_utils import get_os_utils
46
- from pymobiledevice3.remote.core_device.app_service import AppServiceService
47
- from pymobiledevice3.remote.core_device.device_info import DeviceInfoService
48
- from pymobiledevice3.remote.core_device.diagnostics_service import DiagnosticsServiceService
49
- from pymobiledevice3.remote.core_device.file_service import APPLE_DOMAIN_DICT, FileServiceService
50
- from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
51
- from pymobiledevice3.services.accessibilityaudit import AccessibilityAudit
52
- from pymobiledevice3.services.crash_reports import CrashReportsManager
53
- from pymobiledevice3.services.debugserver_applist import DebugServerAppList
54
- from pymobiledevice3.services.device_arbitration import DtDeviceArbitration
55
- from pymobiledevice3.services.dtfetchsymbols import DtFetchSymbols
56
- from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
57
- from pymobiledevice3.services.dvt.instruments.activity_trace_tap import ActivityTraceTap, decode_message_format
58
- from pymobiledevice3.services.dvt.instruments.application_listing import ApplicationListing
59
- from pymobiledevice3.services.dvt.instruments.condition_inducer import ConditionInducer
60
- from pymobiledevice3.services.dvt.instruments.core_profile_session_tap import CoreProfileSessionTap
61
- from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo
62
- from pymobiledevice3.services.dvt.instruments.energy_monitor import EnergyMonitor
63
- from pymobiledevice3.services.dvt.instruments.graphics import Graphics
64
- from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation
65
- from pymobiledevice3.services.dvt.instruments.network_monitor import ConnectionDetectionEvent, NetworkMonitor
66
- from pymobiledevice3.services.dvt.instruments.notifications import Notifications
67
- from pymobiledevice3.services.dvt.instruments.process_control import ProcessControl
68
- from pymobiledevice3.services.dvt.instruments.screenshot import Screenshot
69
- from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap
70
- from pymobiledevice3.services.dvt.testmanaged.xcuitest import XCUITestService
71
- from pymobiledevice3.services.installation_proxy import InstallationProxyService
72
- from pymobiledevice3.services.remote_fetch_symbols import RemoteFetchSymbolsService
73
- from pymobiledevice3.services.remote_server import RemoteServer
74
- from pymobiledevice3.services.screenshot import ScreenshotService
75
- from pymobiledevice3.services.simulate_location import DtSimulateLocation
76
- from pymobiledevice3.tcp_forwarder import LockdownTcpForwarder
77
- from pymobiledevice3.utils import try_decode
78
-
79
- OSUTILS = get_os_utils()
80
- BSC_SUBCLASS = 0x40C
81
- BSC_CLASS = 0x4
82
- VFS_AND_TRACES_SET = {0x03010000, 0x07FF0000}
83
- DEBUGSERVER_CONNECTION_STEPS = """
84
- Follow the following connections steps from LLDB:
85
-
86
- (lldb) platform select remote-ios
87
- (lldb) target create /path/to/local/application.app
88
- (lldb) script lldb.target.module[0].SetPlatformFileSpec(lldb.SBFileSpec('/private/var/containers/Bundle/Application/<APP-UUID>/application.app'))
89
- (lldb) process connect connect://[{host}]:{port} <-- ACTUAL CONNECTION DETAILS!
90
- (lldb) process launch
91
- """
92
-
93
- MatchedProcessByPid = namedtuple("MatchedProcess", "name pid")
94
-
95
- logger = logging.getLogger(__name__)
96
-
97
-
98
- @click.group()
99
- def cli() -> None:
100
- pass
101
-
102
-
103
- @cli.group()
104
- def developer() -> None:
105
- """
106
- Perform developer operations (Requires enable of Developer-Mode)
107
-
108
- These options require the DeveloperDiskImage.dmg to be mounted on the device prior
109
- to execution. You can achieve this using:
110
-
111
- pymobiledevice3 mounter mount
112
-
113
- Also, starting at iOS 17.0, a tunnel must be created to the device for the services
114
- to be accessible. Therefore, every CLI command is retried with a `--tunnel` option
115
- for implicitly accessing tunneld when necessary
116
- """
117
- pass
118
-
119
-
120
- @developer.command("shell", cls=Command)
121
- @click.argument("service")
122
- @click.option("-r", "--remove-ssl-context", is_flag=True)
123
- def developer_shell(service_provider: LockdownClient, service, remove_ssl_context):
124
- """Launch developer IPython shell (used for pymobiledevice3 R&D)"""
125
- with RemoteServer(service_provider, service, remove_ssl_context) as service:
126
- service.shell()
127
-
128
-
129
- @developer.group()
130
- def dvt() -> None:
131
- """Access advanced instrumentation APIs"""
132
- pass
133
-
134
-
135
- @dvt.command("proclist", cls=Command)
136
- def proclist(service_provider: LockdownClient) -> None:
137
- """Show process list"""
138
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
139
- processes = DeviceInfo(dvt).proclist()
140
- for process in processes:
141
- if "startDate" in process:
142
- process["startDate"] = str(process["startDate"])
143
-
144
- print_json(processes)
145
-
146
-
147
- @dvt.command("is-running-pid", cls=Command)
148
- @click.argument("pid", type=click.INT)
149
- def is_running_pid(service_provider: LockdownClient, pid: int) -> None:
150
- """Simple check if PID is running"""
151
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
152
- print_json(DeviceInfo(dvt).is_running_pid(pid))
153
-
154
-
155
- @dvt.command("memlimitoff", cls=Command)
156
- @click.argument("pid", type=click.INT)
157
- def memlimitoff(service_provider: LockdownServiceProvider, pid: int) -> None:
158
- """Disable process memory limit"""
159
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
160
- ProcessControl(dvt).disable_memory_limit_for_pid(pid)
161
-
162
-
163
- @dvt.command("applist", cls=Command)
164
- def applist(service_provider: LockdownServiceProvider) -> None:
165
- """Show application list"""
166
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
167
- apps = ApplicationListing(dvt).applist()
168
- print_json(apps)
169
-
170
-
171
- @dvt.command("signal", cls=Command)
172
- @click.argument("pid", type=click.INT)
173
- @click.argument("sig", type=click.INT, required=False)
174
- @click.option("-s", "--signal-name", type=click.Choice([s.name for s in signal.Signals]))
175
- def send_signal(service_provider, pid, sig, signal_name) -> None:
176
- """Send a signal to process by its PID"""
177
- if not sig and not signal_name:
178
- raise MissingParameter(param_type="argument|option", param_hint="'SIG|SIGNAL-NAME'")
179
- if sig and signal_name:
180
- raise UsageError(message="Cannot give SIG and SIGNAL-NAME together")
181
- sig = sig or signal.Signals[signal_name].value
182
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
183
- ProcessControl(dvt).signal(pid, sig)
184
-
185
-
186
- @dvt.command("kill", cls=Command)
187
- @click.argument("pid", type=click.INT)
188
- def kill(service_provider: LockdownClient, pid) -> None:
189
- """Kill a process by its pid."""
190
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
191
- ProcessControl(dvt).kill(pid)
192
-
193
-
194
- @dvt.command(cls=Command)
195
- @click.argument("app_bundle_identifier")
196
- def process_id_for_bundle_id(service_provider: LockdownServiceProvider, app_bundle_identifier: str) -> None:
197
- """Get PID of a bundle identifier (only returns a valid value if its running)."""
198
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
199
- print(ProcessControl(dvt).process_identifier_for_bundle_identifier(app_bundle_identifier))
200
-
201
-
202
- def get_matching_processes(
203
- service_provider: LockdownServiceProvider, name: Optional[str] = None, bundle_identifier: Optional[str] = None
204
- ) -> list[MatchedProcessByPid]:
205
- result = []
206
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
207
- device_info = DeviceInfo(dvt)
208
- for process in device_info.proclist():
209
- current_name = process.get("name")
210
- current_bundle_identifier = process.get("bundleIdentifier", "")
211
- pid = process["pid"]
212
- if (bundle_identifier is not None and bundle_identifier in current_bundle_identifier) or (
213
- name is not None and name in current_name
214
- ):
215
- result.append(MatchedProcessByPid(name=current_name, pid=pid))
216
- return result
217
-
218
-
219
- @dvt.command("pkill", cls=Command)
220
- @click.argument("expression")
221
- @click.option("--bundle", is_flag=True, help="Treat given expression as a bundle-identifier instead of a process name")
222
- def pkill(service_provider: LockdownServiceProvider, expression: str, bundle: False) -> None:
223
- """kill all processes containing `expression` in their name."""
224
- matching_name = expression if not bundle else None
225
- matching_bundle_identifier = expression if bundle else None
226
- matching_processes = get_matching_processes(
227
- service_provider, name=matching_name, bundle_identifier=matching_bundle_identifier
228
- )
229
-
230
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
231
- process_control = ProcessControl(dvt)
232
-
233
- for process in matching_processes:
234
- logger.info(f"killing {process.name}({process.pid})")
235
- process_control.kill(process.pid)
236
-
237
-
238
- @dvt.command("launch", cls=Command)
239
- @click.argument("arguments", type=click.STRING)
240
- @click.option(
241
- "--kill-existing/--no-kill-existing", default=True, help="Whether to kill an existing instance of this process"
242
- )
243
- @click.option("--suspended", is_flag=True, help="Same as WaitForDebugger")
244
- @click.option(
245
- "--env",
246
- multiple=True,
247
- type=click.Tuple((str, str)),
248
- help="Environment variables to pass to process given as a list of key value",
249
- )
250
- @click.option("--stream", is_flag=True)
251
- def launch(
252
- service_provider: LockdownClient, arguments: str, kill_existing: bool, suspended: bool, env: tuple, stream: bool
253
- ) -> None:
254
- """Launch a process."""
255
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
256
- parsed_arguments = shlex.split(arguments)
257
- process_control = ProcessControl(dvt)
258
- pid = process_control.launch(
259
- bundle_id=parsed_arguments[0],
260
- arguments=parsed_arguments[1:],
261
- kill_existing=kill_existing,
262
- start_suspended=suspended,
263
- environment=dict(env),
264
- )
265
- print(f"Process launched with pid {pid}")
266
- while stream:
267
- for output_received in process_control:
268
- logging.getLogger(f"PID:{output_received.pid}").info(output_received.message.strip())
269
-
270
-
271
- @dvt.command("shell", cls=Command)
272
- def dvt_shell(service_provider: LockdownClient):
273
- """Launch developer shell (used for pymobiledevice3 R&D)"""
274
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
275
- dvt.shell()
276
-
277
-
278
- def show_dirlist(device_info: DeviceInfo, dirname, recursive=False):
279
- try:
280
- filenames = device_info.ls(dirname)
281
- except DvtDirListError:
282
- return
283
-
284
- for filename in filenames:
285
- filename = posixpath.join(dirname, filename)
286
- print(filename)
287
- if recursive:
288
- show_dirlist(device_info, filename, recursive=recursive)
289
-
290
-
291
- @dvt.command("ls", cls=Command)
292
- @click.argument("path", type=click.Path(exists=False, readable=False))
293
- @click.option("-r", "--recursive", is_flag=True)
294
- def ls(service_provider: LockdownClient, path, recursive):
295
- """List directory"""
296
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
297
- show_dirlist(DeviceInfo(dvt), path, recursive=recursive)
298
-
299
-
300
- @dvt.command("device-information", cls=Command)
301
- def device_information(service_provider: LockdownClient):
302
- """Print system information"""
303
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
304
- device_info = DeviceInfo(dvt)
305
- info = {
306
- "hardware": device_info.hardware_information(),
307
- "network": device_info.network_information(),
308
- "kernel-name": device_info.mach_kernel_name(),
309
- "kpep-database": device_info.kpep_database(),
310
- }
311
- with contextlib.suppress(UnrecognizedSelectorError):
312
- info["system"] = device_info.system_information()
313
- print_json(info)
314
-
315
-
316
- @dvt.command("netstat", cls=Command)
317
- def netstat(service_provider: LockdownClient):
318
- """Print information about current network activity."""
319
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, NetworkMonitor(dvt) as monitor:
320
- for event in monitor:
321
- if isinstance(event, ConnectionDetectionEvent):
322
- logger.info(
323
- f"Connection detected: {event.local_address.data.address}:{event.local_address.port} -> "
324
- f"{event.remote_address.data.address}:{event.remote_address.port}"
325
- )
326
-
327
-
328
- @dvt.command("screenshot", cls=Command)
329
- @click.argument("out", type=click.File("wb"))
330
- def dvt_screenshot(service_provider: LockdownClient, out):
331
- """Take device screenshot"""
332
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
333
- out.write(Screenshot(dvt).get_screenshot())
334
-
335
-
336
- @dvt.command("xcuitest", cls=Command)
337
- @click.argument("bundle-id")
338
- def xcuitest(service_provider: LockdownClient, bundle_id: str) -> None:
339
- """\b
340
- Start XCUITest
341
-
342
- Usage example:
343
- python3 -m pymobiledevice3 developer dvt xcuitest com.facebook.WebDriverAgentRunner.xctrunner
344
- """
345
- XCUITestService(service_provider).run(bundle_id)
346
-
347
-
348
- @dvt.group("sysmon")
349
- def sysmon():
350
- """System monitor options."""
351
-
352
-
353
- @sysmon.group("process")
354
- def sysmon_process():
355
- """Process monitor options."""
356
-
357
-
358
- @sysmon_process.command("monitor", cls=Command)
359
- @click.argument("threshold", type=click.FLOAT)
360
- def sysmon_process_monitor(service_provider: LockdownClient, threshold):
361
- """monitor all most consuming processes by given cpuUsage threshold."""
362
-
363
- Process = namedtuple("process", "pid name cpuUsage physFootprint")
364
-
365
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Sysmontap(dvt) as sysmon:
366
- for process_snapshot in sysmon.iter_processes():
367
- entries = []
368
- for process in process_snapshot:
369
- if (process["cpuUsage"] is not None) and (process["cpuUsage"] >= threshold):
370
- entries.append(
371
- Process(
372
- pid=process["pid"],
373
- name=process["name"],
374
- cpuUsage=process["cpuUsage"],
375
- physFootprint=process["physFootprint"],
376
- )
377
- )
378
-
379
- logger.info(entries)
380
-
381
-
382
- @sysmon_process.command("single", cls=Command)
383
- @click.option("-a", "--attributes", multiple=True, help="filter processes by given attribute value given as key=value")
384
- def sysmon_process_single(service_provider: LockdownClient, attributes: list[str]):
385
- """show a single snapshot of currently running processes."""
386
-
387
- count = 0
388
-
389
- result = []
390
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
391
- device_info = DeviceInfo(dvt)
392
-
393
- with Sysmontap(dvt) as sysmon:
394
- for process_snapshot in sysmon.iter_processes():
395
- count += 1
396
-
397
- if count < 2:
398
- # first sample doesn't contain an initialized value for cpuUsage
399
- continue
400
-
401
- for process in process_snapshot:
402
- skip = False
403
- if attributes is not None:
404
- for filter_attr in attributes:
405
- filter_attr, filter_value = filter_attr.split("=")
406
- if str(process[filter_attr]) != filter_value:
407
- skip = True
408
- break
409
-
410
- if skip:
411
- continue
412
-
413
- # adding "artificially" the execName field
414
- process["execName"] = device_info.execname_for_pid(process["pid"])
415
- result.append(process)
416
-
417
- # exit after single snapshot
418
- break
419
- print_json(result)
420
-
421
-
422
- @sysmon_process.command("monitor-single", cls=Command)
423
- @click.option(
424
- "-a",
425
- "--attributes",
426
- multiple=True,
427
- help="filter processes by attribute (key=value). Multiple filters on same attribute use OR logic, different attributes use AND.",
428
- )
429
- @click.option(
430
- "-o",
431
- "--output",
432
- type=click.Path(),
433
- default=None,
434
- help="output file path for JSONL format (optional, defaults to stdout)",
435
- )
436
- @click.option(
437
- "-i", "--interval", type=click.INT, default=None, help="minimum interval in milliseconds between outputs (optional)"
438
- )
439
- @click.option(
440
- "-d",
441
- "--duration",
442
- type=click.INT,
443
- default=None,
444
- help="maximum duration in milliseconds to run monitoring (optional)",
445
- )
446
- def sysmon_process_monitor_single(
447
- service_provider: LockdownClient,
448
- attributes: list[str],
449
- output: Optional[str],
450
- interval: Optional[int],
451
- duration: Optional[int],
452
- ):
453
- """Continuously monitor a single process with comprehensive metrics."""
454
- count = 0
455
- start_time = None
456
-
457
- # Parse attributes into grouped filters: same attribute uses OR, different attributes use AND
458
- parsed_filters: dict[str, list[str]] = {}
459
- if attributes:
460
- for raw in attributes:
461
- key, value = raw.split("=", 1)
462
- parsed_filters.setdefault(key, []).append(value)
463
-
464
- def matches_filters(proc: dict) -> bool:
465
- """Check if process matches all filter criteria."""
466
- if not parsed_filters:
467
- return True
468
- return all(str(proc.get(key)) in values for key, values in parsed_filters.items())
469
-
470
- with contextlib.ExitStack() as stack:
471
- output_file = stack.enter_context(open(output, "w")) if output else None
472
-
473
- dvt = stack.enter_context(DvtSecureSocketProxyService(lockdown=service_provider))
474
- sysmon = stack.enter_context(Sysmontap(dvt))
475
-
476
- for process_snapshot in sysmon.iter_processes():
477
- count += 1
478
-
479
- if count < 2:
480
- continue
481
-
482
- if start_time is None:
483
- start_time = time.time()
484
-
485
- if duration is not None:
486
- elapsed_ms = (time.time() - start_time) * 1000
487
- if elapsed_ms >= duration:
488
- break
489
-
490
- for process in process_snapshot:
491
- if not matches_filters(process):
492
- continue
493
-
494
- process["timestamp"] = datetime.now(timezone.utc).isoformat()
495
-
496
- if output_file:
497
- json_output = json.dumps(process, default=default_json_encoder)
498
- output_file.write(json_output + "\n")
499
- output_file.flush()
500
- else:
501
- print_json(process)
502
-
503
- if interval:
504
- time.sleep(interval / 1000.0)
505
-
506
-
507
- @sysmon.command("system", cls=Command)
508
- @click.option("-f", "--fields", help='field names splitted by ",".')
509
- def sysmon_system(service_provider: LockdownClient, fields):
510
- """show current system stats."""
511
-
512
- if fields is not None:
513
- fields = fields.split(",")
514
-
515
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
516
- sysmontap = Sysmontap(dvt)
517
- with sysmontap as sysmon:
518
- system = None
519
- system_usage = None
520
- system_usage_seen = False # Tracks if the first occurrence of SystemCPUUsage
521
-
522
- for row in sysmon:
523
- if "System" in row and system is None:
524
- system = sysmon.system_attributes_cls(*row["System"])
525
-
526
- if "SystemCPUUsage" in row:
527
- if system_usage_seen:
528
- system_usage = {
529
- **row["SystemCPUUsage"],
530
- **{
531
- "CPUCount": row["CPUCount"],
532
- "EnabledCPUs": row["EnabledCPUs"],
533
- },
534
- }
535
- else: # Ignore the first occurrence because first occurrence always gives a incorrect value - 100 or 0
536
- system_usage_seen = True
537
-
538
- if system and system_usage:
539
- break
540
-
541
- attrs_dict = {**asdict(system), **system_usage}
542
- for name, value in attrs_dict.items():
543
- if (fields is None) or (name in fields):
544
- print(f"{name}: {value}")
545
-
546
-
547
- @dvt.group("core-profile-session")
548
- def core_profile_session():
549
- """Access tailspin features"""
550
-
551
-
552
- bsc_filter = click.option("--bsc/--no-bsc", default=False, help="Whether to print BSC events or not.")
553
- class_filter = click.option(
554
- "-cf", "--class-filters", multiple=True, type=BASED_INT, help="Events class filter. Omit for all."
555
- )
556
- subclass_filter = click.option(
557
- "-sf", "--subclass-filters", multiple=True, type=BASED_INT, help="Events subclass filter. Omit for all."
558
- )
559
-
560
-
561
- def parse_filters(subclasses: list[int], classes: list[int]):
562
- if not subclasses and not classes:
563
- return None
564
- parsed = set()
565
- for subclass in subclasses:
566
- if subclass == BSC_SUBCLASS:
567
- parsed |= VFS_AND_TRACES_SET
568
- parsed.add(subclass << 16)
569
- for class_ in classes:
570
- if class_ == BSC_CLASS:
571
- parsed |= VFS_AND_TRACES_SET
572
- parsed.add((class_ << 24) | 0x00FF0000)
573
- return parsed
574
-
575
-
576
- @core_profile_session.command("live", cls=Command)
577
- @click.option("-c", "--count", type=click.INT, default=-1, help="Number of events to print. Omit to endless sniff.")
578
- @bsc_filter
579
- @class_filter
580
- @subclass_filter
581
- @click.option("--tid", type=click.INT, default=None, help="Thread ID to filter. Omit for all.")
582
- @click.option("--timestamp/--no-timestamp", default=True, help="Whether to print timestamp or not.")
583
- @click.option("--event-name/--no-event-name", default=True, help="Whether to print event name or not.")
584
- @click.option("--func-qual/--no-func-qual", default=True, help="Whether to print function qualifier or not.")
585
- @click.option("--show-tid/--no-show-tid", default=True, help="Whether to print thread id or not.")
586
- @click.option("--process-name/--no-process-name", default=True, help="Whether to print process name or not.")
587
- @click.option("--args/--no-args", default=True, help="Whether to print event arguments or not.")
588
- def live_profile_session(
589
- service_provider: LockdownClient,
590
- count,
591
- bsc,
592
- class_filters,
593
- subclass_filters,
594
- tid,
595
- timestamp,
596
- event_name,
597
- func_qual,
598
- show_tid,
599
- process_name,
600
- args,
601
- ):
602
- """Print kevents received from the device in real time."""
603
-
604
- parser = PyKdebugParser()
605
- parser.filter_class = class_filters
606
- if bsc:
607
- subclass_filters = [*list(subclass_filters), BSC_SUBCLASS]
608
- parser.filter_subclass = subclass_filters
609
- filters = parse_filters(subclass_filters, class_filters)
610
- parser.filter_tid = tid
611
- parser.show_timestamp = timestamp
612
- parser.show_name = event_name
613
- parser.show_func_qual = func_qual
614
- parser.show_tid = show_tid
615
- parser.show_process = process_name
616
- parser.show_args = args
617
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
618
- trace_codes_map = DeviceInfo(dvt).trace_codes()
619
- time_config = CoreProfileSessionTap.get_time_config(dvt)
620
- parser.numer = time_config["numer"]
621
- parser.denom = time_config["denom"]
622
- parser.mach_absolute_time = time_config["mach_absolute_time"]
623
- parser.usecs_since_epoch = time_config["usecs_since_epoch"]
624
- parser.timezone = time_config["timezone"]
625
- with CoreProfileSessionTap(dvt, time_config, filters) as tap:
626
- for i, event in enumerate(parser.formatted_kevents(tap.get_kdbuf_stream(), trace_codes_map)):
627
- print(event)
628
- if i == count:
629
- break
630
-
631
-
632
- @core_profile_session.command("save", cls=Command)
633
- @click.argument("out", type=click.File("wb"))
634
- @bsc_filter
635
- @class_filter
636
- @subclass_filter
637
- def save_profile_session(service_provider: LockdownClient, out, bsc, class_filters, subclass_filters):
638
- """Dump core profiling information."""
639
- if bsc:
640
- subclass_filters = [*list(subclass_filters), BSC_SUBCLASS]
641
- filters = parse_filters(subclass_filters, class_filters)
642
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, CoreProfileSessionTap(dvt, {}, filters) as tap:
643
- tap.dump(out)
644
-
645
-
646
- @core_profile_session.command("stackshot", cls=Command)
647
- @click.option("--out", type=click.File("w"), default=None)
648
- def stackshot(service_provider: LockdownClient, out):
649
- """Dump stackshot information."""
650
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, CoreProfileSessionTap(dvt, {}) as tap:
651
- try:
652
- data = tap.get_stackshot()
653
- except ExtractingStackshotError:
654
- logger.exception("Extracting stackshot failed")
655
- return
656
-
657
- if out is not None:
658
- json.dump(data, out, indent=4, default=default_json_encoder)
659
- else:
660
- print_json(data)
661
-
662
-
663
- @core_profile_session.command("parse-live", cls=Command)
664
- @click.option("-c", "--count", type=click.INT, default=-1, help="Number of events to print. Omit to endless sniff.")
665
- @click.option("--tid", type=click.INT, default=None, help="Thread ID to filter. Omit for all.")
666
- @click.option("--show-tid/--no-show-tid", default=False, help="Whether to print thread id or not.")
667
- @bsc_filter
668
- @class_filter
669
- @subclass_filter
670
- @click.option("--process", default=None, help="Process ID / name to filter. Omit for all.")
671
- def parse_live_profile_session(
672
- service_provider: LockdownClient, count, tid, show_tid, bsc, class_filters, subclass_filters, process
673
- ):
674
- """Print traces (syscalls, thread events, etc.) received from the device in real time."""
675
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
676
- print("Receiving time information")
677
- time_config = CoreProfileSessionTap.get_time_config(dvt)
678
- parser = PyKdebugParser()
679
- parser.filter_class = list(class_filters)
680
- if bsc:
681
- subclass_filters = [*list(subclass_filters), BSC_SUBCLASS]
682
- parser.filter_subclass = subclass_filters
683
- filters = parse_filters(subclass_filters, class_filters)
684
- parser.numer = time_config["numer"]
685
- parser.denom = time_config["denom"]
686
- parser.mach_absolute_time = time_config["mach_absolute_time"]
687
- parser.usecs_since_epoch = time_config["usecs_since_epoch"]
688
- parser.timezone = time_config["timezone"]
689
- parser.filter_tid = tid
690
- parser.filter_process = process
691
- parser.show_tid = show_tid
692
- parser.color = user_requested_colored_output()
693
-
694
- with CoreProfileSessionTap(dvt, time_config, filters) as tap:
695
- if show_tid:
696
- print("{:^32}|{:^11}|{:^33}| Event".format("Time", "Thread", "Process"))
697
- else:
698
- print("{:^32}|{:^33}| Event".format("Time", "Process"))
699
-
700
- for i, trace in enumerate(parser.formatted_traces(tap.get_kdbuf_stream())):
701
- print(trace, flush=True)
702
- if i == count:
703
- break
704
-
705
-
706
- def get_image_name(dsc_uuid_map, image_uuid, current_dsc_map):
707
- if not current_dsc_map:
708
- for dsc_mapping in dsc_uuid_map.values():
709
- if image_uuid in dsc_mapping:
710
- current_dsc_map.update(dsc_mapping)
711
-
712
- return current_dsc_map.get(image_uuid, image_uuid)
713
-
714
-
715
- def format_callstack(callstack, dsc_uuid_map, current_dsc_map):
716
- lines = callstack.splitlines()
717
- for i, line in enumerate(lines[1:]):
718
- if ":" in line:
719
- uuid = line.split(":")[0].strip()
720
- lines[i + 1] = line.replace(uuid, get_image_name(dsc_uuid_map, uuid, current_dsc_map))
721
- return "\n".join(lines)
722
-
723
-
724
- @core_profile_session.command("callstacks-live", cls=Command)
725
- @click.option("-c", "--count", type=click.INT, default=-1, help="Number of events to print. Omit to endless sniff.")
726
- @click.option("--process", default=None, help="Process to filter. Omit for all.")
727
- @click.option("--tid", type=click.INT, default=None, help="Thread ID to filter. Omit for all.")
728
- @click.option("--show-tid/--no-show-tid", default=False, help="Whether to print thread id or not.")
729
- def callstacks_live_profile_session(service_provider: LockdownClient, count, process, tid, show_tid):
730
- """Print callstacks received from the device in real time."""
731
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
732
- print("Receiving time information")
733
- time_config = CoreProfileSessionTap.get_time_config(dvt)
734
- parser = PyKdebugParser()
735
- parser.numer = time_config["numer"]
736
- parser.denom = time_config["denom"]
737
- parser.mach_absolute_time = time_config["mach_absolute_time"]
738
- parser.usecs_since_epoch = time_config["usecs_since_epoch"]
739
- parser.timezone = time_config["timezone"]
740
- parser.filter_tid = tid
741
- parser.filter_process = process
742
- parser.color = user_requested_colored_output()
743
- parser.show_tid = show_tid
744
-
745
- with open(os.path.join(pymobiledevice3.__path__[0], "resources", "dsc_uuid_map.json")) as fd:
746
- dsc_uuid_map = json.load(fd)
747
-
748
- current_dsc_map = {}
749
- with CoreProfileSessionTap(dvt, time_config) as tap:
750
- for i, callstack in enumerate(parser.formatted_callstacks(tap.get_kdbuf_stream())):
751
- print(format_callstack(callstack, dsc_uuid_map, current_dsc_map))
752
- if i == count:
753
- break
754
-
755
-
756
- @dvt.command("trace-codes", cls=Command)
757
- def dvt_trace_codes(service_provider: LockdownClient):
758
- """Print KDebug trace codes."""
759
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
760
- device_info = DeviceInfo(dvt)
761
- print_json({hex(k): v for k, v in device_info.trace_codes().items()})
762
-
763
-
764
- @dvt.command("name-for-uid", cls=Command)
765
- @click.argument("uid", type=click.INT)
766
- def dvt_name_for_uid(service_provider: LockdownClient, uid):
767
- """Print the assiciated username for the given uid."""
768
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
769
- device_info = DeviceInfo(dvt)
770
- print(device_info.name_for_uid(uid))
771
-
772
-
773
- @dvt.command("name-for-gid", cls=Command)
774
- @click.argument("gid", type=click.INT)
775
- def dvt_name_for_gid(service_provider: LockdownClient, gid):
776
- """Print the assiciated group name for the given gid."""
777
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
778
- device_info = DeviceInfo(dvt)
779
- print(device_info.name_for_gid(gid))
780
-
781
-
782
- @dvt.command("oslog", cls=Command)
783
- @click.option("--pid", type=click.INT)
784
- def dvt_oslog(service_provider: LockdownClient, pid):
785
- """Sniff device oslog (not very stable, but includes more data and normal syslog)"""
786
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, ActivityTraceTap(dvt) as tap:
787
- for message in tap:
788
- message_pid = message.process
789
- # without message_type maybe signpost have event_type
790
- message_type = (
791
- message.message_type
792
- if hasattr(message, "message_type")
793
- else message.event_type
794
- if hasattr(message, "event_type")
795
- else "unknown"
796
- )
797
- sender_image_path = message.sender_image_path
798
- image_name = os.path.basename(sender_image_path)
799
- subsystem = message.subsystem
800
- category = message.category
801
- timestamp = datetime.now()
802
-
803
- if pid is not None and message_pid != pid:
804
- continue
805
-
806
- formatted_message = decode_message_format(message.message) if message.message else message.name
807
-
808
- if user_requested_colored_output():
809
- timestamp = click.style(str(timestamp), bold=True)
810
- message_pid = click.style(str(message_pid), "magenta")
811
- subsystem = click.style(subsystem, "green")
812
- category = click.style(category, "green")
813
- image_name = click.style(image_name, "yellow")
814
- message_type = click.style(message_type, "cyan")
815
-
816
- print(
817
- f"[{timestamp}][{subsystem}][{category}][{message_pid}][{image_name}] "
818
- f"<{message_type}>: {formatted_message}"
819
- )
820
-
821
-
822
- @dvt.command("energy", cls=Command)
823
- @click.argument("pid-list", nargs=-1)
824
- def dvt_energy(service_provider: LockdownClient, pid_list):
825
- """Monitor the energy consumption for given PIDs"""
826
-
827
- if len(pid_list) == 0:
828
- logger.error("pid_list must not be empty")
829
- return
830
-
831
- pid_list = [int(pid) for pid in pid_list]
832
-
833
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, EnergyMonitor(dvt, pid_list) as energy_monitor:
834
- for telemetry in energy_monitor:
835
- logger.info(telemetry)
836
-
837
-
838
- @dvt.command("notifications", cls=Command)
839
- def dvt_notifications(service_provider: LockdownClient):
840
- """Monitor memory and app notifications"""
841
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Notifications(dvt) as notifications:
842
- for notification in notifications:
843
- logger.info(notification)
844
-
845
-
846
- @dvt.command("graphics", cls=Command)
847
- def dvt_graphics(service_provider: LockdownClient):
848
- """Monitor graphics-related information"""
849
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Graphics(dvt) as graphics:
850
- for stats in graphics:
851
- logger.info(stats)
852
-
853
-
854
- @developer.group("fetch-symbols")
855
- def fetch_symbols():
856
- """Download the DSC (and dyld) from the device"""
857
- pass
858
-
859
-
860
- async def fetch_symbols_list_task(service_provider: LockdownServiceProvider) -> None:
861
- if Version(service_provider.product_version) < Version("17.0"):
862
- print_json(DtFetchSymbols(service_provider).list_files())
863
- else:
864
- if not isinstance(service_provider, RemoteServiceDiscoveryService):
865
- raise RSDRequiredError(service_provider.identifier)
866
-
867
- async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
868
- print_json([f.file_path for f in await fetch_symbols.get_dsc_file_list()])
869
-
870
-
871
- @fetch_symbols.command("list", cls=Command)
872
- def fetch_symbols_list(service_provider: LockdownServiceProvider) -> None:
873
- """list of files to be downloaded"""
874
- asyncio.run(fetch_symbols_list_task(service_provider), debug=True)
875
-
876
-
877
- async def fetch_symbols_download_task(service_provider: LockdownServiceProvider, out: Optional[str]) -> None:
878
- should_create_device_support_layout = False
879
- if out is None:
880
- out = get_device_support_path(
881
- service_provider.product_type, service_provider.product_version, service_provider.product_build_version
882
- )
883
- should_create_device_support_layout = True
884
-
885
- logger.info(f"Downloading DSC into: {out}")
886
-
887
- out = Path(out)
888
- out.mkdir(parents=True, exist_ok=True)
889
-
890
- if Version(service_provider.product_version) < Version("17.0"):
891
- fetch_symbols = DtFetchSymbols(service_provider)
892
- files = fetch_symbols.list_files()
893
-
894
- downloaded_files = set()
895
-
896
- for i, file in enumerate(files):
897
- if file.startswith("/"):
898
- # trim root to allow relative download
899
- file = file[1:]
900
- file = out / file
901
-
902
- if file not in downloaded_files:
903
- # first time the file was seen in list, means we can safely remove any old copy if any
904
- file.unlink(missing_ok=True)
905
-
906
- downloaded_files.add(file)
907
- file.parent.mkdir(parents=True, exist_ok=True)
908
- with open(file, "ab") as f:
909
- # same file may appear twice, so we'll need to append data into it
910
- logger.info(f"writing to: {file}")
911
- fetch_symbols.get_file(i, f)
912
- else:
913
- if not isinstance(service_provider, RemoteServiceDiscoveryService):
914
- raise RSDRequiredError(service_provider.identifier)
915
- async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
916
- await fetch_symbols.download(out)
917
-
918
- if should_create_device_support_layout:
919
- create_device_support_layout(
920
- service_provider.product_type, service_provider.product_version, service_provider.product_build_version, out
921
- )
922
-
923
-
924
- @fetch_symbols.command("download", cls=Command)
925
- @click.argument("out", type=click.Path(dir_okay=True, file_okay=False), required=False)
926
- def fetch_symbols_download(service_provider: LockdownServiceProvider, out: Optional[str]) -> None:
927
- """
928
- Fetches symbols from the given device and saves them into Xcode DeviceSupport directory.
929
-
930
- This command downloads symbol data. Optionally, the user can specify an output directory where the data will
931
- be stored. If no output directory is provided, the symbols will be downloaded into the Xcode directory directly
932
- (DeviceSupport).
933
- """
934
- asyncio.run(fetch_symbols_download_task(service_provider, out), debug=True)
935
-
936
-
937
- @developer.group("simulate-location")
938
- def simulate_location():
939
- """Simulate device location by given input"""
940
- pass
941
-
942
-
943
- @simulate_location.command("clear", cls=Command)
944
- def simulate_location_clear(service_provider: LockdownClient):
945
- """clear simulated location"""
946
- DtSimulateLocation(service_provider).clear()
947
-
948
-
949
- @simulate_location.command("set", cls=Command)
950
- @click.argument("latitude", type=click.FLOAT)
951
- @click.argument("longitude", type=click.FLOAT)
952
- def simulate_location_set(service_provider: LockdownClient, latitude, longitude):
953
- """
954
- set a simulated location.
955
- try:
956
- ... set -- 40.690008 -74.045843 for liberty island
957
- """
958
- DtSimulateLocation(service_provider).set(latitude, longitude)
959
-
960
-
961
- @simulate_location.command("play", cls=Command)
962
- @click.argument("filename", type=click.Path(exists=True, file_okay=True, dir_okay=False))
963
- @click.argument("timing_randomness_range", type=click.INT)
964
- @click.option("--disable-sleep", is_flag=True, default=False)
965
- def simulate_location_play(service_provider: LockdownClient, filename, timing_randomness_range, disable_sleep):
966
- """play a .gpx file"""
967
- DtSimulateLocation(service_provider).play_gpx_file(filename, timing_randomness_range, disable_sleep=disable_sleep)
968
-
969
-
970
- @developer.group("accessibility")
971
- def accessibility():
972
- """Interact with accessibility-related features"""
973
- pass
974
-
975
-
976
- @accessibility.command("run-audit", cls=Command)
977
- @click.argument("test_types", nargs=-1)
978
- def accessibility_run_audit(service_provider: LockdownServiceProvider, test_types):
979
- """runs accessibility audit tests"""
980
- param = list(test_types)
981
- audit_issues = AccessibilityAudit(service_provider).run_audit(param)
982
- print_json([audit_issue.json() for audit_issue in audit_issues], False)
983
-
984
-
985
- @accessibility.command("supported-audit-types", cls=Command)
986
- def accessibility_supported_audit_types(service_provider: LockdownServiceProvider):
987
- """lists supported accessibility audit test types"""
988
- print_json(AccessibilityAudit(service_provider).supported_audits_types())
989
-
990
-
991
- @accessibility.command("capabilities", cls=Command)
992
- def accessibility_capabilities(service_provider: LockdownClient):
993
- """display accessibility capabilities"""
994
- print_json(AccessibilityAudit(service_provider).capabilities)
995
-
996
-
997
- @accessibility.group("settings")
998
- def accessibility_settings():
999
- """accessibility settings."""
1000
- pass
1001
-
1002
-
1003
- @accessibility_settings.command("show", cls=Command)
1004
- def accessibility_settings_show(service_provider: LockdownClient):
1005
- """show current settings"""
1006
- for setting in AccessibilityAudit(service_provider).settings:
1007
- print(setting)
1008
-
1009
-
1010
- @accessibility_settings.command("set", cls=Command)
1011
- @click.argument("setting")
1012
- @click.argument("value")
1013
- def accessibility_settings_set(service_provider: LockdownClient, setting, value):
1014
- """
1015
- change current settings
1016
-
1017
- in order to list all available use the "show" command
1018
- """
1019
- service = AccessibilityAudit(service_provider)
1020
- service.set_setting(setting, eval(value))
1021
- OSUTILS.wait_return()
1022
-
1023
-
1024
- @accessibility_settings.command("reset", cls=Command)
1025
- def accessibility_settings_reset(service_provider: LockdownClient):
1026
- """
1027
- reset accessibility settings to default
1028
- """
1029
- service = AccessibilityAudit(service_provider)
1030
- service.reset_settings()
1031
-
1032
-
1033
- @accessibility.command("shell", cls=Command)
1034
- def accessibility_shell(service_provider: LockdownClient):
1035
- """start and ipython accessibility shell"""
1036
- AccessibilityAudit(service_provider).shell()
1037
-
1038
-
1039
- @accessibility.command("notifications", cls=Command)
1040
- def accessibility_notifications(service_provider: LockdownClient):
1041
- """show notifications"""
1042
-
1043
- service = AccessibilityAudit(service_provider)
1044
- for event in service.iter_events():
1045
- if event.name in (
1046
- "hostAppStateChanged:",
1047
- "hostInspectorCurrentElementChanged:",
1048
- ):
1049
- for focus_item in event.data:
1050
- logger.info(focus_item)
1051
-
1052
-
1053
- @accessibility.command("list-items", cls=Command)
1054
- def accessibility_list_items(service_provider: LockdownClient):
1055
- """List elements available in the currently shown menu."""
1056
-
1057
- elements = []
1058
- with AccessibilityAudit(service_provider) as service:
1059
- for element in service.iter_elements():
1060
- elements.append(element.to_dict())
1061
- print_json(elements)
1062
-
1063
-
1064
- @developer.group("condition")
1065
- def condition():
1066
- """Force a predefined condition"""
1067
- pass
1068
-
1069
-
1070
- @condition.command("list", cls=Command)
1071
- def condition_list(service_provider: LockdownServiceProvider) -> None:
1072
- """list all available conditions"""
1073
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
1074
- print_json(ConditionInducer(dvt).list())
1075
-
1076
-
1077
- @condition.command("clear", cls=Command)
1078
- def condition_clear(service_provider: LockdownClient):
1079
- """clear current condition"""
1080
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
1081
- ConditionInducer(dvt).clear()
1082
-
1083
-
1084
- @condition.command("set", cls=Command)
1085
- @click.argument("profile_identifier")
1086
- def condition_set(service_provider: LockdownClient, profile_identifier):
1087
- """set a specific condition"""
1088
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
1089
- ConditionInducer(dvt).set(profile_identifier)
1090
- OSUTILS.wait_return()
1091
-
1092
-
1093
- @developer.command(cls=Command)
1094
- @click.argument("out", type=click.File("wb"))
1095
- def screenshot(service_provider: LockdownClient, out):
1096
- """Take a screenshot in PNG format"""
1097
- out.write(ScreenshotService(lockdown=service_provider).take_screenshot())
1098
-
1099
-
1100
- @developer.group("debugserver")
1101
- def debugserver():
1102
- """Interact with debugserver"""
1103
- pass
1104
-
1105
-
1106
- @debugserver.command("applist", cls=Command)
1107
- def debugserver_applist(service_provider: LockdownClient):
1108
- """Get applist xml"""
1109
- print_json(DebugServerAppList(service_provider).get())
1110
-
1111
-
1112
- @debugserver.command("start-server", cls=Command)
1113
- @click.argument("local_port", type=click.INT, required=False)
1114
- def debugserver_start_server(service_provider: LockdownClient, local_port: Optional[int] = None):
1115
- """
1116
- if local_port is provided, start a debugserver at remote listening on a given port locally.
1117
- if local_port is not provided and iOS version >= 17.0 then just print the connect string
1118
-
1119
- Please note the connection must be done soon afterward using your own lldb client.
1120
- This can be done using the following commands within lldb shell.
1121
- """
1122
-
1123
- if Version(service_provider.product_version) < Version("17.0"):
1124
- service_name = "com.apple.debugserver.DVTSecureSocketProxy"
1125
- else:
1126
- service_name = "com.apple.internal.dt.remote.debugproxy"
1127
-
1128
- if local_port is not None:
1129
- print(DEBUGSERVER_CONNECTION_STEPS.format(host="127.0.0.1", port=local_port))
1130
- print("Started port forwarding. Press Ctrl-C to close this shell when done")
1131
- sys.stdout.flush()
1132
- LockdownTcpForwarder(service_provider, local_port, service_name).start()
1133
- elif Version(service_provider.product_version) >= Version("17.0"):
1134
- if not isinstance(service_provider, RemoteServiceDiscoveryService):
1135
- raise RSDRequiredError(service_provider.identifier)
1136
- debugserver_port = service_provider.get_service_port(service_name)
1137
- print(DEBUGSERVER_CONNECTION_STEPS.format(host=service_provider.service.address[0], port=debugserver_port))
1138
- else:
1139
- print("local_port is required for iOS < 17.0")
1140
-
1141
-
1142
- @debugserver.command("lldb", cls=RSDCommand)
1143
- @click.argument("xcodeproj_path", type=click.Path(exists=True, file_okay=False, dir_okay=True))
1144
- @click.option("--configuration", default="Debug", help="Usually Release/Debug")
1145
- @click.option("--lldb-command", default="lldb")
1146
- @click.option("--launch", is_flag=True, default=False, help="Launch the app after connecting to lldb")
1147
- @click.option("breakpoints", "-b", "--break", multiple=True, help="Add multiple startup breakpoints")
1148
- @click.option("user_commands", "--command", "-c", multiple=True, help="Additional commands to run at startup")
1149
- def debugserver_lldb(
1150
- service_provider: LockdownServiceProvider,
1151
- xcodeproj_path: str,
1152
- configuration: str,
1153
- lldb_command: str,
1154
- launch: bool,
1155
- breakpoints: tuple[str],
1156
- user_commands: tuple[str],
1157
- ) -> None:
1158
- """
1159
- Automate lldb launch for a given xcodeproj.
1160
-
1161
- \b
1162
- This will:
1163
- - Build the given xcodeproj
1164
- - Install it
1165
- - Start a debugserver attached to it
1166
- - Place breakpoints if given any
1167
- - Launch the application if requested
1168
- - Execute any additional commands if requested
1169
- - Switch to lldb shell
1170
- """
1171
- if Version(service_provider.product_version) < Version("17.0"):
1172
- logger.error("lldb is only supported on iOS >= 17.0")
1173
- return
1174
-
1175
- commands = []
1176
- xcodeproj_path = Path(xcodeproj_path)
1177
- with local.cwd(xcodeproj_path.parent):
1178
- logger.info(f"Building {xcodeproj_path} for {configuration} configuration")
1179
- local["xcodebuild"]["-configuration", configuration, "build"]()
1180
- local_app = next(iter(Path(f"build/{configuration}-iphoneos").glob("*.app")))
1181
- logger.info(f"Using app: {local_app}")
1182
-
1183
- info_plist_path = local_app / "Info.plist"
1184
- info_plist = plistlib.loads(info_plist_path.read_bytes())
1185
- bundle_identifier = info_plist["CFBundleIdentifier"]
1186
- logger.info(f"Bundle identifier: {bundle_identifier}")
1187
-
1188
- commands.append("platform select remote-ios")
1189
- commands.append(f'target create "{local_app.absolute()}"')
1190
-
1191
- with InstallationProxyService(create_using_usbmux()) as installation_proxy:
1192
- logger.info("Installing app")
1193
- installation_proxy.install_from_local(local_app)
1194
- remote_path = installation_proxy.get_apps(bundle_identifiers=[bundle_identifier])[bundle_identifier]["Path"]
1195
- logger.info(f"Remote path: {remote_path}")
1196
-
1197
- commands.append(f'script lldb.target.module[0].SetPlatformFileSpec(lldb.SBFileSpec("{remote_path}"))')
1198
-
1199
- debugserver_port = service_provider.get_service_port("com.apple.internal.dt.remote.debugproxy")
1200
-
1201
- # Add connection and launch commands
1202
- commands.append(f"process connect connect://[{service_provider.service.address[0]}]:{debugserver_port}")
1203
-
1204
- for bp in breakpoints:
1205
- commands.append(f'breakpoint set -n "{bp}"')
1206
-
1207
- if launch:
1208
- commands.append("process launch")
1209
-
1210
- # Add user commands
1211
- commands += user_commands
1212
-
1213
- logger.info("Starting lldb with automated setup and connection")
1214
-
1215
- # Works only on unix-based systems, so keep these imports here
1216
- import fcntl
1217
- import pty
1218
- import select as select_module
1219
- import termios
1220
- import tty
1221
-
1222
- master, slave = pty.openpty()
1223
-
1224
- process = None # Initialize process variable for signal handler
1225
-
1226
- # Copy terminal size from the current terminal to PTY
1227
- def resize_pty() -> None:
1228
- """Update PTY size to match current terminal size"""
1229
- size = struct.unpack(
1230
- "HHHH", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0))
1231
- )
1232
- fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", *size))
1233
- # Send SIGWINCH to the child process to notify it of the resize
1234
- if process is not None and process.poll() is None:
1235
- process.send_signal(signal.SIGWINCH)
1236
-
1237
- # Initial resize
1238
- resize_pty()
1239
-
1240
- # Set up signal handler for window resize
1241
- def handle_sigwinch(signum, frame):
1242
- resize_pty()
1243
-
1244
- old_sigwinch_handler = signal.signal(signal.SIGWINCH, handle_sigwinch)
1245
-
1246
- # Save original terminal settings
1247
- old_tty = termios.tcgetattr(sys.stdin)
1248
-
1249
- try:
1250
- # Set TERM environment variable to enable colors
1251
- env = os.environ.copy()
1252
- env["TERM"] = os.environ.get("TERM", "xterm-256color")
1253
-
1254
- process = subprocess.Popen([lldb_command], stdin=slave, stdout=slave, stderr=slave, env=env)
1255
- os.close(slave)
1256
-
1257
- # Put terminal in raw mode for proper interaction
1258
- tty.setraw(sys.stdin.fileno())
1259
- # Send all commands through stdin
1260
- for command in commands:
1261
- os.write(master, (command + "\n").encode())
1262
-
1263
- # Now redirect stdin from the terminal to lldb so user can interact
1264
- while True:
1265
- rlist, _, _ = select_module.select([sys.stdin, master], [], [])
1266
-
1267
- if sys.stdin in rlist:
1268
- # User typed something
1269
- data = os.read(sys.stdin.fileno(), 1024)
1270
- if not data:
1271
- break
1272
- os.write(master, data)
1273
-
1274
- if master in rlist:
1275
- # lldb has output
1276
- try:
1277
- data = os.read(master, 1024)
1278
- if not data:
1279
- break
1280
- os.write(sys.stdout.fileno(), data)
1281
- except OSError:
1282
- break
1283
- except (KeyboardInterrupt, OSError):
1284
- pass
1285
- finally:
1286
- # Restore terminal settings
1287
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
1288
- # Restore original SIGWINCH handler
1289
- signal.signal(signal.SIGWINCH, old_sigwinch_handler)
1290
- os.close(master)
1291
- if process is not None:
1292
- process.terminate()
1293
- process.wait()
1294
-
1295
-
1296
- @developer.group("arbitration")
1297
- def arbitration():
1298
- """Mark/Unmark device as "in-use" """
1299
- pass
1300
-
1301
-
1302
- @arbitration.command("version", cls=Command)
1303
- def version(service_provider: LockdownClient):
1304
- """get arbitration version"""
1305
- with DtDeviceArbitration(service_provider) as device_arbitration:
1306
- print_json(device_arbitration.version)
1307
-
1308
-
1309
- @arbitration.command("check-in", cls=Command)
1310
- @click.argument("hostname")
1311
- @click.option("-f", "--force", default=False, is_flag=True)
1312
- def check_in(service_provider: LockdownClient, hostname, force):
1313
- """owner check-in"""
1314
- with DtDeviceArbitration(service_provider) as device_arbitration:
1315
- try:
1316
- device_arbitration.check_in(hostname, force=force)
1317
- OSUTILS.wait_return()
1318
- except DeviceAlreadyInUseError as e:
1319
- logger.error(e.message)
1320
-
1321
-
1322
- @arbitration.command("check-out", cls=Command)
1323
- def check_out(service_provider: LockdownClient):
1324
- """owner check-out"""
1325
- with DtDeviceArbitration(service_provider) as device_arbitration:
1326
- device_arbitration.check_out()
1327
-
1328
-
1329
- @dvt.command("har", cls=Command)
1330
- def dvt_har(service_provider: LockdownClient):
1331
- """
1332
- Enable har-logging
1333
-
1334
- For more information, please read:
1335
- https://github.com/doronz88/harlogger?tab=readme-ov-file#enable-http-instrumentation-method
1336
- """
1337
- with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
1338
- print("> Press Ctrl-C to abort")
1339
- with ActivityTraceTap(dvt, enable_http_archive_logging=True) as tap:
1340
- while True:
1341
- tap.channel.receive_message()
1342
-
1343
-
1344
- @dvt.group("simulate-location")
1345
- def dvt_simulate_location():
1346
- """Simulate device location by given input"""
1347
- pass
1348
-
1349
-
1350
- @dvt_simulate_location.command("clear", cls=Command)
1351
- def dvt_simulate_location_clear(service_provider: LockdownClient):
1352
- """Clear currently simulated location"""
1353
- with DvtSecureSocketProxyService(service_provider) as dvt:
1354
- LocationSimulation(dvt).clear()
1355
-
1356
-
1357
- @dvt_simulate_location.command("set", cls=Command)
1358
- @click.argument("latitude", type=click.FLOAT)
1359
- @click.argument("longitude", type=click.FLOAT)
1360
- def dvt_simulate_location_set(service_provider: LockdownClient, latitude, longitude):
1361
- """
1362
- Set a simulated location.
1363
- For example:
1364
- ... set -- 40.690008 -74.045843 for liberty island
1365
- """
1366
- with DvtSecureSocketProxyService(service_provider) as dvt:
1367
- LocationSimulation(dvt).set(latitude, longitude)
1368
- OSUTILS.wait_return()
1369
-
1370
-
1371
- @dvt_simulate_location.command("play", cls=Command)
1372
- @click.argument("filename", type=click.Path(exists=True, file_okay=True, dir_okay=False))
1373
- @click.argument("timing_randomness_range", type=click.INT, default=0)
1374
- @click.option("--disable-sleep", is_flag=True, default=False)
1375
- def dvt_simulate_location_play(
1376
- service_provider: LockdownClient, filename: str, timing_randomness_range: int, disable_sleep: bool
1377
- ) -> None:
1378
- """Simulate inputs from a given .gpx file"""
1379
- with DvtSecureSocketProxyService(service_provider) as dvt:
1380
- LocationSimulation(dvt).play_gpx_file(
1381
- filename, disable_sleep=disable_sleep, timing_randomness_range=timing_randomness_range
1382
- )
1383
- OSUTILS.wait_return()
1384
-
1385
-
1386
- @developer.group()
1387
- def core_device() -> None:
1388
- """Access features exposed by the DeveloperDiskImage"""
1389
- pass
1390
-
1391
-
1392
- async def core_device_list_directory_task(
1393
- service_provider: RemoteServiceDiscoveryService, domain: str, path: str, identifier: str
1394
- ) -> None:
1395
- async with FileServiceService(service_provider, APPLE_DOMAIN_DICT[domain], identifier) as file_service:
1396
- print_json(await file_service.retrieve_directory_list(path))
1397
-
1398
-
1399
- @core_device.command("list-directory", cls=RSDCommand)
1400
- @click.argument("domain", type=click.Choice(APPLE_DOMAIN_DICT.keys()))
1401
- @click.argument("path")
1402
- @click.option("--identifier", default="")
1403
- def core_device_list_directory(
1404
- service_provider: RemoteServiceDiscoveryService, domain: str, path: str, identifier: str
1405
- ) -> None:
1406
- """List directory at given domain-path"""
1407
- asyncio.run(core_device_list_directory_task(service_provider, domain, path, identifier))
1408
-
1409
-
1410
- async def core_device_read_file_task(
1411
- service_provider: RemoteServiceDiscoveryService, domain: str, path: str, identifier: str, output: Optional[IO]
1412
- ) -> None:
1413
- async with FileServiceService(service_provider, APPLE_DOMAIN_DICT[domain], identifier) as file_service:
1414
- buf = await file_service.retrieve_file(path)
1415
- if output is not None:
1416
- output.write(buf)
1417
- else:
1418
- print(try_decode(buf))
1419
-
1420
-
1421
- @core_device.command("read-file", cls=RSDCommand)
1422
- @click.argument("domain", type=click.Choice(APPLE_DOMAIN_DICT.keys()))
1423
- @click.argument("path")
1424
- @click.option("--identifier", default="")
1425
- @click.option("-o", "--output", type=click.File("wb"))
1426
- def core_device_read_file(
1427
- service_provider: RemoteServiceDiscoveryService, domain: str, path: str, identifier: str, output: Optional[IO]
1428
- ) -> None:
1429
- """Read file from given domain-path"""
1430
- asyncio.run(core_device_read_file_task(service_provider, domain, path, identifier, output))
1431
-
1432
-
1433
- async def core_device_propose_empty_file_task(
1434
- service_provider: RemoteServiceDiscoveryService,
1435
- domain: str,
1436
- path: str,
1437
- identifier: str,
1438
- file_permissions: int,
1439
- uid: int,
1440
- gid: int,
1441
- creation_time: int,
1442
- last_modification_time: int,
1443
- ) -> None:
1444
- async with FileServiceService(service_provider, APPLE_DOMAIN_DICT[domain], identifier) as file_service:
1445
- await file_service.propose_empty_file(path, file_permissions, uid, gid, creation_time, last_modification_time)
1446
-
1447
-
1448
- @core_device.command("propose-empty-file", cls=RSDCommand)
1449
- @click.argument("domain", type=click.Choice(APPLE_DOMAIN_DICT.keys()))
1450
- @click.argument("path")
1451
- @click.option("--identifier", default="")
1452
- @click.option("--file-permissions", type=click.INT, default=0o644)
1453
- @click.option("--uid", type=click.INT, default=501)
1454
- @click.option("--gid", type=click.INT, default=501)
1455
- @click.option("--creation-time", type=click.INT, default=time.time())
1456
- @click.option("--last-modification-time", type=click.INT, default=time.time())
1457
- def core_device_propose_empty_file(
1458
- service_provider: RemoteServiceDiscoveryService,
1459
- domain: str,
1460
- path: str,
1461
- identifier: str,
1462
- file_permissions: int,
1463
- uid: int,
1464
- gid: int,
1465
- creation_time: int,
1466
- last_modification_time: int,
1467
- ) -> None:
1468
- """Write an empty file to given domain-path"""
1469
- asyncio.run(
1470
- core_device_propose_empty_file_task(
1471
- service_provider,
1472
- domain,
1473
- path,
1474
- identifier,
1475
- file_permissions,
1476
- uid,
1477
- gid,
1478
- creation_time,
1479
- last_modification_time,
1480
- )
1481
- )
1482
-
1483
-
1484
- async def core_device_list_launch_application_task(
1485
- service_provider: RemoteServiceDiscoveryService,
1486
- bundle_identifier: str,
1487
- argument: list[str],
1488
- kill_existing: bool,
1489
- suspended: bool,
1490
- env: list[tuple[str, str]],
1491
- ) -> None:
1492
- async with AppServiceService(service_provider) as app_service:
1493
- print_json(
1494
- await app_service.launch_application(bundle_identifier, argument, kill_existing, suspended, dict(env))
1495
- )
1496
-
1497
-
1498
- @core_device.command("launch-application", cls=RSDCommand)
1499
- @click.argument("bundle_identifier")
1500
- @click.argument("argument", nargs=-1)
1501
- @click.option(
1502
- "--kill-existing/--no-kill-existing", default=True, help="Whether to kill an existing instance of this process"
1503
- )
1504
- @click.option("--suspended", is_flag=True, help="Same as WaitForDebugger")
1505
- @click.option(
1506
- "--env",
1507
- multiple=True,
1508
- type=click.Tuple((str, str)),
1509
- help="Environment variables to pass to process given as a list of key value",
1510
- )
1511
- def core_device_launch_application(
1512
- service_provider: RemoteServiceDiscoveryService,
1513
- bundle_identifier: str,
1514
- argument: tuple[str],
1515
- kill_existing: bool,
1516
- suspended: bool,
1517
- env: list[tuple[str, str]],
1518
- ) -> None:
1519
- """Launch application"""
1520
- asyncio.run(
1521
- core_device_list_launch_application_task(
1522
- service_provider, bundle_identifier, list(argument), kill_existing, suspended, env
1523
- )
1524
- )
1525
-
1526
-
1527
- async def core_device_list_processes_task(service_provider: RemoteServiceDiscoveryService) -> None:
1528
- async with AppServiceService(service_provider) as app_service:
1529
- print_json(await app_service.list_processes())
1530
-
1531
-
1532
- @core_device.command("list-processes", cls=RSDCommand)
1533
- def core_device_list_processes(service_provider: RemoteServiceDiscoveryService) -> None:
1534
- """Get process list"""
1535
- asyncio.run(core_device_list_processes_task(service_provider))
1536
-
1537
-
1538
- async def core_device_uninstall_app_task(
1539
- service_provider: RemoteServiceDiscoveryService, bundle_identifier: str
1540
- ) -> None:
1541
- async with AppServiceService(service_provider) as app_service:
1542
- await app_service.uninstall_app(bundle_identifier)
1543
-
1544
-
1545
- @core_device.command("uninstall", cls=RSDCommand)
1546
- @click.argument("bundle_identifier")
1547
- def core_device_uninstall_app(service_provider: RemoteServiceDiscoveryService, bundle_identifier: str) -> None:
1548
- """Uninstall application"""
1549
- asyncio.run(core_device_uninstall_app_task(service_provider, bundle_identifier))
1550
-
1551
-
1552
- async def core_device_send_signal_to_process_task(
1553
- service_provider: RemoteServiceDiscoveryService, pid: int, signal: int
1554
- ) -> None:
1555
- async with AppServiceService(service_provider) as app_service:
1556
- print_json(await app_service.send_signal_to_process(pid, signal))
1557
-
1558
-
1559
- @core_device.command("send-signal-to-process", cls=RSDCommand)
1560
- @click.argument("pid", type=click.INT)
1561
- @click.argument("signal", type=click.INT)
1562
- def core_device_send_signal_to_process(service_provider: RemoteServiceDiscoveryService, pid: int, signal: int) -> None:
1563
- """Send signal to process"""
1564
- asyncio.run(core_device_send_signal_to_process_task(service_provider, pid, signal))
1565
-
1566
-
1567
- async def core_device_get_device_info_task(service_provider: RemoteServiceDiscoveryService) -> None:
1568
- async with DeviceInfoService(service_provider) as app_service:
1569
- print_json(await app_service.get_device_info())
1570
-
1571
-
1572
- @core_device.command("get-device-info", cls=RSDCommand)
1573
- def core_device_get_device_info(service_provider: RemoteServiceDiscoveryService) -> None:
1574
- """Get device information"""
1575
- asyncio.run(core_device_get_device_info_task(service_provider))
1576
-
1577
-
1578
- async def core_device_get_display_info_task(service_provider: RemoteServiceDiscoveryService) -> None:
1579
- async with DeviceInfoService(service_provider) as app_service:
1580
- print_json(await app_service.get_display_info())
1581
-
1582
-
1583
- @core_device.command("get-display-info", cls=RSDCommand)
1584
- def core_device_get_display_info(service_provider: RemoteServiceDiscoveryService) -> None:
1585
- """Get display information"""
1586
- asyncio.run(core_device_get_display_info_task(service_provider))
1587
-
1588
-
1589
- async def core_device_query_mobilegestalt_task(service_provider: RemoteServiceDiscoveryService, key: list[str]) -> None:
1590
- """Query MobileGestalt"""
1591
- async with DeviceInfoService(service_provider) as app_service:
1592
- print_json(await app_service.query_mobilegestalt(key))
1593
-
1594
-
1595
- @core_device.command("query-mobilegestalt", cls=RSDCommand)
1596
- @click.argument("key", nargs=-1, type=click.STRING)
1597
- def core_device_query_mobilegestalt(service_provider: RemoteServiceDiscoveryService, key: tuple[str]) -> None:
1598
- """Query MobileGestalt"""
1599
- asyncio.run(core_device_query_mobilegestalt_task(service_provider, list(key)))
1600
-
1601
-
1602
- async def core_device_get_lockstate_task(service_provider: RemoteServiceDiscoveryService) -> None:
1603
- async with DeviceInfoService(service_provider) as app_service:
1604
- print_json(await app_service.get_lockstate())
1605
-
1606
-
1607
- @core_device.command("get-lockstate", cls=RSDCommand)
1608
- def core_device_get_lockstate(service_provider: RemoteServiceDiscoveryService) -> None:
1609
- """Get lockstate"""
1610
- asyncio.run(core_device_get_lockstate_task(service_provider))
1611
-
1612
-
1613
- async def core_device_list_apps_task(service_provider: RemoteServiceDiscoveryService) -> None:
1614
- async with AppServiceService(service_provider) as app_service:
1615
- print_json(await app_service.list_apps())
1616
-
1617
-
1618
- @core_device.command("list-apps", cls=RSDCommand)
1619
- def core_device_list_apps(service_provider: RemoteServiceDiscoveryService) -> None:
1620
- """Get application list"""
1621
- asyncio.run(core_device_list_apps_task(service_provider))
1622
-
1623
-
1624
- async def core_device_sysdiagnose_task(service_provider: RemoteServiceDiscoveryService, output: str) -> None:
1625
- output = Path(output)
1626
- async with DiagnosticsServiceService(service_provider) as service:
1627
- response = await service.capture_sysdiagnose(False)
1628
- logger.info(f"Operation response: {response}")
1629
- if output.is_dir():
1630
- output /= response.preferred_filename
1631
- logger.info(f"Downloading sysdiagnose to: {output}")
1632
-
1633
- # get the file over lockdownd which is WAYYY faster
1634
- lockdown = create_using_usbmux(service_provider.udid)
1635
- with CrashReportsManager(lockdown) as crash_reports_manager:
1636
- crash_reports_manager.afc.pull(
1637
- posixpath.join(f"/DiagnosticLogs/sysdiagnose/{response.preferred_filename}"), output
1638
- )
1639
-
1640
-
1641
- @core_device.command("sysdiagnose", cls=RSDCommand)
1642
- @click.argument("output", type=click.Path(dir_okay=True, file_okay=True, exists=True))
1643
- def core_device_sysdiagnose(service_provider: RemoteServiceDiscoveryService, output: str) -> None:
1644
- """Execute sysdiagnose and fetch the output file"""
1645
- asyncio.run(core_device_sysdiagnose_task(service_provider, output))