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
@@ -1,28 +1,34 @@
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
33
  from pymobiledevice3.exceptions import (
28
34
  InspectorEvaluateError,
@@ -31,35 +37,34 @@ from pymobiledevice3.exceptions import (
31
37
  WebInspectorNotEnabledError,
32
38
  WirError,
33
39
  )
34
- from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
40
+ from pymobiledevice3.lockdown import create_using_usbmux
35
41
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
36
42
  from pymobiledevice3.osu.os_utils import get_os_utils
37
43
  from pymobiledevice3.services.web_protocol.cdp_server import app
38
44
  from pymobiledevice3.services.web_protocol.driver import By, Cookie, WebDriver
39
45
  from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
40
- from pymobiledevice3.services.webinspector import SAFARI, ApplicationPage, WebinspectorService
41
-
42
- SCRIPT = """
43
- function inspectedPage_evalResult_getCompletions(primitiveType) {{
44
- var resultSet={{}};
45
- var object = primitiveType;
46
- for(var o=object;o;o=o.__proto__) {{
47
-
48
- try{{
49
- var names=Object.getOwnPropertyNames(o);
50
- for(var i=0;i<names.length;++i)
51
- resultSet[names[i]]=true;
52
- }} catch(e){{}}
53
- }}
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
+ }
54
59
  return resultSet;
55
- }}
60
+ }
56
61
 
57
- try {{
58
- inspectedPage_evalResult_getCompletions({object})
59
- }} catch (e) {{}}
60
- """
62
+ try {
63
+ inspectedPage_evalResult_getCompletions(${object})
64
+ } catch (e) {}
65
+ """)
61
66
 
62
- JS_RESERVED_WORDS = [
67
+ JS_RESERVED_WORDS = frozenset({
63
68
  "abstract",
64
69
  "arguments",
65
70
  "await",
@@ -124,54 +129,84 @@ JS_RESERVED_WORDS = [
124
129
  "while",
125
130
  "with",
126
131
  "yield",
127
- ]
132
+ })
128
133
 
129
134
  OSUTILS = get_os_utils()
130
135
  logger = logging.getLogger(__name__)
131
136
 
132
137
 
133
- @click.group()
134
- def cli() -> None:
135
- 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
+ )
136
146
 
137
147
 
138
- @cli.group()
139
- def webinspector() -> None:
140
- """Access webinspector services"""
141
- 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
+ }
142
154
 
155
+ def handle_error(e):
156
+ logger.error(next(msg for exc, msg in errors.items() if isinstance(e, exc)))
143
157
 
144
- def catch_errors(func):
145
- def catch_function(*args, **kwargs):
146
- try:
147
- return func(*args, **kwargs)
148
- except LaunchingApplicationError:
149
- logger.error("Unable to launch application (try to unlock device)")
150
- except WebInspectorNotEnabledError:
151
- logger.error("Web inspector is not enable")
152
- except RemoteAutomationNotEnabledError:
153
- 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)
154
173
 
155
174
  return update_wrapper(catch_function, func)
156
175
 
157
176
 
158
- def reload_pages(inspector: WebinspectorService):
159
- inspector.get_open_pages()
177
+ async def reload_pages(inspector: WebinspectorService) -> None:
178
+ await inspector.get_open_pages()
160
179
  # Best effort.
161
- inspector.flush_input(2)
180
+ await inspector.flush_input(2)
162
181
 
163
182
 
164
- 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]:
165
186
  inspector = WebinspectorService(lockdown=lockdown)
166
- inspector.connect(timeout)
167
- application = inspector.open_app(app)
187
+ await inspector.connect(timeout)
188
+ application = await inspector.open_app(app)
168
189
  return inspector, application
169
190
 
170
191
 
171
- @webinspector.command(cls=Command)
172
- @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()
173
202
  @catch_errors
174
- def opened_tabs(service_provider: LockdownClient, 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:
175
210
  """
176
211
  Show all currently opened tabs.
177
212
 
@@ -181,19 +216,33 @@ def opened_tabs(service_provider: LockdownClient, timeout):
181
216
 
182
217
  iOS < 18: Settings -> Safari -> Advanced -> Web Inspector
183
218
  """
184
- inspector = WebinspectorService(lockdown=service_provider)
185
- inspector.connect(timeout)
186
- application_pages = inspector.get_open_application_pages(timeout=timeout)
187
- for application_page in application_pages:
188
- print(application_page)
189
- inspector.close()
219
+ asyncio.run(opened_tabs_task(service_provider, timeout), debug=True)
190
220
 
191
221
 
192
- @webinspector.command(cls=Command)
193
- @click.argument("url")
194
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
195
222
  @catch_errors
196
- def launch(service_provider: LockdownClient, url, timeout):
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()
237
+ @catch_errors
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:
197
246
  """
198
247
  Launch a specific URL in Safari.
199
248
 
@@ -207,16 +256,7 @@ def launch(service_provider: LockdownClient, url, timeout):
207
256
  Settings -> Safari -> Advanced -> Remote Automation
208
257
 
209
258
  """
210
- inspector, safari = create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
211
- session = inspector.automation_session(safari)
212
- driver = WebDriver(session)
213
- print("Starting session")
214
- driver.start_session()
215
- print("Getting URL")
216
- driver.get(url)
217
- OSUTILS.wait_return()
218
- session.stop_session()
219
- inspector.close()
259
+ asyncio.run(launch_task(service_provider, url, timeout), debug=True)
220
260
 
221
261
 
222
262
  SHELL_USAGE = """
@@ -240,10 +280,35 @@ driver.add_cookie(
240
280
  """
241
281
 
242
282
 
243
- @webinspector.command(cls=Command)
244
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
245
283
  @catch_errors
246
- 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:
247
312
  """
248
313
  Create an IPython shell for interacting with a WebView.
249
314
 
@@ -256,31 +321,26 @@ def shell(service_provider: LockdownClient, timeout):
256
321
  Settings -> Safari -> Advanced -> Web Inspector
257
322
  Settings -> Safari -> Advanced -> Remote Automation
258
323
  """
259
- inspector, safari = create_webinspector_and_launch_app(service_provider, timeout, SAFARI)
260
- session = inspector.automation_session(safari)
261
- driver = WebDriver(session)
262
- try:
263
- IPython.embed(
264
- header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style="native")),
265
- user_ns={
266
- "driver": driver,
267
- "Cookie": Cookie,
268
- "By": By,
269
- },
270
- )
271
- finally:
272
- session.stop_session()
273
- inspector.close()
324
+ asyncio.run(shell_task(service_provider, timeout), debug=True)
274
325
 
275
326
 
276
- @webinspector.command(cls=Command)
277
- @click.option("-t", "--timeout", default=3, show_default=True, type=float)
278
- @click.option("--automation", is_flag=True, help="Use remote automation")
279
- @click.option("--no-open-safari", is_flag=True, help="Avoid opening the Safari app")
280
- @click.argument("url", required=False, default="")
327
+ @cli.command()
281
328
  @catch_errors
282
329
  def js_shell(
283
- service_provider: LockdownServiceProvider, timeout: float, automation: bool, no_open_safari: bool, url: str
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,
284
344
  ) -> None:
285
345
  """
286
346
  Create a javascript shell. This interpreter runs on your local machine,
@@ -297,7 +357,7 @@ def js_shell(
297
357
  """
298
358
 
299
359
  js_shell_class = AutomationJsShell if automation else InspectorJsShell
300
- asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url, not no_open_safari))
360
+ asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url, open_safari))
301
361
 
302
362
 
303
363
  udid = ""
@@ -309,10 +369,8 @@ def create_app():
309
369
  return app
310
370
 
311
371
 
312
- @webinspector.command(cls=Command)
313
- @click.option("--host", default="127.0.0.1")
314
- @click.option("--port", type=click.INT, default=9222)
315
- 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:
316
374
  """
317
375
  Start a CDP server for debugging WebViews.
318
376
 
@@ -323,7 +381,7 @@ def cdp(service_provider: LockdownClient, host, port):
323
381
  global udid
324
382
  udid = service_provider.udid
325
383
  uvicorn.run(
326
- "pymobiledevice3.cli.webinspector:create_app",
384
+ f"{__name__}:{create_app.__name__}",
327
385
  host=host,
328
386
  port=port,
329
387
  factory=True,
@@ -333,47 +391,62 @@ def cdp(service_provider: LockdownClient, host, port):
333
391
  )
334
392
 
335
393
 
336
- def get_js_completions(jsshell: "JsShell", obj: str, prefix: str) -> list[Completion]:
394
+ async def get_js_completions(jsshell: "JsShell", obj: str, prefix: str) -> AsyncIterator[Completion]:
337
395
  if obj in JS_RESERVED_WORDS:
338
- return []
396
+ return
339
397
 
340
- completions = []
341
398
  try:
342
- for key in asyncio.get_running_loop().run_until_complete(
343
- jsshell.evaluate_expression(SCRIPT.format(object=obj), return_by_value=True)
344
- ):
399
+ for key in await jsshell.evaluate_expression(SCRIPT.substitute(object=obj), return_by_value=True):
345
400
  if not key.startswith(prefix):
346
401
  continue
347
- completions.append(Completion(key.removeprefix(prefix), display=key))
348
- except Exception:
402
+ yield Completion(key.removeprefix(prefix), display=key)
403
+ except (Exception, CancelledError):
349
404
  # ignore every possible exception
350
405
  pass
351
- return completions
352
406
 
353
407
 
354
408
  class JsShellCompleter(Completer):
355
- def __init__(self, jsshell: "JsShell"):
356
- self.jsshell = jsshell
357
-
358
- def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]:
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
359
418
  text = f"globalThis.{document.text_before_cursor}"
360
- text = re.findall("[a-zA-Z_][a-zA-Z_0-9.]+", text)
361
- if len(text) == 0:
362
- return []
363
- text = text[-1]
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
425
+
426
+ text = matches[-1]
364
427
  if "." in text:
365
428
  js_obj, prefix = text.rsplit(".", 1)
366
429
  else:
367
430
  js_obj = text
368
431
  prefix = ""
369
432
 
370
- 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 []
371
444
 
372
445
 
373
446
  class JsShell(ABC):
374
447
  def __init__(self) -> None:
375
448
  super().__init__()
376
- self.prompt_session = PromptSession(
449
+ self.prompt_session: PromptSession = PromptSession(
377
450
  lexer=PygmentsLexer(lexers.JavascriptLexer),
378
451
  auto_suggest=AutoSuggestFromHistory(),
379
452
  style=style_from_pygments_cls(get_style_by_name("stata-dark")),
@@ -383,18 +456,17 @@ class JsShell(ABC):
383
456
 
384
457
  @classmethod
385
458
  @abstractmethod
386
- def create(cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool) -> None:
387
- pass
459
+ def create(
460
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
461
+ ) -> "AbstractAsyncContextManager[JsShell]": ...
388
462
 
389
463
  @abstractmethod
390
- async def evaluate_expression(self, exp, return_by_value: bool = False):
391
- pass
464
+ async def evaluate_expression(self, exp, return_by_value: bool = False) -> Any: ...
392
465
 
393
466
  @abstractmethod
394
- async def navigate(self, url: str):
395
- pass
467
+ async def navigate(self, url: str) -> None: ...
396
468
 
397
- async def js_iter(self):
469
+ async def js_iter(self) -> None:
398
470
  with patch_stdout(True):
399
471
  exp = await self.prompt_session.prompt_async(HTML('<style fg="cyan"><b>&gt;</b></style> '))
400
472
 
@@ -407,14 +479,14 @@ class JsShell(ABC):
407
479
  )
408
480
  print(colorful_result, end="")
409
481
 
410
- async def start(self, url: str = ""):
482
+ async def start(self, url: str = "") -> None:
411
483
  if url:
412
484
  await self.navigate(url)
413
485
  while True:
414
486
  try:
415
487
  await self.js_iter()
416
- except (WirError, InspectorEvaluateError):
417
- logger.error("Failed in js_iter")
488
+ except (WirError, InspectorEvaluateError) as e:
489
+ logger.error(e)
418
490
  except KeyboardInterrupt: # KeyboardInterrupt Control-C
419
491
  pass
420
492
  except EOFError: # Control-D
@@ -426,45 +498,49 @@ class JsShell(ABC):
426
498
 
427
499
 
428
500
  class AutomationJsShell(JsShell):
429
- def __init__(self, driver: WebDriver):
501
+ def __init__(self, driver: WebDriver) -> None:
430
502
  super().__init__()
431
- self.driver = driver
503
+ self.driver: WebDriver = driver
432
504
 
433
505
  @classmethod
434
506
  @asynccontextmanager
435
- async def create(cls, lockdown: LockdownClient, timeout: float, open_safari: bool) -> "AutomationJsShell":
436
- inspector, application = create_webinspector_and_launch_app(lockdown, timeout, SAFARI)
437
- 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)
438
512
  driver = WebDriver(automation_session)
439
- driver.start_session()
513
+ await driver.start_session()
440
514
  try:
441
515
  yield cls(driver)
442
516
  finally:
443
- automation_session.stop_session()
444
- inspector.close()
517
+ await automation_session.stop_session()
518
+ await inspector.close()
445
519
 
446
- async def evaluate_expression(self, exp: str, return_by_value: bool = False):
447
- 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}")
448
522
 
449
- async def navigate(self, url: str):
450
- self.driver.get(url)
523
+ async def navigate(self, url: str) -> None:
524
+ await self.driver.get(url)
451
525
 
452
526
 
453
527
  class InspectorJsShell(JsShell):
454
- def __init__(self, inspector_session: InspectorSession):
528
+ def __init__(self, inspector_session: InspectorSession) -> None:
455
529
  super().__init__()
456
- self.inspector_session = inspector_session
530
+ self.inspector_session: InspectorSession = inspector_session
457
531
 
458
532
  @classmethod
459
533
  @asynccontextmanager
460
- async def create(cls, lockdown: LockdownClient, timeout: float, open_safari: bool) -> "InspectorJsShell":
534
+ async def create(
535
+ cls, lockdown: LockdownServiceProvider, timeout: float, open_safari: bool
536
+ ) -> "AsyncIterator[InspectorJsShell]":
461
537
  inspector = WebinspectorService(lockdown=lockdown)
462
- inspector.connect(timeout)
538
+ await inspector.connect(timeout)
463
539
  if open_safari:
464
- _ = inspector.open_app(SAFARI)
465
- application_page = cls.query_page(inspector, bundle_identifier=SAFARI if open_safari else None)
540
+ _ = await inspector.open_app(SAFARI)
541
+ application_page = await cls.query_page(inspector, bundle_identifier=SAFARI if open_safari else None)
466
542
  if application_page is None:
467
- raise click.exceptions.Exit()
543
+ raise typer.Exit()
468
544
 
469
545
  inspector_session = await inspector.inspector_session(application_page.application, application_page.page)
470
546
  await inspector_session.console_enable()
@@ -473,19 +549,19 @@ class InspectorJsShell(JsShell):
473
549
  try:
474
550
  yield cls(inspector_session)
475
551
  finally:
476
- inspector.close()
552
+ await inspector.close()
477
553
 
478
- 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:
479
555
  return await self.inspector_session.runtime_evaluate(exp, return_by_value=return_by_value)
480
556
 
481
557
  async def navigate(self, url: str):
482
558
  await self.inspector_session.navigate_to_url(url)
483
559
 
484
560
  @staticmethod
485
- def query_page(
561
+ async def query_page(
486
562
  inspector: WebinspectorService, bundle_identifier: Optional[str] = None
487
563
  ) -> Optional[ApplicationPage]:
488
- available_pages = inspector.get_open_application_pages(timeout=1)
564
+ available_pages = await inspector.get_open_application_pages(timeout=1)
489
565
  if bundle_identifier is not None:
490
566
  available_pages = [
491
567
  application_page
@@ -107,7 +107,10 @@ class PasswordRequiredError(PairingError):
107
107
 
108
108
 
109
109
  class StartServiceError(PyMobileDevice3Exception):
110
- pass
110
+ def __init__(self, service_name: str, message: str) -> None:
111
+ super().__init__()
112
+ self.service_name = service_name
113
+ self.message = message
111
114
 
112
115
 
113
116
  class FatalPairingError(PyMobileDevice3Exception):
@@ -163,9 +166,10 @@ class ArgumentError(PyMobileDevice3Exception):
163
166
 
164
167
 
165
168
  class AfcException(PyMobileDevice3Exception, OSError):
166
- def __init__(self, message, status):
169
+ def __init__(self, message: str, status: str, filename: Optional[str] = None) -> None:
167
170
  OSError.__init__(self, status, message)
168
171
  self.status = status
172
+ self.filename = filename
169
173
 
170
174
 
171
175
  class AfcFileNotFoundError(AfcException):
@@ -221,6 +221,10 @@ class LockdownClient(ABC, LockdownServiceProvider):
221
221
  def product_version(self) -> str:
222
222
  return self.all_values.get("ProductVersion") or "1.0"
223
223
 
224
+ @property
225
+ def product_build_version(self) -> str:
226
+ return self.all_values.get("BuildVersion")
227
+
224
228
  @property
225
229
  def device_class(self) -> DeviceClass:
226
230
  try:
@@ -576,7 +580,7 @@ class LockdownClient(ABC, LockdownServiceProvider):
576
580
  raise PasswordRequiredError(
577
581
  "your device is protected with password, please enter password in device and try again"
578
582
  )
579
- raise StartServiceError(response.get("Error"))
583
+ raise StartServiceError(name, response.get("Error"))
580
584
  return response
581
585
 
582
586
  @_reconnect_on_remote_close
@@ -17,6 +17,11 @@ class LockdownServiceProvider:
17
17
  def product_version(self) -> str:
18
18
  pass
19
19
 
20
+ @property
21
+ @abstractmethod
22
+ def product_build_version(self) -> str:
23
+ pass
24
+
20
25
  @property
21
26
  @abstractmethod
22
27
  def ecid(self) -> int: