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