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.
- pymobiledevice3/__main__.py +136 -44
- pymobiledevice3/_version.py +2 -2
- pymobiledevice3/bonjour.py +19 -20
- pymobiledevice3/cli/activation.py +24 -22
- pymobiledevice3/cli/afc.py +49 -41
- pymobiledevice3/cli/amfi.py +13 -18
- pymobiledevice3/cli/apps.py +71 -65
- pymobiledevice3/cli/backup.py +134 -93
- pymobiledevice3/cli/bonjour.py +31 -29
- pymobiledevice3/cli/cli_common.py +179 -232
- pymobiledevice3/cli/companion_proxy.py +12 -12
- pymobiledevice3/cli/crash.py +95 -52
- pymobiledevice3/cli/developer/__init__.py +62 -0
- pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
- pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
- pymobiledevice3/cli/developer/arbitration.py +50 -0
- pymobiledevice3/cli/developer/condition.py +33 -0
- pymobiledevice3/cli/developer/core_device.py +294 -0
- pymobiledevice3/cli/developer/debugserver.py +244 -0
- pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
- pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
- pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
- pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
- pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
- pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
- pymobiledevice3/cli/developer/simulate_location.py +51 -0
- pymobiledevice3/cli/diagnostics/__init__.py +75 -0
- pymobiledevice3/cli/diagnostics/battery.py +47 -0
- pymobiledevice3/cli/idam.py +18 -22
- pymobiledevice3/cli/lockdown.py +70 -75
- pymobiledevice3/cli/mounter.py +99 -57
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +36 -20
- pymobiledevice3/cli/power_assertion.py +15 -16
- pymobiledevice3/cli/processes.py +11 -17
- pymobiledevice3/cli/profile.py +120 -75
- pymobiledevice3/cli/provision.py +27 -26
- pymobiledevice3/cli/remote.py +108 -99
- pymobiledevice3/cli/restore.py +134 -129
- pymobiledevice3/cli/springboard.py +50 -50
- pymobiledevice3/cli/syslog.py +138 -74
- pymobiledevice3/cli/usbmux.py +66 -27
- pymobiledevice3/cli/version.py +2 -5
- pymobiledevice3/cli/webinspector.py +149 -103
- pymobiledevice3/remote/remote_service_discovery.py +11 -10
- pymobiledevice3/restore/device.py +28 -4
- pymobiledevice3/service_connection.py +1 -1
- pymobiledevice3/services/mobilebackup2.py +4 -1
- pymobiledevice3/services/screenshot.py +2 -2
- pymobiledevice3/services/web_protocol/automation_session.py +4 -2
- pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
- pymobiledevice3/services/web_protocol/element.py +3 -3
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
- pymobiledevice3/cli/completions.py +0 -50
- pymobiledevice3/cli/developer.py +0 -1645
- pymobiledevice3/cli/diagnostics.py +0 -110
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
- {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))
|