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.
Files changed (164) hide show
  1. misc/plist_sniffer.py +15 -15
  2. misc/remotexpc_sniffer.py +29 -28
  3. misc/understanding_idevice_protocol_layers.md +15 -10
  4. pymobiledevice3/__main__.py +317 -127
  5. pymobiledevice3/_version.py +22 -4
  6. pymobiledevice3/bonjour.py +358 -113
  7. pymobiledevice3/ca.py +253 -16
  8. pymobiledevice3/cli/activation.py +31 -23
  9. pymobiledevice3/cli/afc.py +49 -40
  10. pymobiledevice3/cli/amfi.py +16 -21
  11. pymobiledevice3/cli/apps.py +87 -42
  12. pymobiledevice3/cli/backup.py +160 -90
  13. pymobiledevice3/cli/bonjour.py +44 -40
  14. pymobiledevice3/cli/cli_common.py +204 -198
  15. pymobiledevice3/cli/companion_proxy.py +14 -14
  16. pymobiledevice3/cli/crash.py +105 -56
  17. pymobiledevice3/cli/developer/__init__.py +62 -0
  18. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  19. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  20. pymobiledevice3/cli/developer/arbitration.py +50 -0
  21. pymobiledevice3/cli/developer/condition.py +33 -0
  22. pymobiledevice3/cli/developer/core_device.py +294 -0
  23. pymobiledevice3/cli/developer/debugserver.py +244 -0
  24. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  25. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  26. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  27. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  28. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  29. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  30. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  31. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  32. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  33. pymobiledevice3/cli/idam.py +42 -0
  34. pymobiledevice3/cli/lockdown.py +108 -103
  35. pymobiledevice3/cli/mounter.py +158 -99
  36. pymobiledevice3/cli/notification.py +38 -26
  37. pymobiledevice3/cli/pcap.py +45 -24
  38. pymobiledevice3/cli/power_assertion.py +18 -17
  39. pymobiledevice3/cli/processes.py +17 -23
  40. pymobiledevice3/cli/profile.py +165 -109
  41. pymobiledevice3/cli/provision.py +35 -34
  42. pymobiledevice3/cli/remote.py +217 -129
  43. pymobiledevice3/cli/restore.py +159 -143
  44. pymobiledevice3/cli/springboard.py +63 -53
  45. pymobiledevice3/cli/syslog.py +193 -86
  46. pymobiledevice3/cli/usbmux.py +73 -33
  47. pymobiledevice3/cli/version.py +5 -7
  48. pymobiledevice3/cli/webinspector.py +376 -214
  49. pymobiledevice3/common.py +3 -1
  50. pymobiledevice3/exceptions.py +182 -58
  51. pymobiledevice3/irecv.py +52 -53
  52. pymobiledevice3/irecv_devices.py +1489 -464
  53. pymobiledevice3/lockdown.py +473 -275
  54. pymobiledevice3/lockdown_service_provider.py +15 -8
  55. pymobiledevice3/osu/os_utils.py +27 -9
  56. pymobiledevice3/osu/posix_util.py +34 -15
  57. pymobiledevice3/osu/win_util.py +14 -8
  58. pymobiledevice3/pair_records.py +102 -21
  59. pymobiledevice3/remote/common.py +8 -4
  60. pymobiledevice3/remote/core_device/app_service.py +94 -67
  61. pymobiledevice3/remote/core_device/core_device_service.py +17 -14
  62. pymobiledevice3/remote/core_device/device_info.py +5 -5
  63. pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
  64. pymobiledevice3/remote/core_device/file_service.py +53 -23
  65. pymobiledevice3/remote/remote_service_discovery.py +79 -45
  66. pymobiledevice3/remote/remotexpc.py +73 -44
  67. pymobiledevice3/remote/tunnel_service.py +442 -317
  68. pymobiledevice3/remote/utils.py +14 -13
  69. pymobiledevice3/remote/xpc_message.py +145 -125
  70. pymobiledevice3/resources/dsc_uuid_map.py +19 -19
  71. pymobiledevice3/resources/firmware_notifications.py +20 -16
  72. pymobiledevice3/resources/notifications.txt +144 -0
  73. pymobiledevice3/restore/asr.py +27 -27
  74. pymobiledevice3/restore/base_restore.py +110 -21
  75. pymobiledevice3/restore/consts.py +87 -66
  76. pymobiledevice3/restore/device.py +59 -12
  77. pymobiledevice3/restore/fdr.py +46 -48
  78. pymobiledevice3/restore/ftab.py +19 -19
  79. pymobiledevice3/restore/img4.py +163 -0
  80. pymobiledevice3/restore/mbn.py +587 -0
  81. pymobiledevice3/restore/recovery.py +151 -151
  82. pymobiledevice3/restore/restore.py +562 -544
  83. pymobiledevice3/restore/restore_options.py +131 -110
  84. pymobiledevice3/restore/restored_client.py +51 -31
  85. pymobiledevice3/restore/tss.py +385 -267
  86. pymobiledevice3/service_connection.py +252 -59
  87. pymobiledevice3/services/accessibilityaudit.py +202 -120
  88. pymobiledevice3/services/afc.py +962 -365
  89. pymobiledevice3/services/amfi.py +24 -30
  90. pymobiledevice3/services/companion.py +23 -19
  91. pymobiledevice3/services/crash_reports.py +71 -47
  92. pymobiledevice3/services/debugserver_applist.py +3 -3
  93. pymobiledevice3/services/device_arbitration.py +8 -8
  94. pymobiledevice3/services/device_link.py +101 -79
  95. pymobiledevice3/services/diagnostics.py +973 -967
  96. pymobiledevice3/services/dtfetchsymbols.py +8 -8
  97. pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
  98. pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
  99. pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
  100. pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
  101. pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
  102. pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
  103. pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
  104. pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
  105. pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
  106. pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
  107. pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
  108. pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
  109. pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
  110. pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
  111. pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
  112. pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
  113. pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
  114. pymobiledevice3/services/file_relay.py +10 -10
  115. pymobiledevice3/services/heartbeat.py +9 -8
  116. pymobiledevice3/services/house_arrest.py +16 -15
  117. pymobiledevice3/services/idam.py +20 -0
  118. pymobiledevice3/services/installation_proxy.py +173 -81
  119. pymobiledevice3/services/lockdown_service.py +20 -10
  120. pymobiledevice3/services/misagent.py +22 -19
  121. pymobiledevice3/services/mobile_activation.py +147 -64
  122. pymobiledevice3/services/mobile_config.py +331 -294
  123. pymobiledevice3/services/mobile_image_mounter.py +141 -113
  124. pymobiledevice3/services/mobilebackup2.py +203 -145
  125. pymobiledevice3/services/notification_proxy.py +11 -11
  126. pymobiledevice3/services/os_trace.py +134 -74
  127. pymobiledevice3/services/pcapd.py +314 -302
  128. pymobiledevice3/services/power_assertion.py +10 -9
  129. pymobiledevice3/services/preboard.py +4 -4
  130. pymobiledevice3/services/remote_fetch_symbols.py +21 -14
  131. pymobiledevice3/services/remote_server.py +176 -146
  132. pymobiledevice3/services/restore_service.py +16 -16
  133. pymobiledevice3/services/screenshot.py +15 -12
  134. pymobiledevice3/services/simulate_location.py +7 -7
  135. pymobiledevice3/services/springboard.py +15 -15
  136. pymobiledevice3/services/syslog.py +5 -5
  137. pymobiledevice3/services/web_protocol/alert.py +11 -11
  138. pymobiledevice3/services/web_protocol/automation_session.py +251 -239
  139. pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
  140. pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
  141. pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
  142. pymobiledevice3/services/web_protocol/driver.py +114 -111
  143. pymobiledevice3/services/web_protocol/element.py +124 -111
  144. pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
  145. pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
  146. pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
  147. pymobiledevice3/services/web_protocol/switch_to.py +30 -27
  148. pymobiledevice3/services/webinspector.py +189 -155
  149. pymobiledevice3/tcp_forwarder.py +87 -69
  150. pymobiledevice3/tunneld/__init__.py +0 -0
  151. pymobiledevice3/tunneld/api.py +63 -0
  152. pymobiledevice3/tunneld/server.py +603 -0
  153. pymobiledevice3/usbmux.py +198 -147
  154. pymobiledevice3/utils.py +14 -11
  155. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
  156. pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
  157. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
  158. pymobiledevice3/cli/developer.py +0 -1215
  159. pymobiledevice3/cli/diagnostics.py +0 -99
  160. pymobiledevice3/tunneld.py +0 -524
  161. pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
  162. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  163. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
  164. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,294 @@
