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
@@ -0,0 +1,387 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import posixpath
5
+ import shlex
6
+ import signal
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Annotated, NamedTuple, Optional
10
+
11
+ import typer
12
+ from click.exceptions import MissingParameter, UsageError
13
+ from typer_injector import InjectingTyper
14
+
15
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json, user_requested_colored_output
16
+ from pymobiledevice3.cli.developer.dvt import core_profile_session, simulate_location, sysmon
17
+ from pymobiledevice3.exceptions import DvtDirListError, UnrecognizedSelectorError
18
+ from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
19
+ from pymobiledevice3.services.dvt.instruments.activity_trace_tap import ActivityTraceTap, decode_message_format
20
+ from pymobiledevice3.services.dvt.instruments.application_listing import ApplicationListing
21
+ from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo
22
+ from pymobiledevice3.services.dvt.instruments.energy_monitor import EnergyMonitor
23
+ from pymobiledevice3.services.dvt.instruments.graphics import Graphics
24
+ from pymobiledevice3.services.dvt.instruments.network_monitor import ConnectionDetectionEvent, NetworkMonitor
25
+ from pymobiledevice3.services.dvt.instruments.notifications import Notifications
26
+ from pymobiledevice3.services.dvt.instruments.process_control import ProcessControl
27
+ from pymobiledevice3.services.dvt.instruments.screenshot import Screenshot
28
+ from pymobiledevice3.services.dvt.testmanaged.xcuitest import XCUITestService
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class MatchedProcessByPid(NamedTuple):
34
+ name: str
35
+ pid: int
36
+
37
+
38
+ cli = InjectingTyper(
39
+ name="dvt",
40
+ help="Drive DVT instrumentation APIs (process control, metrics, traces).",
41
+ no_args_is_help=True,
42
+ )
43
+
44
+ cli.add_typer(sysmon.cli)
45
+ cli.add_typer(core_profile_session.cli)
46
+ cli.add_typer(simulate_location.cli)
47
+
48
+
49
+ @cli.command("proclist")
50
+ def proclist(service_provider: ServiceProviderDep) -> None:
51
+ """Show processes (with start times) via DVT."""
52
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
53
+ processes = DeviceInfo(dvt).proclist()
54
+ for process in processes:
55
+ if "startDate" in process:
56
+ process["startDate"] = str(process["startDate"])
57
+
58
+ print_json(processes)
59
+
60
+
61
+ @cli.command("is-running-pid")
62
+ def is_running_pid(service_provider: ServiceProviderDep, pid: int) -> None:
63
+ """Check if a PID is currently running."""
64
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
65
+ print_json(DeviceInfo(dvt).is_running_pid(pid))
66
+
67
+
68
+ @cli.command("memlimitoff")
69
+ def memlimitoff(service_provider: ServiceProviderDep, pid: int) -> None:
70
+ """Disable jetsam memory limit for a PID."""
71
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
72
+ ProcessControl(dvt).disable_memory_limit_for_pid(pid)
73
+
74
+
75
+ @cli.command("applist")
76
+ def applist(service_provider: ServiceProviderDep) -> None:
77
+ """List installed applications via DVT."""
78
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
79
+ apps = ApplicationListing(dvt).applist()
80
+ print_json(apps)
81
+
82
+
83
+ @cli.command("signal")
84
+ def send_signal(
85
+ service_provider: ServiceProviderDep,
86
+ pid: int,
87
+ sig: Optional[int] = None,
88
+ signal_name: Annotated[
89
+ Optional[signal.Signals],
90
+ typer.Option("--signal-name", "-s"),
91
+ ] = None,
92
+ ) -> None:
93
+ """Send a signal to a PID (choose numeric SIG or --signal-name)."""
94
+ if not sig and not signal_name:
95
+ raise MissingParameter(param_type="argument|option", param_hint="'SIG|SIGNAL-NAME'")
96
+ if sig and signal_name:
97
+ raise UsageError(message="Cannot give SIG and SIGNAL-NAME together")
98
+ sig = sig or signal_name.value
99
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
100
+ ProcessControl(dvt).signal(pid, sig)
101
+
102
+
103
+ @cli.command("kill")
104
+ def kill(service_provider: ServiceProviderDep, pid: int) -> None:
105
+ """Kill a process by PID."""
106
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
107
+ ProcessControl(dvt).kill(pid)
108
+
109
+
110
+ @cli.command()
111
+ def process_id_for_bundle_id(service_provider: ServiceProviderDep, app_bundle_identifier: str) -> None:
112
+ """Get PID of a bundle identifier (only returns a valid value if its running)."""
113
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
114
+ print(ProcessControl(dvt).process_identifier_for_bundle_identifier(app_bundle_identifier))
115
+
116
+
117
+ def get_matching_processes(
118
+ service_provider: ServiceProviderDep,
119
+ name: Optional[str] = None,
120
+ bundle_identifier: Optional[str] = None,
121
+ ) -> list[MatchedProcessByPid]:
122
+ result: list[MatchedProcessByPid] = []
123
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
124
+ device_info = DeviceInfo(dvt)
125
+ for process in device_info.proclist():
126
+ current_name = process["name"]
127
+ current_bundle_identifier = process.get("bundleIdentifier", "")
128
+ pid = process["pid"]
129
+ if (bundle_identifier is not None and bundle_identifier in current_bundle_identifier) or (
130
+ name is not None and name in current_name
131
+ ):
132
+ result.append(MatchedProcessByPid(name=current_name, pid=pid))
133
+ return result
134
+
135
+
136
+ @cli.command("pkill")
137
+ def pkill(
138
+ service_provider: ServiceProviderDep,
139
+ expression: str,
140
+ bundle: Annotated[
141
+ bool,
142
+ typer.Option(help="Treat given expression as a bundle-identifier instead of a process name"),
143
+ ] = False,
144
+ ) -> None:
145
+ """Kill all processes containing `expression` in their name."""
146
+ matching_name = expression if not bundle else None
147
+ matching_bundle_identifier = expression if bundle else None
148
+ matching_processes = get_matching_processes(
149
+ service_provider, name=matching_name, bundle_identifier=matching_bundle_identifier
150
+ )
151
+
152
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
153
+ process_control = ProcessControl(dvt)
154
+
155
+ for process in matching_processes:
156
+ logger.info(f"killing {process.name}({process.pid})")
157
+ process_control.kill(process.pid)
158
+
159
+
160
+ @cli.command("launch")
161
+ def launch(
162
+ service_provider: ServiceProviderDep,
163
+ arguments: str,
164
+ kill_existing: Annotated[
165
+ bool,
166
+ typer.Option(help="Whether to kill an existing instance of this process"),
167
+ ] = True,
168
+ suspended: Annotated[
169
+ bool,
170
+ typer.Option(help="Same as WaitForDebugger"),
171
+ ] = False,
172
+ env: Annotated[
173
+ Optional[list[str]],
174
+ typer.Option(
175
+ help="Environment variable to pass to process given as key=value (can be specified multiple times)"
176
+ ),
177
+ ] = None,
178
+ stream: bool = False,
179
+ ) -> None:
180
+ """Launch a process."""
181
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
182
+ parsed_arguments = shlex.split(arguments)
183
+ process_control = ProcessControl(dvt)
184
+ pid = process_control.launch(
185
+ bundle_id=parsed_arguments[0],
186
+ arguments=parsed_arguments[1:],
187
+ kill_existing=kill_existing,
188
+ start_suspended=suspended,
189
+ environment=dict(var.split("=", 1) for var in env or ()),
190
+ )
191
+ print(f"Process launched with pid {pid}")
192
+ while stream:
193
+ for output_received in process_control:
194
+ logging.getLogger(f"PID:{output_received.pid}").info(output_received.message.strip())
195
+
196
+
197
+ @cli.command("shell")
198
+ def dvt_shell(service_provider: ServiceProviderDep) -> None:
199
+ """Launch developer shell (used for pymobiledevice3 R&D)"""
200
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
201
+ dvt.shell()
202
+
203
+
204
+ def show_dirlist(device_info: DeviceInfo, dirname: str, recursive: bool = False) -> None:
205
+ try:
206
+ filenames = device_info.ls(dirname)
207
+ except DvtDirListError:
208
+ return
209
+
210
+ for filename in filenames:
211
+ filename = posixpath.join(dirname, filename)
212
+ print(filename)
213
+ if recursive:
214
+ show_dirlist(device_info, filename, recursive=recursive)
215
+
216
+
217
+ @cli.command("ls")
218
+ def ls(
219
+ service_provider: ServiceProviderDep,
220
+ path: Path,
221
+ recursive: Annotated[
222
+ bool,
223
+ typer.Option("--recursive", "-r"),
224
+ ] = False,
225
+ ) -> None:
226
+ """List directory"""
227
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
228
+ show_dirlist(DeviceInfo(dvt), str(path), recursive=recursive)
229
+
230
+
231
+ @cli.command("device-information")
232
+ def device_information(service_provider: ServiceProviderDep) -> None:
233
+ """Print system information"""
234
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
235
+ device_info = DeviceInfo(dvt)
236
+ info = {
237
+ "hardware": device_info.hardware_information(),
238
+ "network": device_info.network_information(),
239
+ "kernel-name": device_info.mach_kernel_name(),
240
+ "kpep-database": device_info.kpep_database(),
241
+ }
242
+ with contextlib.suppress(UnrecognizedSelectorError):
243
+ info["system"] = device_info.system_information()
244
+ print_json(info)
245
+
246
+
247
+ @cli.command("netstat")
248
+ def netstat(service_provider: ServiceProviderDep) -> None:
249
+ """Print information about current network activity."""
250
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, NetworkMonitor(dvt) as monitor:
251
+ for event in monitor:
252
+ if isinstance(event, ConnectionDetectionEvent):
253
+ local_host, local_port = event.local_address.split(":")
254
+ remote_host, remote_port = event.local_address.split(":")
255
+ logger.info(f"Connection detected: {local_host}:{local_port} -> {remote_host}:{remote_port}")
256
+
257
+
258
+ @cli.command("screenshot")
259
+ def dvt_screenshot(service_provider: ServiceProviderDep, out: Path) -> None:
260
+ """Take device screenshot"""
261
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
262
+ out.write_bytes(Screenshot(dvt).get_screenshot())
263
+
264
+
265
+ @cli.command("xcuitest")
266
+ def xcuitest(service_provider: ServiceProviderDep, bundle_id: str) -> None:
267
+ """
268
+ Start XCUITest
269
+
270
+ \b
271
+ Usage example:
272
+ \b python3 -m pymobiledevice3 developer dvt xcuitest com.facebook.WebDriverAgentRunner.xctrunner
273
+ """
274
+ XCUITestService(service_provider).run(bundle_id)
275
+
276
+
277
+ @cli.command("trace-codes")
278
+ def dvt_trace_codes(service_provider: ServiceProviderDep) -> None:
279
+ """Print KDebug trace codes."""
280
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
281
+ device_info = DeviceInfo(dvt)
282
+ print_json({hex(k): v for k, v in device_info.trace_codes().items()})
283
+
284
+
285
+ @cli.command("name-for-uid")
286
+ def dvt_name_for_uid(service_provider: ServiceProviderDep, uid: int) -> None:
287
+ """Print the assiciated username for the given uid."""
288
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
289
+ device_info = DeviceInfo(dvt)
290
+ print(device_info.name_for_uid(uid))
291
+
292
+
293
+ @cli.command("name-for-gid")
294
+ def dvt_name_for_gid(service_provider: ServiceProviderDep, gid: int) -> None:
295
+ """Print the assiciated group name for the given gid."""
296
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
297
+ device_info = DeviceInfo(dvt)
298
+ print(device_info.name_for_gid(gid))
299
+
300
+
301
+ @cli.command("oslog")
302
+ def dvt_oslog(service_provider: ServiceProviderDep, pid: int) -> None:
303
+ """Sniff device oslog (not very stable, but includes more data and normal syslog)"""
304
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, ActivityTraceTap(dvt) as tap:
305
+ for message in tap:
306
+ message_pid = message.process
307
+ # without message_type maybe signpost have event_type
308
+ message_type = (
309
+ message.message_type
310
+ if hasattr(message, "message_type")
311
+ else message.event_type
312
+ if hasattr(message, "event_type")
313
+ else "unknown"
314
+ )
315
+ sender_image_path = message.sender_image_path
316
+ image_name = os.path.basename(sender_image_path)
317
+ subsystem = message.subsystem
318
+ category = message.category
319
+ timestamp = datetime.now()
320
+
321
+ if pid is not None and message_pid != pid:
322
+ continue
323
+
324
+ formatted_message = decode_message_format(message.message) if message.message else message.name
325
+
326
+ if user_requested_colored_output():
327
+ timestamp = typer.style(str(timestamp), bold=True)
328
+ message_pid = typer.style(str(message_pid), "magenta")
329
+ subsystem = typer.style(subsystem, "green")
330
+ category = typer.style(category, "green")
331
+ image_name = typer.style(image_name, "yellow")
332
+ message_type = typer.style(message_type, "cyan")
333
+
334
+ print(
335
+ f"[{timestamp}][{subsystem}][{category}][{message_pid}][{image_name}] "
336
+ f"<{message_type}>: {formatted_message}"
337
+ )
338
+
339
+
340
+ @cli.command("energy")
341
+ def dvt_energy(service_provider: ServiceProviderDep, pid_list: list[str]) -> None:
342
+ """Monitor the energy consumption for given PIDs"""
343
+
344
+ if len(pid_list) == 0:
345
+ logger.error("pid_list must not be empty")
346
+ return
347
+
348
+ pid_int_list = [int(pid) for pid in pid_list]
349
+
350
+ with (
351
+ DvtSecureSocketProxyService(lockdown=service_provider) as dvt,
352
+ EnergyMonitor(dvt, pid_int_list) as energy_monitor,
353
+ ):
354
+ for telemetry in energy_monitor:
355
+ logger.info(telemetry)
356
+
357
+
358
+ @cli.command("notifications")
359
+ def dvt_notifications(service_provider: ServiceProviderDep) -> None:
360
+ """Monitor memory and app notifications"""
361
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Notifications(dvt) as notifications:
362
+ for notification in notifications:
363
+ logger.info(notification)
364
+
365
+
366
+ @cli.command("graphics")
367
+ def dvt_graphics(service_provider: ServiceProviderDep) -> None:
368
+ """Monitor graphics-related information"""
369
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Graphics(dvt) as graphics:
370
+ for stats in graphics:
371
+ logger.info(stats)
372
+
373
+
374
+ @cli.command("har")
375
+ def dvt_har(service_provider: ServiceProviderDep) -> None:
376
+ """
377
+ Enable har-logging
378
+
379
+ \b
380
+ For more information, please read:
381
+ \b https://github.com/doronz88/harlogger?tab=readme-ov-file#enable-http-instrumentation-method
382
+ """
383
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
384
+ print("> Press Ctrl-C to abort")
385
+ with ActivityTraceTap(dvt, enable_http_archive_logging=True) as tap:
386
+ while True:
387
+ tap.channel.receive_message()
@@ -0,0 +1,295 @@
1
+ import importlib.resources
2
+ import json
3
+ import logging
4
+ from itertools import islice
5
+ from pathlib import Path
6
+ from typing import Annotated, Optional
7
+
8
+ import typer
9
+ from pykdebugparser.pykdebugparser import PyKdebugParser
10
+ from typer_injector import InjectingTyper
11
+
12
+ import pymobiledevice3.resources
13
+ from pymobiledevice3.cli.cli_common import (
14
+ BASED_INT,
15
+ ServiceProviderDep,
16
+ default_json_encoder,
17
+ print_json,
18
+ user_requested_colored_output,
19
+ )
20
+ from pymobiledevice3.exceptions import ExtractingStackshotError
21
+ from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
22
+ from pymobiledevice3.services.dvt.instruments.core_profile_session_tap import CoreProfileSessionTap
23
+ from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ BSC_SUBCLASS = 0x40C
28
+ BSC_CLASS = 0x4
29
+ VFS_AND_TRACES_SET = {0x03010000, 0x07FF0000}
30
+
31
+
32
+ cli = InjectingTyper(
33
+ name="core-profile-session",
34
+ help="Access tailspin features",
35
+ no_args_is_help=True,
36
+ )
37
+
38
+
39
+ BSCFilter = Annotated[
40
+ bool,
41
+ typer.Option(help="Whether to print BSC events or not."),
42
+ ]
43
+ ClassFilter = Annotated[
44
+ list[int],
45
+ typer.Option(
46
+ "--class-filters",
47
+ "-cf",
48
+ click_type=BASED_INT,
49
+ default_factory=list,
50
+ show_default=False,
51
+ help="Events class filter. Omit for all. Can be specified multiple times.",
52
+ ),
53
+ ]
54
+ SubclassFilter = Annotated[
55
+ list[int],
56
+ typer.Option(
57
+ "--subclass-filters",
58
+ "-sf",
59
+ click_type=BASED_INT,
60
+ default_factory=list,
61
+ show_default=False,
62
+ help="Events subclass filter. Omit for all. Can be specified multiple times.",
63
+ ),
64
+ ]
65
+ Count = Annotated[
66
+ Optional[int],
67
+ typer.Option(
68
+ "--count",
69
+ "-c",
70
+ help="Number of events to print. Omit to endless sniff.",
71
+ ),
72
+ ]
73
+ ThreadID = Annotated[
74
+ Optional[int],
75
+ typer.Option(help="Thread ID to filter. Omit for all."),
76
+ ]
77
+ ShowThreadID = Annotated[
78
+ bool,
79
+ typer.Option(help="Whether to print thread ID or not."),
80
+ ]
81
+
82
+
83
+ def parse_filters(subclasses: list[int], classes: list[int]) -> Optional[set[int]]:
84
+ if not subclasses and not classes:
85
+ return None
86
+ parsed: set[int] = set()
87
+ for subclass in subclasses:
88
+ if subclass == BSC_SUBCLASS:
89
+ parsed |= VFS_AND_TRACES_SET
90
+ parsed.add(subclass << 16)
91
+ for class_ in classes:
92
+ if class_ == BSC_CLASS:
93
+ parsed |= VFS_AND_TRACES_SET
94
+ parsed.add((class_ << 24) | 0x00FF0000)
95
+ return parsed
96
+
97
+
98
+ @cli.command("live")
99
+ def live_profile_session(
100
+ service_provider: ServiceProviderDep,
101
+ *,
102
+ count: Count = -1,
103
+ bsc: BSCFilter = False,
104
+ class_filters: ClassFilter,
105
+ subclass_filters: SubclassFilter,
106
+ tid: ThreadID = None,
107
+ timestamp: Annotated[
108
+ bool,
109
+ typer.Option(help="Whether to print timestamp or not."),
110
+ ] = True,
111
+ event_name: Annotated[
112
+ bool,
113
+ typer.Option(help="Whether to print event name or not."),
114
+ ] = True,
115
+ func_qual: Annotated[
116
+ bool,
117
+ typer.Option(help="Whether to print function qualifier or not."),
118
+ ] = True,
119
+ show_tid: ShowThreadID = True,
120
+ process_name: Annotated[
121
+ bool,
122
+ typer.Option(help="Whether to print process name or not."),
123
+ ] = True,
124
+ args: Annotated[
125
+ bool,
126
+ typer.Option(help="Whether to print event arguments or not."),
127
+ ] = True,
128
+ ) -> None:
129
+ """Print kevents received from the device in real time."""
130
+ parser = PyKdebugParser()
131
+ class_filters = class_filters
132
+ subclass_filters = subclass_filters
133
+ parser.filter_class = class_filters
134
+ if bsc:
135
+ subclass_filters.append(BSC_SUBCLASS)
136
+ parser.filter_subclass = subclass_filters
137
+ filters = parse_filters(subclass_filters, class_filters)
138
+ parser.filter_tid = tid
139
+ parser.show_timestamp = timestamp
140
+ parser.show_name = event_name
141
+ parser.show_func_qual = func_qual
142
+ parser.show_tid = show_tid
143
+ parser.show_process = process_name
144
+ parser.show_args = args
145
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
146
+ trace_codes_map = DeviceInfo(dvt).trace_codes()
147
+ time_config = CoreProfileSessionTap.get_time_config(dvt)
148
+ parser.numer = time_config["numer"]
149
+ parser.denom = time_config["denom"]
150
+ parser.mach_absolute_time = time_config["mach_absolute_time"]
151
+ parser.usecs_since_epoch = time_config["usecs_since_epoch"]
152
+ parser.timezone = time_config["timezone"]
153
+ with CoreProfileSessionTap(dvt, time_config, filters) as tap:
154
+ for i, event in enumerate(parser.formatted_kevents(tap.get_kdbuf_stream(), trace_codes_map)):
155
+ print(event)
156
+ if i == count:
157
+ break
158
+
159
+
160
+ @cli.command("save")
161
+ def save_profile_session(
162
+ service_provider: ServiceProviderDep,
163
+ out: Path,
164
+ *,
165
+ bsc: BSCFilter = False,
166
+ class_filters: ClassFilter,
167
+ subclass_filters: SubclassFilter,
168
+ ) -> None:
169
+ """Dump core profiling information."""
170
+ if bsc:
171
+ subclass_filters.append(BSC_SUBCLASS)
172
+ filters = parse_filters(subclass_filters, class_filters)
173
+ with (
174
+ DvtSecureSocketProxyService(lockdown=service_provider) as dvt,
175
+ CoreProfileSessionTap(dvt, {}, filters) as tap,
176
+ out.open("wb") as out_file,
177
+ ):
178
+ tap.dump(out_file)
179
+
180
+
181
+ @cli.command("stackshot")
182
+ def stackshot(
183
+ service_provider: ServiceProviderDep,
184
+ out: Annotated[Optional[Path], typer.Option()] = None,
185
+ ) -> None:
186
+ """Dump stackshot information."""
187
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, CoreProfileSessionTap(dvt, {}) as tap:
188
+ try:
189
+ data = tap.get_stackshot()
190
+ except ExtractingStackshotError:
191
+ logger.exception("Extracting stackshot failed")
192
+ return
193
+
194
+ if out is not None:
195
+ out.write_text(json.dumps(data, indent=4, default=default_json_encoder))
196
+ else:
197
+ print_json(data)
198
+
199
+
200
+ @cli.command("parse-live")
201
+ def parse_live_profile_session(
202
+ service_provider: ServiceProviderDep,
203
+ *,
204
+ count: Count = None,
205
+ bsc: BSCFilter = False,
206
+ class_filters: ClassFilter,
207
+ subclass_filters: SubclassFilter,
208
+ tid: ThreadID = None,
209
+ show_tid: ShowThreadID = False,
210
+ process: Annotated[
211
+ Optional[str],
212
+ typer.Option(help="Process ID / name to filter. Omit for all."),
213
+ ] = None,
214
+ ) -> None:
215
+ """Print traces (syscalls, thread events, etc.) received from the device in real time."""
216
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
217
+ print("Receiving time information")
218
+ time_config = CoreProfileSessionTap.get_time_config(dvt)
219
+ parser = PyKdebugParser()
220
+ parser.filter_class = list(class_filters)
221
+ if bsc:
222
+ subclass_filters.append(BSC_SUBCLASS)
223
+ parser.filter_subclass = subclass_filters
224
+ filters = parse_filters(subclass_filters, class_filters)
225
+ parser.numer = time_config["numer"]
226
+ parser.denom = time_config["denom"]
227
+ parser.mach_absolute_time = time_config["mach_absolute_time"]
228
+ parser.usecs_since_epoch = time_config["usecs_since_epoch"]
229
+ parser.timezone = time_config["timezone"]
230
+ parser.filter_tid = tid
231
+ parser.filter_process = process
232
+ parser.show_tid = show_tid
233
+ parser.color = user_requested_colored_output()
234
+
235
+ with CoreProfileSessionTap(dvt, time_config, filters) as tap:
236
+ if show_tid:
237
+ print("{:^32}|{:^11}|{:^33}| Event".format("Time", "Thread", "Process"))
238
+ else:
239
+ print("{:^32}|{:^33}| Event".format("Time", "Process"))
240
+
241
+ for trace in islice(parser.formatted_traces(tap.get_kdbuf_stream()), count):
242
+ print(trace, flush=True)
243
+
244
+
245
+ def get_image_name(dsc_uuid_map, image_uuid, current_dsc_map):
246
+ if not current_dsc_map:
247
+ for dsc_mapping in dsc_uuid_map.values():
248
+ if image_uuid in dsc_mapping:
249
+ current_dsc_map.update(dsc_mapping)
250
+
251
+ return current_dsc_map.get(image_uuid, image_uuid)
252
+
253
+
254
+ def format_callstack(callstack: str, dsc_uuid_map, current_dsc_map) -> str:
255
+ lines = callstack.splitlines()
256
+ for i, line in enumerate(lines[1:]):
257
+ if ":" in line:
258
+ uuid = line.split(":")[0].strip()
259
+ lines[i + 1] = line.replace(uuid, get_image_name(dsc_uuid_map, uuid, current_dsc_map))
260
+ return "\n".join(lines)
261
+
262
+
263
+ @cli.command("callstacks-live")
264
+ def callstacks_live_profile_session(
265
+ service_provider: ServiceProviderDep,
266
+ count: Count = -1,
267
+ process: Annotated[
268
+ Optional[str],
269
+ typer.Option(help="Process ID / name to filter. Omit for all."),
270
+ ] = None,
271
+ tid: ThreadID = None,
272
+ show_tid: ShowThreadID = False,
273
+ ) -> None:
274
+ """Print callstacks received from the device in real time."""
275
+ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
276
+ print("Receiving time information")
277
+ time_config = CoreProfileSessionTap.get_time_config(dvt)
278
+ parser = PyKdebugParser()
279
+ parser.numer = time_config["numer"]
280
+ parser.denom = time_config["denom"]
281
+ parser.mach_absolute_time = time_config["mach_absolute_time"]
282
+ parser.usecs_since_epoch = time_config["usecs_since_epoch"]
283
+ parser.timezone = time_config["timezone"]
284
+ parser.filter_tid = tid
285
+ parser.filter_process = process
286
+ parser.color = user_requested_colored_output()
287
+ parser.show_tid = show_tid
288
+
289
+ with importlib.resources.open_text(pymobiledevice3.resources, "dsc_uuid_map.json") as fd:
290
+ dsc_uuid_map = json.load(fd)
291
+
292
+ current_dsc_map = {}
293
+ with CoreProfileSessionTap(dvt, time_config) as tap:
294
+ for callstack in islice(parser.formatted_callstacks(tap.get_kdbuf_stream()), count):
295
+ print(format_callstack(callstack, dsc_uuid_map, current_dsc_map))