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
@@ -1,166 +1,265 @@
1
1
  import asyncio
2
+ import inspect
2
3
  import logging
3
4
  import re
4
5
  from abc import ABC, abstractmethod
5
- from collections.abc import Iterable
6
- from contextlib import asynccontextmanager
6
+ from asyncio import CancelledError
7
+ from collections.abc import AsyncIterator, Iterable
8
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
7
9
  from functools import update_wrapper
8
- from typing import Optional
10
+ from string import Template
11
+ from typing import Annotated, Any, Optional
9
12
 
10
- import click
11
13
  import inquirer3
12
14
  import IPython
15
+ import nest_asyncio
16
+ import typer
13
17
  import uvicorn
14
18
  from inquirer3.themes import GreenPassion
15
19
  from prompt_toolkit import HTML, PromptSession
16
20
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
17
- from prompt_toolkit.completion.base import CompleteEvent, Completer, Completion, Document
21
+ from prompt_toolkit.completion.base import CompleteEvent, Completer, Completion
22
+ from prompt_toolkit.document import Document
18
23
  from prompt_toolkit.history import FileHistory
19
24
  from prompt_toolkit.lexers import PygmentsLexer
20
25
  from prompt_toolkit.patch_stdout import patch_stdout
21
26
  from prompt_toolkit.styles import style_from_pygments_cls
22
27
  from pygments import formatters, highlight, lexers
23
28
  from pygments.styles import get_style_by_name
29
+ from typer_injector import InjectingTyper
24
30
 
25
- from pymobiledevice3.cli.cli_common import Command
31
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep
26
32
  from pymobiledevice3.common import get_home_folder
27
- from pymobiledevice3.exceptions import InspectorEvaluateError, LaunchingApplicationError, \
28
- RemoteAutomationNotEnabledError, WebInspectorNotEnabledError, WirError
29
- from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
33
+ from pymobiledevice3.exceptions import (
34
+ InspectorEvaluateError,
35
+ LaunchingApplicationError,
36
+ RemoteAutomationNotEnabledError,
37
+ WebInspectorNotEnabledError,
38
+ WirError,
39
+ )
40
+ from pymobiledevice3.lockdown import create_using_usbmux
41
+ from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
30
42
  from pymobiledevice3.osu.os_utils import get_os_utils
31
43
  from pymobiledevice3.services.web_protocol.cdp_server import app
32
44
  from pymobiledevice3.services.web_protocol.driver import By, Cookie, WebDriver
33
45
  from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
