pymobiledevice3 5.0.1__py3-none-any.whl → 5.0.2__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.
Potentially problematic release.
This version of pymobiledevice3 might be problematic. Click here for more details.
- misc/plist_sniffer.py +15 -15
- misc/remotexpc_sniffer.py +29 -28
- pymobiledevice3/__main__.py +128 -102
- pymobiledevice3/_version.py +2 -2
- pymobiledevice3/bonjour.py +26 -49
- pymobiledevice3/ca.py +32 -24
- pymobiledevice3/cli/activation.py +7 -7
- pymobiledevice3/cli/afc.py +19 -19
- pymobiledevice3/cli/amfi.py +4 -4
- pymobiledevice3/cli/apps.py +51 -39
- pymobiledevice3/cli/backup.py +58 -32
- pymobiledevice3/cli/bonjour.py +25 -18
- pymobiledevice3/cli/cli_common.py +112 -81
- pymobiledevice3/cli/companion_proxy.py +4 -4
- pymobiledevice3/cli/completions.py +10 -10
- pymobiledevice3/cli/crash.py +37 -31
- pymobiledevice3/cli/developer.py +602 -520
- pymobiledevice3/cli/diagnostics.py +38 -33
- pymobiledevice3/cli/lockdown.py +79 -74
- pymobiledevice3/cli/mounter.py +85 -68
- pymobiledevice3/cli/notification.py +10 -10
- pymobiledevice3/cli/pcap.py +19 -14
- pymobiledevice3/cli/power_assertion.py +12 -10
- pymobiledevice3/cli/processes.py +10 -10
- pymobiledevice3/cli/profile.py +88 -77
- pymobiledevice3/cli/provision.py +17 -17
- pymobiledevice3/cli/remote.py +186 -110
- pymobiledevice3/cli/restore.py +43 -40
- pymobiledevice3/cli/springboard.py +30 -28
- pymobiledevice3/cli/syslog.py +85 -58
- pymobiledevice3/cli/usbmux.py +21 -20
- pymobiledevice3/cli/version.py +3 -2
- pymobiledevice3/cli/webinspector.py +157 -79
- pymobiledevice3/common.py +1 -1
- pymobiledevice3/exceptions.py +154 -60
- pymobiledevice3/irecv.py +49 -53
- pymobiledevice3/irecv_devices.py +1489 -492
- pymobiledevice3/lockdown.py +394 -241
- pymobiledevice3/lockdown_service_provider.py +5 -7
- pymobiledevice3/osu/os_utils.py +18 -9
- pymobiledevice3/osu/posix_util.py +28 -15
- pymobiledevice3/osu/win_util.py +14 -8
- pymobiledevice3/pair_records.py +19 -19
- pymobiledevice3/remote/common.py +4 -4
- pymobiledevice3/remote/core_device/app_service.py +94 -67
- pymobiledevice3/remote/core_device/core_device_service.py +17 -14
- pymobiledevice3/remote/core_device/device_info.py +5 -5
- pymobiledevice3/remote/core_device/diagnostics_service.py +10 -8
- pymobiledevice3/remote/core_device/file_service.py +47 -33
- pymobiledevice3/remote/remote_service_discovery.py +53 -35
- pymobiledevice3/remote/remotexpc.py +62 -41
- pymobiledevice3/remote/tunnel_service.py +371 -293
- pymobiledevice3/remote/utils.py +12 -11
- pymobiledevice3/remote/xpc_message.py +145 -125
- pymobiledevice3/resources/dsc_uuid_map.py +19 -19
- pymobiledevice3/resources/firmware_notifications.py +16 -16
- pymobiledevice3/restore/asr.py +27 -27
- pymobiledevice3/restore/base_restore.py +90 -47
- pymobiledevice3/restore/consts.py +87 -66
- pymobiledevice3/restore/device.py +11 -11
- pymobiledevice3/restore/fdr.py +46 -46
- pymobiledevice3/restore/ftab.py +19 -19
- pymobiledevice3/restore/img4.py +130 -133
- pymobiledevice3/restore/mbn.py +35 -54
- pymobiledevice3/restore/recovery.py +125 -135
- pymobiledevice3/restore/restore.py +524 -523
- pymobiledevice3/restore/restore_options.py +122 -115
- pymobiledevice3/restore/restored_client.py +25 -22
- pymobiledevice3/restore/tss.py +378 -270
- pymobiledevice3/service_connection.py +50 -46
- pymobiledevice3/services/accessibilityaudit.py +136 -126
- pymobiledevice3/services/afc.py +350 -291
- pymobiledevice3/services/amfi.py +21 -18
- pymobiledevice3/services/companion.py +23 -19
- pymobiledevice3/services/crash_reports.py +60 -46
- pymobiledevice3/services/debugserver_applist.py +3 -3
- pymobiledevice3/services/device_arbitration.py +8 -8
- pymobiledevice3/services/device_link.py +55 -47
- pymobiledevice3/services/diagnostics.py +971 -968
- pymobiledevice3/services/dtfetchsymbols.py +8 -8
- pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
- pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
- pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
- pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
- pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
- pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +442 -421
- pymobiledevice3/services/dvt/instruments/device_info.py +11 -11
- pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
- pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
- pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
- pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
- pymobiledevice3/services/dvt/instruments/process_control.py +25 -10
- pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
- pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
- pymobiledevice3/services/dvt/testmanaged/xcuitest.py +40 -50
- pymobiledevice3/services/file_relay.py +10 -10
- pymobiledevice3/services/heartbeat.py +8 -7
- pymobiledevice3/services/house_arrest.py +12 -15
- pymobiledevice3/services/installation_proxy.py +119 -100
- pymobiledevice3/services/lockdown_service.py +12 -5
- pymobiledevice3/services/misagent.py +22 -19
- pymobiledevice3/services/mobile_activation.py +84 -72
- pymobiledevice3/services/mobile_config.py +330 -301
- pymobiledevice3/services/mobile_image_mounter.py +137 -116
- pymobiledevice3/services/mobilebackup2.py +188 -150
- pymobiledevice3/services/notification_proxy.py +11 -11
- pymobiledevice3/services/os_trace.py +69 -51
- pymobiledevice3/services/pcapd.py +306 -306
- pymobiledevice3/services/power_assertion.py +10 -9
- pymobiledevice3/services/preboard.py +4 -4
- pymobiledevice3/services/remote_fetch_symbols.py +16 -14
- pymobiledevice3/services/remote_server.py +176 -146
- pymobiledevice3/services/restore_service.py +16 -16
- pymobiledevice3/services/screenshot.py +13 -10
- pymobiledevice3/services/simulate_location.py +7 -7
- pymobiledevice3/services/springboard.py +15 -15
- pymobiledevice3/services/syslog.py +5 -5
- pymobiledevice3/services/web_protocol/alert.py +3 -3
- pymobiledevice3/services/web_protocol/automation_session.py +180 -176
- pymobiledevice3/services/web_protocol/cdp_screencast.py +44 -36
- pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
- pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
- pymobiledevice3/services/web_protocol/driver.py +47 -45
- pymobiledevice3/services/web_protocol/element.py +74 -63
- pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
- pymobiledevice3/services/web_protocol/selenium_api.py +2 -2
- pymobiledevice3/services/web_protocol/session_protocol.py +15 -10
- pymobiledevice3/services/web_protocol/switch_to.py +11 -12
- pymobiledevice3/services/webinspector.py +127 -116
- pymobiledevice3/tcp_forwarder.py +35 -22
- pymobiledevice3/tunneld/api.py +20 -15
- pymobiledevice3/tunneld/server.py +212 -133
- pymobiledevice3/usbmux.py +183 -138
- pymobiledevice3/utils.py +14 -11
- {pymobiledevice3-5.0.1.dist-info → pymobiledevice3-5.0.2.dist-info}/METADATA +1 -1
- pymobiledevice3-5.0.2.dist-info/RECORD +173 -0
- pymobiledevice3-5.0.1.dist-info/RECORD +0 -173
- {pymobiledevice3-5.0.1.dist-info → pymobiledevice3-5.0.2.dist-info}/WHEEL +0 -0
- {pymobiledevice3-5.0.1.dist-info → pymobiledevice3-5.0.2.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-5.0.1.dist-info → pymobiledevice3-5.0.2.dist-info}/licenses/LICENSE +0 -0
- {pymobiledevice3-5.0.1.dist-info → pymobiledevice3-5.0.2.dist-info}/top_level.txt +0 -0
pymobiledevice3/cli/backup.py
CHANGED
|
@@ -8,11 +8,12 @@ from pymobiledevice3.lockdown import LockdownClient
|
|
|
8
8
|
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
|
|
9
9
|
from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service
|
|
10
10
|
|
|
11
|
-
source_option = click.option(
|
|
12
|
-
password_option = click.option(
|
|
13
|
-
backup_directory_arg = click.argument(
|
|
14
|
-
backup_directory_option = click.option(
|
|
15
|
-
|
|
11
|
+
source_option = click.option("--source", default="", help="The UDID of the source device.")
|
|
12
|
+
password_option = click.option("-p", "--password", default="", help="Backup password.")
|
|
13
|
+
backup_directory_arg = click.argument("backup-directory", type=click.Path(exists=True, file_okay=False))
|
|
14
|
+
backup_directory_option = click.option(
|
|
15
|
+
"-b", "--backup-directory", type=click.Path(exists=True, file_okay=False), default="."
|
|
16
|
+
)
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -24,14 +25,17 @@ def cli() -> None:
|
|
|
24
25
|
|
|
25
26
|
@cli.group()
|
|
26
27
|
def backup2() -> None:
|
|
27
|
-
"""
|
|
28
|
+
"""Backup/Restore options"""
|
|
28
29
|
pass
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
@backup2.command(cls=Command)
|
|
32
|
-
@click.argument(
|
|
33
|
-
@click.option(
|
|
34
|
-
|
|
33
|
+
@click.argument("backup-directory", type=click.Path(file_okay=False))
|
|
34
|
+
@click.option(
|
|
35
|
+
"--full",
|
|
36
|
+
is_flag=True,
|
|
37
|
+
help=("Whether to do a full backup. If full is True, any previous backup attempts will be discarded."),
|
|
38
|
+
)
|
|
35
39
|
def backup(service_provider: LockdownServiceProvider, backup_directory: str, full: bool) -> None:
|
|
36
40
|
"""
|
|
37
41
|
Backup device.
|
|
@@ -40,6 +44,7 @@ def backup(service_provider: LockdownServiceProvider, backup_directory: str, ful
|
|
|
40
44
|
"""
|
|
41
45
|
backup_client = Mobilebackup2Service(service_provider)
|
|
42
46
|
with tqdm(total=100, dynamic_ncols=True) as pbar:
|
|
47
|
+
|
|
43
48
|
def update_bar(percentage):
|
|
44
49
|
pbar.n = percentage
|
|
45
50
|
pbar.refresh()
|
|
@@ -49,16 +54,26 @@ def backup(service_provider: LockdownServiceProvider, backup_directory: str, ful
|
|
|
49
54
|
|
|
50
55
|
@backup2.command(cls=Command)
|
|
51
56
|
@backup_directory_arg
|
|
52
|
-
@click.option(
|
|
53
|
-
@click.option(
|
|
54
|
-
@click.option(
|
|
55
|
-
@click.option(
|
|
56
|
-
@click.option(
|
|
57
|
-
@click.option(
|
|
57
|
+
@click.option("--system/--no-system", default=False, help="Restore system files.")
|
|
58
|
+
@click.option("--reboot/--no-reboot", default=True, help="Reboot the device when done.")
|
|
59
|
+
@click.option("--copy/--no-copy", default=False, help="Create a copy of backup folder before restoring.")
|
|
60
|
+
@click.option("--settings/--no-settings", default=True, help="Restore device settings.")
|
|
61
|
+
@click.option("--remove/--no-remove", default=False, help="Remove items which aren't being restored.")
|
|
62
|
+
@click.option("--skip-apps", is_flag=True, help="Do not trigger re-installation of apps after restore")
|
|
58
63
|
@password_option
|
|
59
64
|
@source_option
|
|
60
|
-
def restore(
|
|
61
|
-
|
|
65
|
+
def restore(
|
|
66
|
+
service_provider: LockdownServiceProvider,
|
|
67
|
+
backup_directory: str,
|
|
68
|
+
system: bool,
|
|
69
|
+
reboot: bool,
|
|
70
|
+
copy: bool,
|
|
71
|
+
settings: bool,
|
|
72
|
+
remove: bool,
|
|
73
|
+
skip_apps: bool,
|
|
74
|
+
password: str,
|
|
75
|
+
source: str,
|
|
76
|
+
) -> None:
|
|
62
77
|
"""
|
|
63
78
|
Restore a backup to a device.
|
|
64
79
|
|
|
@@ -66,13 +81,23 @@ def restore(service_provider: LockdownServiceProvider, backup_directory: str, sy
|
|
|
66
81
|
"""
|
|
67
82
|
backup_client = Mobilebackup2Service(service_provider)
|
|
68
83
|
with tqdm(total=100, dynamic_ncols=True) as pbar:
|
|
84
|
+
|
|
69
85
|
def update_bar(percentage):
|
|
70
86
|
pbar.n = percentage
|
|
71
87
|
pbar.refresh()
|
|
72
88
|
|
|
73
|
-
backup_client.restore(
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
backup_client.restore(
|
|
90
|
+
backup_directory=backup_directory,
|
|
91
|
+
progress_callback=update_bar,
|
|
92
|
+
system=system,
|
|
93
|
+
reboot=reboot,
|
|
94
|
+
copy=copy,
|
|
95
|
+
settings=settings,
|
|
96
|
+
remove=remove,
|
|
97
|
+
password=password,
|
|
98
|
+
source=source,
|
|
99
|
+
skip_apps=skip_apps,
|
|
100
|
+
)
|
|
76
101
|
|
|
77
102
|
|
|
78
103
|
@backup2.command(cls=Command)
|
|
@@ -86,7 +111,7 @@ def info(service_provider: LockdownClient, backup_directory, source):
|
|
|
86
111
|
print(backup_client.info(backup_directory=backup_directory, source=source))
|
|
87
112
|
|
|
88
113
|
|
|
89
|
-
@backup2.command(
|
|
114
|
+
@backup2.command("list", cls=Command)
|
|
90
115
|
@backup_directory_arg
|
|
91
116
|
@source_option
|
|
92
117
|
def list_(service_provider: LockdownClient, backup_directory, source):
|
|
@@ -110,8 +135,8 @@ def unback(service_provider: LockdownClient, backup_directory, password, source)
|
|
|
110
135
|
|
|
111
136
|
|
|
112
137
|
@backup2.command(cls=Command)
|
|
113
|
-
@click.argument(
|
|
114
|
-
@click.argument(
|
|
138
|
+
@click.argument("domain-name")
|
|
139
|
+
@click.argument("relative-path")
|
|
115
140
|
@backup_directory_arg
|
|
116
141
|
@password_option
|
|
117
142
|
@source_option
|
|
@@ -123,13 +148,14 @@ def extract(service_provider: LockdownClient, domain_name, relative_path, backup
|
|
|
123
148
|
will be extracted to the BACKUP_DIRECTORY.
|
|
124
149
|
"""
|
|
125
150
|
backup_client = Mobilebackup2Service(service_provider)
|
|
126
|
-
backup_client.extract(
|
|
127
|
-
|
|
151
|
+
backup_client.extract(
|
|
152
|
+
domain_name, relative_path, backup_directory=backup_directory, password=password, source=source
|
|
153
|
+
)
|
|
128
154
|
|
|
129
155
|
|
|
130
156
|
@backup2.command(cls=Command)
|
|
131
|
-
@click.argument(
|
|
132
|
-
@click.argument(
|
|
157
|
+
@click.argument("mode", type=click.Choice(["on", "off"], case_sensitive=False))
|
|
158
|
+
@click.argument("password")
|
|
133
159
|
@backup_directory_option
|
|
134
160
|
def encryption(service_provider: LockdownClient, backup_directory, mode, password):
|
|
135
161
|
"""
|
|
@@ -139,9 +165,9 @@ def encryption(service_provider: LockdownClient, backup_directory, mode, passwor
|
|
|
139
165
|
When off, PASSWORD is the current backup password.
|
|
140
166
|
"""
|
|
141
167
|
backup_client = Mobilebackup2Service(service_provider)
|
|
142
|
-
should_encrypt = mode.lower() ==
|
|
168
|
+
should_encrypt = mode.lower() == "on"
|
|
143
169
|
if should_encrypt == backup_client.will_encrypt:
|
|
144
|
-
logger.error(
|
|
170
|
+
logger.error("Encryption already " + ("on!" if should_encrypt else "off!"))
|
|
145
171
|
return
|
|
146
172
|
if should_encrypt:
|
|
147
173
|
backup_client.change_password(backup_directory, new=password)
|
|
@@ -150,8 +176,8 @@ def encryption(service_provider: LockdownClient, backup_directory, mode, passwor
|
|
|
150
176
|
|
|
151
177
|
|
|
152
178
|
@backup2.command(cls=Command)
|
|
153
|
-
@click.argument(
|
|
154
|
-
@click.argument(
|
|
179
|
+
@click.argument("old-password")
|
|
180
|
+
@click.argument("new-password")
|
|
155
181
|
@backup_directory_option
|
|
156
182
|
def change_password(service_provider: LockdownClient, old_password, new_password, backup_directory):
|
|
157
183
|
"""
|
|
@@ -159,7 +185,7 @@ def change_password(service_provider: LockdownClient, old_password, new_password
|
|
|
159
185
|
"""
|
|
160
186
|
backup_client = Mobilebackup2Service(service_provider)
|
|
161
187
|
if not backup_client.will_encrypt:
|
|
162
|
-
logger.error(
|
|
188
|
+
logger.error("Encryption is not turned on!")
|
|
163
189
|
return
|
|
164
190
|
backup_client.change_password(backup_directory, old=old_password, new=new_password)
|
|
165
191
|
|
pymobiledevice3/cli/bonjour.py
CHANGED
|
@@ -14,9 +14,9 @@ def cli() -> None:
|
|
|
14
14
|
pass
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
@cli.group(
|
|
17
|
+
@cli.group("bonjour")
|
|
18
18
|
def bonjour_cli() -> None:
|
|
19
|
-
"""
|
|
19
|
+
"""Browse devices over bonjour"""
|
|
20
20
|
pass
|
|
21
21
|
|
|
22
22
|
|
|
@@ -24,17 +24,20 @@ async def cli_mobdev2_task(timeout: float, pair_records: Optional[str]) -> None:
|
|
|
24
24
|
output = []
|
|
25
25
|
async for ip, lockdown in get_mobdev2_lockdowns(timeout=timeout, pair_records=pair_records):
|
|
26
26
|
short_info = lockdown.short_info
|
|
27
|
-
short_info[
|
|
27
|
+
short_info["ip"] = ip
|
|
28
28
|
output.append(short_info)
|
|
29
29
|
print_json(output)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
@bonjour_cli.command(
|
|
33
|
-
@click.option(
|
|
34
|
-
@click.option(
|
|
35
|
-
|
|
32
|
+
@bonjour_cli.command("mobdev2", cls=BaseCommand)
|
|
33
|
+
@click.option("--timeout", default=DEFAULT_BONJOUR_TIMEOUT, type=click.INT)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--pair-records",
|
|
36
|
+
type=click.Path(dir_okay=True, file_okay=False, exists=True),
|
|
37
|
+
help="pair records to attempt validation with",
|
|
38
|
+
)
|
|
36
39
|
def cli_mobdev2(timeout: float, pair_records: Optional[str]) -> None:
|
|
37
|
-
"""
|
|
40
|
+
"""browse for mobdev2 devices over bonjour"""
|
|
38
41
|
asyncio.run(cli_mobdev2_task(timeout, pair_records))
|
|
39
42
|
|
|
40
43
|
|
|
@@ -42,14 +45,14 @@ async def cli_remotepairing_task(timeout: float) -> None:
|
|
|
42
45
|
output = []
|
|
43
46
|
for answer in await browse_remotepairing(timeout=timeout):
|
|
44
47
|
for address in answer.addresses:
|
|
45
|
-
output.append({
|
|
48
|
+
output.append({"hostname": address.full_ip, "port": answer.port})
|
|
46
49
|
print_json(output)
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
@bonjour_cli.command(
|
|
50
|
-
@click.option(
|
|
52
|
+
@bonjour_cli.command("remotepairing", cls=BaseCommand)
|
|
53
|
+
@click.option("--timeout", default=DEFAULT_BONJOUR_TIMEOUT, type=click.FLOAT)
|
|
51
54
|
def cli_remotepairing(timeout: float) -> None:
|
|
52
|
-
"""
|
|
55
|
+
"""browse for remotepairing devices over bonjour (without attempting pair verification)"""
|
|
53
56
|
asyncio.run(cli_remotepairing_task(timeout=timeout))
|
|
54
57
|
|
|
55
58
|
|
|
@@ -57,14 +60,18 @@ async def cli_remotepairing_manual_pairing_task(timeout: float) -> None:
|
|
|
57
60
|
output = []
|
|
58
61
|
for answer in await browse_remotepairing_manual_pairing(timeout=timeout):
|
|
59
62
|
for address in answer.addresses:
|
|
60
|
-
output.append({
|
|
63
|
+
output.append({
|
|
64
|
+
"hostname": address.full_ip,
|
|
65
|
+
"port": answer.port,
|
|
66
|
+
"name": answer.properties[b"name"].decode(),
|
|
67
|
+
})
|
|
61
68
|
print_json(output)
|
|
62
69
|
|
|
63
70
|
|
|
64
|
-
@bonjour_cli.command(
|
|
65
|
-
@click.option(
|
|
71
|
+
@bonjour_cli.command("remotepairing-manual-pairing", cls=BaseCommand)
|
|
72
|
+
@click.option("--timeout", default=DEFAULT_BONJOUR_TIMEOUT, type=click.FLOAT)
|
|
66
73
|
def cli_remotepairing_manual_pairing(timeout: float) -> None:
|
|
67
|
-
"""
|
|
74
|
+
"""browse for remotepairing-manual-pairing devices over bonjour"""
|
|
68
75
|
asyncio.run(cli_remotepairing_manual_pairing_task(timeout=timeout))
|
|
69
76
|
|
|
70
77
|
|
|
@@ -72,7 +79,7 @@ async def cli_browse_rsd() -> None:
|
|
|
72
79
|
print_json(await browse_rsd())
|
|
73
80
|
|
|
74
81
|
|
|
75
|
-
@bonjour_cli.command(
|
|
82
|
+
@bonjour_cli.command("rsd", cls=BaseCommand)
|
|
76
83
|
def cli_rsd() -> None:
|
|
77
|
-
"""
|
|
84
|
+
"""browse RemoteXPC devices using bonjour"""
|
|
78
85
|
asyncio.run(cli_browse_rsd(), debug=True)
|
|
@@ -24,51 +24,50 @@ from pymobiledevice3.tunneld.api import TUNNELD_DEFAULT_ADDRESS, async_get_tunne
|
|
|
24
24
|
from pymobiledevice3.usbmux import select_devices_by_connection_type
|
|
25
25
|
|
|
26
26
|
COLORED_OUTPUT = True
|
|
27
|
-
UDID_ENV_VAR =
|
|
28
|
-
TUNNEL_ENV_VAR =
|
|
29
|
-
USBMUX_ENV_VAR =
|
|
27
|
+
UDID_ENV_VAR = "PYMOBILEDEVICE3_UDID"
|
|
28
|
+
TUNNEL_ENV_VAR = "PYMOBILEDEVICE3_TUNNEL"
|
|
29
|
+
USBMUX_ENV_VAR = "PYMOBILEDEVICE3_USBMUX"
|
|
30
30
|
OSUTILS = get_os_utils()
|
|
31
31
|
|
|
32
|
-
USBMUX_OPTION_HELP = (
|
|
33
|
-
|
|
32
|
+
USBMUX_OPTION_HELP = (
|
|
33
|
+
f"usbmuxd listener address (in the form of either /path/to/unix/socket OR HOST:PORT). "
|
|
34
|
+
f"Can be specified via {USBMUX_ENV_VAR} envvar"
|
|
35
|
+
)
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class RSDOption(Option):
|
|
37
39
|
def __init__(self, *args, **kwargs):
|
|
38
|
-
self.mutually_exclusive = set(kwargs.pop(
|
|
39
|
-
|
|
40
|
+
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
|
|
41
|
+
help_option = kwargs.get("help", "")
|
|
40
42
|
if self.mutually_exclusive:
|
|
41
|
-
ex_str =
|
|
42
|
-
kwargs[
|
|
43
|
-
|
|
44
|
-
' arguments: [' + ex_str + '].'
|
|
43
|
+
ex_str = ", ".join(self.mutually_exclusive)
|
|
44
|
+
kwargs["help"] = help_option + (
|
|
45
|
+
"\nNOTE: This argument is mutually exclusive with arguments: [" + ex_str + "]."
|
|
45
46
|
)
|
|
46
47
|
super().__init__(*args, **kwargs)
|
|
47
48
|
|
|
48
49
|
def handle_parse_result(self, ctx, opts, args):
|
|
49
|
-
if (
|
|
50
|
-
|
|
50
|
+
if (
|
|
51
|
+
isinstance(ctx.command, RSDCommand)
|
|
52
|
+
and not (isinstance(ctx.command, Command))
|
|
53
|
+
and ("rsd_service_provider_using_tunneld" not in opts)
|
|
54
|
+
and ("rsd_service_provider_manually" not in opts)
|
|
55
|
+
):
|
|
51
56
|
# defaulting to `--tunnel ''` if no remote option was specified
|
|
52
|
-
opts[
|
|
57
|
+
opts["rsd_service_provider_using_tunneld"] = ""
|
|
53
58
|
if self.mutually_exclusive.intersection(opts) and self.name in opts:
|
|
54
59
|
raise UsageError(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
self.name,
|
|
58
|
-
', '.join(self.mutually_exclusive)
|
|
60
|
+
"Illegal usage: `{}` is mutually exclusive with arguments `{}`.".format(
|
|
61
|
+
self.name, ", ".join(self.mutually_exclusive)
|
|
59
62
|
)
|
|
60
63
|
)
|
|
61
64
|
|
|
62
|
-
return super().handle_parse_result(
|
|
63
|
-
ctx,
|
|
64
|
-
opts,
|
|
65
|
-
args
|
|
66
|
-
)
|
|
65
|
+
return super().handle_parse_result(ctx, opts, args)
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
def default_json_encoder(obj):
|
|
70
69
|
if isinstance(obj, bytes):
|
|
71
|
-
return f
|
|
70
|
+
return f"<{obj.hex()}>"
|
|
72
71
|
if isinstance(obj, datetime.datetime):
|
|
73
72
|
return str(obj)
|
|
74
73
|
if isinstance(obj, uuid.UUID):
|
|
@@ -81,8 +80,9 @@ def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder
|
|
|
81
80
|
colored = user_requested_colored_output()
|
|
82
81
|
formatted_json = json.dumps(buf, sort_keys=True, indent=4, default=default)
|
|
83
82
|
if colored and os.isatty(sys.stdout.fileno()):
|
|
84
|
-
colorful_json = highlight(
|
|
85
|
-
|
|
83
|
+
colorful_json = highlight(
|
|
84
|
+
formatted_json, lexers.JsonLexer(), formatters.Terminal256Formatter(style="stata-dark")
|
|
85
|
+
)
|
|
86
86
|
print(colorful_json)
|
|
87
87
|
return colorful_json
|
|
88
88
|
else:
|
|
@@ -91,11 +91,11 @@ def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def print_hex(data, colored=True):
|
|
94
|
-
hex_dump = hexdump.hexdump(data, result=
|
|
94
|
+
hex_dump = hexdump.hexdump(data, result="return")
|
|
95
95
|
if colored:
|
|
96
|
-
print(highlight(hex_dump, lexers.HexdumpLexer(), formatters.Terminal256Formatter(style=
|
|
96
|
+
print(highlight(hex_dump, lexers.HexdumpLexer(), formatters.Terminal256Formatter(style="native")))
|
|
97
97
|
else:
|
|
98
|
-
print(hex_dump, end=
|
|
98
|
+
print(hex_dump, end="\n\n")
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def set_verbosity(ctx, param, value):
|
|
@@ -116,7 +116,7 @@ def user_requested_colored_output() -> bool:
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def get_last_used_terminal_formatting(buf: str) -> str:
|
|
119
|
-
return
|
|
119
|
+
return "\x1b" + buf.rsplit("\x1b", 1)[1].split("m")[0] + "m"
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def sudo_required(func):
|
|
@@ -131,24 +131,24 @@ def sudo_required(func):
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
def prompt_selection(choices: list[Any], message: str, idx: bool = False) -> Any:
|
|
134
|
-
question = [inquirer3.List(
|
|
134
|
+
question = [inquirer3.List("selection", message=message, choices=choices, carousel=True)]
|
|
135
135
|
try:
|
|
136
136
|
result = inquirer3.prompt(question, theme=GreenPassion(), raise_keyboard_interrupt=True)
|
|
137
|
-
except KeyboardInterrupt:
|
|
138
|
-
raise click.ClickException(
|
|
139
|
-
return result[
|
|
137
|
+
except KeyboardInterrupt as e:
|
|
138
|
+
raise click.ClickException("No selection was made") from e
|
|
139
|
+
return result["selection"] if not idx else choices.index(result["selection"])
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
def prompt_device_list(device_list: list):
|
|
143
|
-
return prompt_selection(device_list,
|
|
143
|
+
return prompt_selection(device_list, "Choose device")
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def choose_service_provider(callback: Callable):
|
|
147
147
|
def wrap_callback_calling(**kwargs: dict) -> None:
|
|
148
148
|
service_provider = None
|
|
149
|
-
lockdown_service_provider = kwargs.pop(
|
|
150
|
-
rsd_service_provider_manually = kwargs.pop(
|
|
151
|
-
rsd_service_provider_using_tunneld = kwargs.pop(
|
|
149
|
+
lockdown_service_provider = kwargs.pop("lockdown_service_provider", None)
|
|
150
|
+
rsd_service_provider_manually = kwargs.pop("rsd_service_provider_manually", None)
|
|
151
|
+
rsd_service_provider_using_tunneld = kwargs.pop("rsd_service_provider_using_tunneld", None)
|
|
152
152
|
if lockdown_service_provider is not None:
|
|
153
153
|
service_provider = lockdown_service_provider
|
|
154
154
|
if rsd_service_provider_manually is not None:
|
|
@@ -161,20 +161,23 @@ def choose_service_provider(callback: Callable):
|
|
|
161
161
|
|
|
162
162
|
|
|
163
163
|
def is_invoked_for_completion() -> bool:
|
|
164
|
-
"""
|
|
165
|
-
for env in os.environ
|
|
166
|
-
if env.startswith('_') and env.endswith('_COMPLETE'):
|
|
167
|
-
return True
|
|
168
|
-
return False
|
|
164
|
+
"""Returns True if the command is ivoked for autocompletion."""
|
|
165
|
+
return any(env.startswith("_") and env.endswith("_COMPLETE") for env in os.environ)
|
|
169
166
|
|
|
170
167
|
|
|
171
168
|
class BaseCommand(click.Command):
|
|
172
169
|
def __init__(self, *args, **kwargs):
|
|
173
170
|
super().__init__(*args, **kwargs)
|
|
174
171
|
self.params[:0] = [
|
|
175
|
-
click.Option((
|
|
176
|
-
click.Option(
|
|
177
|
-
|
|
172
|
+
click.Option(("verbosity", "-v", "--verbose"), count=True, callback=set_verbosity, expose_value=False),
|
|
173
|
+
click.Option(
|
|
174
|
+
("color", "--color/--no-color"),
|
|
175
|
+
default=True,
|
|
176
|
+
callback=set_color_flag,
|
|
177
|
+
is_flag=True,
|
|
178
|
+
expose_value=False,
|
|
179
|
+
help="colorize output",
|
|
180
|
+
),
|
|
178
181
|
]
|
|
179
182
|
|
|
180
183
|
|
|
@@ -191,20 +194,35 @@ class LockdownCommand(BaseServiceProviderCommand):
|
|
|
191
194
|
self.usbmux_address = None
|
|
192
195
|
self.mobdev2_option = None
|
|
193
196
|
self.params[:0] = [
|
|
194
|
-
click.Option(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
click.Option(
|
|
198
|
+
("mobdev2", "--mobdev2"),
|
|
199
|
+
callback=self.mobdev2,
|
|
200
|
+
expose_value=False,
|
|
201
|
+
default=None,
|
|
202
|
+
help="Use bonjour browse for mobdev2 devices. Expected value IP address of the interface to "
|
|
203
|
+
"use. Leave empty to browse through all interfaces",
|
|
204
|
+
),
|
|
205
|
+
click.Option(
|
|
206
|
+
("usbmux", "--usbmux"),
|
|
207
|
+
callback=self.usbmux,
|
|
208
|
+
expose_value=False,
|
|
209
|
+
envvar=USBMUX_ENV_VAR,
|
|
210
|
+
help=USBMUX_OPTION_HELP,
|
|
211
|
+
),
|
|
212
|
+
click.Option(
|
|
213
|
+
("lockdown_service_provider", "--udid"),
|
|
214
|
+
envvar=UDID_ENV_VAR,
|
|
215
|
+
callback=self.udid,
|
|
216
|
+
help=f"Device unique identifier. You may pass {UDID_ENV_VAR} environment variable to pass this"
|
|
217
|
+
f" option as well",
|
|
218
|
+
),
|
|
202
219
|
]
|
|
203
220
|
|
|
204
|
-
async def get_mobdev2_devices(
|
|
205
|
-
|
|
221
|
+
async def get_mobdev2_devices(
|
|
222
|
+
self, udid: Optional[str] = None, ips: Optional[list[str]] = None
|
|
223
|
+
) -> list[TcpLockdownClient]:
|
|
206
224
|
result = []
|
|
207
|
-
async for
|
|
225
|
+
async for _ip, lockdown in get_mobdev2_lockdowns(udid=udid, ips=ips):
|
|
208
226
|
result.append(lockdown)
|
|
209
227
|
return result
|
|
210
228
|
|
|
@@ -225,8 +243,11 @@ class LockdownCommand(BaseServiceProviderCommand):
|
|
|
225
243
|
return self.service_provider
|
|
226
244
|
|
|
227
245
|
if self.mobdev2_option is not None:
|
|
228
|
-
devices = asyncio.run(
|
|
229
|
-
|
|
246
|
+
devices = asyncio.run(
|
|
247
|
+
self.get_mobdev2_devices(
|
|
248
|
+
udid=value if value else None, ips=[self.mobdev2_option] if self.mobdev2_option else None
|
|
249
|
+
)
|
|
250
|
+
)
|
|
230
251
|
if not devices:
|
|
231
252
|
raise NoDeviceConnectedError()
|
|
232
253
|
|
|
@@ -240,29 +261,37 @@ class LockdownCommand(BaseServiceProviderCommand):
|
|
|
240
261
|
if value is not None:
|
|
241
262
|
return create_using_usbmux(serial=value)
|
|
242
263
|
|
|
243
|
-
devices = select_devices_by_connection_type(connection_type=
|
|
264
|
+
devices = select_devices_by_connection_type(connection_type="USB", usbmux_address=self.usbmux_address)
|
|
244
265
|
if len(devices) <= 1:
|
|
245
266
|
return create_using_usbmux(usbmux_address=self.usbmux_address)
|
|
246
267
|
|
|
247
|
-
return prompt_device_list(
|
|
248
|
-
|
|
268
|
+
return prompt_device_list([
|
|
269
|
+
create_using_usbmux(serial=device.serial, usbmux_address=self.usbmux_address) for device in devices
|
|
270
|
+
])
|
|
249
271
|
|
|
250
272
|
|
|
251
273
|
class RSDCommand(BaseServiceProviderCommand):
|
|
252
274
|
def __init__(self, *args, **kwargs):
|
|
253
275
|
super().__init__(*args, **kwargs)
|
|
254
276
|
self.params[:0] = [
|
|
255
|
-
RSDOption(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
RSDOption(
|
|
278
|
+
("rsd_service_provider_manually", "--rsd"),
|
|
279
|
+
type=(str, int),
|
|
280
|
+
callback=self.rsd,
|
|
281
|
+
mutually_exclusive=["rsd_service_provider_using_tunneld"],
|
|
282
|
+
help="\b\nRSD hostname and port number (as provided by a `start-tunnel` subcommand).",
|
|
283
|
+
),
|
|
284
|
+
RSDOption(
|
|
285
|
+
("rsd_service_provider_using_tunneld", "--tunnel"),
|
|
286
|
+
callback=self.tunneld,
|
|
287
|
+
mutually_exclusive=["rsd_service_provider_manually"],
|
|
288
|
+
envvar=TUNNEL_ENV_VAR,
|
|
289
|
+
help="\b\n"
|
|
290
|
+
"Either an empty string to force tunneld device selection, or a UDID of a tunneld "
|
|
291
|
+
"discovered device.\n"
|
|
292
|
+
"The string may be suffixed with :PORT in case tunneld is not serving at the default port.\n"
|
|
293
|
+
f"This option may also be transferred as an environment variable: {TUNNEL_ENV_VAR}",
|
|
294
|
+
),
|
|
266
295
|
]
|
|
267
296
|
|
|
268
297
|
def rsd(self, ctx, param: str, value: Optional[tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
|
|
@@ -278,19 +307,21 @@ class RSDCommand(BaseServiceProviderCommand):
|
|
|
278
307
|
|
|
279
308
|
udid = udid.strip()
|
|
280
309
|
port = TUNNELD_DEFAULT_ADDRESS[1]
|
|
281
|
-
if
|
|
282
|
-
udid, port = udid.split(
|
|
310
|
+
if ":" in udid:
|
|
311
|
+
udid, port = udid.split(":")
|
|
283
312
|
|
|
284
313
|
rsds = await async_get_tunneld_devices((TUNNELD_DEFAULT_ADDRESS[0], port))
|
|
285
314
|
if len(rsds) == 0:
|
|
286
315
|
raise NoDeviceConnectedError()
|
|
287
316
|
|
|
288
|
-
if udid !=
|
|
317
|
+
if udid != "":
|
|
289
318
|
try:
|
|
290
319
|
# Connect to the specified device
|
|
291
|
-
self.service_provider =
|
|
292
|
-
|
|
293
|
-
|
|
320
|
+
self.service_provider = next(
|
|
321
|
+
rsd for rsd in rsds if rsd.udid == udid or rsd.udid.replace("-", "") == udid
|
|
322
|
+
)
|
|
323
|
+
except IndexError as e:
|
|
324
|
+
raise DeviceNotFoundError(udid) from e
|
|
294
325
|
else:
|
|
295
326
|
if len(rsds) == 1:
|
|
296
327
|
self.service_provider = rsds[0]
|
|
@@ -323,13 +354,13 @@ class CommandWithoutAutopair(Command):
|
|
|
323
354
|
|
|
324
355
|
|
|
325
356
|
class BasedIntParamType(click.ParamType):
|
|
326
|
-
name =
|
|
357
|
+
name = "based int"
|
|
327
358
|
|
|
328
359
|
def convert(self, value, param, ctx):
|
|
329
360
|
try:
|
|
330
361
|
return int(value, 0)
|
|
331
362
|
except ValueError:
|
|
332
|
-
self.fail(f
|
|
363
|
+
self.fail(f"{value!r} is not a valid int.", param, ctx)
|
|
333
364
|
|
|
334
365
|
|
|
335
366
|
BASED_INT = BasedIntParamType()
|
|
@@ -12,11 +12,11 @@ def cli() -> None:
|
|
|
12
12
|
|
|
13
13
|
@cli.group()
|
|
14
14
|
def companion() -> None:
|
|
15
|
-
"""
|
|
15
|
+
"""List paired "companion" devices"""
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@companion.command(
|
|
19
|
+
@companion.command("list", cls=Command)
|
|
20
20
|
def companion_list(service_provider: LockdownClient):
|
|
21
|
-
"""
|
|
22
|
-
print_json(CompanionProxyService(service_provider).list(), default=lambda x:
|
|
21
|
+
"""list all paired companion devices"""
|
|
22
|
+
print_json(CompanionProxyService(service_provider).list(), default=lambda x: "<non-serializable>")
|