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
@@ -2,38 +2,41 @@ import logging
2
2
  import os
3
3
  import posixpath
4
4
  import re
5
- from typing import Optional, TextIO
6
-
7
- import click
8
-
9
- from pymobiledevice3.cli.cli_common import Command, get_last_used_terminal_formatting, user_requested_colored_output
10
- from pymobiledevice3.lockdown import LockdownClient
5
+ from contextlib import nullcontext
6
+ from pathlib import Path
7
+ from typing import Annotated, Optional, TextIO
8
+
9
+ import typer
10
+ from typer_injector import InjectingTyper
11
+
12
+ from pymobiledevice3.cli.cli_common import (
13
+ ServiceProviderDep,
14
+ get_last_used_terminal_formatting,
15
+ user_requested_colored_output,
16
+ )
11
17
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
12
- from pymobiledevice3.services.os_trace import OsTraceService, SyslogLogLevel
18
+ from pymobiledevice3.services.os_trace import OsTraceService, SyslogEntry, SyslogLogLevel
13
19
  from pymobiledevice3.services.syslog import SyslogService
14
20
 
15
21
  logger = logging.getLogger(__name__)
16
22
 
17
-
18
- @click.group()
19
- def cli() -> None:
20
- pass
21
-
22
-
23
- @cli.group()
24
- def syslog() -> None:
25
- """Watch syslog messages"""
26
- pass
23
+ cli = InjectingTyper(
24
+ name="syslog",
25
+ help="Watch syslog messages",
26
+ no_args_is_help=True,
27
+ )
27
28
 
28
29
 
29
- @syslog.command("live-old", cls=Command)
30
- def syslog_live_old(service_provider: LockdownClient):
30
+ @cli.command("live-old")
31
+ def syslog_live_old(service_provider: ServiceProviderDep) -> None:
31
32
  """view live syslog lines in raw bytes form from old relay"""
32
33
  for line in SyslogService(service_provider=service_provider).watch():
33
34
  print(line)
34
35
 
35
36
 
36
- def format_line(color, pid, syslog_entry, include_label):
37
+ def format_line(
38
+ color: bool, pid: int, syslog_entry: SyslogEntry, include_label: bool, image_offset: bool = False
39
+ ) -> Optional[str]:
37
40
  log_level_colors = {
38
41
  SyslogLogLevel.NOTICE.name: "white",
39
42
  SyslogLogLevel.INFO.name: "white",
@@ -50,6 +53,7 @@ def format_line(color, pid, syslog_entry, include_label):
50
53
  image_name = posixpath.basename(syslog_entry.image_name)
51
54
  message = syslog_entry.message
52
55
  process_name = posixpath.basename(filename)
56
+ image_offset_str = f"+0x{syslog_entry.image_offset:x}" if image_offset and image_name else ""
53
57
  label = ""
54
58
 
55
59
  if (pid != -1) and (syslog_pid != pid):
@@ -59,17 +63,19 @@ def format_line(color, pid, syslog_entry, include_label):
59
63
  label = f"[{syslog_entry.label.subsystem}][{syslog_entry.label.category}]"
60
64
 
61
65
  if color:
62
- timestamp = click.style(str(timestamp), "green")
63
- process_name = click.style(process_name, "magenta")
66
+ timestamp = typer.style(str(timestamp), "green")
67
+ process_name = typer.style(process_name, "magenta")
64
68
  if len(image_name) > 0:
65
- image_name = click.style(image_name, "magenta")
66
- syslog_pid = click.style(syslog_pid, "cyan")
69
+ image_name = typer.style(image_name, "magenta")
70
+ if image_offset:
71
+ image_offset_str = typer.style(image_offset_str, "blue")
72
+ syslog_pid = typer.style(syslog_pid, "cyan")
67
73
  log_level_color = log_level_colors[level]
68
- level = click.style(level, log_level_color)
69
- label = click.style(label, "cyan")
70
- message = click.style(message, log_level_color)
74
+ level = typer.style(level, log_level_color)
75
+ label = typer.style(label, "cyan")
76
+ message = typer.style(message, log_level_color)
71
77
 
72
- line_format = "{timestamp} {process_name}{{{image_name}}}[{pid}] <{level}>: {message}"
78
+ line_format = "{timestamp} {process_name}{{{image_name}{image_offset_str}}}[{pid}] <{level}>: {message}"
73
79
 
74
80
  if include_label:
75
81
  line_format += f" {label}"
@@ -81,6 +87,7 @@ def format_line(color, pid, syslog_entry, include_label):
81
87
  pid=syslog_pid,
82
88
  level=level,
83
89
  message=message,
90
+ image_offset_str=image_offset_str,
84
91
  )