34
- from pymobiledevice3.services.webinspector import SAFARI, Page, WebinspectorService
35
-
36
- SCRIPT = '''
37
- function inspectedPage_evalResult_getCompletions(primitiveType) {{
38
- var resultSet={{}};
39
- var object = primitiveType;
40
- for(var o=object;o;o=o.__proto__) {{
41
-
42
- try{{
43
- var names=Object.getOwnPropertyNames(o);
44
- for(var i=0;i<names.length;++i)
45
- resultSet[names[i]]=true;
46
- }} catch(e){{}}
47
- }}
46
+ from pymobiledevice3.services.webinspector import SAFARI, Application, ApplicationPage, WebinspectorService
47
+
48
+ SCRIPT = Template("""
49
+ function inspectedPage_evalResult_getCompletions(primitiveType) {
50
+ let resultSet = {};
51
+ let object = primitiveType;
52
+ for (let o = object; o; o = o.__proto__) {
53
+ try {
54
+ let names = Object.getOwnPropertyNames(o);
55
+ for (let i = 0; i < names.length; ++i)
56
+ resultSet[names[i]] = true;
57
+ } catch(e) {}
58
+ }
48
59
  return resultSet;
49
- }}
50
-
51
- try {{
52
- inspectedPage_evalResult_getCompletions({object})
53
- }} catch (e) {{}}
54
- '''
55
-
56
- JS_RESERVED_WORDS = ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class',
57
- 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval',
58
- 'export', 'extends', 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if',
59
- 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new',
60
- 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'super',
61
- 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var',
62
- 'void', 'volatile', 'while', 'with', 'yield', ]
60
+ }
61
+
62
+ try {
63
+ inspectedPage_evalResult_getCompletions(${object})
64
+ } catch (e) {}
65
+ """)
66
+
67
+ JS_RESERVED_WORDS = frozenset({
68
+ "abstract",
69
+ "arguments",
70
+ "await",
71
+ "boolean",
72
+ "break",
73
+ "byte",
74
+ "case",
75
+ "catch",
76
+ "char",
77
+ "class",
78
+ "const",
79
+ "continue",
80
+ "debugger",
81
+ "default",
82
+ "delete",
83
+ "do",
84
+ "double",
85
+ "else",
86
+ "enum",
87
+ "eval",
88
+ "export",
89
+ "extends",
90
+ "false",
91
+ "final",
92
+ "finally",
93
+ "float",
94
+ "for",
95
+ "function",
96
+ "goto",
97
+ "if",
98
+ "implements",
99
+ "import",
100
+ "in",
101
+ "instanceof",
102
+ "int",
103
+ "interface",
104
+ "let",
105
+ "long",
106
+ "native",
107
+ "new",
108
+ "null",
109
+ "package",
110
+ "private",
111
+ "protected",
112
+ "public",
113
+ "return",
114
+ "short",
115
+ "static",
116
+ "super",
117
+ "switch",
118
+ "synchronized",
119
+ "this",
120
+ "throw",
121
+ "throws",
122
+ "transient",
123
+ "true",
124
+ "try",
125
+ "typeof",
126
+ "var",
127
+ "void",
128
+ "volatile",
129
+ "while",
130
+ "with",
131
+ "yield",
132
+ })
63
133
 
64
134
  OSUTILS = get_os_utils()
65
135
  logger = logging.getLogger(__name__)
66
136
 
67
137
 
68
- @click.group()
69
- def cli() -> None:
70
- pass
138
+ cli = InjectingTyper(
139
+ name="webinspector",
140
+ help=(
141
+ "Control Safari/WebViews (tabs, automation, JS shells, CDP). "
142
+ "Requires Web Inspector and Remote Automation enabled on the device."
143
+ ),
144
+ no_args_is_help=True,
145
+ )
71
146
 
72
147
 
73
- @cli.group()
74
- def webinspector() -> None:
75
- """ Access webinspector services """
76
- pass
148
+ def catch_errors(func):
149
+ errors = {
150
+ LaunchingApplicationError: "Unable to launch application (try to unlock device)",
151
+ WebInspectorNotEnabledError: "Web inspector is not enabled",
152
+ RemoteAutomationNotEnabledError: "Remote automation is not enabled",
153
+ }
77
154
 
155
+ def handle_error(e):
156
+ logger.error(next(msg for exc, msg in errors.items() if isinstance(e, exc)))
78
157
 
79
- def catch_errors(func):
80
- def catch_function(*args, **kwargs):
81
- try:
82
- return func(*args, **kwargs)
83
- except LaunchingApplicationError:
84
- logger.error('Unable to launch application (try to unlock device)')
85
- except WebInspectorNotEnabledError:
86
- logger.error('Web inspector is not enable')
87
- except RemoteAutomationNotEnabledError:
88
- logger.error('Remote automation is not enable')
158
+ if inspect.iscoroutinefunction(func):
159
+
160
+ async def catch_function(*args, **kwargs):
161
+ try:
162
+ return await func(*args, **kwargs)
163
+ except tuple(errors) as e:
164
+ handle_error(e)
165
+
166
+ else:
167
+
168
+ def catch_function(*args, **kwargs):
169
+ try:
170
+ return func(*args, **kwargs)
171
+ except tuple(errors) as e:
172
+ handle_error(e)
89
173
 
90
174
  return update_wrapper(catch_function, func)
91
175
 
92
176
 
93
- def reload_pages(inspector: WebinspectorService):
94
- inspector.get_open_pages()
177
+ async def reload_pages(inspector: WebinspectorService) -> None:
178
+ await inspector.get_open_pages()
95
179
  # Best effort.
96
- inspector.flush_input(2)
180
+ await inspector.flush_input(2)
97
181
 
98
182
 
99
- def create_webinspector_and_launch_app(lockdown: LockdownClient, timeout: float, app: str):
183
+ async def create_webinspector_and_launch_app(
184
+ lockdown: LockdownServiceProvider, timeout: float, app: str
185
+ ) -> tuple[WebinspectorService, Application]:
100
186
  inspector = WebinspectorService(lockdown=lockdown)
101
- inspector.connect(timeout)
102
- application = inspector.open_app(app)
187
+ await inspector.connect(timeout)
188
+ application = await inspector.open_app(app)
103
189
  return inspector, application
104
190
 
105
191
 
106
- @webinspector.command(cls=Command)
107
- @click.option('-v', '--verbose', is_flag=True)
108
- @click.option('-t', '--timeout', default=3, show_default=True, type=float)
192
+ async def opened_tabs_task(service_provider: LockdownServiceProvider, timeout: float) -> None:
193
+ inspector = WebinspectorService(lockdown=service_provider)
194
+ await inspector.connect(timeout)
195
+ application_pages = await inspector.get_open_application_pages(timeout=timeout)
196
+ for application_page in application_pages:
197
+ print(application_page)
198
+ await inspector.close()
199
+
200
+
201
+ @cli.command()
109
202
  @catch_errors
110
- def opened_tabs(service_provider: LockdownClient, verbose, timeout):
203
+ def opened_tabs(
204
+ service_provider: ServiceProviderDep,
205
+ timeout: Annotated[
206
+ float,
207
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
208
+ ] = 3.0,
209
+ ) -> None:
111
210
  """
