pymobiledevice3 5.0.4__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.
Files changed (79) hide show
  1. misc/understanding_idevice_protocol_layers.md +10 -5
  2. pymobiledevice3/__main__.py +171 -46
  3. pymobiledevice3/_version.py +2 -2
  4. pymobiledevice3/bonjour.py +22 -21
  5. pymobiledevice3/cli/activation.py +24 -22
  6. pymobiledevice3/cli/afc.py +49 -41
  7. pymobiledevice3/cli/amfi.py +13 -18
  8. pymobiledevice3/cli/apps.py +71 -65
  9. pymobiledevice3/cli/backup.py +134 -93
  10. pymobiledevice3/cli/bonjour.py +31 -29
  11. pymobiledevice3/cli/cli_common.py +175 -232
  12. pymobiledevice3/cli/companion_proxy.py +12 -12
  13. pymobiledevice3/cli/crash.py +95 -52
  14. pymobiledevice3/cli/developer/__init__.py +62 -0
  15. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  16. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  17. pymobiledevice3/cli/developer/arbitration.py +50 -0
  18. pymobiledevice3/cli/developer/condition.py +33 -0
  19. pymobiledevice3/cli/developer/core_device.py +294 -0
  20. pymobiledevice3/cli/developer/debugserver.py +244 -0
  21. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  22. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  23. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  25. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  26. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  27. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  28. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  29. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  30. pymobiledevice3/cli/idam.py +42 -0
  31. pymobiledevice3/cli/lockdown.py +70 -75
  32. pymobiledevice3/cli/mounter.py +99 -57
  33. pymobiledevice3/cli/notification.py +38 -26
  34. pymobiledevice3/cli/pcap.py +36 -20
  35. pymobiledevice3/cli/power_assertion.py +15 -16
  36. pymobiledevice3/cli/processes.py +11 -17
  37. pymobiledevice3/cli/profile.py +120 -75
  38. pymobiledevice3/cli/provision.py +27 -26
  39. pymobiledevice3/cli/remote.py +109 -100
  40. pymobiledevice3/cli/restore.py +134 -129
  41. pymobiledevice3/cli/springboard.py +50 -50
  42. pymobiledevice3/cli/syslog.py +145 -65
  43. pymobiledevice3/cli/usbmux.py +66 -27
  44. pymobiledevice3/cli/version.py +2 -5
  45. pymobiledevice3/cli/webinspector.py +232 -156
  46. pymobiledevice3/exceptions.py +6 -2
  47. pymobiledevice3/lockdown.py +5 -1
  48. pymobiledevice3/lockdown_service_provider.py +5 -0
  49. pymobiledevice3/remote/remote_service_discovery.py +18 -10
  50. pymobiledevice3/restore/device.py +28 -4
  51. pymobiledevice3/restore/restore.py +2 -2
  52. pymobiledevice3/service_connection.py +15 -12
  53. pymobiledevice3/services/afc.py +731 -220
  54. pymobiledevice3/services/device_link.py +45 -31
  55. pymobiledevice3/services/idam.py +20 -0
  56. pymobiledevice3/services/lockdown_service.py +12 -9
  57. pymobiledevice3/services/mobile_config.py +1 -0
  58. pymobiledevice3/services/mobilebackup2.py +6 -3
  59. pymobiledevice3/services/os_trace.py +97 -55
  60. pymobiledevice3/services/remote_fetch_symbols.py +13 -8
  61. pymobiledevice3/services/screenshot.py +2 -2
  62. pymobiledevice3/services/web_protocol/alert.py +8 -8
  63. pymobiledevice3/services/web_protocol/automation_session.py +87 -79
  64. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  65. pymobiledevice3/services/web_protocol/driver.py +71 -70
  66. pymobiledevice3/services/web_protocol/element.py +58 -56
  67. pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
  68. pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
  69. pymobiledevice3/services/web_protocol/switch_to.py +23 -19
  70. pymobiledevice3/services/webinspector.py +42 -67
  71. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
  72. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
  73. pymobiledevice3/cli/completions.py +0 -50
  74. pymobiledevice3/cli/developer.py +0 -1539
  75. pymobiledevice3/cli/diagnostics.py +0 -110
  76. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
  77. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  78. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
  79. {pymobiledevice3-5.0.4.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)