85
92
 
86
93
  return line
@@ -89,28 +96,32 @@ def format_line(color, pid, syslog_entry, include_label):
89
96
  def syslog_live(
90
97
  service_provider: LockdownServiceProvider,
91
98
  out: Optional[TextIO],
92
- pid: Optional[int],
99
+ pid: int,
93
100
  process_name: Optional[str],
94
101
  match: list[str],
95
102
  match_insensitive: list[str],
96
103
  include_label: bool,
97
104
  regex: list[str],
98
105
  insensitive_regex: list[str],
106
+ image_offset: bool = False,
99
107
  ) -> None:
100
108
  match_regex = [re.compile(f".*({r}).*", re.DOTALL) for r in regex]
101
109
  match_regex += [re.compile(f".*({r}).*", re.IGNORECASE | re.DOTALL) for r in insensitive_regex]
102
110
 
103
111
  def replace(m):
104
- if len(m.groups()):
105
- return line.replace(m.group(1), click.style(m.group(1), bold=True, underline=True))
106
- return None
112
+ if len(m.groups()) and line:
113
+ return line.replace(m.group(1), typer.style(m.group(1), bold=True, underline=True))
114
+ return ""
107
115
 
108
116
  for syslog_entry in OsTraceService(lockdown=service_provider).syslog(pid=pid):
109
117
  if process_name and posixpath.basename(syslog_entry.filename) != process_name:
110
118
  continue
111
119
 
112
- line_no_style = format_line(False, pid, syslog_entry, include_label)
113
- line = format_line(user_requested_colored_output(), pid, syslog_entry, include_label)
120
+ line_no_style = format_line(False, pid, syslog_entry, include_label, image_offset)
121
+ line = format_line(user_requested_colored_output(), pid, syslog_entry, include_label, image_offset)
122
+
123
+ if line_no_style is None or line is None:
124
+ continue
114
125
 
115
126
  skip = False
116
127
 
