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 CHANGED
@@ -38,7 +38,7 @@ class AbstractDriver(abc.ABC):
38
38
 
39
39
  @classmethod
40
40
  @abc.abstractmethod
41
- def setDeviceSerial(self):
41
+ def setDevice(self):
42
42
  pass
43
43
 
44
44
  @classmethod
kea2/adbUtils.py CHANGED
@@ -1,10 +1,250 @@
1
+ import sys
1
2
  import subprocess
2
- from typing import List, Optional, Set
3
- from .utils import getLogger
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["device"] not in devices:
308
+ if kwargs.get("device"):
69
309
  output = run_adb_command(["-s", kwargs["device"], "get-state"])
70
- if output.strip() != "device":
71
- raise RuntimeError(f"[ERROR] {kwargs['device']} not connected. Please check.\n{output}")
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
- return run_adb_command(["-s", device, "shell"] + cmd)
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
- return run_adb_command(["-s", device, "install", apk_path])
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
- return run_adb_command(["-s", device, "uninstall", package_name])
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
- return run_adb_command(["-s", device, "push", local_path, remote_path])
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
- return run_adb_command(["-s", device, "pull", remote_path, local_path])
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] = None) -> Set[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
- cmd = ["-s", device, "shell", "pm", "list", "packages"]
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