Kea2-python 0.1.3__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 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,242 @@
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
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["device"] not in devices:
300
+ if kwargs.get("device"):
69
301
  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}")
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
- return run_adb_command(["-s", device, "shell"] + cmd)
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
- return run_adb_command(["-s", device, "install", apk_path])
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
- return run_adb_command(["-s", device, "uninstall", package_name])
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
- return run_adb_command(["-s", device, "push", local_path, remote_path])
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
- return run_adb_command(["-s", device, "pull", remote_path, local_path])
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] = None) -> Set[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
- cmd = ["-s", device, "shell", "pm", "list", "packages"]
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)