voidremote 1.0.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.
- voidremote/__init__.py +31 -0
- voidremote/adb/__init__.py +28 -0
- voidremote/adb/client.py +385 -0
- voidremote/adb/device_parser.py +286 -0
- voidremote/cli/__init__.py +5 -0
- voidremote/cli/main.py +987 -0
- voidremote/config/__init__.py +37 -0
- voidremote/config/settings.py +203 -0
- voidremote/controllers/__init__.py +5 -0
- voidremote/controllers/app_controller.py +222 -0
- voidremote/core/__init__.py +1 -0
- voidremote/core/automation.py +184 -0
- voidremote/models/__init__.py +31 -0
- voidremote/models/device.py +263 -0
- voidremote/network/__init__.py +1 -0
- voidremote/network/discovery.py +116 -0
- voidremote/services/__init__.py +13 -0
- voidremote/services/device_service.py +340 -0
- voidremote/services/input_service.py +181 -0
- voidremote/services/monitor_service.py +198 -0
- voidremote/ui/__init__.py +1 -0
- voidremote/ui/app.py +70 -0
- voidremote/ui/dialogs/__init__.py +6 -0
- voidremote/ui/dialogs/connect_dialog.py +149 -0
- voidremote/ui/dialogs/pair_dialog.py +189 -0
- voidremote/ui/main_window.py +371 -0
- voidremote/ui/theme.py +529 -0
- voidremote/ui/views/__init__.py +15 -0
- voidremote/ui/views/dashboard_view.py +233 -0
- voidremote/ui/views/files_view.py +305 -0
- voidremote/ui/views/monitor_view.py +236 -0
- voidremote/ui/views/settings_view.py +257 -0
- voidremote/ui/views/shell_view.py +234 -0
- voidremote/ui/widgets/__init__.py +14 -0
- voidremote/ui/widgets/device_card.py +259 -0
- voidremote/ui/widgets/log_view.py +159 -0
- voidremote/ui/widgets/metric_gauge.py +90 -0
- voidremote/utils/__init__.py +28 -0
- voidremote/utils/logging.py +120 -0
- voidremote/utils/security.py +216 -0
- voidremote-1.0.0.dist-info/METADATA +578 -0
- voidremote-1.0.0.dist-info/RECORD +46 -0
- voidremote-1.0.0.dist-info/WHEEL +5 -0
- voidremote-1.0.0.dist-info/entry_points.txt +5 -0
- voidremote-1.0.0.dist-info/licenses/LICENSE +21 -0
- voidremote-1.0.0.dist-info/top_level.txt +1 -0
voidremote/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VoidRemote - Wireless Android Remote Controller over ADB
|
|
3
|
+
========================================================
|
|
4
|
+
|
|
5
|
+
A cross-platform desktop application for controlling Android devices
|
|
6
|
+
wirelessly through ADB (Android Debug Bridge).
|
|
7
|
+
|
|
8
|
+
Author: V0IDNETWORK <ilianothingg@gmail.com>
|
|
9
|
+
Website: http://voidNetwork.ir/
|
|
10
|
+
GitHub: https://github.com/V0IDNETWORK/VoidRemote
|
|
11
|
+
License: MIT
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
__author__ = "V0IDNETWORK"
|
|
16
|
+
__email__ = "ilianothingg@gmail.com"
|
|
17
|
+
__license__ = "MIT"
|
|
18
|
+
__url__ = "https://github.com/V0IDNETWORK/VoidRemote"
|
|
19
|
+
__description__ = "Wireless Android Remote Controller over ADB"
|
|
20
|
+
|
|
21
|
+
from voidremote.utils.logging import setup_logging
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"__author__",
|
|
26
|
+
"__email__",
|
|
27
|
+
"__license__",
|
|
28
|
+
"__url__",
|
|
29
|
+
"__description__",
|
|
30
|
+
"setup_logging",
|
|
31
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""VoidRemote ADB communication layer."""
|
|
2
|
+
|
|
3
|
+
from voidremote.adb.client import AdbClient, AdbCommandResult, AdbError, AdbNotFoundError
|
|
4
|
+
from voidremote.adb.device_parser import (
|
|
5
|
+
build_device_from_raw,
|
|
6
|
+
parse_battery,
|
|
7
|
+
parse_devices_output,
|
|
8
|
+
parse_ip_address,
|
|
9
|
+
parse_meminfo,
|
|
10
|
+
parse_screen_resolution,
|
|
11
|
+
parse_storage,
|
|
12
|
+
state_from_string,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AdbClient",
|
|
17
|
+
"AdbCommandResult",
|
|
18
|
+
"AdbError",
|
|
19
|
+
"AdbNotFoundError",
|
|
20
|
+
"build_device_from_raw",
|
|
21
|
+
"parse_battery",
|
|
22
|
+
"parse_devices_output",
|
|
23
|
+
"parse_ip_address",
|
|
24
|
+
"parse_meminfo",
|
|
25
|
+
"parse_screen_resolution",
|
|
26
|
+
"parse_storage",
|
|
27
|
+
"state_from_string",
|
|
28
|
+
]
|
voidremote/adb/client.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ADB client - subprocess-based ADB communication layer.
|
|
3
|
+
|
|
4
|
+
Wraps the system `adb` binary to provide a clean async-friendly API
|
|
5
|
+
for all ADB operations used by VoidRemote.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from voidremote.models.device import ConnectionConfig, PairingInfo
|
|
19
|
+
from voidremote.utils.security import (
|
|
20
|
+
sanitize_shell_arg,
|
|
21
|
+
validate_device_path,
|
|
22
|
+
validate_local_path,
|
|
23
|
+
validate_package_name,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AdbError(Exception):
|
|
30
|
+
"""Raised when an ADB operation fails."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, returncode: int = -1, stderr: str = "") -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.returncode = returncode
|
|
35
|
+
self.stderr = stderr
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AdbNotFoundError(AdbError):
|
|
39
|
+
"""Raised when the ADB binary cannot be found."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AdbCommandResult:
|
|
43
|
+
"""Result of a completed ADB command."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, stdout: str, stderr: str, returncode: int) -> None:
|
|
46
|
+
self.stdout = stdout.strip()
|
|
47
|
+
self.stderr = stderr.strip()
|
|
48
|
+
self.returncode = returncode
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def success(self) -> bool:
|
|
52
|
+
return self.returncode == 0
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return (
|
|
56
|
+
f"AdbCommandResult(returncode={self.returncode}, "
|
|
57
|
+
f"stdout={self.stdout[:60]!r})"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AdbClient:
|
|
62
|
+
"""
|
|
63
|
+
Low-level ADB client that wraps the system adb binary.
|
|
64
|
+
|
|
65
|
+
All command execution goes through _run() which handles timeouts,
|
|
66
|
+
retries, and error normalization.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
adb_path: str = "adb",
|
|
72
|
+
default_timeout: float = 30.0,
|
|
73
|
+
server_port: int = 5037,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._adb_path = adb_path
|
|
76
|
+
self._default_timeout = default_timeout
|
|
77
|
+
self._server_port = server_port
|
|
78
|
+
self._verified = False
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Initialization
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def verify_adb(self) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Verify ADB binary is available and return its version string.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
AdbNotFoundError: If ADB binary not found or not executable.
|
|
90
|
+
"""
|
|
91
|
+
resolved = shutil.which(self._adb_path)
|
|
92
|
+
if resolved is None:
|
|
93
|
+
raise AdbNotFoundError(
|
|
94
|
+
f"ADB binary not found: {self._adb_path!r}. "
|
|
95
|
+
"Install Android SDK Platform Tools or specify path in settings."
|
|
96
|
+
)
|
|
97
|
+
result = self._run_sync(["version"])
|
|
98
|
+
self._verified = True
|
|
99
|
+
version_line = result.stdout.split("\n")[0]
|
|
100
|
+
logger.info("ADB available: %s", version_line)
|
|
101
|
+
return version_line
|
|
102
|
+
|
|
103
|
+
def start_server(self) -> None:
|
|
104
|
+
"""Start ADB server."""
|
|
105
|
+
self._run_sync(["start-server"])
|
|
106
|
+
logger.debug("ADB server started")
|
|
107
|
+
|
|
108
|
+
def kill_server(self) -> None:
|
|
109
|
+
"""Kill ADB server."""
|
|
110
|
+
self._run_sync(["kill-server"])
|
|
111
|
+
logger.debug("ADB server killed")
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Device management
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def list_devices_raw(self) -> str:
|
|
118
|
+
"""Return raw output of `adb devices -l`."""
|
|
119
|
+
result = self._run_sync(["devices", "-l"])
|
|
120
|
+
return result.stdout
|
|
121
|
+
|
|
122
|
+
def get_state(self, serial: str) -> str:
|
|
123
|
+
"""Return connection state for a device serial."""
|
|
124
|
+
result = self._run_sync(["-s", serial, "get-state"], timeout=5.0)
|
|
125
|
+
return result.stdout.lower()
|
|
126
|
+
|
|
127
|
+
def wait_for_device(self, serial: str, timeout: float = 30.0) -> None:
|
|
128
|
+
"""Block until device is online."""
|
|
129
|
+
self._run_sync(["-s", serial, "wait-for-device"], timeout=timeout)
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Wireless pairing / connection
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def pair(self, pairing: PairingInfo) -> AdbCommandResult:
|
|
136
|
+
"""Pair with a device using wireless debugging pairing code."""
|
|
137
|
+
address = f"{pairing.host}:{pairing.port}"
|
|
138
|
+
logger.info("Pairing with %s", address)
|
|
139
|
+
result = self._run_sync(
|
|
140
|
+
["pair", address, pairing.pairing_code],
|
|
141
|
+
timeout=pairing.timeout,
|
|
142
|
+
)
|
|
143
|
+
if "Successfully paired" not in result.stdout and result.returncode != 0:
|
|
144
|
+
raise AdbError(
|
|
145
|
+
f"Pairing failed: {result.stderr or result.stdout}",
|
|
146
|
+
result.returncode,
|
|
147
|
+
)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
def connect(self, config: ConnectionConfig) -> AdbCommandResult:
|
|
151
|
+
"""Connect to a device over TCP/IP."""
|
|
152
|
+
address = f"{config.host}:{config.port}"
|
|
153
|
+
logger.info("Connecting to %s", address)
|
|
154
|
+
for attempt in range(1, config.retry_count + 1):
|
|
155
|
+
result = self._run_sync(["connect", address], timeout=config.timeout)
|
|
156
|
+
if "connected" in result.stdout.lower():
|
|
157
|
+
logger.info("Connected to %s", address)
|
|
158
|
+
return result
|
|
159
|
+
if attempt < config.retry_count:
|
|
160
|
+
logger.warning(
|
|
161
|
+
"Connect attempt %d/%d failed, retrying in %.1fs",
|
|
162
|
+
attempt, config.retry_count, config.retry_delay,
|
|
163
|
+
)
|
|
164
|
+
time.sleep(config.retry_delay)
|
|
165
|
+
raise AdbError(
|
|
166
|
+
f"Could not connect to {address}: {result.stderr or result.stdout}",
|
|
167
|
+
result.returncode,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def disconnect(self, host: str, port: int = 5555) -> AdbCommandResult:
|
|
171
|
+
"""Disconnect a wireless device."""
|
|
172
|
+
address = f"{host}:{port}"
|
|
173
|
+
logger.info("Disconnecting from %s", address)
|
|
174
|
+
return self._run_sync(["disconnect", address])
|
|
175
|
+
|
|
176
|
+
def disconnect_all(self) -> AdbCommandResult:
|
|
177
|
+
"""Disconnect all wireless devices."""
|
|
178
|
+
return self._run_sync(["disconnect"])
|
|
179
|
+
|
|
180
|
+
def tcpip(self, serial: str, port: int = 5555) -> AdbCommandResult:
|
|
181
|
+
"""Switch device to TCP/IP mode on given port."""
|
|
182
|
+
return self._run_sync(["-s", serial, "tcpip", str(port)])
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# Shell execution
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def shell(
|
|
189
|
+
self,
|
|
190
|
+
serial: str,
|
|
191
|
+
command: str,
|
|
192
|
+
timeout: Optional[float] = None,
|
|
193
|
+
) -> AdbCommandResult:
|
|
194
|
+
"""Execute a shell command on the device."""
|
|
195
|
+
return self._run_sync(
|
|
196
|
+
["-s", serial, "shell", command],
|
|
197
|
+
timeout=timeout or self._default_timeout,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def shell_raw(
|
|
201
|
+
self,
|
|
202
|
+
serial: str,
|
|
203
|
+
args: list[str],
|
|
204
|
+
timeout: Optional[float] = None,
|
|
205
|
+
) -> AdbCommandResult:
|
|
206
|
+
"""Execute shell with a list of args (each individually quoted)."""
|
|
207
|
+
safe_args = [sanitize_shell_arg(a) for a in args]
|
|
208
|
+
return self._run_sync(
|
|
209
|
+
["-s", serial, "shell"] + safe_args,
|
|
210
|
+
timeout=timeout or self._default_timeout,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# File operations
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def push(self, serial: str, local: Path, remote: str) -> AdbCommandResult:
|
|
218
|
+
"""Push a local file to the device."""
|
|
219
|
+
validated_local = validate_local_path(local)
|
|
220
|
+
validated_remote = validate_device_path(remote)
|
|
221
|
+
if not validated_local.exists():
|
|
222
|
+
raise FileNotFoundError(f"Local file not found: {validated_local}")
|
|
223
|
+
return self._run_sync(
|
|
224
|
+
["-s", serial, "push", str(validated_local), validated_remote]
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def pull(self, serial: str, remote: str, local: Path) -> AdbCommandResult:
|
|
228
|
+
"""Pull a file from the device to local."""
|
|
229
|
+
validated_remote = validate_device_path(remote)
|
|
230
|
+
local.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
return self._run_sync(
|
|
232
|
+
["-s", serial, "pull", validated_remote, str(local)]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
# APK management
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def install(
|
|
240
|
+
self,
|
|
241
|
+
serial: str,
|
|
242
|
+
apk_path: Path,
|
|
243
|
+
replace: bool = True,
|
|
244
|
+
allow_downgrade: bool = False,
|
|
245
|
+
) -> AdbCommandResult:
|
|
246
|
+
"""Install an APK on the device."""
|
|
247
|
+
validated = validate_local_path(apk_path)
|
|
248
|
+
if not validated.exists():
|
|
249
|
+
raise FileNotFoundError(f"APK not found: {validated}")
|
|
250
|
+
args = ["-s", serial, "install"]
|
|
251
|
+
if replace:
|
|
252
|
+
args.append("-r")
|
|
253
|
+
if allow_downgrade:
|
|
254
|
+
args.append("-d")
|
|
255
|
+
args.append(str(validated))
|
|
256
|
+
return self._run_sync(args, timeout=120.0)
|
|
257
|
+
|
|
258
|
+
def uninstall(
|
|
259
|
+
self,
|
|
260
|
+
serial: str,
|
|
261
|
+
package: str,
|
|
262
|
+
keep_data: bool = False,
|
|
263
|
+
) -> AdbCommandResult:
|
|
264
|
+
"""Uninstall a package from the device."""
|
|
265
|
+
validated_pkg = validate_package_name(package)
|
|
266
|
+
args = ["-s", serial, "uninstall"]
|
|
267
|
+
if keep_data:
|
|
268
|
+
args.append("-k")
|
|
269
|
+
args.append(validated_pkg)
|
|
270
|
+
return self._run_sync(args)
|
|
271
|
+
|
|
272
|
+
# ------------------------------------------------------------------
|
|
273
|
+
# Reboot
|
|
274
|
+
# ------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
def reboot(self, serial: str, mode: str = "") -> AdbCommandResult:
|
|
277
|
+
"""Reboot device. mode can be '', 'bootloader', or 'recovery'."""
|
|
278
|
+
args = ["-s", serial, "reboot"]
|
|
279
|
+
if mode in ("bootloader", "recovery"):
|
|
280
|
+
args.append(mode)
|
|
281
|
+
return self._run_sync(args)
|
|
282
|
+
|
|
283
|
+
# ------------------------------------------------------------------
|
|
284
|
+
# Screenshots & recording
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def screenshot(self, serial: str, local_path: Path) -> AdbCommandResult:
|
|
288
|
+
"""Capture a screenshot and save it locally."""
|
|
289
|
+
remote_tmp = "/sdcard/void_screenshot.png"
|
|
290
|
+
self.shell(serial, f"screencap -p {remote_tmp}")
|
|
291
|
+
result = self.pull(serial, remote_tmp, local_path)
|
|
292
|
+
self.shell(serial, f"rm {remote_tmp}")
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
def screenrecord(
|
|
296
|
+
self,
|
|
297
|
+
serial: str,
|
|
298
|
+
remote_path: str = "/sdcard/void_screenrecord.mp4",
|
|
299
|
+
time_limit: int = 180,
|
|
300
|
+
bit_rate: int = 8000000,
|
|
301
|
+
size: Optional[str] = None,
|
|
302
|
+
) -> subprocess.Popen[bytes]:
|
|
303
|
+
"""Start screen recording and return the Popen handle."""
|
|
304
|
+
cmd = [
|
|
305
|
+
self._adb_path, "-s", serial, "shell",
|
|
306
|
+
"screenrecord",
|
|
307
|
+
f"--time-limit={time_limit}",
|
|
308
|
+
f"--bit-rate={bit_rate}",
|
|
309
|
+
]
|
|
310
|
+
if size:
|
|
311
|
+
cmd += ["--size", size]
|
|
312
|
+
cmd.append(remote_path)
|
|
313
|
+
logger.info("Starting screen record: %s", " ".join(cmd))
|
|
314
|
+
return subprocess.Popen(cmd)
|
|
315
|
+
|
|
316
|
+
# ------------------------------------------------------------------
|
|
317
|
+
# Internal helpers
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
def _build_cmd(self, args: list[str]) -> list[str]:
|
|
321
|
+
"""Prefix all commands with adb binary and server port."""
|
|
322
|
+
base = [self._adb_path, "-P", str(self._server_port)]
|
|
323
|
+
return base + args
|
|
324
|
+
|
|
325
|
+
def _run_sync(
|
|
326
|
+
self,
|
|
327
|
+
args: list[str],
|
|
328
|
+
timeout: Optional[float] = None,
|
|
329
|
+
input_data: Optional[bytes] = None,
|
|
330
|
+
) -> AdbCommandResult:
|
|
331
|
+
"""Execute an ADB command synchronously."""
|
|
332
|
+
cmd = self._build_cmd(args)
|
|
333
|
+
effective_timeout = timeout or self._default_timeout
|
|
334
|
+
logger.debug("ADB command: %s", " ".join(cmd))
|
|
335
|
+
try:
|
|
336
|
+
proc = subprocess.run(
|
|
337
|
+
cmd,
|
|
338
|
+
capture_output=True,
|
|
339
|
+
timeout=effective_timeout,
|
|
340
|
+
input=input_data,
|
|
341
|
+
)
|
|
342
|
+
result = AdbCommandResult(
|
|
343
|
+
proc.stdout.decode("utf-8", errors="replace"),
|
|
344
|
+
proc.stderr.decode("utf-8", errors="replace"),
|
|
345
|
+
proc.returncode,
|
|
346
|
+
)
|
|
347
|
+
if result.stderr and result.returncode != 0:
|
|
348
|
+
logger.debug("ADB stderr: %s", result.stderr)
|
|
349
|
+
return result
|
|
350
|
+
except subprocess.TimeoutExpired as exc:
|
|
351
|
+
raise AdbError(
|
|
352
|
+
f"ADB command timed out after {effective_timeout}s: {' '.join(cmd)}"
|
|
353
|
+
) from exc
|
|
354
|
+
except FileNotFoundError as exc:
|
|
355
|
+
raise AdbNotFoundError(
|
|
356
|
+
f"ADB binary not found: {self._adb_path!r}"
|
|
357
|
+
) from exc
|
|
358
|
+
|
|
359
|
+
async def _run_async(
|
|
360
|
+
self,
|
|
361
|
+
args: list[str],
|
|
362
|
+
timeout: Optional[float] = None,
|
|
363
|
+
) -> AdbCommandResult:
|
|
364
|
+
"""Execute an ADB command asynchronously."""
|
|
365
|
+
cmd = self._build_cmd(args)
|
|
366
|
+
effective_timeout = timeout or self._default_timeout
|
|
367
|
+
logger.debug("ADB async command: %s", " ".join(cmd))
|
|
368
|
+
try:
|
|
369
|
+
proc = await asyncio.create_subprocess_exec(
|
|
370
|
+
*cmd,
|
|
371
|
+
stdout=asyncio.subprocess.PIPE,
|
|
372
|
+
stderr=asyncio.subprocess.PIPE,
|
|
373
|
+
)
|
|
374
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
375
|
+
proc.communicate(), timeout=effective_timeout
|
|
376
|
+
)
|
|
377
|
+
return AdbCommandResult(
|
|
378
|
+
stdout_bytes.decode("utf-8", errors="replace"),
|
|
379
|
+
stderr_bytes.decode("utf-8", errors="replace"),
|
|
380
|
+
proc.returncode or 0,
|
|
381
|
+
)
|
|
382
|
+
except asyncio.TimeoutError as exc:
|
|
383
|
+
raise AdbError(
|
|
384
|
+
f"ADB async command timed out after {effective_timeout}s"
|
|
385
|
+
) from exc
|