pymobiledevice3 6.2.0__py3-none-any.whl → 7.0.0__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.
- pymobiledevice3/__main__.py +136 -44
- pymobiledevice3/_version.py +2 -2
- pymobiledevice3/bonjour.py +19 -20
- pymobiledevice3/cli/activation.py +24 -22
- pymobiledevice3/cli/afc.py +49 -41
- pymobiledevice3/cli/amfi.py +13 -18
- pymobiledevice3/cli/apps.py +71 -65
- pymobiledevice3/cli/backup.py +134 -93
- pymobiledevice3/cli/bonjour.py +31 -29
- pymobiledevice3/cli/cli_common.py +179 -232
- pymobiledevice3/cli/companion_proxy.py +12 -12
- pymobiledevice3/cli/crash.py +95 -52
- pymobiledevice3/cli/developer/__init__.py +62 -0
- pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
- pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
- pymobiledevice3/cli/developer/arbitration.py +50 -0
- pymobiledevice3/cli/developer/condition.py +33 -0
- pymobiledevice3/cli/developer/core_device.py +294 -0
- pymobiledevice3/cli/developer/debugserver.py +244 -0
- pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
- pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
- pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
- pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
- pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
- pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
- pymobiledevice3/cli/developer/simulate_location.py +51 -0
- pymobiledevice3/cli/diagnostics/__init__.py +75 -0
- pymobiledevice3/cli/diagnostics/battery.py +47 -0
- pymobiledevice3/cli/idam.py +18 -22
- pymobiledevice3/cli/lockdown.py +70 -75
- pymobiledevice3/cli/mounter.py +99 -57
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +36 -20
- pymobiledevice3/cli/power_assertion.py +15 -16
- pymobiledevice3/cli/processes.py +11 -17
- pymobiledevice3/cli/profile.py +120 -75
- pymobiledevice3/cli/provision.py +27 -26
- pymobiledevice3/cli/remote.py +108 -99
- pymobiledevice3/cli/restore.py +134 -129
- pymobiledevice3/cli/springboard.py +50 -50
- pymobiledevice3/cli/syslog.py +138 -74
- pymobiledevice3/cli/usbmux.py +66 -27
- pymobiledevice3/cli/version.py +2 -5
- pymobiledevice3/cli/webinspector.py +149 -103
- pymobiledevice3/remote/remote_service_discovery.py +11 -10
- pymobiledevice3/restore/device.py +28 -4
- pymobiledevice3/service_connection.py +1 -1
- pymobiledevice3/services/mobilebackup2.py +4 -1
- pymobiledevice3/services/screenshot.py +2 -2
- pymobiledevice3/services/web_protocol/automation_session.py +4 -2
- pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
- pymobiledevice3/services/web_protocol/element.py +3 -3
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
- pymobiledevice3/cli/completions.py +0 -50
- pymobiledevice3/cli/developer.py +0 -1645
- pymobiledevice3/cli/diagnostics.py +0 -110
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from typer_injector import InjectingTyper
|
|
6
|
+
|
|
7
|
+
from pymobiledevice3.cli.cli_common import OSUTILS, ServiceProviderDep
|
|
8
|
+
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
|
|
9
|
+
from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation
|
|
10
|
+
|
|
11
|
+
cli = InjectingTyper(
|
|
12
|
+
name="simulate-location",
|
|
13
|
+
help="Simulate device location by given input",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cli.command("clear")
|
|
19
|
+
def dvt_simulate_location_clear(service_provider: ServiceProviderDep) -> None:
|
|
20
|
+
"""Clear currently simulated location"""
|
|
21
|
+
with DvtSecureSocketProxyService(service_provider) as dvt:
|
|
22
|
+
LocationSimulation(dvt).clear()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cli.command("set")
|
|
26
|
+
def dvt_simulate_location_set(service_provider: ServiceProviderDep, latitude: float, longitude: float) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Set a simulated location.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
For example:
|
|
32
|
+
\b ... set -- 40.690008 -74.045843 for liberty island
|
|
33
|
+
"""
|
|
34
|
+
with DvtSecureSocketProxyService(service_provider) as dvt:
|
|
35
|
+
LocationSimulation(dvt).set(latitude, longitude)
|
|
36
|
+
OSUTILS.wait_return()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command("play")
|
|
40
|
+
def dvt_simulate_location_play(
|
|
41
|
+
service_provider: ServiceProviderDep,
|
|
42
|
+
filename: Annotated[
|
|
43
|
+
Path,
|
|
44
|
+
typer.Argument(exists=True, file_okay=True, dir_okay=False),
|
|
45
|
+
],
|
|
46
|
+
timing_randomness_range: int = 0,
|
|
47
|
+
disable_sleep: Annotated[bool, typer.Option()] = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Simulate inputs from a given .gpx file"""
|
|
50
|
+
with DvtSecureSocketProxyService(service_provider) as dvt:
|
|
51
|
+
LocationSimulation(dvt).play_gpx_file(
|
|
52
|
+
str(filename),
|
|
53
|
+
disable_sleep=disable_sleep,
|
|
54
|
+
timing_randomness_range=timing_randomness_range,
|
|
55
|
+
)
|
|
56
|
+
OSUTILS.wait_return()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from typing import Annotated, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer_injector import InjectingTyper
|
|
7
|
+
|
|
8
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep
|
|
9
|
+
from pymobiledevice3.cli.developer.dvt.sysmon import process
|
|
10
|
+
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
|
|
11
|
+
from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
cli = InjectingTyper(
|
|
17
|
+
name="sysmon",
|
|
18
|
+
help="System monitor options.",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
cli.add_typer(process.cli)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli.command("system")
|
|
25
|
+
def sysmon_system(
|
|
26
|
+
service_provider: ServiceProviderDep,
|
|
27
|
+
fields: Annotated[
|
|
28
|
+
Optional[str],
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--fields",
|
|
31
|
+
"-f",
|
|
32
|
+
help='field names separated by ",".',
|
|
33
|
+
),
|
|
34
|
+
] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""show current system stats."""
|
|
37
|
+
|
|
38
|
+
split_fields = fields.split(",") if fields is not None else None
|
|
39
|
+
|
|
40
|
+
with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
|
|
41
|
+
sysmontap = Sysmontap(dvt)
|
|
42
|
+
with sysmontap as sysmon:
|
|
43
|
+
system = None
|
|
44
|
+
system_usage = None
|
|
45
|
+
system_usage_seen = False # Tracks if the first occurrence of SystemCPUUsage
|
|
46
|
+
|
|
47
|
+
for row in sysmon:
|
|
48
|
+
if "System" in row and system is None:
|
|
49
|
+
system = sysmon.system_attributes_cls(*row["System"])
|
|
50
|
+
|
|
51
|
+
if "SystemCPUUsage" in row:
|
|
52
|
+
if system_usage_seen:
|
|
53
|
+
system_usage = {
|
|
54
|
+
**row["SystemCPUUsage"],
|
|
55
|
+
"CPUCount": row["CPUCount"],
|
|
56
|
+
"EnabledCPUs": row["EnabledCPUs"],
|
|
57
|
+
}
|
|
58
|
+
else: # Ignore the first occurrence because first occurrence always gives a incorrect value - 100 or 0
|
|
59
|
+
system_usage_seen = True
|
|
60
|
+
|
|
61
|
+
if system and system_usage:
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
assert system is not None and system_usage is not None # for type checker
|
|
65
|
+
|
|
66
|
+
attrs_dict = {**asdict(system), **system_usage}
|
|
67
|
+
for name, value in attrs_dict.items():
|
|
68
|
+
if (split_fields is None) or (name in fields):
|
|
69
|
+
print(f"{name}: {value}")
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from collections import namedtuple
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from typer_injector import InjectingTyper
|
|
12
|
+
|
|
13
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep, default_json_encoder, print_json
|
|
14
|
+
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
|
|
15
|
+
from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo
|
|
16
|
+
from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli = InjectingTyper(
|
|
22
|
+
name="process",
|
|
23
|
+
help="Process monitor options.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cli.command("monitor")
|
|
29
|
+
def sysmon_process_monitor(service_provider: ServiceProviderDep, threshold: float) -> None:
|
|
30
|
+
"""monitor all most consuming processes by given cpuUsage threshold."""
|
|
31
|
+
|
|
32
|
+
Process = namedtuple("process", "pid name cpuUsage physFootprint")
|
|
33
|
+
|
|
34
|
+
with DvtSecureSocketProxyService(lockdown=service_provider) as dvt, Sysmontap(dvt) as sysmon:
|
|
35
|
+
for process_snapshot in sysmon.iter_processes():
|
|
36
|
+
entries = []
|
|
37
|
+
for process in process_snapshot:
|
|
38
|
+
if (process["cpuUsage"] is not None) and (process["cpuUsage"] >= threshold):
|
|
39
|
+
entries.append(
|
|
40
|
+
Process(
|
|
41
|
+
pid=process["pid"],
|
|
42
|
+
name=process["name"],
|
|
43
|
+
cpuUsage=process["cpuUsage"],
|
|
44
|
+
physFootprint=process["physFootprint"],
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger.info(entries)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@cli.command("single")
|
|
52
|
+
def sysmon_process_single(
|
|
53
|
+
service_provider: ServiceProviderDep,
|
|
54
|
+
attributes: Annotated[
|
|
55
|
+
Optional[list[str]],
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--attributes",
|
|
58
|
+
"-a",
|
|
59
|
+
help="filter processes by given attribute value given as key=value. Can be specified multiple times.",
|
|
60
|
+
),
|
|
61
|
+
] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""show a single snapshot of currently running processes."""
|
|
64
|
+
|
|
65
|
+
count = 0
|
|
66
|
+
|
|
67
|
+
result = []
|
|
68
|
+
with DvtSecureSocketProxyService(lockdown=service_provider) as dvt:
|
|
69
|
+
device_info = DeviceInfo(dvt)
|
|
70
|
+
|
|
71
|
+
with Sysmontap(dvt) as sysmon:
|
|
72
|
+
for process_snapshot in sysmon.iter_processes():
|
|
73
|
+
count += 1
|
|
74
|
+
|
|
75
|
+
if count < 2:
|
|
76
|
+
# first sample doesn't contain an initialized value for cpuUsage
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
for process in process_snapshot:
|
|
80
|
+
skip = False
|
|
81
|
+
if attributes is not None:
|
|
82
|
+
for filter_attr in attributes:
|
|
83
|
+
filter_attr, filter_value = filter_attr.split("=", 1)
|
|
84
|
+
if str(process[filter_attr]) != filter_value:
|
|
85
|
+
skip = True
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
if skip:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# adding "artificially" the execName field
|
|
92
|
+
process["execName"] = device_info.execname_for_pid(process["pid"])
|
|
93
|
+
result.append(process)
|
|
94
|
+
|
|
95
|
+
# exit after single snapshot
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
print_json(result)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@cli.command("monitor-single")
|
|
102
|
+
def sysmon_process_monitor_single(
|
|
103
|
+
service_provider: ServiceProviderDep,
|
|
104
|
+
attributes: Annotated[
|
|
105
|
+
Optional[list[str]],
|
|
106
|
+
typer.Option(
|
|
107
|
+
"--attributes",
|
|
108
|
+
"-a",
|
|
109
|
+
help="filter processes by attribute (key=value). Multiple filters on same attribute use OR logic, different attributes use AND.",
|
|
110
|
+
),
|
|
111
|
+
] = None,
|
|
112
|
+
output: Annotated[
|
|
113
|
+
Optional[Path],
|
|
114
|
+
typer.Option(
|
|
115
|
+
"--output",
|
|
116
|
+
"-o",
|
|
117
|
+
help="output file path for JSONL format (optional, defaults to stdout)",
|
|
118
|
+
),
|
|
119
|
+
] = None,
|
|
120
|
+
interval: Annotated[
|
|
121
|
+
Optional[int],
|
|
122
|
+
typer.Option(
|
|
123
|
+
"--interval",
|
|
124
|
+
"-i",
|
|
125
|
+
help="minimum interval in milliseconds between outputs (optional)",
|
|
126
|
+
),
|
|
127
|
+
] = None,
|
|
128
|
+
duration: Annotated[
|
|
129
|
+
Optional[int],
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--duration",
|
|
132
|
+
"-d",
|
|
133
|
+
help="maximum duration in milliseconds to run monitoring (optional)",
|
|
134
|
+
),
|
|
135
|
+
] = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Continuously monitor a single process with comprehensive metrics."""
|
|
138
|
+
count = 0
|
|
139
|
+
start_time = None
|
|
140
|
+
|
|
141
|
+
# Parse attributes into grouped filters: same attribute uses OR, different attributes use AND
|
|
142
|
+
parsed_filters: dict[str, list[str]] = {}
|
|
143
|
+
if attributes:
|
|
144
|
+
for raw in attributes:
|
|
145
|
+
key, value = raw.split("=", 1)
|
|
146
|
+
parsed_filters.setdefault(key, []).append(value)
|
|
147
|
+
|
|
148
|
+
def matches_filters(proc: dict) -> bool:
|
|
149
|
+
"""Check if process matches all filter criteria."""
|
|
150
|
+
if not parsed_filters:
|
|
151
|
+
return True
|
|
152
|
+
return all(str(proc.get(key)) in values for key, values in parsed_filters.items())
|
|
153
|
+
|
|
154
|
+
with contextlib.ExitStack() as stack:
|
|
155
|
+
output_file = stack.enter_context(open(output, "w")) if output else None
|
|
156
|
+
|
|
157
|
+
dvt = stack.enter_context(DvtSecureSocketProxyService(lockdown=service_provider))
|
|
158
|
+
sysmon = stack.enter_context(Sysmontap(dvt))
|
|
159
|
+
|
|
160
|
+
for process_snapshot in sysmon.iter_processes():
|
|
161
|
+
count += 1
|
|
162
|
+
|
|
163
|
+
if count < 2:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if start_time is None:
|
|
167
|
+
start_time = time.time()
|
|
168
|
+
|
|
169
|
+
if duration is not None:
|
|
170
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
171
|
+
if elapsed_ms >= duration:
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
for process in process_snapshot:
|
|
175
|
+
if not matches_filters(process):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
process["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
179
|
+
|
|
180
|
+
if output_file:
|
|
181
|
+
json_output = json.dumps(process, default=default_json_encoder)
|
|
182
|
+
output_file.write(json_output + "\n")
|
|
183
|
+
output_file.flush()
|
|
184
|
+
else:
|
|
185
|
+
print_json(process)
|
|
186
|
+
|
|
187
|
+
if interval:
|
|
188
|
+
time.sleep(interval / 1000.0)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from ipsw_parser.dsc import create_device_support_layout, get_device_support_path
|
|
8
|
+
from packaging.version import Version
|
|
9
|
+
from typer_injector import InjectingTyper
|
|
10
|
+
|
|
11
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
|
|
12
|
+
from pymobiledevice3.exceptions import RSDRequiredError
|
|
13
|
+
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
|
|
14
|
+
from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
|
|
15
|
+
from pymobiledevice3.services.dtfetchsymbols import DtFetchSymbols
|
|
16
|
+
from pymobiledevice3.services.remote_fetch_symbols import RemoteFetchSymbolsService
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli = InjectingTyper(
|
|
22
|
+
name="fetch-symbols",
|
|
23
|
+
help="Download the DSC (and dyld) from the device",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def fetch_symbols_list_task(service_provider: LockdownServiceProvider) -> None:
|
|
29
|
+
if Version(service_provider.product_version) < Version("17.0"):
|
|
30
|
+
print_json(DtFetchSymbols(service_provider).list_files())
|
|
31
|
+
else:
|
|
32
|
+
if not isinstance(service_provider, RemoteServiceDiscoveryService):
|
|
33
|
+
raise RSDRequiredError(service_provider.identifier)
|
|
34
|
+
|
|
35
|
+
async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
|
|
36
|
+
print_json([f.file_path for f in await fetch_symbols.get_dsc_file_list()])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command("list")
|
|
40
|
+
def fetch_symbols_list(service_provider: ServiceProviderDep) -> None:
|
|
41
|
+
"""list of files to be downloaded"""
|
|
42
|
+
asyncio.run(fetch_symbols_list_task(service_provider), debug=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def fetch_symbols_download_task(service_provider: LockdownServiceProvider, out: Optional[Path] = None) -> None:
|
|
46
|
+
should_create_device_support_layout = False
|
|
47
|
+
if out is None:
|
|
48
|
+
assert service_provider.product_type is not None # for type checker
|
|
49
|
+
out = get_device_support_path(
|
|
50
|
+
service_provider.product_type, service_provider.product_version, service_provider.product_build_version
|
|
51
|
+
)
|
|
52
|
+
should_create_device_support_layout = True
|
|
53
|
+
|
|
54
|
+
logger.info(f"Downloading DSC into: {out}")
|
|
55
|
+
|
|
56
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
if Version(service_provider.product_version) < Version("17.0"):
|
|
59
|
+
fetch_symbols = DtFetchSymbols(service_provider)
|
|
60
|
+
files = fetch_symbols.list_files()
|
|
61
|
+
|
|
62
|
+
downloaded_files = set()
|
|
63
|
+
|
|
64
|
+
for i, file in enumerate(files):
|
|
65
|
+
if file.startswith("/"):
|
|
66
|
+
# trim root to allow relative download
|
|
67
|
+
file = file[1:]
|
|
68
|
+
file = out / file
|
|
69
|
+
|
|
70
|
+
if file not in downloaded_files:
|
|
71
|
+
# first time the file was seen in list, means we can safely remove any old copy if any
|
|
72
|
+
file.unlink(missing_ok=True)
|
|
73
|
+
|
|
74
|
+
downloaded_files.add(file)
|
|
75
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
with open(file, "ab") as f:
|
|
77
|
+
# same file may appear twice, so we'll need to append data into it
|
|
78
|
+
logger.info(f"writing to: {file}")
|
|
79
|
+
fetch_symbols.get_file(i, f)
|
|
80
|
+
else:
|
|
81
|
+
if not isinstance(service_provider, RemoteServiceDiscoveryService):
|
|
82
|
+
raise RSDRequiredError(service_provider.identifier)
|
|
83
|
+
async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
|
|
84
|
+
await fetch_symbols.download(out)
|
|
85
|
+
|
|
86
|
+
if should_create_device_support_layout:
|
|
87
|
+
assert service_provider.product_type is not None # for type checker
|
|
88
|
+
create_device_support_layout(
|
|
89
|
+
service_provider.product_type, service_provider.product_version, service_provider.product_build_version, out
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@cli.command("download")
|
|
94
|
+
def fetch_symbols_download(
|
|
95
|
+
service_provider: ServiceProviderDep,
|
|
96
|
+
out: Annotated[
|
|
97
|
+
Optional[Path],
|
|
98
|
+
typer.Argument(dir_okay=True, file_okay=False),
|
|
99
|
+
] = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Fetches symbols from the given device and saves them into Xcode DeviceSupport directory.
|
|
103
|
+
|
|
104
|
+
This command downloads symbol data. Optionally, the user can specify an output directory where the data will
|
|
105
|
+
be stored. If no output directory is provided, the symbols will be downloaded into the Xcode directory directly
|
|
106
|
+
(DeviceSupport).
|
|
107
|
+
"""
|
|
108
|
+
asyncio.run(fetch_symbols_download_task(service_provider, out), debug=True)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer_injector import InjectingTyper
|
|
7
|
+
|
|
8
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep
|
|
9
|
+
from pymobiledevice3.services.simulate_location import DtSimulateLocation
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
cli = InjectingTyper(
|
|
15
|
+
name="simulate-location",
|
|
16
|
+
help="Simulate GPS location (set, clear, or replay GPX routes).",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cli.command("clear")
|
|
22
|
+
def simulate_location_clear(service_provider: ServiceProviderDep) -> None:
|
|
23
|
+
"""Stop location simulation and resume real GPS."""
|
|
24
|
+
DtSimulateLocation(service_provider).clear()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli.command("set")
|
|
28
|
+
def simulate_location_set(service_provider: ServiceProviderDep, latitude: float, longitude: float) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Set a fixed simulated location (latitude, longitude).
|
|
31
|
+
Example: `set 40.690008 -74.045843` (Liberty Island).
|
|
32
|
+
"""
|
|
33
|
+
DtSimulateLocation(service_provider).set(latitude, longitude)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command("play")
|
|
37
|
+
def simulate_location_play(
|
|
38
|
+
service_provider: ServiceProviderDep,
|
|
39
|
+
filename: Annotated[
|
|
40
|
+
Path,
|
|
41
|
+
typer.Argument(exists=True, file_okay=True, dir_okay=False),
|
|
42
|
+
],
|
|
43
|
+
timing_randomness_range: int,
|
|
44
|
+
disable_sleep: Annotated[bool, typer.Option()] = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Replay a GPX route; optionally disable sleeps and add timing jitter."""
|
|
47
|
+
DtSimulateLocation(service_provider).play_gpx_file(
|
|
48
|
+
str(filename),
|
|
49
|
+
disable_sleep=disable_sleep,
|
|
50
|
+
timing_randomness_range=timing_randomness_range,
|
|
51
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Annotated, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from typer_injector import InjectingTyper
|
|
6
|
+
|
|
7
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
|
|
8
|
+
from pymobiledevice3.cli.diagnostics import battery
|
|
9
|
+
from pymobiledevice3.lockdown import retry_create_using_usbmux
|
|
10
|
+
from pymobiledevice3.services.diagnostics import DiagnosticsService
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
cli = InjectingTyper(
|
|
16
|
+
name="diagnostics",
|
|
17
|
+
help="Reboot/Shutdown device or access other diagnostics services",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
cli.add_typer(battery.cli)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cli.command("restart")
|
|
24
|
+
def diagnostics_restart(
|
|
25
|
+
service_provider: ServiceProviderDep,
|
|
26
|
+
reconnect: Annotated[
|
|
27
|
+
bool,
|
|
28
|
+
typer.Option(
|
|
29
|
+
"--reconnect",
|
|
30
|
+
"-r",
|
|
31
|
+
help="Wait until the device reconnects before finishing the operation.",
|
|
32
|
+
),
|
|
33
|
+
] = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Restart device"""
|
|
36
|
+
DiagnosticsService(lockdown=service_provider).restart()
|
|
37
|
+
if reconnect:
|
|
38
|
+
# Wait for the device to be available again
|
|
39
|
+
with retry_create_using_usbmux(None, serial=service_provider.udid):
|
|
40
|
+
print(f"Device Reconnected ({service_provider.udid}).")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cli.command("shutdown")
|
|
44
|
+
def diagnostics_shutdown(service_provider: ServiceProviderDep) -> None:
|
|
45
|
+
"""Shutdown device"""
|
|
46
|
+
DiagnosticsService(lockdown=service_provider).shutdown()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@cli.command("sleep")
|
|
50
|
+
def diagnostics_sleep(service_provider: ServiceProviderDep) -> None:
|
|
51
|
+
"""Put device into sleep"""
|
|
52
|
+
DiagnosticsService(lockdown=service_provider).sleep()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cli.command("info")
|
|
56
|
+
def diagnostics_info(service_provider: ServiceProviderDep) -> None:
|
|
57
|
+
"""Get diagnostics info"""
|
|
58
|
+
print_json(DiagnosticsService(lockdown=service_provider).info())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@cli.command("ioregistry")
|
|
62
|
+
def diagnostics_ioregistry(
|
|
63
|
+
service_provider: ServiceProviderDep,
|
|
64
|
+
plane: Annotated[Optional[str], typer.Option()] = None,
|
|
65
|
+
name: Annotated[Optional[str], typer.Option()] = None,
|
|
66
|
+
ioclass: Annotated[Optional[str], typer.Option()] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Get ioregistry info"""
|
|
69
|
+
print_json(DiagnosticsService(lockdown=service_provider).ioregistry(plane=plane, name=name, ioclass=ioclass))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cli.command("mg")
|
|
73
|
+
def diagnostics_mg(service_provider: ServiceProviderDep, keys: Optional[list[str]] = None) -> None:
|
|
74
|
+
"""Get MobileGestalt key values from given list. If empty, return all known."""
|
|
75
|
+
print_json(DiagnosticsService(lockdown=service_provider).mobilegestalt(keys=keys))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from typer_injector import InjectingTyper
|
|
5
|
+
|
|
6
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
|
|
7
|
+
from pymobiledevice3.services.diagnostics import DiagnosticsService
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
cli = InjectingTyper(
|
|
13
|
+
name="battery",
|
|
14
|
+
help="Battery options",
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@cli.command("single")
|
|
20
|
+
def diagnostics_battery_single(service_provider: ServiceProviderDep) -> None:
|
|
21
|
+
"""get single snapshot of battery data"""
|
|
22
|
+
raw_info = DiagnosticsService(lockdown=service_provider).get_battery()
|
|
23
|
+
print_json(raw_info)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@cli.command("monitor")
|
|
27
|
+
def diagnostics_battery_monitor(service_provider: ServiceProviderDep) -> None:
|
|
28
|
+
"""monitor battery usage"""
|
|
29
|
+
diagnostics = DiagnosticsService(lockdown=service_provider)
|
|
30
|
+
while True:
|
|
31
|
+
raw_info = diagnostics.get_battery()
|
|
32
|
+
info = {
|
|
33
|
+
"InstantAmperage": raw_info.get("InstantAmperage"),
|
|
34
|
+
"Temperature": raw_info.get("Temperature"),
|
|
35
|
+
"Voltage": raw_info.get("Voltage"),
|
|
36
|
+
"IsCharging": raw_info.get("IsCharging"),
|
|
37
|
+
"CurrentCapacity": raw_info.get("CurrentCapacity"),
|
|
38
|
+
}
|
|
39
|
+
logger.info(info)
|
|
40
|
+
time.sleep(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cli.command("wifi")
|
|
44
|
+
def diagnostics_wifi(service_provider: ServiceProviderDep) -> None:
|
|
45
|
+
"""Query WiFi info from IORegistry"""
|
|
46
|
+
raw_info = DiagnosticsService(lockdown=service_provider).get_wifi()
|
|
47
|
+
print_json(raw_info)
|
pymobiledevice3/cli/idam.py
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from textwrap import dedent
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
from typer_injector import InjectingTyper
|
|
4
5
|
|
|
5
|
-
from pymobiledevice3.cli.cli_common import
|
|
6
|
-
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
|
|
6
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
|
|
7
7
|
from pymobiledevice3.services.idam import IDAMService
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
cli = InjectingTyper(
|
|
13
|
+
name="idam",
|
|
14
|
+
help=dedent("""\
|
|
15
|
+
Access IDAM (Inter-Device Audio and MIDI) configuration
|
|
15
16
|
|
|
17
|
+
For more info refer to:
|
|
18
|
+
<https://www.youtube.com/watch?v=IXmP938brnc>
|
|
19
|
+
"""),
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
16
22
|
|
|
17
|
-
@cli.group("idam")
|
|
18
|
-
def idam() -> None:
|
|
19
|
-
"""
|
|
20
|
-
Access IDAM (Inter-Device Audio and MIDI) configuration
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@idam.command(cls=Command)
|
|
29
|
-
def configuration_inquiry(service_provider: LockdownServiceProvider) -> None:
|
|
24
|
+
@cli.command()
|
|
25
|
+
def configuration_inquiry(service_provider: ServiceProviderDep) -> None:
|
|
30
26
|
"""Inquiry IDAM configuration"""
|
|
31
27
|
with IDAMService(service_provider) as idam:
|
|
32
28
|
print_json(idam.configuration_inquiry())
|
|
33
29
|
|
|
34
30
|
|
|
35
|
-
@
|
|
36
|
-
def enable(service_provider:
|
|
31
|
+
@cli.command()
|
|
32
|
+
def enable(service_provider: ServiceProviderDep) -> None:
|
|
37
33
|
"""Enable IDAM"""
|
|
38
34
|
with IDAMService(service_provider) as idam:
|
|
39
35
|
idam.set_idam_configuration(True)
|
|
40
36
|
|
|
41
37
|
|
|
42
|
-
@
|
|
43
|
-
def disable(service_provider:
|
|
38
|
+
@cli.command()
|
|
39
|
+
def disable(service_provider: ServiceProviderDep) -> None:
|
|
44
40
|
"""Disable IDAM"""
|
|
45
41
|
with IDAMService(service_provider) as idam:
|
|
46
42
|
idam.set_idam_configuration(False)
|