112
211
  Show all currently opened tabs.
113
212
 
114
213
  \b
115
214
  Opt-in:
116
- Settings -> Safari -> Advanced -> Web Inspector
215
+ iOS >= 18: Settings -> Apps -> Safari -> Advanced -> Web Inspector
216
+
217
+ iOS < 18: Settings -> Safari -> Advanced -> Web Inspector
117
218
  """
118
- inspector = WebinspectorService(lockdown=service_provider, loop=asyncio.get_event_loop())
119
- inspector.connect(timeout)
120
- while not inspector.connected_application:
121
- inspector.flush_input()
122
- reload_pages(inspector)
123
- for app_id, app_ in inspector.connected_application.items():
124
- if app_id not in inspector.application_pages:
125
- continue
126
- if verbose:
127
- print(f'{app_.name} id: {app_id}')
128
- else:
129
- print(app_.name)
130
- for page_id, page in inspector.application_pages[app_id].items():
131
- if verbose:
132
- print(f' - {page.web_url} id: {page_id}')
133
- else:
134
- print(f' - {page.web_url}')
135
- inspector.close()
136
-
137
-
138
- @webinspector.command(cls=Command)
139
- @click.argument('url')
140
- @click.option('-t', '--timeout', default=3, show_default=True, type=float)
219
+ asyncio.run(opened_tabs_task(service_provider, timeout), debug=True)
220
+
221
+
222
+ @catch_errors
223
+ async def launch_task(service_provider: LockdownServiceProvider, url, timeout) -> None:
224
+ inspector, safari = await create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
225
+ session = await inspector.automation_session(safari)
226
+ driver = WebDriver(session)
227
+ print("Starting session")
228
+ await driver.start_session()
229
+ print("Getting URL")
230
+ await driver.get(url)
231
+ OSUTILS.wait_return()
232
+ await session.stop_session()
233
+ await inspector.close()
234
+
235
+
236
+ @cli.command()
141
237
  @catch_errors
142
- def launch(service_provider: LockdownClient, url, timeout):
238
+ def launch(
239
+ service_provider: ServiceProviderDep,
240
+ url: str,
241
+ timeout: Annotated[
242
+ float,
243
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
244
+ ] = 3.0,
245
+ ) -> None:
143
246
  """
