uiautodev 0.3.3__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 uiautodev might be problematic. Click here for more details.
- uiautodev/__init__.py +12 -0
- uiautodev/__main__.py +10 -0
- uiautodev/app.py +92 -0
- uiautodev/appium_proxy.py +53 -0
- uiautodev/case.py +137 -0
- uiautodev/cli.py +171 -0
- uiautodev/command_proxy.py +154 -0
- uiautodev/command_types.py +89 -0
- uiautodev/driver/android.py +228 -0
- uiautodev/driver/appium.py +136 -0
- uiautodev/driver/base_driver.py +76 -0
- uiautodev/driver/ios.py +114 -0
- uiautodev/driver/mock.py +74 -0
- uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- uiautodev/driver/udt/udt.py +259 -0
- uiautodev/exceptions.py +32 -0
- uiautodev/model.py +37 -0
- uiautodev/provider.py +76 -0
- uiautodev/router/device.py +104 -0
- uiautodev/router/xml.py +28 -0
- uiautodev/static/demo.html +34 -0
- uiautodev/utils/common.py +166 -0
- uiautodev/utils/exceptions.py +43 -0
- uiautodev/utils/usbmux.py +485 -0
- uiautodev-0.3.3.dist-info/LICENSE +21 -0
- uiautodev-0.3.3.dist-info/METADATA +56 -0
- uiautodev-0.3.3.dist-info/RECORD +29 -0
- uiautodev-0.3.3.dist-info/WHEEL +4 -0
- uiautodev-0.3.3.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import json as sysjson
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
import socket
|
|
8
|
+
import sys
|
|
9
|
+
import typing
|
|
10
|
+
import uuid
|
|
11
|
+
from http.client import HTTPConnection, HTTPResponse
|
|
12
|
+
from typing import Optional, TypeVar, Union
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
from pygments import formatters, highlight, lexers
|
|
16
|
+
|
|
17
|
+
from uiautodev.exceptions import RequestError
|
|
18
|
+
from uiautodev.model import Node
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_output_terminal() -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Check if the standard output is attached to a terminal.
|
|
24
|
+
"""
|
|
25
|
+
return sys.stdout.isatty()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def enable_windows_ansi_support():
|
|
29
|
+
if platform.system().lower() == "windows" and is_output_terminal():
|
|
30
|
+
import ctypes
|
|
31
|
+
|
|
32
|
+
kernel32 = ctypes.windll.kernel32
|
|
33
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def default_json_encoder(obj):
|
|
37
|
+
if isinstance(obj, bytes):
|
|
38
|
+
return f'<{obj.hex()}>'
|
|
39
|
+
if isinstance(obj, datetime.datetime):
|
|
40
|
+
return str(obj)
|
|
41
|
+
if isinstance(obj, uuid.UUID):
|
|
42
|
+
return str(obj)
|
|
43
|
+
if isinstance(obj, BaseModel):
|
|
44
|
+
return obj.model_dump()
|
|
45
|
+
raise TypeError()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_json(buf, colored=None, default=default_json_encoder):
|
|
49
|
+
""" copy from pymobiledevice3 """
|
|
50
|
+
formatted_json = sysjson.dumps(buf, sort_keys=True, indent=4, default=default)
|
|
51
|
+
if colored is None:
|
|
52
|
+
if is_output_terminal():
|
|
53
|
+
colored = True
|
|
54
|
+
enable_windows_ansi_support()
|
|
55
|
+
else:
|
|
56
|
+
colored = False
|
|
57
|
+
|
|
58
|
+
if colored:
|
|
59
|
+
colorful_json = highlight(formatted_json, lexers.JsonLexer(),
|
|
60
|
+
formatters.TerminalTrueColorFormatter(style='stata-dark'))
|
|
61
|
+
print(colorful_json)
|
|
62
|
+
else:
|
|
63
|
+
print(formatted_json)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_T = TypeVar("_T")
|
|
67
|
+
|
|
68
|
+
def convert_to_type(value: str, _type: _T) -> _T:
|
|
69
|
+
""" usage example:
|
|
70
|
+
convert_to_type("123", int)
|
|
71
|
+
"""
|
|
72
|
+
if _type in (int, float, str):
|
|
73
|
+
return _type(value)
|
|
74
|
+
if _type == bool:
|
|
75
|
+
return value.lower() in ("true", "1")
|
|
76
|
+
if _type == Union[int, float]:
|
|
77
|
+
return float(value) if "." in value else int(value)
|
|
78
|
+
if _type == re.Pattern:
|
|
79
|
+
return re.compile(value)
|
|
80
|
+
raise NotImplementedError(f"convert {value} to {_type}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def convert_params_to_model(params: list[str], model: BaseModel) -> BaseModel:
|
|
84
|
+
""" used in cli.py """
|
|
85
|
+
assert len(params) > 0
|
|
86
|
+
if len(params) == 1:
|
|
87
|
+
try:
|
|
88
|
+
return model.model_validate_json(params)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print("module_parse_error", e)
|
|
91
|
+
|
|
92
|
+
value = {}
|
|
93
|
+
type_hints = typing.get_type_hints(model)
|
|
94
|
+
for p in params:
|
|
95
|
+
if "=" not in p:
|
|
96
|
+
_type = type_hints.get(p)
|
|
97
|
+
if _type == bool:
|
|
98
|
+
value[p] = True
|
|
99
|
+
continue
|
|
100
|
+
elif _type is None:
|
|
101
|
+
print(f"unknown key: {p}")
|
|
102
|
+
continue
|
|
103
|
+
raise ValueError(f"missing value for {p}")
|
|
104
|
+
k, v = p.split("=", 1)
|
|
105
|
+
_type = type_hints.get(k)
|
|
106
|
+
if _type is None:
|
|
107
|
+
print(f"unknown key: {k}")
|
|
108
|
+
continue
|
|
109
|
+
value[k] = convert_to_type(v, _type)
|
|
110
|
+
return model.model_validate(value)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SocketHTTPConnection(HTTPConnection):
|
|
114
|
+
def __init__(self, conn: socket.socket, timeout: float):
|
|
115
|
+
super().__init__("localhost", timeout=timeout)
|
|
116
|
+
self.__conn = conn
|
|
117
|
+
|
|
118
|
+
def connect(self):
|
|
119
|
+
self.sock = self.__conn
|
|
120
|
+
|
|
121
|
+
def __enter__(self) -> HTTPConnection:
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
125
|
+
self.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MySocketHTTPConnection(SocketHTTPConnection):
|
|
129
|
+
def connect(self):
|
|
130
|
+
super().connect()
|
|
131
|
+
self.sock.settimeout(self.timeout)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def fetch_through_socket(sock: socket.socket, path: str, method: str = "GET", json: Optional[dict] = None, timeout: float = 60) -> bytearray:
|
|
135
|
+
""" usage example:
|
|
136
|
+
with socket.create_connection((host, port)) as s:
|
|
137
|
+
request_through_socket(s, "GET", "/")
|
|
138
|
+
"""
|
|
139
|
+
conn = MySocketHTTPConnection(sock, timeout)
|
|
140
|
+
try:
|
|
141
|
+
if json is None:
|
|
142
|
+
conn.request(method, path)
|
|
143
|
+
else:
|
|
144
|
+
conn.request(method, path, body=sysjson.dumps(json), headers={"Content-Type": "application/json"})
|
|
145
|
+
response = conn.getresponse()
|
|
146
|
+
if response.getcode() != 200:
|
|
147
|
+
raise RequestError(f"request {method} {path}, status: {response.getcode()}")
|
|
148
|
+
content = bytearray()
|
|
149
|
+
while chunk := response.read(40960):
|
|
150
|
+
content.extend(chunk)
|
|
151
|
+
return content
|
|
152
|
+
finally:
|
|
153
|
+
conn.close()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def node_travel(node: Node, dfs: bool = True):
|
|
157
|
+
""" usage example:
|
|
158
|
+
for n in node_travel(node):
|
|
159
|
+
print(n)
|
|
160
|
+
"""
|
|
161
|
+
if not dfs:
|
|
162
|
+
yield node
|
|
163
|
+
for child in node.children:
|
|
164
|
+
yield from node_travel(child, dfs)
|
|
165
|
+
if dfs:
|
|
166
|
+
yield node
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Tue Mar 05 2024 10:18:09 by codeskyblue
|
|
5
|
+
|
|
6
|
+
Copy from https://github.com/doronz88/pymobiledevice3
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from uiautodev.exceptions import IOSDriverException
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotPairedError(IOSDriverException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MuxException(IOSDriverException):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MuxVersionError(MuxException):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BadCommandError(MuxException):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BadDevError(MuxException):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConnectionFailedError(MuxException):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ConnectionFailedToUsbmuxdError(ConnectionFailedError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ArgumentError(IOSDriverException):
|
|
43
|
+
pass
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copy from https://github.com/doronz88/pymobiledevice3
|
|
3
|
+
|
|
4
|
+
Add http.client.HTTPConnection
|
|
5
|
+
"""
|
|
6
|
+
import abc
|
|
7
|
+
import plistlib
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from http.client import HTTPConnection
|
|
13
|
+
from typing import List, Mapping, Optional
|
|
14
|
+
|
|
15
|
+
from construct import Const, CString, Enum, FixedSized, GreedyBytes, Int16ul, Int32ul, Padding, Prefixed, StreamError, \
|
|
16
|
+
Struct, Switch, this
|
|
17
|
+
|
|
18
|
+
from uiautodev.utils.exceptions import BadCommandError, BadDevError, ConnectionFailedError, \
|
|
19
|
+
ConnectionFailedToUsbmuxdError, MuxException, MuxVersionError, NotPairedError
|
|
20
|
+
|
|
21
|
+
usbmuxd_version = Enum(Int32ul,
|
|
22
|
+
BINARY=0,
|
|
23
|
+
PLIST=1,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
usbmuxd_result = Enum(Int32ul,
|
|
27
|
+
OK=0,
|
|
28
|
+
BADCOMMAND=1,
|
|
29
|
+
BADDEV=2,
|
|
30
|
+
CONNREFUSED=3,
|
|
31
|
+
BADVERSION=6,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
usbmuxd_msgtype = Enum(Int32ul,
|
|
35
|
+
RESULT=1,
|
|
36
|
+
CONNECT=2,
|
|
37
|
+
LISTEN=3,
|
|
38
|
+
ADD=4,
|
|
39
|
+
REMOVE=5,
|
|
40
|
+
PAIRED=6,
|
|
41
|
+
PLIST=8,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
usbmuxd_header = Struct(
|
|
45
|
+
'version' / usbmuxd_version, # protocol version
|
|
46
|
+
'message' / usbmuxd_msgtype, # message type
|
|
47
|
+
'tag' / Int32ul, # responses to this query will echo back this tag
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
usbmuxd_request = Prefixed(Int32ul, Struct(
|
|
51
|
+
'header' / usbmuxd_header,
|
|
52
|
+
'data' / Switch(this.header.message, {
|
|
53
|
+
usbmuxd_msgtype.CONNECT: Struct(
|
|
54
|
+
'device_id' / Int32ul,
|
|
55
|
+
'port' / Int16ul, # TCP port number
|
|
56
|
+
'reserved' / Const(0, Int16ul),
|
|
57
|
+
),
|
|
58
|
+
usbmuxd_msgtype.PLIST: GreedyBytes,
|
|
59
|
+
}),
|
|
60
|
+
), includelength=True)
|
|
61
|
+
|
|
62
|
+
usbmuxd_device_record = Struct(
|
|
63
|
+
'device_id' / Int32ul,
|
|
64
|
+
'product_id' / Int16ul,
|
|
65
|
+
'serial_number' / FixedSized(256, CString('ascii')),
|
|
66
|
+
Padding(2),
|
|
67
|
+
'location' / Int32ul
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
usbmuxd_response = Prefixed(Int32ul, Struct(
|
|
71
|
+
'header' / usbmuxd_header,
|
|
72
|
+
'data' / Switch(this.header.message, {
|
|
73
|
+
usbmuxd_msgtype.RESULT: Struct(
|
|
74
|
+
'result' / usbmuxd_result,
|
|
75
|
+
),
|
|
76
|
+
usbmuxd_msgtype.ADD: usbmuxd_device_record,
|
|
77
|
+
usbmuxd_msgtype.REMOVE: Struct(
|
|
78
|
+
'device_id' / Int32ul,
|
|
79
|
+
),
|
|
80
|
+
usbmuxd_msgtype.PLIST: GreedyBytes,
|
|
81
|
+
}),
|
|
82
|
+
), includelength=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class MuxDevice:
|
|
89
|
+
devid: int
|
|
90
|
+
serial: str
|
|
91
|
+
connection_type: str
|
|
92
|
+
|
|
93
|
+
def connect(self, port: int, usbmux_address: Optional[str] = None) -> socket.socket:
|
|
94
|
+
mux = create_mux(usbmux_address=usbmux_address)
|
|
95
|
+
try:
|
|
96
|
+
return mux.connect(self, port)
|
|
97
|
+
except: # noqa: E722
|
|
98
|
+
mux.close()
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def is_usb(self) -> bool:
|
|
103
|
+
return self.connection_type == 'USB'
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def is_network(self) -> bool:
|
|
107
|
+
return self.connection_type == 'Network'
|
|
108
|
+
|
|
109
|
+
def matches_udid(self, udid: str) -> bool:
|
|
110
|
+
return self.serial.replace('-', '') == udid.replace('-', '')
|
|
111
|
+
|
|
112
|
+
def make_http_connection(self, port: int) -> HTTPConnection:
|
|
113
|
+
return USBMuxHTTPConnection(self, port)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SafeStreamSocket:
|
|
117
|
+
""" wrapper to native python socket object to be used with construct as a stream """
|
|
118
|
+
|
|
119
|
+
def __init__(self, address, family):
|
|
120
|
+
self._offset = 0
|
|
121
|
+
self.sock = socket.socket(family, socket.SOCK_STREAM)
|
|
122
|
+
self.sock.connect(address)
|
|
123
|
+
|
|
124
|
+
def send(self, msg: bytes) -> int:
|
|
125
|
+
self._offset += len(msg)
|
|
126
|
+
self.sock.sendall(msg)
|
|
127
|
+
return len(msg)
|
|
128
|
+
|
|
129
|
+
def recv(self, size: int) -> bytes:
|
|
130
|
+
msg = b''
|
|
131
|
+
while len(msg) < size:
|
|
132
|
+
chunk = self.sock.recv(size - len(msg))
|
|
133
|
+
self._offset += len(chunk)
|
|
134
|
+
if not chunk:
|
|
135
|
+
raise MuxException('socket connection broken')
|
|
136
|
+
msg += chunk
|
|
137
|
+
return msg
|
|
138
|
+
|
|
139
|
+
def close(self) -> None:
|
|
140
|
+
self.sock.close()
|
|
141
|
+
|
|
142
|
+
def settimeout(self, interval: float) -> None:
|
|
143
|
+
self.sock.settimeout(interval)
|
|
144
|
+
|
|
145
|
+
def setblocking(self, blocking: bool) -> None:
|
|
146
|
+
self.sock.setblocking(blocking)
|
|
147
|
+
|
|
148
|
+
def tell(self) -> int:
|
|
149
|
+
return self._offset
|
|
150
|
+
|
|
151
|
+
read = recv
|
|
152
|
+
write = send
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MuxConnection:
|
|
156
|
+
# used on Windows
|
|
157
|
+
ITUNES_HOST = ('127.0.0.1', 27015)
|
|
158
|
+
|
|
159
|
+
# used for macOS and Linux
|
|
160
|
+
USBMUXD_PIPE = '/var/run/usbmuxd'
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def create_usbmux_socket(usbmux_address: Optional[str] = None) -> SafeStreamSocket:
|
|
164
|
+
try:
|
|
165
|
+
if usbmux_address is not None:
|
|
166
|
+
if ':' in usbmux_address:
|
|
167
|
+
# assume tcp address
|
|
168
|
+
hostname, port = usbmux_address.split(':')
|
|
169
|
+
port = int(port)
|
|
170
|
+
address = (hostname, port)
|
|
171
|
+
family = socket.AF_INET
|
|
172
|
+
else:
|
|
173
|
+
# assume unix domain address
|
|
174
|
+
address = usbmux_address
|
|
175
|
+
family = socket.AF_UNIX
|
|
176
|
+
else:
|
|
177
|
+
if sys.platform in ['win32', 'cygwin']:
|
|
178
|
+
address = MuxConnection.ITUNES_HOST
|
|
179
|
+
family = socket.AF_INET
|
|
180
|
+
else:
|
|
181
|
+
address = MuxConnection.USBMUXD_PIPE
|
|
182
|
+
family = socket.AF_UNIX
|
|
183
|
+
return SafeStreamSocket(address, family)
|
|
184
|
+
except ConnectionRefusedError:
|
|
185
|
+
raise ConnectionFailedToUsbmuxdError()
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def create(usbmux_address: Optional[str] = None):
|
|
189
|
+
# first attempt to connect with possibly the wrong version header (plist protocol)
|
|
190
|
+
sock = MuxConnection.create_usbmux_socket(usbmux_address=usbmux_address)
|
|
191
|
+
|
|
192
|
+
message = usbmuxd_request.build({
|
|
193
|
+
'header': {'version': usbmuxd_version.PLIST, 'message': usbmuxd_msgtype.PLIST, 'tag': 1},
|
|
194
|
+
'data': plistlib.dumps({'MessageType': 'ReadBUID'})
|
|
195
|
+
})
|
|
196
|
+
sock.send(message)
|
|
197
|
+
response = usbmuxd_response.parse_stream(sock)
|
|
198
|
+
|
|
199
|
+
# if we sent a bad request, we should re-create the socket in the correct version this time
|
|
200
|
+
sock.close()
|
|
201
|
+
sock = MuxConnection.create_usbmux_socket(usbmux_address=usbmux_address)
|
|
202
|
+
|
|
203
|
+
if response.header.version == usbmuxd_version.BINARY:
|
|
204
|
+
return BinaryMuxConnection(sock)
|
|
205
|
+
elif response.header.version == usbmuxd_version.PLIST:
|
|
206
|
+
return PlistMuxConnection(sock)
|
|
207
|
+
|
|
208
|
+
raise MuxVersionError(f'usbmuxd returned unsupported version: {response.version}')
|
|
209
|
+
|
|
210
|
+
def __init__(self, sock: SafeStreamSocket):
|
|
211
|
+
self._sock = sock
|
|
212
|
+
|
|
213
|
+
# after initiating the "Connect" packet, this same socket will be used to transfer data into the service
|
|
214
|
+
# residing inside the target device. when this happens, we can no longer send/receive control commands to
|
|
215
|
+
# usbmux on same socket
|
|
216
|
+
self._connected = False
|
|
217
|
+
|
|
218
|
+
# message sequence number. used when verifying the response matched the request
|
|
219
|
+
self._tag = 1
|
|
220
|
+
|
|
221
|
+
self.devices = []
|
|
222
|
+
|
|
223
|
+
@abc.abstractmethod
|
|
224
|
+
def _connect(self, device_id: int, port: int):
|
|
225
|
+
""" initiate a "Connect" request to target port """
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
@abc.abstractmethod
|
|
229
|
+
def get_device_list(self, timeout: float = None):
|
|
230
|
+
"""
|
|
231
|
+
request an update to current device list
|
|
232
|
+
"""
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
def connect(self, device: MuxDevice, port: int) -> socket.socket:
|
|
236
|
+
""" connect to a relay port on target machine and get a raw python socket object for the connection """
|
|
237
|
+
self._connect(device.devid, socket.htons(port))
|
|
238
|
+
self._connected = True
|
|
239
|
+
return self._sock.sock
|
|
240
|
+
|
|
241
|
+
def close(self):
|
|
242
|
+
""" close current socket """
|
|
243
|
+
self._sock.close()
|
|
244
|
+
|
|
245
|
+
def _assert_not_connected(self):
|
|
246
|
+
""" verify active state is in state for control messages """
|
|
247
|
+
if self._connected:
|
|
248
|
+
raise MuxException('Mux is connected, cannot issue control packets')
|
|
249
|
+
|
|
250
|
+
def _raise_mux_exception(self, result: int, message: str = None):
|
|
251
|
+
exceptions = {
|
|
252
|
+
int(usbmuxd_result.BADCOMMAND): BadCommandError,
|
|
253
|
+
int(usbmuxd_result.BADDEV): BadDevError,
|
|
254
|
+
int(usbmuxd_result.CONNREFUSED): ConnectionFailedError,
|
|
255
|
+
int(usbmuxd_result.BADVERSION): MuxVersionError,
|
|
256
|
+
}
|
|
257
|
+
exception = exceptions.get(result, MuxException)
|
|
258
|
+
raise exception(message)
|
|
259
|
+
|
|
260
|
+
def __enter__(self):
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
264
|
+
self.close()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class BinaryMuxConnection(MuxConnection):
|
|
268
|
+
""" old binary protocol """
|
|
269
|
+
|
|
270
|
+
def __init__(self, sock: SafeStreamSocket):
|
|
271
|
+
super().__init__(sock)
|
|
272
|
+
self._version = usbmuxd_version.BINARY
|
|
273
|
+
|
|
274
|
+
def get_device_list(self, timeout: float = None):
|
|
275
|
+
""" use timeout to wait for the device list to be fully populated """
|
|
276
|
+
self._assert_not_connected()
|
|
277
|
+
end = time.time() + timeout
|
|
278
|
+
self.listen()
|
|
279
|
+
while time.time() < end:
|
|
280
|
+
self._sock.settimeout(end - time.time())
|
|
281
|
+
try:
|
|
282
|
+
self._receive_device_state_update()
|
|
283
|
+
except (BlockingIOError, StreamError):
|
|
284
|
+
continue
|
|
285
|
+
except IOError:
|
|
286
|
+
try:
|
|
287
|
+
self._sock.setblocking(True)
|
|
288
|
+
self.close()
|
|
289
|
+
except OSError:
|
|
290
|
+
pass
|
|
291
|
+
raise MuxException('Exception in listener socket')
|
|
292
|
+
|
|
293
|
+
def listen(self):
|
|
294
|
+
""" start listening for events of attached and detached devices """
|
|
295
|
+
self._send_receive(usbmuxd_msgtype.LISTEN)
|
|
296
|
+
|
|
297
|
+
def _connect(self, device_id: int, port: int):
|
|
298
|
+
self._send({'header': {'version': self._version,
|
|
299
|
+
'message': usbmuxd_msgtype.CONNECT,
|
|
300
|
+
'tag': self._tag},
|
|
301
|
+
'data': {'device_id': device_id, 'port': port},
|
|
302
|
+
})
|
|
303
|
+
response = self._receive()
|
|
304
|
+
if response.header.message != usbmuxd_msgtype.RESULT:
|
|
305
|
+
raise MuxException(f'unexpected message type received: {response}')
|
|
306
|
+
|
|
307
|
+
if response.data.result != usbmuxd_result.OK:
|
|
308
|
+
raise self._raise_mux_exception(int(response.data.result),
|
|
309
|
+
f'failed to connect to device: {device_id} at port: {port}. reason: '
|
|
310
|
+
f'{response.data.result}')
|
|
311
|
+
|
|
312
|
+
def _send(self, data: Mapping):
|
|
313
|
+
self._assert_not_connected()
|
|
314
|
+
self._sock.send(usbmuxd_request.build(data))
|
|
315
|
+
self._tag += 1
|
|
316
|
+
|
|
317
|
+
def _receive(self, expected_tag: int = None):
|
|
318
|
+
self._assert_not_connected()
|
|
319
|
+
response = usbmuxd_response.parse_stream(self._sock)
|
|
320
|
+
if expected_tag and response.header.tag != expected_tag:
|
|
321
|
+
raise MuxException(f'Reply tag mismatch: expected {expected_tag}, got {response.header.tag}')
|
|
322
|
+
return response
|
|
323
|
+
|
|
324
|
+
def _send_receive(self, message_type: int):
|
|
325
|
+
self._send({'header': {'version': self._version, 'message': message_type, 'tag': self._tag},
|
|
326
|
+
'data': b''})
|
|
327
|
+
response = self._receive(self._tag - 1)
|
|
328
|
+
if response.header.message != usbmuxd_msgtype.RESULT:
|
|
329
|
+
raise MuxException(f'unexpected message type received: {response}')
|
|
330
|
+
|
|
331
|
+
result = response.data.result
|
|
332
|
+
if result != usbmuxd_result.OK:
|
|
333
|
+
raise self._raise_mux_exception(int(result), f'{message_type} failed: error {result}')
|
|
334
|
+
|
|
335
|
+
def _add_device(self, device: MuxDevice):
|
|
336
|
+
self.devices.append(device)
|
|
337
|
+
|
|
338
|
+
def _remove_device(self, device_id: int):
|
|
339
|
+
self.devices = [device for device in self.devices if device.devid != device_id]
|
|
340
|
+
|
|
341
|
+
def _receive_device_state_update(self):
|
|
342
|
+
response = self._receive()
|
|
343
|
+
if response.header.message == usbmuxd_msgtype.ADD:
|
|
344
|
+
# old protocol only supported USB devices
|
|
345
|
+
self._add_device(MuxDevice(response.data.device_id, response.data.serial_number, 'USB'))
|
|
346
|
+
elif response.header.message == usbmuxd_msgtype.REMOVE:
|
|
347
|
+
self._remove_device(response.data.device_id)
|
|
348
|
+
else:
|
|
349
|
+
raise MuxException(f'Invalid packet type received: {response}')
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class PlistMuxConnection(BinaryMuxConnection):
|
|
353
|
+
def __init__(self, sock: SafeStreamSocket):
|
|
354
|
+
super().__init__(sock)
|
|
355
|
+
self._version = usbmuxd_version.PLIST
|
|
356
|
+
|
|
357
|
+
def listen(self) -> None:
|
|
358
|
+
self._send_receive({'MessageType': 'Listen'})
|
|
359
|
+
|
|
360
|
+
def get_pair_record(self, serial: str) -> Mapping:
|
|
361
|
+
# serials are saved inside usbmuxd without '-'
|
|
362
|
+
self._send({'MessageType': 'ReadPairRecord', 'PairRecordID': serial})
|
|
363
|
+
response = self._receive(self._tag - 1)
|
|
364
|
+
pair_record = response.get('PairRecordData')
|
|
365
|
+
if pair_record is None:
|
|
366
|
+
raise NotPairedError('device should be paired first')
|
|
367
|
+
return plistlib.loads(pair_record)
|
|
368
|
+
|
|
369
|
+
def get_device_list(self, timeout: float = None) -> None:
|
|
370
|
+
""" get device list synchronously without waiting the timeout """
|
|
371
|
+
self.devices = []
|
|
372
|
+
self._send({'MessageType': 'ListDevices'})
|
|
373
|
+
for response in self._receive(self._tag - 1)['DeviceList']:
|
|
374
|
+
if response['MessageType'] == 'Attached':
|
|
375
|
+
super()._add_device(MuxDevice(response['DeviceID'], response['Properties']['SerialNumber'],
|
|
376
|
+
response['Properties']['ConnectionType']))
|
|
377
|
+
elif response['MessageType'] == 'Detached':
|
|
378
|
+
super()._remove_device(response['DeviceID'])
|
|
379
|
+
else:
|
|
380
|
+
raise MuxException(f'Invalid packet type received: {response}')
|
|
381
|
+
|
|
382
|
+
def get_buid(self) -> str:
|
|
383
|
+
""" get SystemBUID """
|
|
384
|
+
self._send({'MessageType': 'ReadBUID'})
|
|
385
|
+
return self._receive(self._tag - 1)['BUID']
|
|
386
|
+
|
|
387
|
+
def save_pair_record(self, serial: str, device_id: int, record_data: bytes):
|
|
388
|
+
# serials are saved inside usbmuxd without '-'
|
|
389
|
+
self._send_receive({'MessageType': 'SavePairRecord',
|
|
390
|
+
'PairRecordID': serial,
|
|
391
|
+
'PairRecordData': record_data,
|
|
392
|
+
'DeviceID': device_id})
|
|
393
|
+
|
|
394
|
+
def _connect(self, device_id: int, port: int):
|
|
395
|
+
self._send_receive({'MessageType': 'Connect', 'DeviceID': device_id, 'PortNumber': port})
|
|
396
|
+
|
|
397
|
+
def _send(self, data: Mapping):
|
|
398
|
+
request = {'ClientVersionString': 'qt4i-usbmuxd', 'ProgName': 'pymobiledevice3', 'kLibUSBMuxVersion': 3}
|
|
399
|
+
request.update(data)
|
|
400
|
+
super()._send({'header': {'version': self._version,
|
|
401
|
+
'message': usbmuxd_msgtype.PLIST,
|
|
402
|
+
'tag': self._tag},
|
|
403
|
+
'data': plistlib.dumps(request),
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
def _receive(self, expected_tag: int = None) -> Mapping:
|
|
407
|
+
response = super()._receive(expected_tag=expected_tag)
|
|
408
|
+
if response.header.message != usbmuxd_msgtype.PLIST:
|
|
409
|
+
raise MuxException(f'Received non-plist type {response}')
|
|
410
|
+
return plistlib.loads(response.data)
|
|
411
|
+
|
|
412
|
+
def _send_receive(self, data: Mapping):
|
|
413
|
+
self._send(data)
|
|
414
|
+
response = self._receive(self._tag - 1)
|
|
415
|
+
if response['MessageType'] != 'Result':
|
|
416
|
+
raise MuxException(f'got an invalid message: {response}')
|
|
417
|
+
if response['Number'] != 0:
|
|
418
|
+
raise self._raise_mux_exception(response['Number'], f'got an error message: {response}')
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def create_mux(usbmux_address: Optional[str] = None) -> MuxConnection:
|
|
422
|
+
return MuxConnection.create(usbmux_address=usbmux_address)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def list_devices(usbmux_address: Optional[str] = None) -> List[MuxDevice]:
|
|
426
|
+
mux = create_mux(usbmux_address=usbmux_address)
|
|
427
|
+
mux.get_device_list(0.1)
|
|
428
|
+
devices = mux.devices
|
|
429
|
+
mux.close()
|
|
430
|
+
return devices
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def select_device(udid: str = None, connection_type: str = None, usbmux_address: Optional[str] = None) \
|
|
434
|
+
-> Optional[MuxDevice]:
|
|
435
|
+
"""
|
|
436
|
+
select a UsbMux device according to given arguments.
|
|
437
|
+
if more than one device could be selected, always prefer the usb one.
|
|
438
|
+
"""
|
|
439
|
+
tmp = None
|
|
440
|
+
for device in list_devices(usbmux_address=usbmux_address):
|
|
441
|
+
if connection_type is not None and device.connection_type != connection_type:
|
|
442
|
+
# if a specific connection_type was desired and not of this one then skip
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
if udid is not None and not device.matches_udid(udid):
|
|
446
|
+
# if a specific udid was desired and not of this one then skip
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
# save best result as a temporary
|
|
450
|
+
tmp = device
|
|
451
|
+
|
|
452
|
+
if device.is_usb:
|
|
453
|
+
# always prefer usb connection
|
|
454
|
+
return device
|
|
455
|
+
|
|
456
|
+
return tmp
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def select_devices_by_connection_type(connection_type: str, usbmux_address: Optional[str] = None) -> List[MuxDevice]:
|
|
460
|
+
"""
|
|
461
|
+
select all UsbMux devices by connection type
|
|
462
|
+
"""
|
|
463
|
+
tmp = []
|
|
464
|
+
for device in list_devices(usbmux_address=usbmux_address):
|
|
465
|
+
if device.connection_type == connection_type:
|
|
466
|
+
tmp.append(device)
|
|
467
|
+
|
|
468
|
+
return tmp
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class USBMuxHTTPConnection(HTTPConnection):
|
|
473
|
+
def __init__(self, device: MuxDevice, port=8100):
|
|
474
|
+
super().__init__("localhost", port)
|
|
475
|
+
self.__device = device
|
|
476
|
+
self.__port = port
|
|
477
|
+
|
|
478
|
+
def connect(self):
|
|
479
|
+
self.sock = self.__device.connect(self.__port)
|
|
480
|
+
|
|
481
|
+
def __enter__(self) -> HTTPConnection:
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
485
|
+
self.close()
|