Kea2-python 0.1.2__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of Kea2-python might be problematic. Click here for more details.
- kea2/absDriver.py +1 -1
- kea2/adbUtils.py +273 -20
- kea2/bug_report_generator.py +333 -321
- kea2/fastbotManager.py +105 -47
- kea2/keaUtils.py +100 -95
- kea2/kea_launcher.py +15 -0
- kea2/resultSyncer.py +18 -8
- kea2/templates/bug_report_template.html +9 -50
- kea2/u2Driver.py +148 -137
- kea2/utils.py +1 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/METADATA +44 -33
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/RECORD +16 -16
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/WHEEL +0 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/top_level.txt +0 -0
kea2/absDriver.py
CHANGED
kea2/adbUtils.py
CHANGED
|
@@ -1,10 +1,242 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import subprocess
|
|
2
|
-
|
|
3
|
-
from
|
|
3
|
+
import threading
|
|
4
|
+
from typing import List, Optional, Set, Tuple
|
|
5
|
+
|
|
6
|
+
from kea2.utils import getLogger
|
|
7
|
+
from adbutils import AdbDevice, adb
|
|
8
|
+
from typing import IO, TYPE_CHECKING, Generator, Optional, List, Union
|
|
9
|
+
from adbutils import AdbConnection
|
|
4
10
|
|
|
5
11
|
logger = getLogger(__name__)
|
|
6
12
|
|
|
7
13
|
|
|
14
|
+
class ADBDevice(AdbDevice):
|
|
15
|
+
_instance = None
|
|
16
|
+
serial: Optional[str] = None
|
|
17
|
+
transport_id: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
def __new__(cls):
|
|
20
|
+
if cls._instance is None:
|
|
21
|
+
cls._instance = super().__new__(cls)
|
|
22
|
+
return cls._instance
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def setDevice(cls, serial: Optional[str] = None, transport_id: Optional[str] = None):
|
|
26
|
+
ADBDevice.serial = serial or ADBDevice.serial
|
|
27
|
+
ADBDevice.transport_id = transport_id or ADBDevice.transport_id
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> AdbDevice:
|
|
30
|
+
"""
|
|
31
|
+
Initializes the ADBDevice instance.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
device (str, optional): The device serial number. If None, it is resolved automatically when only one device is connected.
|
|
35
|
+
transport_id (str, optional): The transport ID for the device.
|
|
36
|
+
"""
|
|
37
|
+
if not ADBDevice.serial and not ADBDevice.transport_id:
|
|
38
|
+
devices = [d.serial for d in adb.list() if d.state == "device"]
|
|
39
|
+
if len(devices) > 1:
|
|
40
|
+
raise RuntimeError("Multiple devices connected. Please specify a device")
|
|
41
|
+
if len(devices) == 0:
|
|
42
|
+
raise RuntimeError("No device connected.")
|
|
43
|
+
ADBDevice.serial = devices[0]
|
|
44
|
+
super().__init__(client=adb, serial=ADBDevice.serial, transport_id=ADBDevice.transport_id)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def stream_shell(self) -> "ADBStreamShell_V2":
|
|
48
|
+
if "shell_v2" in self.get_features():
|
|
49
|
+
return ADBStreamShell_V2(session=self)
|
|
50
|
+
logger.warning("Using ADBStreamShell_V1. All output will be printed to stdout.")
|
|
51
|
+
return ADBStreamShell_V1(session=self)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StreamShell:
|
|
55
|
+
def __init__(self, session: "ADBDevice"):
|
|
56
|
+
self.dev: ADBDevice = session
|
|
57
|
+
self._thread = None
|
|
58
|
+
self._exit_code = 255
|
|
59
|
+
self.stdout = sys.stdout
|
|
60
|
+
self.stderr = sys.stderr
|
|
61
|
+
|
|
62
|
+
def __call__(self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
63
|
+
stderr: IO = None, timeout: Union[float, None] = None) -> "StreamShell":
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def _write_stdout(self, data: bytes, decode=True):
|
|
67
|
+
text = data.decode('utf-8', errors='ignore') if decode else data
|
|
68
|
+
self.stdout.write(text)
|
|
69
|
+
self.stdout.flush()
|
|
70
|
+
|
|
71
|
+
def _write_stderr(self, data: bytes, decode=True):
|
|
72
|
+
text = data.decode('utf-8', errors='ignore') if decode else data
|
|
73
|
+
self.stderr.write(text)
|
|
74
|
+
self.stderr.flush()
|
|
75
|
+
|
|
76
|
+
def wait(self):
|
|
77
|
+
""" Wait for the shell command to finish and return the exit code.
|
|
78
|
+
Returns:
|
|
79
|
+
int: The exit code of the shell command.
|
|
80
|
+
"""
|
|
81
|
+
if self._thread:
|
|
82
|
+
self._thread.join()
|
|
83
|
+
return self._exit_code
|
|
84
|
+
|
|
85
|
+
def is_running(self) -> bool:
|
|
86
|
+
""" Check if the shell command is still running.
|
|
87
|
+
Returns:
|
|
88
|
+
bool: True if the command is still running, False otherwise.
|
|
89
|
+
"""
|
|
90
|
+
return not self._finished and self._thread and self._thread.is_alive()
|
|
91
|
+
|
|
92
|
+
def poll(self):
|
|
93
|
+
"""
|
|
94
|
+
Check if the shell command is still running.
|
|
95
|
+
Returns:
|
|
96
|
+
int: The exit code if the command has finished, None otherwise.
|
|
97
|
+
"""
|
|
98
|
+
if self._thread and self._thread.is_alive():
|
|
99
|
+
return None
|
|
100
|
+
return self._exit_code
|
|
101
|
+
|
|
102
|
+
def join(self):
|
|
103
|
+
if self._thread and self._thread.is_alive():
|
|
104
|
+
self._thread.join()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ADBStreamShell_V1(StreamShell):
|
|
108
|
+
|
|
109
|
+
def __call__(
|
|
110
|
+
self, cmdargs: Union[List[str]], stdout: IO = None,
|
|
111
|
+
stderr: IO = None, timeout: Union[float, None] = None
|
|
112
|
+
) -> "StreamShell":
|
|
113
|
+
return self.shell_v1(cmdargs, stdout, stderr, timeout)
|
|
114
|
+
|
|
115
|
+
def shell_v1(
|
|
116
|
+
self, cmdargs: Union[List[str], str],
|
|
117
|
+
stdout: IO = None, stderr: IO = None,
|
|
118
|
+
timeout: Union[float, None] = None
|
|
119
|
+
):
|
|
120
|
+
self._finished = False
|
|
121
|
+
self.stdout: IO = stdout if stdout else sys.stdout
|
|
122
|
+
self.stderr: IO = stdout if stderr else sys.stdout
|
|
123
|
+
|
|
124
|
+
cmd = " ".join(cmdargs) if isinstance(cmdargs, list) else cmdargs
|
|
125
|
+
self._generator = self._shell_v1(cmd, timeout)
|
|
126
|
+
self._thread = threading.Thread(target=self._process_output, daemon=True)
|
|
127
|
+
self._thread.start()
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _shell_v1(self, cmdargs: str, timeout: Optional[float] = None) -> Generator[Tuple[str, str], None, None]:
|
|
132
|
+
if not isinstance(cmdargs, str):
|
|
133
|
+
raise RuntimeError("_shell_v1 args must be str")
|
|
134
|
+
MAGIC = "X4EXIT:"
|
|
135
|
+
newcmd = cmdargs + f"; echo {MAGIC}$?"
|
|
136
|
+
with self.dev.open_transport(timeout=timeout) as c:
|
|
137
|
+
c.send_command(f"shell:{newcmd}")
|
|
138
|
+
c.check_okay()
|
|
139
|
+
with c.conn.makefile("r", encoding="utf-8") as f:
|
|
140
|
+
for line in f:
|
|
141
|
+
rindex = line.rfind(MAGIC)
|
|
142
|
+
if rindex == -1:
|
|
143
|
+
yield "output", line
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
yield "exit", line[rindex + len(MAGIC):]
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
def _process_output(self):
|
|
150
|
+
try:
|
|
151
|
+
for msg_type, data in self._generator:
|
|
152
|
+
|
|
153
|
+
if msg_type == 'output':
|
|
154
|
+
self._write_stdout(data, decode=False)
|
|
155
|
+
elif msg_type == 'exit':
|
|
156
|
+
self._exit_code = int(data.strip())
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"ADBStreamShell execution error: {e}")
|
|
161
|
+
self._exit_code = -1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ADBStreamShell_V2(StreamShell):
|
|
165
|
+
def __init__(self, session: "ADBDevice"):
|
|
166
|
+
self.dev: ADBDevice = session
|
|
167
|
+
self._thread = None
|
|
168
|
+
self._exit_code = 255
|
|
169
|
+
|
|
170
|
+
def __call__(
|
|
171
|
+
self, cmdargs: Union[List[str]], stdout: IO = None,
|
|
172
|
+
stderr: IO = None, timeout: Union[float, None] = None
|
|
173
|
+
) -> "StreamShell":
|
|
174
|
+
return self.shell_v2(cmdargs, stdout, stderr, timeout)
|
|
175
|
+
|
|
176
|
+
def shell_v2(
|
|
177
|
+
self, cmdargs: Union[List[str], str],
|
|
178
|
+
stdout: IO = None, stderr: IO = None,
|
|
179
|
+
timeout: Union[float, None] = None
|
|
180
|
+
):
|
|
181
|
+
""" Start a shell command on the device and stream its output.
|
|
182
|
+
Args:
|
|
183
|
+
cmdargs (Union[List[str], str]): The command to execute, either as a list of arguments or a single string.
|
|
184
|
+
stdout (IO, optional): The output stream for standard output. Defaults to sys.stdout.
|
|
185
|
+
stderr (IO, optional): The output stream for standard error. Defaults to sys.stderr.
|
|
186
|
+
timeout (Union[float, None], optional): Timeout for the command execution. Defaults to None.
|
|
187
|
+
Returns:
|
|
188
|
+
ADBStreamShell: An instance of ADBStreamShell that can be used to interact with the shell command.
|
|
189
|
+
"""
|
|
190
|
+
self._finished = False
|
|
191
|
+
self.stdout: IO = stdout if stdout else sys.stdout
|
|
192
|
+
self.stderr: IO = stderr if stderr else sys.stderr
|
|
193
|
+
|
|
194
|
+
cmd = " ".join(cmdargs) if isinstance(cmdargs, list) else cmdargs
|
|
195
|
+
self._generator = self._shell_v2(cmd, timeout)
|
|
196
|
+
self._thread = threading.Thread(target=self._process_output, daemon=True)
|
|
197
|
+
self._thread.start()
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def _process_output(self):
|
|
201
|
+
try:
|
|
202
|
+
for msg_type, data in self._generator:
|
|
203
|
+
|
|
204
|
+
if msg_type == 'stdout':
|
|
205
|
+
self._write_stdout(data)
|
|
206
|
+
elif msg_type == 'stderr':
|
|
207
|
+
self._write_stderr(data)
|
|
208
|
+
elif msg_type == 'exit':
|
|
209
|
+
self._exit_code = data
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print(f"ADBStreamShell execution error: {e}")
|
|
214
|
+
self._exit_code = -1
|
|
215
|
+
|
|
216
|
+
def _shell_v2(self, cmd, timeout) -> Generator[Tuple[str, bytes], None, None]:
|
|
217
|
+
with self.dev.open_transport(timeout=timeout) as c:
|
|
218
|
+
c.send_command(f"shell,v2:{cmd}")
|
|
219
|
+
c.check_okay()
|
|
220
|
+
|
|
221
|
+
while True:
|
|
222
|
+
header = c.read_exact(5)
|
|
223
|
+
msg_id = header[0]
|
|
224
|
+
length = int.from_bytes(header[1:5], byteorder="little")
|
|
225
|
+
|
|
226
|
+
if length == 0:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
data = c.read_exact(length)
|
|
230
|
+
|
|
231
|
+
if msg_id == 1:
|
|
232
|
+
yield ('stdout', data)
|
|
233
|
+
elif msg_id == 2:
|
|
234
|
+
yield ('stderr', data)
|
|
235
|
+
elif msg_id == 3:
|
|
236
|
+
yield ('exit', data[0])
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
|
|
8
240
|
def run_adb_command(cmd: List[str], timeout=10):
|
|
9
241
|
"""
|
|
10
242
|
Runs an adb command and returns its output.
|
|
@@ -37,7 +269,7 @@ def get_devices():
|
|
|
37
269
|
Returns:
|
|
38
270
|
list: A list of device serial numbers.
|
|
39
271
|
"""
|
|
40
|
-
output = run_adb_command(["devices"])
|
|
272
|
+
output = run_adb_command(["devices", "-l"])
|
|
41
273
|
devices = []
|
|
42
274
|
if output:
|
|
43
275
|
lines = output.splitlines()
|
|
@@ -59,21 +291,23 @@ def ensure_device(func):
|
|
|
59
291
|
"""
|
|
60
292
|
def wrapper(*args, **kwargs):
|
|
61
293
|
devices = get_devices()
|
|
62
|
-
if kwargs.get("device") is None:
|
|
294
|
+
if kwargs.get("device") is None and kwargs.get("transport_id") is None:
|
|
63
295
|
if not devices:
|
|
64
296
|
raise RuntimeError("No connected devices.")
|
|
65
297
|
if len(devices) > 1:
|
|
66
298
|
raise RuntimeError("Multiple connected devices detected. Please specify a device.")
|
|
67
299
|
kwargs["device"] = devices[0]
|
|
68
|
-
if kwargs
|
|
300
|
+
if kwargs.get("device"):
|
|
69
301
|
output = run_adb_command(["-s", kwargs["device"], "get-state"])
|
|
70
|
-
|
|
71
|
-
|
|
302
|
+
elif kwargs.get("transport_id"):
|
|
303
|
+
output = run_adb_command(["-t", kwargs["transport_id"], "get-state"])
|
|
304
|
+
if output.strip() != "device":
|
|
305
|
+
raise RuntimeError(f"[ERROR] {kwargs['device']} not connected. Please check.\n{output}")
|
|
72
306
|
return func(*args, **kwargs)
|
|
73
307
|
return wrapper
|
|
74
308
|
|
|
75
309
|
@ensure_device
|
|
76
|
-
def adb_shell(cmd: List[str], device:Optional[str]=None):
|
|
310
|
+
def adb_shell(cmd: List[str], device:Optional[str]=None, transport_id:Optional[str]=None):
|
|
77
311
|
"""
|
|
78
312
|
run adb shell commands
|
|
79
313
|
|
|
@@ -81,11 +315,15 @@ def adb_shell(cmd: List[str], device:Optional[str]=None):
|
|
|
81
315
|
cmd (List[str])
|
|
82
316
|
device (str, optional): The device serial number. If None, it's resolved automatically when only one device is connected.
|
|
83
317
|
"""
|
|
84
|
-
|
|
318
|
+
if device:
|
|
319
|
+
return run_adb_command(["-s", device, "shell"] + cmd)
|
|
320
|
+
if transport_id:
|
|
321
|
+
return run_adb_command(["-t", transport_id, "shell"] + cmd)
|
|
322
|
+
|
|
85
323
|
|
|
86
324
|
|
|
87
325
|
@ensure_device
|
|
88
|
-
def install_app(apk_path: str, device: Optional[str]=None):
|
|
326
|
+
def install_app(apk_path: str, device: Optional[str]=None, transport_id:Optional[str]=None):
|
|
89
327
|
"""
|
|
90
328
|
Installs an APK application on the specified device.
|
|
91
329
|
|
|
@@ -96,11 +334,14 @@ def install_app(apk_path: str, device: Optional[str]=None):
|
|
|
96
334
|
Returns:
|
|
97
335
|
str: The output from the install command.
|
|
98
336
|
"""
|
|
99
|
-
|
|
337
|
+
if device:
|
|
338
|
+
return run_adb_command(["-s", device, "install", apk_path])
|
|
339
|
+
if transport_id:
|
|
340
|
+
return run_adb_command(["-t", transport_id, "install", apk_path])
|
|
100
341
|
|
|
101
342
|
|
|
102
343
|
@ensure_device
|
|
103
|
-
def uninstall_app(package_name: str, device: Optional[str] = None):
|
|
344
|
+
def uninstall_app(package_name: str, device: Optional[str] = None, transport_id:Optional[str]=None):
|
|
104
345
|
"""
|
|
105
346
|
Uninstalls an app from the specified device.
|
|
106
347
|
|
|
@@ -111,11 +352,13 @@ def uninstall_app(package_name: str, device: Optional[str] = None):
|
|
|
111
352
|
Returns:
|
|
112
353
|
str: The output from the uninstall command.
|
|
113
354
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
355
|
+
if device:
|
|
356
|
+
return run_adb_command(["-s", device, "uninstall", package_name])
|
|
357
|
+
if transport_id:
|
|
358
|
+
return run_adb_command(["-t", transport_id, "uninstall", package_name])
|
|
116
359
|
|
|
117
360
|
@ensure_device
|
|
118
|
-
def push_file(local_path: str, remote_path: str, device: Optional[str] = None):
|
|
361
|
+
def push_file(local_path: str, remote_path: str, device: Optional[str] = None, transport_id:Optional[str]=None):
|
|
119
362
|
"""
|
|
120
363
|
Pushes a file to the specified device.
|
|
121
364
|
|
|
@@ -129,11 +372,14 @@ def push_file(local_path: str, remote_path: str, device: Optional[str] = None):
|
|
|
129
372
|
"""
|
|
130
373
|
local_path = str(local_path)
|
|
131
374
|
remote_path = str(remote_path)
|
|
132
|
-
|
|
375
|
+
if device:
|
|
376
|
+
return run_adb_command(["-s", device, "push", local_path, remote_path])
|
|
377
|
+
if transport_id:
|
|
378
|
+
return run_adb_command(["-t", transport_id, "push", local_path, remote_path])
|
|
133
379
|
|
|
134
380
|
|
|
135
381
|
@ensure_device
|
|
136
|
-
def pull_file(remote_path: str, local_path: str, device: Optional[str] = None):
|
|
382
|
+
def pull_file(remote_path: str, local_path: str, device: Optional[str] = None, transport_id:Optional[str]=None):
|
|
137
383
|
"""
|
|
138
384
|
Pulls a file from the device to a local path.
|
|
139
385
|
|
|
@@ -145,7 +391,10 @@ def pull_file(remote_path: str, local_path: str, device: Optional[str] = None):
|
|
|
145
391
|
Returns:
|
|
146
392
|
str: The output from the pull command.
|
|
147
393
|
"""
|
|
148
|
-
|
|
394
|
+
if device:
|
|
395
|
+
return run_adb_command(["-s", device, "pull", remote_path, local_path])
|
|
396
|
+
if transport_id:
|
|
397
|
+
return run_adb_command(["-t", transport_id, "pull", remote_path, local_path])
|
|
149
398
|
|
|
150
399
|
# Forward-related functions
|
|
151
400
|
|
|
@@ -223,7 +472,7 @@ def remove_all_forwards(device: Optional[str] = None):
|
|
|
223
472
|
|
|
224
473
|
|
|
225
474
|
@ensure_device
|
|
226
|
-
def get_packages(device: Optional[str] =
|
|
475
|
+
def get_packages(device: Optional[str]=None, transport_id: Optional[str]=None) -> Set[str]:
|
|
227
476
|
"""
|
|
228
477
|
Retrieves packages that match the specified regular expression pattern.
|
|
229
478
|
|
|
@@ -236,7 +485,10 @@ def get_packages(device: Optional[str] = None) -> Set[str]:
|
|
|
236
485
|
"""
|
|
237
486
|
import re
|
|
238
487
|
|
|
239
|
-
|
|
488
|
+
if device:
|
|
489
|
+
cmd = ["-s", device, "shell", "pm", "list", "packages"]
|
|
490
|
+
if transport_id:
|
|
491
|
+
cmd = ["-t", transport_id, "shell", "pm", "list", "packages"]
|
|
240
492
|
output = run_adb_command(cmd)
|
|
241
493
|
|
|
242
494
|
packages = set()
|
|
@@ -252,6 +504,7 @@ def get_packages(device: Optional[str] = None) -> Set[str]:
|
|
|
252
504
|
|
|
253
505
|
if __name__ == '__main__':
|
|
254
506
|
# For testing: print the list of currently connected devices.
|
|
507
|
+
adb_shell(["ls", "vendor"], transport_id="2")
|
|
255
508
|
devices = get_devices()
|
|
256
509
|
if devices:
|
|
257
510
|
print("Connected devices:", flush=True)
|