@@ -122,7 +133,7 @@ def syslog_live(
122
133
  break
123
134
  else:
124
135
  if user_requested_colored_output():
125
- match_line = match_line.replace(m, click.style(m, bold=True, underline=True))
136
+ match_line = match_line.replace(m, typer.style(m, bold=True, underline=True))
126
137
  line = match_line
127
138
 
128
139
  if match_insensitive is not None:
@@ -138,7 +149,7 @@ def syslog_live(
138
149
  last_color_formatting = get_last_used_terminal_formatting(line[:start])
139
150
  line = (
140
151
  line[:start]
141
- + click.style(line[start:end], bold=True, underline=True)
152
+ + typer.style(line[start:end], bold=True, underline=True)
142
153
  + last_color_formatting
143
154
  + line[end:]
144
155
  )
@@ -163,39 +174,108 @@ def syslog_live(
163
174
  print(line_no_style, file=out, flush=True)
164
175
 
165
176
 
166
- @syslog.command("live", cls=Command)
167
- @click.option("-o", "--out", type=click.File("wt"), help="log file")
168
- @click.option("--pid", type=click.INT, default=-1, help="pid to filter. -1 for all")
169
- @click.option("-pn", "--process-name", help="process name to filter")
170
- @click.option("-m", "--match", multiple=True, help="match expression")
171
- @click.option("-mi", "--match-insensitive", multiple=True, help="insensitive match expression")
172
- @click.option("include_label", "--label", is_flag=True, help="should include label")
173
- @click.option("-e", "--regex", multiple=True, help="filter only lines matching given regex")
174
- @click.option("-ei", "--insensitive-regex", multiple=True, help="filter only lines matching given regex (insensitive)")
177
+ @cli.command("live")
175
178
  def cli_syslog_live(
176
- service_provider: LockdownServiceProvider,
177
- out: Optional[TextIO],
178
- pid: Optional[int],
179
- process_name: Optional[str],
180
- match: list[str],
181
- match_insensitive: list[str],
182
- include_label: bool,
183
- regex: list[str],
184
- insensitive_regex: list[str],
179
+ service_provider: ServiceProviderDep,
180
+ out: Annotated[
181
+ Optional[Path],
182
+ typer.Option(
183
+ "--out",
184
+ "-o",
185
+ help="log file",
186
+ ),
187
+ ] = None,
188
+ pid: Annotated[
189
+ int,
190
+ typer.Option(help="pid to filter. -1 for all"),
191
+ ] = -1,
192
+ process_name: Annotated[
193
+ Optional[str],
194
+ typer.Option(
195
+ "--process-name",
196
+ "-pn",
197
+ help="process name to filter",
198
+ ),
199
+ ] = None,
200
+ match: Annotated[
201
+ Optional[list[str]],
202
+ typer.Option(
203
+ "--match",
204
+ "-m",
205
+ help="match expression",
206
+ ),
207
+ ] = None,
208
+ match_insensitive: Annotated[
209
+ Optional[list[str]],
210
+ typer.Option(
211
+ "--match-insensitive",
212
+ "-mi",
213
+ help="case-insensitive match expression",
214
+ ),
215
+ ] = None,
216
+ include_label: Annotated[
217
+ bool,
218
+ typer.Option(
219
+ "--label",
220
+ help="should include label",
221
+ ),
222
+ ] = False,
223
+ regex: Annotated[
224
+ Optional[list[str]],
225
+ typer.Option(
226
+ "--regex",
227
+ "-e",
228
+ help="filter only lines matching given regex",
229
+ ),
230
+ ] = None,
231
+ insensitive_regex: Annotated[
232
+ Optional[list[str]],
233
+ typer.Option(
234
+ "--insensitive-regex",
235
+ "-ei",
236
+ help="filter only lines matching given regex (insensitive)",
237
+ ),
238
+ ] = None,
239
+ image_offset: Annotated[
240
+ bool,
241
+ typer.Option(
242
+ "--image-offset",
243
+ "-io",
244
+ help="Include image offset in log line",
245
+ ),
246
+ ] = False,
185
247
  ) -> None:
186
248
  """view live syslog lines"""
187
249
 
188
- syslog_live(
189
- service_provider, out, pid, process_name, match, match_insensitive, include_label, regex, insensitive_regex
190
- )
250
+ with out.open("wt") if out else nullcontext() as out_file:
251
+ syslog_live(
252
+ service_provider,
253
+ out_file,
254
+ pid,
255
+ process_name,
256
+ match or [],
257
+ match_insensitive or [],
258
+ include_label,
259
+ regex or [],
260
+ insensitive_regex or [],
261
+ )
191
262
 
192
263
 
193
- @syslog.command("collect", cls=Command)
194
- @click.argument("out", type=click.Path(exists=False, dir_okay=True, file_okay=False))
195
- @click.option("--size-limit", type=click.INT)
196
- @click.option("--age-limit", type=click.INT)
197
- @click.option("--start-time", type=click.INT)
198
- def syslog_collect(service_provider: LockdownClient, out, size_limit, age_limit, start_time):
264
+ @cli.command("collect")
265
+ def syslog_collect(
266
+ service_provider: ServiceProviderDep,
267
+ out: Annotated[
268
+ Path,
269
+ typer.Argument(
270
+ exists=False,
271
+ dir_okay=True,
272
+ file_okay=False,
273
+ ),
274
+ ],
275
+ size_limit: int,
276
+ age_limit: int,
277
+ start_time: int,
278
+ ) -> None:
199
279
  """
200
280
  Collect the system logs into a .logarchive that can be viewed later with tools such as log or Console.
201
281
  If the filename doesn't exist, system_logs.logarchive will be created in the given directory.
@@ -203,12 +283,12 @@ def syslog_collect(service_provider: LockdownClient, out, size_limit, age_limit,
203
283
  if not os.path.exists(out):
204
284
  os.makedirs(out)
205
285
 
206
- if not out.endswith(".logarchive"):
286
+ if out.suffix != ".logarchive":
207
287
  logger.warning(
208
288
  "given out path doesn't end with a .logarchive - consider renaming to be able to view "
209
289
  "the file with the likes of the Console.app and the `log show` utilities"
210
290
  )
211
291
 
212
292
  OsTraceService(lockdown=service_provider).collect(
213
- out, size_limit=size_limit, age_limit=age_limit, start_time=start_time
293
+ str(out), size_limit=size_limit, age_limit=age_limit, start_time=start_time
214
294
  )
@@ -1,35 +1,53 @@
1
1
  import logging
2
2
  import tempfile
3
+ from typing import Annotated, Optional
3
4
 
4
- import click
5
+ import typer
6
+ from typer_injector import InjectingTyper
5
7
 
6
8
  from pymobiledevice3 import usbmux
7
- from pymobiledevice3.cli.cli_common import USBMUX_OPTION_HELP, BaseCommand, print_json
9
+ from pymobiledevice3.cli.cli_common import USBMUX_OPTION_HELP, print_json
8
10
  from pymobiledevice3.lockdown import create_using_usbmux
9
11
  from pymobiledevice3.tcp_forwarder import UsbmuxTcpForwarder
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
13
15
 
14
- @click.group()
15
- def cli() -> None:
16
- pass
17
-
18
-
19
- @cli.group("usbmux")
20
- def usbmux_cli() -> None:
21
- """List devices or forward a TCP port"""
22
- pass
23
-
24
-
25
- @usbmux_cli.command("forward", cls=BaseCommand)
26
- @click.option("usbmux_address", "--usbmux", help=USBMUX_OPTION_HELP)
27
- @click.argument("src_port", type=click.IntRange(1, 0xFFFF))
28
- @click.argument("dst_port", type=click.IntRange(1, 0xFFFF))
29
- @click.option("--serial", help="device serial number")
30
- @click.option("-d", "--daemonize", is_flag=True)
31
- def usbmux_forward(usbmux_address: str, src_port: int, dst_port: int, serial: str, daemonize: bool):
32
- """forward tcp port"""
16
+ cli = InjectingTyper(
17
+ name="usbmux",
18
+ help="Inspect usbmuxd-connected devices and forward TCP ports to them.",
19
+ no_args_is_help=True,
20
+ )
21
+
22
+
23
+ @cli.command("forward")
24
+ def usbmux_forward(
25
+ src_port: Annotated[
26
+ int,
27
+ typer.Argument(min=1, max=0xFFFF),
28
+ ],
29
+ dst_port: Annotated[
30
+ int,
31
+ typer.Argument(min=1, max=0xFFFF),
32
+ ],
33
+ *,
34
+ usbmux_address: Annotated[
35
+ Optional[str],
36
+ typer.Option(
37
+ "--usbmux",
38
+ help=USBMUX_OPTION_HELP,
39
+ ),
40
+ ] = None,
41
+ serial: Annotated[
42
+ str,
43
+ typer.Option(help="Device serial/UDID to forward traffic to."),
44
+ ],
45
+ daemonize: Annotated[
46
+ bool,
47
+ typer.Option("--daemonize", "-d", help="Run the forwarder in the background."),
48
+ ] = False,
49
+ ) -> None:
50
+ """Forward a local TCP port to the device via usbmuxd."""
33
51
  forwarder = UsbmuxTcpForwarder(serial, dst_port, src_port, usbmux_address=usbmux_address)
34
52
 
35
53
  if daemonize:
@@ -45,12 +63,33 @@ def usbmux_forward(usbmux_address: str, src_port: int, dst_port: int, serial: st
45
63
  forwarder.start()
46
64
 
47
65
 
48
- @usbmux_cli.command("list", cls=BaseCommand)
49
- @click.option("usbmux_address", "--usbmux", help=USBMUX_OPTION_HELP)
50
- @click.option("-u", "--usb", is_flag=True, help="show only usb devices")
51
- @click.option("-n", "--network", is_flag=True, help="show only network devices")
52
- def usbmux_list(usbmux_address: str, usb: bool, network: bool) -> None:
53
- """list connected devices"""
66
+ @cli.command("list")
67
+ def usbmux_list(
68
+ usbmux_address: Annotated[
69
+ Optional[str],
70
+ typer.Option(
71
+ "--usbmux",
72
+ help=USBMUX_OPTION_HELP,
73
+ ),
74
+ ] = None,
75
+ usb: Annotated[
76
+ bool,
77
+ typer.Option(
78
+ "--usb",
79
+ "-u",
80
+ help="show only USB devices",
81
+ ),
82
+ ] = False,
83
+ network: Annotated[
84
+ bool,
85
+ typer.Option(
86
+ "--network",
87
+ "-n",
88
+ help="show only network devices",
89
+ ),
90
+ ] = False,
91
+ ) -> None:
92
+ """List devices known to usbmuxd (USB and Wi-Fi)."""
54
93
  connected_devices = []
55
94
  for device in usbmux.list_devices(usbmux_address=usbmux_address):
56
95
  udid = device.serial
@@ -1,9 +1,6 @@
1
- import click
1
+ from typer import Typer
2
2
 
3
-
4
- @click.group()
5
- def cli() -> None:
6
- pass
3
+ cli = Typer()
7
4
 
8
5
 
9
6
  @cli.command()