144
247
  Launch a specific URL in Safari.
145
248
 
146
249
  \b
147
- Opt-in:
250
+ Opt-in (iOS >= 18):
251
+ Settings -> Apps -> Safari -> Advanced -> Web Inspector
252
+ Settings -> Apps -> Safari -> Advanced -> Remote Automation
253
+
254
+ Opt-in (iOS < 18):
148
255
  Settings -> Safari -> Advanced -> Web Inspector
149
256
  Settings -> Safari -> Advanced -> Remote Automation
257
+
150
258
  """
151
- inspector, safari = create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
152
- session = inspector.automation_session(safari)
153
- driver = WebDriver(session)
154
- print('Starting session')
155
- driver.start_session()
156
- print('Getting URL')
157
- driver.get(url)
158
- OSUTILS.wait_return()
159
- session.stop_session()
160
- inspector.close()
259
+ asyncio.run(launch_task(service_provider, url, timeout), debug=True)
161
260
 
162
261
 
163
- SHELL_USAGE = '''
262
+ SHELL_USAGE = """
164
263
  # This shell allows you to control the web with selenium like API.
165
264
  # The first thing you should do is creating a session:
166
265
  driver.start_session()
@@ -178,61 +277,90 @@ driver.add_cookie(
178
277
  )
179
278
 
180
279
  # See selenium api for more features.
181
- '''
280
+ """
182
281
 
183
282
 
184
- @webinspector.command(cls=Command)
185
- @click.option('-t', '--timeout', default=3, show_default=True, type=float)
186
283
  @catch_errors
187
- def shell(service_provider: LockdownClient, timeout):
284
+ async def shell_task(service_provider: LockdownServiceProvider, timeout: float) -> None:
285
+ inspector, safari = await create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
286
+ session = await inspector.automation_session(safari)
287
+ driver = WebDriver(session)
288
+ try:
289
+ nest_asyncio.apply()
290
+ IPython.embed(
291
+ header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style="native")),
292
+ user_ns={
293
+ "driver": driver,
294
+ "Cookie": Cookie,
295
+ "By": By,
296
+ },
297
+ )
298
+ finally:
299
+ await session.stop_session()
300
+ await inspector.close()
301
+
302
+
303
+ @cli.command()
304
+ @catch_errors
305
+ def shell(
306
+ service_provider: ServiceProviderDep,
307
+ timeout: Annotated[
308
+ float,
309
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
310
+ ] = 3.0,
311
+ ) -> None:
188
312
  """
189
313
  Create an IPython shell for interacting with a WebView.
190
314
 
191
315
  \b
192
- Opt-in:
316
+ Opt-in (iOS >= 18):
317
+ Settings -> Apps -> Safari -> Advanced -> Web Inspector
318
+ Settings -> Apps -> Safari -> Advanced -> Remote Automation
319
+
320
+ Opt-in (iOS < 18):
193
321
  Settings -> Safari -> Advanced -> Web Inspector
194
322
  Settings -> Safari -> Advanced -> Remote Automation
195
323
  """
196
- inspector, safari = create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
197
- session = inspector.automation_session(safari)
198
- driver = WebDriver(session)
199
- try:
200
- IPython.embed(
201
- header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style='native')),
202
- user_ns={
203
- 'driver': driver,
204
- 'Cookie': Cookie,
205
- 'By': By,
206
- })
207
- finally:
208
- session.stop_session()
209
- inspector.close()
324
+ asyncio.run(shell_task(service_provider, timeout), debug=True)
210
325
 
211
326
 
212
- @webinspector.command(cls=Command)
213
- @click.option('-t', '--timeout', default=3, show_default=True, type=float)
214
- @click.option('--automation', is_flag=True, help='Use remote automation')
215
- @click.argument('url', required=False, default='')
327
+ @cli.command()
216
328
  @catch_errors
217
- def js_shell(service_provider: LockdownClient, timeout, automation, url):
329
+ def js_shell(
330
+ service_provider: ServiceProviderDep,
331
+ url: str = "",
332
+ timeout: Annotated[
333
+ float,
334
+ typer.Option("--timeout", "-t", help="Seconds to wait for WebInspector to respond."),
335
+ ] = 3.0,
336
+ automation: Annotated[
337
+ bool,
338
+ typer.Option(help="Use remote automation (requires Remote Automation toggle)."),
339
+ ] = False,
340
+ open_safari: Annotated[
341
+ bool,
342
+ typer.Option(help="Use an existing WebView; skip auto-opening Safari."),
343
+ ] = False,
344
+ ) -> None:
218
345
  """
219
346
  Create a javascript shell. This interpreter runs on your local machine,
220
347
  but evaluates each expression on the remote
221
348
 
222
349
  \b
223
350
  Opt-in:
224
- Settings -> Safari -> Advanced -> Web Inspector
225
-
351
+ iOS >= 18: Settings -> Apps -> Safari -> Advanced -> Web Inspector
352
+ iOS < 18: Settings -> Safari -> Advanced -> Web Inspector
226
353
  \b
227
354
  for automation also enable:
228
- Settings -> Safari -> Advanced -> Remote Automation
355
+ iOS >= 18: Settings -> Apps -> Safari -> Advanced -> Remote Automation
356
+ iOS < 18: Settings -> Safari -> Advanced -> Remote Automation
229
357
  """
230
358
 
231
359
  js_shell_class = AutomationJsShell if automation else InspectorJsShell
232
- asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url))
360
+ asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url, open_safari))
233
361
 
234
362
 
235
- udid = ''
363
+ udid = ""
236
364
 
237
365
 
238
366
  def create_app():
@@ -241,10 +369,8 @@ def create_app():
241
369
  return app
242
370
 
243
371
 
244
- @webinspector.command(cls=Command)
245
- @click.option('--host', default='127.0.0.1')
246
- @click.option('--port', type=click.INT, default=9222)
247
- def cdp(service_provider: LockdownClient, host, port):
372
+ @cli.command()
373
+ def cdp(service_provider: ServiceProviderDep, host: str = "127.0.0.1", port: int = 9222) -> None:
248
374
  """
249
375
  Start a CDP server for debugging WebViews.
250
376
 
@@ -254,71 +380,93 @@ def cdp(service_provider: LockdownClient, host, port):
254
380
  """
255
381
  global udid
256
382
  udid = service_provider.udid
257
- uvicorn.run('pymobiledevice3.cli.webinspector:create_app', host=host, port=port, factory=True,
258
- ws_ping_timeout=None, ws='wsproto', loop='asyncio')
259
-
260
-
261
- def get_js_completions(jsshell: 'JsShell', obj: str, prefix: str) -> list[Completion]:
383
+ uvicorn.run(
384
+ f"{__name__}:{create_app.__name__}",
385
+ host=host,
386
+ port=port,
387
+ factory=True,
388
+ ws_ping_timeout=None,
389
+ ws="wsproto",
390
+ loop="asyncio",
391
+ )
392
+
393
+
394
+ async def get_js_completions(jsshell: "JsShell", obj: str, prefix: str) -> AsyncIterator[Completion]:
262
395
  if obj in JS_RESERVED_WORDS:
263
- return []
396
+ return
264
397
 
265
- completions = []
266
398
  try:
267
- for key in asyncio.get_running_loop().run_until_complete(
268
- jsshell.evaluate_expression(SCRIPT.format(object=obj), return_by_value=True)):
399
+ for key in await jsshell.evaluate_expression(SCRIPT.substitute(object=obj), return_by_value=True):
269
400
  if not key.startswith(prefix):
270
401
  continue
271
- completions.append(Completion(key.removeprefix(prefix), display=key))
272
- except Exception:
402
+ yield Completion(key.removeprefix(prefix), display=key)
403
+ except (Exception, CancelledError):
273
404
  # ignore every possible exception
274
405
  pass
275
- return completions
276
406
 
277
407
 
278
408
  class JsShellCompleter(Completer):
279
- def __init__(self, jsshell: 'JsShell'):
280
- self.jsshell = jsshell
409
+ def __init__(self, jsshell: "JsShell") -> None:
410
+ self.jsshell: JsShell = jsshell
411
+
412
+ async def get_completions_async(
413
+ self,
414
+ document: Document,
415
+ complete_event: CompleteEvent,
416
+ ) -> AsyncIterator[Completion]:
417
+ # Build the JS expression we want to inspect
418
+ text = f"globalThis.{document.text_before_cursor}"
419
+
420
+ # Extract identifiers / dotted paths
421
+ matches = re.findall(r"[a-zA-Z_][a-zA-Z_0-9.]+", text)
422
+ if not matches:
423
+ # async *generator*: just end, don't return a list
424
+ return
281
425
 
282
- def get_completions(
283
- self, document: Document, complete_event: CompleteEvent
284
- ) -> Iterable[Completion]:
285
- text = f'globalThis.{document.text_before_cursor}'
286
- text = re.findall('[a-zA-Z_][a-zA-Z_0-9.]+', text)
287
- if len(text) == 0:
288
- return []
289
- text = text[-1]
290
- if '.' in text:
291
- js_obj, prefix = text.rsplit('.', 1)
426
+ text = matches[-1]
427
+ if "." in text:
428
+ js_obj, prefix = text.rsplit(".", 1)
292
429
  else:
293
430
  js_obj = text
294
- prefix = ''
431
+ prefix = ""
295
432
 
296
- return get_js_completions(self.jsshell, js_obj, prefix)
433
+ # This should return an iterable of Completion (or something we can wrap)
434
+ async for completion in get_js_completions(self.jsshell, js_obj, prefix):
435
+ yield completion
436
+
437
+ # Optional: keep sync completions empty so PTK knows we prefer async
438
+ def get_completions(
439
+ self,
440
+ document: Document,
441
+ complete_event: CompleteEvent,
442
+ ) -> Iterable[Completion]:
443
+ return []
297
444
 
298
445
 
299
446
  class JsShell(ABC):
300
- def __init__(self):
447
+ def __init__(self) -> None:
301
448
  super().__init__()
302
- self.prompt_session = PromptSession(lexer=PygmentsLexer(lexers.JavascriptLexer),
303
- auto_suggest=AutoSuggestFromHistory(),
304
- style=style_from_pygments_cls(get_style_by_name('stata-dark')),
305
- history=FileHistory(self.webinspector_history_path()),
306
- completer=JsShellCompleter(self))
449
+ self.prompt_session: PromptSession = PromptSession(
450
+ lexer=PygmentsLexer(lexers.JavascriptLexer),
451
+ auto_suggest=AutoSuggestFromHistory(),
452
+ style=style_from_pygments_cls(get_style_by_name("stata-dark")),
453
+ history=FileHistory(self.webinspector_history_path()),
454
+ completer=JsShellCompleter(self),
455
+ )
307
456
 
308
457
  @classmethod
309
458
  @abstractmethod
310
- def create(cls, lockdown: LockdownClient, timeout: float, app: str):
311
- pass
459
+ def create(
460
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
461
+ ) -> "AbstractAsyncContextManager[JsShell]": ...
312
462
 
313
463
  @abstractmethod
314
- async def evaluate_expression(self, exp, return_by_value: bool = False):
315
- pass
464
+ async def evaluate_expression(self, exp, return_by_value: bool = False) -> Any: ...
316
465
 
317
466
  @abstractmethod
318
- async def navigate(self, url: str):
319
- pass
467
+ async def navigate(self, url: str) -> None: ...
320
468
 
321
- async def js_iter(self):
469
+ async def js_iter(self) -> None:
322
470
  with patch_stdout(True):
323
471
  exp = await self.prompt_session.prompt_async(HTML('<style fg="cyan"><b>&gt;</b></style> '))
324
472
 
@@ -326,19 +474,18 @@ class JsShell(ABC):
326
474
  return
327
475
 
328
476
  result = await self.evaluate_expression(exp)
329
- colorful_result = highlight(f'{result}', lexers.JavascriptLexer(),
330
- formatters.Terminal256Formatter(style='stata-dark'))
331
- print(colorful_result, end='')
477
+ colorful_result = highlight(
478
+ f"{result}", lexers.JavascriptLexer(), formatters.Terminal256Formatter(style="stata-dark")
479
+ )
480
+ print(colorful_result, end="")
332
481
 
333
- async def start(self, url: str = ''):
482
+ async def start(self, url: str = "") -> None:
334
483
  if url:
335
484
  await self.navigate(url)
336
485
  while True:
337
486
  try:
338
487
  await self.js_iter()
339
- except WirError as e:
340
- logger.error(e)
341
- except InspectorEvaluateError as e:
488
+ except (WirError, InspectorEvaluateError) as e:
342
489
  logger.error(e)
343
490
  except KeyboardInterrupt: # KeyboardInterrupt Control-C
344
491
  pass
@@ -347,76 +494,91 @@ class JsShell(ABC):
347
494
 
348
495
  @staticmethod
349
496
  def webinspector_history_path() -> str:
350
- return str(get_home_folder() / 'webinspector_history')
497
+ return str(get_home_folder() / "webinspector_history")
351
498
 
352
499
 
353
500
  class AutomationJsShell(JsShell):
354
- def __init__(self, driver: WebDriver):
501
+ def __init__(self, driver: WebDriver) -> None:
355
502
  super().__init__()
356
- self.driver = driver
503
+ self.driver: WebDriver = driver
357
504
 
358
505
  @classmethod
359
506
  @asynccontextmanager
360
- async def create(cls, lockdown: LockdownClient, timeout: float, app: str) -> 'AutomationJsShell':
361
- inspector, application = create_webinspector_and_launch_app(lockdown, timeout, app)
362
- automation_session = inspector.automation_session(application)
507
+ async def create(
508
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
509
+ ) -> "AsyncIterator[AutomationJsShell]":
510
+ inspector, application = await create_webinspector_and_launch_app(lockdown, timeout, SAFARI)
511
+ automation_session = await inspector.automation_session(application)
363
512
  driver = WebDriver(automation_session)
364
- driver.start_session()
513
+ await driver.start_session()
365
514
  try:
366
515
  yield cls(driver)
367
516
  finally:
368
- automation_session.stop_session()
369
- inspector.close()
517
+ await automation_session.stop_session()
518
+ await inspector.close()
370
519
 
371
- async def evaluate_expression(self, exp: str, return_by_value: bool = False):
372
- return self.driver.execute_script(f'return {exp}')
520
+ async def evaluate_expression(self, exp: str, return_by_value: bool = False) -> Any:
521
+ return await self.driver.execute_script(f"return {exp}")
373
522
 
374
- async def navigate(self, url: str):
375
- self.driver.get(url)
523
+ async def navigate(self, url: str) -> None:
524
+ await self.driver.get(url)
376
525
 
377
526
 
378
527
  class InspectorJsShell(JsShell):
379
- def __init__(self, inspector_session: InspectorSession):
528
+ def __init__(self, inspector_session: InspectorSession) -> None:
380
529
  super().__init__()
381
- self.inspector_session = inspector_session
530
+ self.inspector_session: InspectorSession = inspector_session
382
531
 
383
532
  @classmethod
384
533
  @asynccontextmanager
385
- async def create(cls, lockdown: LockdownClient, timeout: float, app: str) -> 'InspectorJsShell':
386
- inspector, application = create_webinspector_and_launch_app(lockdown, timeout, app)
387
- page = InspectorJsShell.query_page(inspector)
388
- if page is None:
389
- raise click.exceptions.Exit()
390
-
391
- inspector_session = await inspector.inspector_session(application, page)
534
+ async def create(
535
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
536
+ ) -> "AsyncIterator[InspectorJsShell]":
537
+ inspector = WebinspectorService(lockdown=lockdown)
538
+ await inspector.connect(timeout)
539
+ if open_safari:
540
+ _ = await inspector.open_app(SAFARI)
541
+ application_page = await cls.query_page(inspector, bundle_identifier=SAFARI if open_safari else None)
542
+ if application_page is None:
543
+ raise typer.Exit()
544
+
545
+ inspector_session = await inspector.inspector_session(application_page.application, application_page.page)
392
546
  await inspector_session.console_enable()
393
547
  await inspector_session.runtime_enable()
394
548
 
395
549
  try:
396
550
  yield cls(inspector_session)
397
551
  finally:
398
- inspector.close()
552
+ await inspector.close()
399
553
 
400
- async def evaluate_expression(self, exp: str, return_by_value: bool = False):
554
+ async def evaluate_expression(self, exp: str, return_by_value: bool = False) -> Any:
401
555
  return await self.inspector_session.runtime_evaluate(exp, return_by_value=return_by_value)
402
556
 
403
557
  async def navigate(self, url: str):
404
558
  await self.inspector_session.navigate_to_url(url)
405
559
 
406
560
  @staticmethod
407
- def query_page(inspector: WebinspectorService) -> Optional[Page]:
408
- reload_pages(inspector)
409
- available_pages = list(inspector.get_open_pages().get('Safari', []))
561
+ async def query_page(
562
+ inspector: WebinspectorService, bundle_identifier: Optional[str] = None
563
+ ) -> Optional[ApplicationPage]:
564
+ available_pages = await inspector.get_open_application_pages(timeout=1)
565
+ if bundle_identifier is not None:
566
+ available_pages = [
567
+ application_page
568
+ for application_page in available_pages
569
+ if application_page.application.bundle == bundle_identifier
570
+ ]
410
571
  if not available_pages:
411
- logger.error('Unable to find available pages (try to unlock device)')
412
- return
572
+ logger.error("Unable to find available pages (try to unlock device)")
573
+ return None
413
574
 
414
- page_query = [inquirer3.List('page', message='choose page', choices=available_pages, carousel=True)]
415
- page = inquirer3.prompt(page_query, theme=GreenPassion(), raise_keyboard_interrupt=True)['page']
575
+ page_query = [inquirer3.List("page", message="choose page", choices=available_pages, carousel=True)]
576
+ page = inquirer3.prompt(page_query, theme=GreenPassion(), raise_keyboard_interrupt=True)["page"]
416
577
  return page
417
578
 
418
579
 
419
- async def run_js_shell(js_shell_class: type[JsShell], lockdown: LockdownClient,
420
- timeout: float, url: str):
421
- async with js_shell_class.create(lockdown, timeout, SAFARI) as js_shell_instance:
580
+ async def run_js_shell(
581
+ js_shell_class: type[JsShell], lockdown: LockdownServiceProvider, timeout: float, url: str, open_safari: bool
582
+ ) -> None:
583
+ async with js_shell_class.create(lockdown, timeout, open_safari) as js_shell_instance:
422
584
  await js_shell_instance.start(url)