1
+ import asyncio
2
+ import logging
3
+ import posixpath
4
+ import time
5
+ from pathlib import Path
6
+ from typing import IO, Annotated, Optional
7
+
8
+ import click
9
+ import typer
10
+ from typer_injector import InjectingTyper
11
+
12
+ from pymobiledevice3.cli.cli_common import RSDServiceProviderDep, print_json
13
+ from pymobiledevice3.lockdown import create_using_usbmux
14
+ from pymobiledevice3.remote.core_device.app_service import AppServiceService
15
+ from pymobiledevice3.remote.core_device.device_info import DeviceInfoService
16
+ from pymobiledevice3.remote.core_device.diagnostics_service import DiagnosticsServiceService
17
+ from pymobiledevice3.remote.core_device.file_service import APPLE_DOMAIN_DICT, FileServiceService
18
+ from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
19
+ from pymobiledevice3.services.crash_reports import CrashReportsManager
20
+ from pymobiledevice3.utils import try_decode
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ cli = InjectingTyper(
26
+ name="core-device",
27
+ help="Access DeveloperDiskImage services (files, processes, app launch, diagnostics).",
28
+ no_args_is_help=True,
29
+ )
30
+
31
+
32
+ async def core_device_list_directory_task(
33
+ service_provider: RemoteServiceDiscoveryService, domain: str, path: str, identifier: str
34
+ ) -> None:
35
+ async with FileServiceService(service_provider, APPLE_DOMAIN_DICT[domain], identifier) as file_service:
36
+ print_json(await file_service.retrieve_directory_list(path))
37
+
38
+
39
+ @cli.command("list-directory")
40
+ def core_device_list_directory(
41
+ service_provider: RSDServiceProviderDep,
42
+ domain: Annotated[
43
+ str,
44
+ typer.Argument(click_type=click.Choice(APPLE_DOMAIN_DICT)),
45
+ ],
46
+ path: str,
47
+ identifier: Annotated[str, typer.Option()] = "",
48
+ ) -> None:
49
+ """List directory contents for a given domain/path."""
50
+ asyncio.run(core_device_list_directory_task(service_provider, domain, path, identifier))
51
+
52
+
53
+ async def core_device_read_file_task(
54
+ service_provider: RemoteServiceDiscoveryService,
55
+ domain: str,
56
+ path: str,
57
+ identifier: str,
58
+ output: Optional[IO],
59
+ ) -> None:
60
+ async with FileServiceService(service_provider, APPLE_DOMAIN_DICT[domain], identifier) as file_service:
61
+ buf = await file_service.retrieve_file(path)
62
+ if output is not None:
63
+ output.write(buf)
64
+ else:
65
+ print(try_decode(buf))
66
+
67
+
68
+ @cli.command("read-file")
69
+ def core_device_read_file(
70
+ service_provider: RSDServiceProviderDep,
71
+ domain: Annotated[
72
+ str,
73
+ typer.Argument(click_type=click.Choice(APPLE_DOMAIN_DICT)),
74
+ ],
75
+ path: str,
76
+ *,
77
+ identifier: Annotated[str, typer.Option()] = "",
78
+ output: Annotated[
79
+ Path,
80
+ typer.Option("--output", "-o"),
81
+ ],
82
+ ) -> None:
83
+ """Read a file from a domain/path to stdout or --output."""
84
+ with output.open("wb") as output_file:
85
+ asyncio.run(core_device_read_file_task(service_provider, domain, path, identifier, output_file))
86
+
87
+
88
+ async def core_device_propose_empty_file_task(
89
+ service_provider: RemoteServiceDiscoveryService,
90
+ domain: str,
91
+ path: str,
92
+ identifier: str,
93
+ file_permissions: int,
94
+ uid: int,
95
+ gid: int,
96
+ creation_time: int,
97
+ last_modification_time: int,
98
+ ) -> None:
99
+ async with FileServiceService(service_provider, APPLE_DOMAIN_DICT[domain], identifier) as file_service:
100
+ await file_service.propose_empty_file(path, file_permissions, uid, gid, creation_time, last_modification_time)
101
+
102
+
103
+ @cli.command("propose-empty-file")
104
+ def core_device_propose_empty_file(
105
+ service_provider: RSDServiceProviderDep,
106
+ domain: Annotated[
107
+ str,
108
+ typer.Argument(click_type=click.Choice(APPLE_DOMAIN_DICT)),
109
+ ],
110
+ path: str,
111
+ identifier: Annotated[str, typer.Option()] = "",
112
+ file_permissions: Annotated[int, typer.Option()] = 0o644,
113
+ uid: Annotated[int, typer.Option()] = 501,
114
+ gid: Annotated[int, typer.Option()] = 501,
115
+ creation_time: Annotated[Optional[int], typer.Option()] = None,
116
+ last_modification_time: Annotated[Optional[int], typer.Option()] = None,
117
+ ) -> None:
118
+ """Create an empty file at the given domain/path with custom permissions/owner/timestamps."""
119
+ asyncio.run(
120
+ core_device_propose_empty_file_task(
121
+ service_provider,
122
+ domain,
123
+ path,
124
+ identifier,
125
+ file_permissions,
126
+ uid,
127
+ gid,
128
+ creation_time if creation_time is not None else int(time.time()),
129
+ last_modification_time if last_modification_time is not None else int(time.time()),
130
+ )
131
+ )
132
+
133
+
134
+ async def core_device_list_launch_application_task(
135
+ service_provider: RemoteServiceDiscoveryService,
136
+ bundle_identifier: str,
137
+ argument: list[str],
138
+ kill_existing: bool,
139
+ suspended: bool,
140
+ env: dict[str, str],
141
+ ) -> None:
142
+ async with AppServiceService(service_provider) as app_service:
143
+ print_json(await app_service.launch_application(bundle_identifier, argument, kill_existing, suspended, env))
144
+
145
+
146
+ @cli.command("launch-application")
147
+ def core_device_launch_application(
148
+ service_provider: RSDServiceProviderDep,
149
+ bundle_identifier: str,
150
+ argument: list[str],
151
+ kill_existing: Annotated[
152
+ bool,
153
+ typer.Option(help="Whether to kill an existing instance of this process"),
154
+ ] = True,
155
+ suspended: Annotated[bool, typer.Option(help="Same as WaitForDebugger")] = False,
156
+ env: Annotated[
157
+ Optional[list[str]],
158
+ typer.Option(
159
+ help="Environment variable to pass to process given as key=value (can be specified multiple times)"
160
+ ),
161
+ ] = None,
162
+ ) -> None:
163
+ """Launch an app; optionally kill existing, wait for debugger, or set env vars."""
164
+ asyncio.run(
165
+ core_device_list_launch_application_task(
166
+ service_provider,
167
+ bundle_identifier,
168
+ list(argument),
169
+ kill_existing,
170
+ suspended,
171
+ dict(var.split("=", 1) for var in env or ()),
172
+ )
173
+ )
174
+
175
+
176
+ async def core_device_list_processes_task(service_provider: RemoteServiceDiscoveryService) -> None:
177
+ async with AppServiceService(service_provider) as app_service:
178
+ print_json(await app_service.list_processes())
179
+
180
+
181
+ @cli.command("list-processes")
182
+ def core_device_list_processes(service_provider: RSDServiceProviderDep) -> None:
183
+ """List running processes via CoreDevice."""
184
+ asyncio.run(core_device_list_processes_task(service_provider))
185
+
186
+
187
+ async def core_device_uninstall_app_task(
188
+ service_provider: RemoteServiceDiscoveryService, bundle_identifier: str
189
+ ) -> None:
190
+ async with AppServiceService(service_provider) as app_service:
191
+ await app_service.uninstall_app(bundle_identifier)
192
+
193
+
194
+ @cli.command("uninstall")
195
+ def core_device_uninstall_app(service_provider: RSDServiceProviderDep, bundle_identifier: str) -> None:
196
+ """Uninstall an app by bundle identifier via CoreDevice."""
197
+ asyncio.run(core_device_uninstall_app_task(service_provider, bundle_identifier))
198
+
199
+
200
+ async def core_device_send_signal_to_process_task(
201
+ service_provider: RemoteServiceDiscoveryService, pid: int, signal: int
202
+ ) -> None:
203
+ async with AppServiceService(service_provider) as app_service:
204
+ print_json(await app_service.send_signal_to_process(pid, signal))
205
+
206
+
207
+ @cli.command("send-signal-to-process")
208
+ def core_device_send_signal_to_process(service_provider: RSDServiceProviderDep, pid: int, signal: int) -> None:
209
+ """Send signal to process"""
210
+ asyncio.run(core_device_send_signal_to_process_task(service_provider, pid, signal))
211
+
212
+
213
+ async def core_device_get_device_info_task(service_provider: RemoteServiceDiscoveryService) -> None:
214
+ async with DeviceInfoService(service_provider) as app_service:
215
+ print_json(await app_service.get_device_info())
216
+
217
+
218
+ @cli.command("get-device-info")
219
+ def core_device_get_device_info(service_provider: RSDServiceProviderDep) -> None:
220
+ """Get device information"""
221
+ asyncio.run(core_device_get_device_info_task(service_provider))
222
+
223
+
224
+ async def core_device_get_display_info_task(service_provider: RemoteServiceDiscoveryService) -> None:
225
+ async with DeviceInfoService(service_provider) as app_service:
226
+ print_json(await app_service.get_display_info())
227
+
228
+
229
+ @cli.command("get-display-info")
230
+ def core_device_get_display_info(service_provider: RSDServiceProviderDep) -> None:
231
+ """Get display information"""
232
+ asyncio.run(core_device_get_display_info_task(service_provider))
233
+
234
+
235
+ async def core_device_query_mobilegestalt_task(service_provider: RemoteServiceDiscoveryService, key: list[str]) -> None:
236
+ """Query MobileGestalt"""
237
+ async with DeviceInfoService(service_provider) as app_service:
238
+ print_json(await app_service.query_mobilegestalt(key))
239
+
240
+
241
+ @cli.command("query-mobilegestalt")
242
+ def core_device_query_mobilegestalt(service_provider: RSDServiceProviderDep, key: list[str]) -> None:
243
+ """Query MobileGestalt"""
244
+ asyncio.run(core_device_query_mobilegestalt_task(service_provider, key))
245
+
246
+
247
+ async def core_device_get_lockstate_task(service_provider: RemoteServiceDiscoveryService) -> None:
248
+ async with DeviceInfoService(service_provider) as app_service:
249
+ print_json(await app_service.get_lockstate())
250
+
251
+
252
+ @cli.command("get-lockstate")
253
+ def core_device_get_lockstate(service_provider: RSDServiceProviderDep) -> None:
254
+ """Get lockstate"""
255
+ asyncio.run(core_device_get_lockstate_task(service_provider))
256
+
257
+
258
+ async def core_device_list_apps_task(service_provider: RemoteServiceDiscoveryService) -> None:
259
+ async with AppServiceService(service_provider) as app_service:
260
+ print_json(await app_service.list_apps())
261
+
262
+
263
+ @cli.command("list-apps")
264
+ def core_device_list_apps(service_provider: RSDServiceProviderDep) -> None:
265
+ """Get application list"""
266
+ asyncio.run(core_device_list_apps_task(service_provider))
267
+
268
+
269
+ async def core_device_sysdiagnose_task(service_provider: RemoteServiceDiscoveryService, output: Path) -> None:
270
+ async with DiagnosticsServiceService(service_provider) as service:
271
+ response = await service.capture_sysdiagnose(False)
272
+ logger.info(f"Operation response: {response}")
273
+ if output.is_dir():
274
+ output /= response.preferred_filename
275
+ logger.info(f"Downloading sysdiagnose to: {output}")
276
+
277
+ # get the file over lockdownd which is WAYYY faster
278
+ lockdown = create_using_usbmux(service_provider.udid)
279
+ with CrashReportsManager(lockdown) as crash_reports_manager:
280
+ crash_reports_manager.afc.pull(
281
+ posixpath.join(f"/DiagnosticLogs/sysdiagnose/{response.preferred_filename}"), str(output)
282
+ )
283
+
284
+
285
+ @cli.command("sysdiagnose")
286
+ def core_device_sysdiagnose(
287
+ service_provider: RSDServiceProviderDep,
288
+ output: Annotated[
289
+ Path,
290
+ typer.Argument(dir_okay=True, file_okay=True, exists=True),
291
+ ],
292
+ ) -> None:
293
+ """Execute sysdiagnose and fetch the output file"""
294
+ asyncio.run(core_device_sysdiagnose_task(service_provider, output))
@@ -0,0 +1,244 @@
1
+ import logging
2
+ import os
3
+ import plistlib
4
+ import signal
5
+ import struct
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Annotated, Optional
10
+
11
+ import typer
12
+ from packaging.version import Version
13
+ from plumbum import local
14
+ from typer_injector import InjectingTyper
15
+
16
+ from pymobiledevice3.cli.cli_common import RSDServiceProviderDep, ServiceProviderDep, print_json
17
+ from pymobiledevice3.exceptions import RSDRequiredError
18
+ from pymobiledevice3.lockdown import create_using_usbmux
19
+ from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
20
+ from pymobiledevice3.services.debugserver_applist import DebugServerAppList
21
+ from pymobiledevice3.services.installation_proxy import InstallationProxyService
22
+ from pymobiledevice3.tcp_forwarder import LockdownTcpForwarder
23
+
24
+ DEBUGSERVER_CONNECTION_STEPS = """
25
+ Follow the following connections steps from LLDB:
26
+
27
+ (lldb) platform select remote-ios
28
+ (lldb) target create /path/to/local/application.app
29
+ (lldb) script lldb.target.module[0].SetPlatformFileSpec(lldb.SBFileSpec('/private/var/containers/Bundle/Application/<APP-UUID>/application.app'))
30
+ (lldb) process connect connect://[{host}]:{port} <-- ACTUAL CONNECTION DETAILS!
31
+ (lldb) process launch
32
+ """
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ cli = InjectingTyper(
39
+ name="debugserver",
40
+ help="Start and drive debugserver sessions (RSD for iOS 17+, usbmux for older).",
41
+ no_args_is_help=True,
42
+ )
43
+
44
+
45
+ @cli.command("applist")
46
+ def debugserver_applist(service_provider: ServiceProviderDep) -> None:
47
+ """Print the debugserver applist XML for the device."""
48
+ print_json(DebugServerAppList(service_provider).get())
49
+
50
+
51
+ @cli.command("start-server")
52
+ def debugserver_start_server(service_provider: ServiceProviderDep, local_port: Optional[int] = None) -> None:
53
+ """
54
+ Start debugserver and print the LLDB connect string.
55
+
56
+ - For iOS < 17, you must forward to a local port (--local-port).
57
+ - For iOS >= 17, if connected over RSD, the remote host:port is printed for LLDB.
58
+ Connect quickly with your own LLDB client using the printed steps.
59
+ """
60
+
61
+ if Version(service_provider.product_version) < Version("17.0"):
62
+ service_name = "com.apple.debugserver.DVTSecureSocketProxy"
63
+ else:
64
+ service_name = "com.apple.internal.dt.remote.debugproxy"
65
+
66
+ if local_port is not None:
67
+ print(DEBUGSERVER_CONNECTION_STEPS.format(host="127.0.0.1", port=local_port))
68
+ print("Started port forwarding. Press Ctrl-C to close this shell when done")
69
+ sys.stdout.flush()
70
+ LockdownTcpForwarder(service_provider, local_port, service_name).start()
71
+ elif Version(service_provider.product_version) >= Version("17.0"):
72
+ if not isinstance(service_provider, RemoteServiceDiscoveryService):
73
+ raise RSDRequiredError(service_provider.identifier)
74
+ debugserver_port = service_provider.get_service_port(service_name)
75
+ print(DEBUGSERVER_CONNECTION_STEPS.format(host=service_provider.service.address[0], port=debugserver_port))
76
+ else:
77
+ print("local_port is required for iOS < 17.0")
78
+
79
+
80
+ @cli.command("lldb")
81
+ def debugserver_lldb(
82
+ service_provider: RSDServiceProviderDep,
83
+ xcodeproj_path: Annotated[
84
+ Path,
85
+ typer.Argument(exists=True, file_okay=False, dir_okay=True),
86
+ ],
87
+ configuration: Annotated[
88
+ str,
89
+ typer.Option(help="Build configuration to invoke (e.g., Debug or Release)."),
90
+ ] = "Debug",
91
+ lldb_command: Annotated[
92
+ str,
93
+ typer.Option(help="Path to the lldb executable to run."),
94
+ ] = "lldb",
95
+ launch: Annotated[
96
+ bool,
97
+ typer.Option(help="Automatically launch the app after attaching."),
98
+ ] = False,
99
+ breakpoints: Annotated[
100
+ Optional[list[str]],
101
+ typer.Option("--break", "-b", help="Add multiple startup breakpoints"),
102
+ ] = None,
103
+ user_commands: Annotated[
104
+ Optional[list[str]],
105
+ typer.Option("--command", "-c", help="Additional commands to run at startup"),
106
+ ] = None,
107
+ ) -> None:
108
+ """
109
+ Automate lldb launch for a given xcodeproj.
110
+
111
+ \b
112
+ This will:
113
+ - Build the given xcodeproj
114
+ - Install it
115
+ - Start a debugserver attached to it
116
+ - Place breakpoints if given any
117
+ - Launch the application if requested
118
+ - Execute any additional commands if requested
119
+ - Switch to lldb shell
120
+ """
121
+ if Version(service_provider.product_version) < Version("17.0"):
122
+ logger.error("lldb is only supported on iOS >= 17.0")
123
+ return
124
+
125
+ commands = []
126
+ with local.cwd(xcodeproj_path.parent):
127
+ logger.info(f"Building {xcodeproj_path} for {configuration} configuration")
128
+ local["xcodebuild"]["-configuration", configuration, "build"]()
129
+ local_app = next(iter(Path(f"build/{configuration}-iphoneos").glob("*.app")))
130
+ logger.info(f"Using app: {local_app}")
131
+
132
+ info_plist_path = local_app / "Info.plist"
133
+ info_plist = plistlib.loads(info_plist_path.read_bytes())
134
+ bundle_identifier = info_plist["CFBundleIdentifier"]
135
+ logger.info(f"Bundle identifier: {bundle_identifier}")
136
+
137
+ commands.append("platform select remote-ios")
138
+ commands.append(f'target create "{local_app.absolute()}"')
139
+
140
+ with InstallationProxyService(create_using_usbmux()) as installation_proxy:
141
+ logger.info("Installing app")
142
+ installation_proxy.install_from_local(local_app)
143
+ remote_path = installation_proxy.get_apps(bundle_identifiers=[bundle_identifier])[bundle_identifier]["Path"]
144
+ logger.info(f"Remote path: {remote_path}")
145
+
146
+ commands.append(f'script lldb.target.module[0].SetPlatformFileSpec(lldb.SBFileSpec("{remote_path}"))')
147
+
148
+ debugserver_port = service_provider.get_service_port("com.apple.internal.dt.remote.debugproxy")
149
+
150
+ # Add connection and launch commands
151
+ commands.append(f"process connect connect://[{service_provider.service.address[0]}]:{debugserver_port}")
152
+
153
+ if breakpoints:
154
+ for bp in breakpoints:
155
+ commands.append(f'breakpoint set -n "{bp}"')
156
+
157
+ if launch:
158
+ commands.append("process launch")
159
+
160
+ if user_commands:
161
+ # Add user commands
162
+ commands += user_commands
163
+
164
+ logger.info("Starting lldb with automated setup and connection")
165
+
166
+ # Works only on unix-based systems, so keep these imports here
167
+ import fcntl
168
+ import pty
169
+ import select as select_module
170
+ import termios
171
+ import tty
172
+
173
+ master, slave = pty.openpty()
174
+
175
+ process = None # Initialize process variable for signal handler
176
+
177
+ # Copy terminal size from the current terminal to PTY
178
+ def resize_pty() -> None:
179
+ """Update PTY size to match current terminal size"""
180
+ size = struct.unpack(
181
+ "HHHH", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0))
182
+ )
183
+ fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", *size))
184
+ # Send SIGWINCH to the child process to notify it of the resize
185
+ if process is not None and process.poll() is None:
186
+ process.send_signal(signal.SIGWINCH)
187
+
188
+ # Initial resize
189
+ resize_pty()
190
+
191
+ # Set up signal handler for window resize
192
+ def handle_sigwinch(signum, frame):
193
+ resize_pty()
194
+
195
+ old_sigwinch_handler = signal.signal(signal.SIGWINCH, handle_sigwinch)
196
+
197
+ # Save original terminal settings
198
+ old_tty = termios.tcgetattr(sys.stdin)
199
+
200
+ try:
201
+ # Set TERM environment variable to enable colors
202
+ env = os.environ.copy()
203
+ env["TERM"] = os.environ.get("TERM", "xterm-256color")
204
+
205
+ process = subprocess.Popen([lldb_command], stdin=slave, stdout=slave, stderr=slave, env=env)
206
+ os.close(slave)
207
+
208
+ # Put terminal in raw mode for proper interaction
209
+ tty.setraw(sys.stdin.fileno())
210
+ # Send all commands through stdin
211
+ for command in commands:
212
+ os.write(master, (command + "\n").encode())
213
+
214
+ # Now redirect stdin from the terminal to lldb so user can interact
215
+ while True:
216
+ rlist, _, _ = select_module.select([sys.stdin, master], [], [])
217
+
218
+ if sys.stdin in rlist:
219
+ # User typed something
220
+ data = os.read(sys.stdin.fileno(), 1024)
221
+ if not data:
222
+ break
223
+ os.write(master, data)
224
+
225
+ if master in rlist:
226
+ # lldb has output
227
+ try:
228
+ data = os.read(master, 1024)
229
+ if not data:
230
+ break
231
+ os.write(sys.stdout.fileno(), data)
232
+ except OSError:
233
+ break
234
+ except (KeyboardInterrupt, OSError):
235
+ pass
236
+ finally:
237
+ # Restore terminal settings
238
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
239
+ # Restore original SIGWINCH handler
240
+ signal.signal(signal.SIGWINCH, old_sigwinch_handler)
241
+ os.close(master)
242
+ if process is not None:
243
+ process.terminate()
244
+ process.wait()