pymobiledevice3 4.14.6__py3-none-any.whl → 7.0.6__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.
- misc/plist_sniffer.py +15 -15
- misc/remotexpc_sniffer.py +29 -28
- misc/understanding_idevice_protocol_layers.md +15 -10
- pymobiledevice3/__main__.py +317 -127
- pymobiledevice3/_version.py +22 -4
- pymobiledevice3/bonjour.py +358 -113
- pymobiledevice3/ca.py +253 -16
- pymobiledevice3/cli/activation.py +31 -23
- pymobiledevice3/cli/afc.py +49 -40
- pymobiledevice3/cli/amfi.py +16 -21
- pymobiledevice3/cli/apps.py +87 -42
- pymobiledevice3/cli/backup.py +160 -90
- pymobiledevice3/cli/bonjour.py +44 -40
- pymobiledevice3/cli/cli_common.py +204 -198
- pymobiledevice3/cli/companion_proxy.py +14 -14
- pymobiledevice3/cli/crash.py +105 -56
- 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 +438 -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 +42 -0
- pymobiledevice3/cli/lockdown.py +108 -103
- pymobiledevice3/cli/mounter.py +158 -99
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +45 -24
- pymobiledevice3/cli/power_assertion.py +18 -17
- pymobiledevice3/cli/processes.py +17 -23
- pymobiledevice3/cli/profile.py +165 -109
- pymobiledevice3/cli/provision.py +35 -34
- pymobiledevice3/cli/remote.py +217 -129
- pymobiledevice3/cli/restore.py +159 -143
- pymobiledevice3/cli/springboard.py +63 -53
- pymobiledevice3/cli/syslog.py +193 -86
- pymobiledevice3/cli/usbmux.py +73 -33
- pymobiledevice3/cli/version.py +5 -7
- pymobiledevice3/cli/webinspector.py +376 -214
- pymobiledevice3/common.py +3 -1
- pymobiledevice3/exceptions.py +182 -58
- pymobiledevice3/irecv.py +52 -53
- pymobiledevice3/irecv_devices.py +1489 -464
- pymobiledevice3/lockdown.py +473 -275
- pymobiledevice3/lockdown_service_provider.py +15 -8
- pymobiledevice3/osu/os_utils.py +27 -9
- pymobiledevice3/osu/posix_util.py +34 -15
- pymobiledevice3/osu/win_util.py +14 -8
- pymobiledevice3/pair_records.py +102 -21
- pymobiledevice3/remote/common.py +8 -4
- pymobiledevice3/remote/core_device/app_service.py +94 -67
- pymobiledevice3/remote/core_device/core_device_service.py +17 -14
- pymobiledevice3/remote/core_device/device_info.py +5 -5
- pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
- pymobiledevice3/remote/core_device/file_service.py +53 -23
- pymobiledevice3/remote/remote_service_discovery.py +79 -45
- pymobiledevice3/remote/remotexpc.py +73 -44
- pymobiledevice3/remote/tunnel_service.py +442 -317
- pymobiledevice3/remote/utils.py +14 -13
- pymobiledevice3/remote/xpc_message.py +145 -125
- pymobiledevice3/resources/dsc_uuid_map.py +19 -19
- pymobiledevice3/resources/firmware_notifications.py +20 -16
- pymobiledevice3/resources/notifications.txt +144 -0
- pymobiledevice3/restore/asr.py +27 -27
- pymobiledevice3/restore/base_restore.py +110 -21
- pymobiledevice3/restore/consts.py +87 -66
- pymobiledevice3/restore/device.py +59 -12
- pymobiledevice3/restore/fdr.py +46 -48
- pymobiledevice3/restore/ftab.py +19 -19
- pymobiledevice3/restore/img4.py +163 -0
- pymobiledevice3/restore/mbn.py +587 -0
- pymobiledevice3/restore/recovery.py +151 -151
- pymobiledevice3/restore/restore.py +562 -544
- pymobiledevice3/restore/restore_options.py +131 -110
- pymobiledevice3/restore/restored_client.py +51 -31
- pymobiledevice3/restore/tss.py +385 -267
- pymobiledevice3/service_connection.py +252 -59
- pymobiledevice3/services/accessibilityaudit.py +202 -120
- pymobiledevice3/services/afc.py +962 -365
- pymobiledevice3/services/amfi.py +24 -30
- pymobiledevice3/services/companion.py +23 -19
- pymobiledevice3/services/crash_reports.py +71 -47
- pymobiledevice3/services/debugserver_applist.py +3 -3
- pymobiledevice3/services/device_arbitration.py +8 -8
- pymobiledevice3/services/device_link.py +101 -79
- pymobiledevice3/services/diagnostics.py +973 -967
- pymobiledevice3/services/dtfetchsymbols.py +8 -8
- pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
- pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
- pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
- pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
- pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
- pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
- pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
- pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
- pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
- pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
- pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
- pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
- pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
- pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
- pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
- pymobiledevice3/services/file_relay.py +10 -10
- pymobiledevice3/services/heartbeat.py +9 -8
- pymobiledevice3/services/house_arrest.py +16 -15
- pymobiledevice3/services/idam.py +20 -0
- pymobiledevice3/services/installation_proxy.py +173 -81
- pymobiledevice3/services/lockdown_service.py +20 -10
- pymobiledevice3/services/misagent.py +22 -19
- pymobiledevice3/services/mobile_activation.py +147 -64
- pymobiledevice3/services/mobile_config.py +331 -294
- pymobiledevice3/services/mobile_image_mounter.py +141 -113
- pymobiledevice3/services/mobilebackup2.py +203 -145
- pymobiledevice3/services/notification_proxy.py +11 -11
- pymobiledevice3/services/os_trace.py +134 -74
- pymobiledevice3/services/pcapd.py +314 -302
- pymobiledevice3/services/power_assertion.py +10 -9
- pymobiledevice3/services/preboard.py +4 -4
- pymobiledevice3/services/remote_fetch_symbols.py +21 -14
- pymobiledevice3/services/remote_server.py +176 -146
- pymobiledevice3/services/restore_service.py +16 -16
- pymobiledevice3/services/screenshot.py +15 -12
- pymobiledevice3/services/simulate_location.py +7 -7
- pymobiledevice3/services/springboard.py +15 -15
- pymobiledevice3/services/syslog.py +5 -5
- pymobiledevice3/services/web_protocol/alert.py +11 -11
- pymobiledevice3/services/web_protocol/automation_session.py +251 -239
- pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
- pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
- pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
- pymobiledevice3/services/web_protocol/driver.py +114 -111
- pymobiledevice3/services/web_protocol/element.py +124 -111
- pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
- pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
- pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
- pymobiledevice3/services/web_protocol/switch_to.py +30 -27
- pymobiledevice3/services/webinspector.py +189 -155
- pymobiledevice3/tcp_forwarder.py +87 -69
- pymobiledevice3/tunneld/__init__.py +0 -0
- pymobiledevice3/tunneld/api.py +63 -0
- pymobiledevice3/tunneld/server.py +603 -0
- pymobiledevice3/usbmux.py +198 -147
- pymobiledevice3/utils.py +14 -11
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
- pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
- pymobiledevice3/cli/developer.py +0 -1215
- pymobiledevice3/cli/diagnostics.py +0 -99
- pymobiledevice3/tunneld.py +0 -524
- pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
|
@@ -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))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from typer_injector import InjectingTyper
|
|
6
|
+
|
|
7
|
+
from pymobiledevice3.cli.cli_common import OSUTILS, ServiceProviderDep
|
|
8
|
+
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
|
|
9
|
+
from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation
|
|
10
|
+
|
|
11
|
+
cli = InjectingTyper(
|
|
12
|
+
name="simulate-location",
|
|
13
|
+
help="Simulate device location by given input",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cli.command("clear")
|
|
19
|
+
def dvt_simulate_location_clear(service_provider: ServiceProviderDep) -> None:
|
|
20
|
+
"""Clear currently simulated location"""
|
|
21
|
+
with DvtSecureSocketProxyService(service_provider) as dvt:
|
|
22
|
+
LocationSimulation(dvt).clear()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.command("set")
|
|
26
|
+
def dvt_simulate_location_set(service_provider: ServiceProviderDep, latitude: float, longitude: float) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Set a simulated location.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
For example:
|
|
32
|
+
\b ... set -- 40.690008 -74.045843 for liberty island
|
|
33
|
+
"""
|
|
34
|
+
with DvtSecureSocketProxyService(service_provider) as dvt:
|
|
35
|
+
LocationSimulation(dvt).set(latitude, longitude)
|
|
36
|
+
OSUTILS.wait_return()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command("play")
|
|
40
|
+
def dvt_simulate_location_play(
|
|
41
|
+
service_provider: ServiceProviderDep,
|
|
42
|
+
filename: Annotated[
|
|
43
|
+
Path,
|
|
44
|
+
typer.Argument(exists=True, file_okay=True, dir_okay=False),
|
|
45
|
+
],
|
|
46
|
+
timing_randomness_range: int = 0,
|
|
47
|
+
disable_sleep: Annotated[bool, typer.Option()] = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Simulate inputs from a given .gpx file"""
|
|
50
|
+
with DvtSecureSocketProxyService(service_provider) as dvt:
|
|
51
|
+
LocationSimulation(dvt).play_gpx_file(
|
|
52
|
+
str(filename),
|
|
53
|
+
disable_sleep=disable_sleep,
|
|
54
|
+
timing_randomness_range=timing_randomness_range,
|
|
55
|
+
)
|
|
56
|
+
OSUTILS.wait_return()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from typing import Annotated, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer_injector import InjectingTyper
|
|
7
|
+
|
|
8
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep
|
|
9
|
+
from pymobiledevice3.cli.developer.dvt.sysmon import process
|
|
10
|
+
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
|
|
11
|
+
from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
cli = InjectingTyper(
|
|
17
|
+
name="sysmon",
|
|
18
|
+
help="System monitor options.",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
cli.add_typer(process.cli)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli.command("system")
|
|
25
|
+
def sysmon_system(
|
|
26
|
+
service_provider: ServiceProviderDep,
|
|
27
|
+
fields: Annotated[
|
|
28
|
+
Optional[str],
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--fields",
|
|
31
|
+
"-f",
|
|
32
|
+
help='field names separated by ",".',
|
|
33
|
+
),
|
|
34
|
+
] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""show current system stats."""
|
|
37
|
+
|
|
38
|
+
split_fields = fields.split(",") if fields is not None else None
|
|
39
|
+
|
|
40
|
+
with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
|
|
41
|
+
sysmontap = Sysmontap(dvt)
|
|
42
|
+
with sysmontap as sysmon:
|
|
43
|
+
system = None
|
|
44
|
+
system_usage = None
|
|
45
|
+
system_usage_seen = False # Tracks if the first occurrence of SystemCPUUsage
|
|
46
|
+
|
|
47
|
+
for row in sysmon:
|
|
48
|
+
if "System" in row and system is None:
|
|
49
|
+
system = sysmon.system_attributes_cls(*row["System"])
|
|
50
|
+
|
|
51
|
+
if "SystemCPUUsage" in row:
|
|
52
|
+
if system_usage_seen:
|
|
53
|
+
system_usage = {
|
|
54
|
+
**row["SystemCPUUsage"],
|
|
55
|
+
"CPUCount": row["CPUCount"],
|
|
56
|
+
"EnabledCPUs": row["EnabledCPUs"],
|
|
57
|
+
}
|
|
58
|
+
else: # Ignore the first occurrence because first occurrence always gives a incorrect value - 100 or 0
|
|
59
|
+
system_usage_seen = True
|
|
60
|
+
|
|
61
|
+
if system and system_usage:
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
assert system is not None and system_usage is not None # for type checker
|
|
65
|
+
|
|
66
|
+
attrs_dict = {**asdict(system), **system_usage}
|
|
67
|
+
for name, value in attrs_dict.items():
|
|
68
|
+
if (split_fields is None) or (name in fields):
|
|
69
|
+
print(f"{name}: {value}")
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from collections import namedtuple
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from typer_injector import InjectingTyper
|
|
12
|
+
|
|
13
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep, default_json_encoder, print_json
|
|
14
|
+
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
|
|
15
|
+
from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo
|
|
16
|
+
from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli = InjectingTyper(
|
|
22
|
+
name="process",
|
|
23
|
+
help="Process monitor options.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cli.command("monitor")
|
|
29
|
+
def sysmon_process_monitor(service_provider: ServiceProviderDep, threshold: float) -> None:
|
|
30
|
+
"""monitor all most consuming processes by given cpuUsage threshold."""
|
|
31
|
+
|
|
32
|
+
Process = namedtuple("process", "pid name cpuUsage physFootprint")
|
|
33
|
+
|
|
34
|
+
with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Sysmontap(dvt) as sysmon:
|
|
35
|
+
for process_snapshot in sysmon.iter_processes():
|
|
36
|
+
entries = []
|
|
37
|
+
for process in process_snapshot:
|
|
38
|
+
if (process["cpuUsage"] is not None) and (process["cpuUsage"] >= threshold):
|
|
39
|
+
entries.append(
|
|
40
|
+
Process(
|
|
41
|
+
pid=process["pid"],
|
|
42
|
+
name=process["name"],
|
|
43
|
+
cpuUsage=process["cpuUsage"],
|
|
44
|
+
physFootprint=process["physFootprint"],
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger.info(entries)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.command("single")
|
|
52
|
+
def sysmon_process_single(
|
|
53
|
+
service_provider: ServiceProviderDep,
|
|
54
|
+
attributes: Annotated[
|
|
55
|
+
Optional[list[str]],
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--attributes",
|
|
58
|
+
"-a",
|
|
59
|
+
help="filter processes by given attribute value given as key=value. Can be specified multiple times.",
|
|
60
|
+
),
|
|
61
|
+
] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""show a single snapshot of currently running processes."""
|
|
64
|
+
|
|
65
|
+
count = 0
|
|
66
|
+
|
|
67
|
+
result = []
|
|
68
|
+
with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
|
|
69
|
+
device_info = DeviceInfo(dvt)
|
|
70
|
+
|
|
71
|
+
with Sysmontap(dvt) as sysmon:
|
|
72
|
+
for process_snapshot in sysmon.iter_processes():
|
|
73
|
+
count += 1
|
|
74
|
+
|
|
75
|
+
if count < 2:
|
|
76
|
+
# first sample doesn't contain an initialized value for cpuUsage
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
for process in process_snapshot:
|
|
80
|
+
skip = False
|
|
81
|
+
if attributes is not None:
|
|
82
|
+
for filter_attr in attributes:
|
|
83
|
+
filter_attr, filter_value = filter_attr.split("=", 1)
|
|
84
|
+
if str(process[filter_attr]) != filter_value:
|
|
85
|
+
skip = True
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
if skip:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# adding "artificially" the execName field
|
|
92
|
+
process["execName"] = device_info.execname_for_pid(process["pid"])
|
|
93
|
+
result.append(process)
|
|
94
|
+
|
|
95
|
+
# exit after single snapshot
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
print_json(result)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@cli.command("monitor-single")
|
|
102
|
+
def sysmon_process_monitor_single(
|
|
103
|
+
service_provider: ServiceProviderDep,
|
|
104
|
+
attributes: Annotated[
|
|
105
|
+
Optional[list[str]],
|
|
106
|
+
typer.Option(
|
|
107
|
+
"--attributes",
|
|
108
|
+
"-a",
|
|
109
|
+
help="filter processes by attribute (key=value). Multiple filters on same attribute use OR logic, different attributes use AND.",
|
|
110
|
+
),
|
|
111
|
+
] = None,
|
|
112
|
+
output: Annotated[
|
|
113
|
+
Optional[Path],
|
|
114
|
+
typer.Option(
|
|
115
|
+
"--output",
|
|
116
|
+
"-o",
|
|
117
|
+
help="output file path for JSONL format (optional, defaults to stdout)",
|
|
118
|
+
),
|
|
119
|
+
] = None,
|
|
120
|
+
interval: Annotated[
|
|
121
|
+
Optional[int],
|
|
122
|
+
typer.Option(
|
|
123
|
+
"--interval",
|
|
124
|
+
"-i",
|
|
125
|
+
help="minimum interval in milliseconds between outputs (optional)",
|
|
126
|
+
),
|
|
127
|
+
] = None,
|
|
128
|
+
duration: Annotated[
|
|
129
|
+
Optional[int],
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--duration",
|
|
132
|
+
"-d",
|
|
133
|
+
help="maximum duration in milliseconds to run monitoring (optional)",
|
|
134
|
+
),
|
|
135
|
+
] = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Continuously monitor a single process with comprehensive metrics."""
|
|
138
|
+
count = 0
|
|
139
|
+
start_time = None
|
|
140
|
+
|
|
141
|
+
# Parse attributes into grouped filters: same attribute uses OR, different attributes use AND
|
|
142
|
+
parsed_filters: dict[str, list[str]] = {}
|
|
143
|
+
if attributes:
|
|
144
|
+
for raw in attributes:
|
|
145
|
+
key, value = raw.split("=", 1)
|
|
146
|
+
parsed_filters.setdefault(key, []).append(value)
|
|
147
|
+
|
|
148
|
+
def matches_filters(proc: dict) -> bool:
|
|
149
|
+
"""Check if process matches all filter criteria."""
|
|
150
|
+
if not parsed_filters:
|
|
151
|
+
return True
|
|
152
|
+
return all(str(proc.get(key)) in values for key, values in parsed_filters.items())
|
|
153
|
+
|
|
154
|
+
with contextlib.ExitStack() as stack:
|
|
155
|
+
output_file = stack.enter_context(open(output, "w")) if output else None
|
|
156
|
+
|
|
157
|
+
dvt = stack.enter_context(DvtSecureSocketProxyService(lockdown=service_provider))
|
|
158
|
+
sysmon = stack.enter_context(Sysmontap(dvt))
|
|
159
|
+
|
|
160
|
+
for process_snapshot in sysmon.iter_processes():
|
|
161
|
+
count += 1
|
|
162
|
+
|
|
163
|
+
if count < 2:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if start_time is None:
|
|
167
|
+
start_time = time.time()
|
|
168
|
+
|
|
169
|
+
if duration is not None:
|
|
170
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
171
|
+
if elapsed_ms >= duration:
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
for process in process_snapshot:
|
|
175
|
+
if not matches_filters(process):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
process["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
179
|
+
|
|
180
|
+
if output_file:
|
|
181
|
+
json_output = json.dumps(process, default=default_json_encoder)
|
|
182
|
+
output_file.write(json_output + "\n")
|
|
183
|
+
output_file.flush()
|
|
184
|
+
else:
|
|
185
|
+
print_json(process)
|
|
186
|
+
|
|
187
|
+
if interval:
|
|
188
|
+
time.sleep(interval / 1000.0)
|