adbuart 0.1.0__tar.gz
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.
- adbuart-0.1.0/.github/workflows/ci.yml +25 -0
- adbuart-0.1.0/.github/workflows/publish.yml +27 -0
- adbuart-0.1.0/LICENSE +21 -0
- adbuart-0.1.0/PKG-INFO +112 -0
- adbuart-0.1.0/README.md +82 -0
- adbuart-0.1.0/pyproject.toml +45 -0
- adbuart-0.1.0/src/adbuart/__init__.py +3 -0
- adbuart-0.1.0/src/adbuart/cli.py +570 -0
- adbuart-0.1.0/tests/test_cli.py +68 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ['3.10', '3.11', '3.12', '3.13']
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install package
|
|
21
|
+
run: python -m pip install -e '.[dev]'
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: pytest -q
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: python -m build
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.12'
|
|
22
|
+
- name: Install build backend
|
|
23
|
+
run: python -m pip install build
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: python -m build
|
|
26
|
+
- name: Publish package
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
adbuart-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XuNeo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
adbuart-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adbuart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Expose an Android device UART as a local PTY over adb.
|
|
5
|
+
Project-URL: Homepage, https://github.com/XuNeo/adbuart
|
|
6
|
+
Project-URL: Repository, https://github.com/XuNeo/adbuart
|
|
7
|
+
Project-URL: Issues, https://github.com/XuNeo/adbuart/issues
|
|
8
|
+
Author: XuNeo
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: adb,nuttx,pty,rtos,serial,uart
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
23
|
+
Classifier: Topic :: System :: Hardware
|
|
24
|
+
Classifier: Topic :: Terminals :: Serial
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# adbuart
|
|
32
|
+
|
|
33
|
+
Expose an Android-device UART as a local PTY over `adb`, so host serial tools such as `minicom`, `picocom`, or pyserial can talk to it like a normal serial port.
|
|
34
|
+
|
|
35
|
+
This is useful when a device has no host-side USB serial adapter and the RTOS console is reachable only through an Android UART device, for example `/dev/ttyHS1` on G2 Air style targets.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Linux or another POSIX-like host with Python 3.10+
|
|
40
|
+
- `adb` installed and available in `PATH`
|
|
41
|
+
- An adb-connected Android device where `adb root` is allowed
|
|
42
|
+
- Target-side UART device access, defaulting to `/dev/ttyHS1`
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python3 -m pip install adbuart
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
Start the bridge in one terminal:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
adbuart -n g2air
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
With multiple adb devices attached, pass the adb serial explicitly:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
adbuart -s d7a6340b -n g2air
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The command creates `/tmp/g2air` and keeps running in the foreground. It prints a minicom command when the PTY is ready.
|
|
65
|
+
|
|
66
|
+
Open the PTY from another terminal:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
minicom -D /tmp/g2air -b 2000000
|
|
70
|
+
# or
|
|
71
|
+
python3 -m serial.tools.miniterm /tmp/g2air 2000000
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Stop the bridge with `Ctrl-C` in the bridge terminal. Clean shutdown restores RTOS logging when it was enabled before startup.
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
Run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
adbuart --help
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Important options:
|
|
85
|
+
|
|
86
|
+
- `-n, --name` creates `/tmp/<name>` and is required.
|
|
87
|
+
- `-s, --serial` selects an adb device. If omitted, exactly one device must be connected.
|
|
88
|
+
- `--remote-tty` selects the target UART device path. Default: `/dev/ttyHS1`.
|
|
89
|
+
- `--baud` sets the initial remote UART baud rate. Default: `2000000`.
|
|
90
|
+
- `--cmd-before` runs a remote shell command before bridging. Repeatable.
|
|
91
|
+
- `--cmd-after` runs a remote shell command during teardown. Repeatable.
|
|
92
|
+
|
|
93
|
+
By default the tool sets `persist.rtoslogd.uart` to `0` before bridging so Android log collection releases the UART. During teardown it restores the property to `1` only when it was `1` at startup.
|
|
94
|
+
|
|
95
|
+
## Operating notes
|
|
96
|
+
|
|
97
|
+
- The bridge owns the target UART exclusively while it is running. Stop it before flashing, resetting, or opening the same UART through another tool.
|
|
98
|
+
- The local node is protected with a lock file, but unrelated tools can still collide at the device UART level.
|
|
99
|
+
- Avoid `kill -9`; it skips teardown and can leave RTOS logging disabled or a stale `/tmp/<name>` symlink.
|
|
100
|
+
- Serial disconnects during target reset are normal. Restart the bridge after the adb UART stream closes.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python3 -m pip install -e '.[dev]'
|
|
106
|
+
pytest -q
|
|
107
|
+
python3 -m build
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
adbuart-0.1.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# adbuart
|
|
2
|
+
|
|
3
|
+
Expose an Android-device UART as a local PTY over `adb`, so host serial tools such as `minicom`, `picocom`, or pyserial can talk to it like a normal serial port.
|
|
4
|
+
|
|
5
|
+
This is useful when a device has no host-side USB serial adapter and the RTOS console is reachable only through an Android UART device, for example `/dev/ttyHS1` on G2 Air style targets.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Linux or another POSIX-like host with Python 3.10+
|
|
10
|
+
- `adb` installed and available in `PATH`
|
|
11
|
+
- An adb-connected Android device where `adb root` is allowed
|
|
12
|
+
- Target-side UART device access, defaulting to `/dev/ttyHS1`
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python3 -m pip install adbuart
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Start the bridge in one terminal:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
adbuart -n g2air
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With multiple adb devices attached, pass the adb serial explicitly:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
adbuart -s d7a6340b -n g2air
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The command creates `/tmp/g2air` and keeps running in the foreground. It prints a minicom command when the PTY is ready.
|
|
35
|
+
|
|
36
|
+
Open the PTY from another terminal:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
minicom -D /tmp/g2air -b 2000000
|
|
40
|
+
# or
|
|
41
|
+
python3 -m serial.tools.miniterm /tmp/g2air 2000000
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Stop the bridge with `Ctrl-C` in the bridge terminal. Clean shutdown restores RTOS logging when it was enabled before startup.
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
Run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
adbuart --help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Important options:
|
|
55
|
+
|
|
56
|
+
- `-n, --name` creates `/tmp/<name>` and is required.
|
|
57
|
+
- `-s, --serial` selects an adb device. If omitted, exactly one device must be connected.
|
|
58
|
+
- `--remote-tty` selects the target UART device path. Default: `/dev/ttyHS1`.
|
|
59
|
+
- `--baud` sets the initial remote UART baud rate. Default: `2000000`.
|
|
60
|
+
- `--cmd-before` runs a remote shell command before bridging. Repeatable.
|
|
61
|
+
- `--cmd-after` runs a remote shell command during teardown. Repeatable.
|
|
62
|
+
|
|
63
|
+
By default the tool sets `persist.rtoslogd.uart` to `0` before bridging so Android log collection releases the UART. During teardown it restores the property to `1` only when it was `1` at startup.
|
|
64
|
+
|
|
65
|
+
## Operating notes
|
|
66
|
+
|
|
67
|
+
- The bridge owns the target UART exclusively while it is running. Stop it before flashing, resetting, or opening the same UART through another tool.
|
|
68
|
+
- The local node is protected with a lock file, but unrelated tools can still collide at the device UART level.
|
|
69
|
+
- Avoid `kill -9`; it skips teardown and can leave RTOS logging disabled or a stale `/tmp/<name>` symlink.
|
|
70
|
+
- Serial disconnects during target reset are normal. Restart the bridge after the adb UART stream closes.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python3 -m pip install -e '.[dev]'
|
|
76
|
+
pytest -q
|
|
77
|
+
python3 -m build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "adbuart"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Expose an Android device UART as a local PTY over adb."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "XuNeo" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["adb", "uart", "pty", "serial", "nuttx", "rtos"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
28
|
+
"Topic :: System :: Hardware",
|
|
29
|
+
"Topic :: Terminals :: Serial",
|
|
30
|
+
]
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"build>=1.2",
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/XuNeo/adbuart"
|
|
41
|
+
Repository = "https://github.com/XuNeo/adbuart"
|
|
42
|
+
Issues = "https://github.com/XuNeo/adbuart/issues"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
adbuart = "adbuart.cli:main"
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import errno
|
|
4
|
+
import fcntl
|
|
5
|
+
import os
|
|
6
|
+
import pty
|
|
7
|
+
import re
|
|
8
|
+
import select
|
|
9
|
+
import signal
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import termios
|
|
13
|
+
import time
|
|
14
|
+
import tty
|
|
15
|
+
|
|
16
|
+
DEFAULT_REMOTE_TTY = "/dev/ttyHS1"
|
|
17
|
+
DEFAULT_BAUD = "2000000"
|
|
18
|
+
DEFAULT_CMD_BEFORE = "setprop persist.rtoslogd.uart 0"
|
|
19
|
+
DEFAULT_CMD_AFTER = "setprop persist.rtoslogd.uart 1"
|
|
20
|
+
BAUD_SYNC_POLL_MS = 20
|
|
21
|
+
SAFE_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
22
|
+
BAUD_TO_TERMIOS = {
|
|
23
|
+
name[1:]: value
|
|
24
|
+
for name, value in vars(termios).items()
|
|
25
|
+
if name.startswith("B") and name[1:].isdigit()
|
|
26
|
+
}
|
|
27
|
+
TERMIOS_TO_BAUD = {value: name for name, value in BAUD_TO_TERMIOS.items()}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def warn_stderr(message):
|
|
31
|
+
print(f"warning: {message}", file=sys.stderr)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_name(name: str) -> str:
|
|
35
|
+
if not name or name in (".", ".."):
|
|
36
|
+
raise ValueError("--name must be a non-empty basename")
|
|
37
|
+
if "/" in name or name.strip() != name or not SAFE_NAME_RE.match(name):
|
|
38
|
+
raise ValueError("--name may contain only letters, digits, dot, underscore, and dash")
|
|
39
|
+
return name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
description="Expose an Android device UART as a local /tmp/<name> PTY over adb."
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"-s",
|
|
48
|
+
"--serial",
|
|
49
|
+
help="ADB serial, passed to every adb command with -s; auto-selects when exactly one device is connected",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument("-n", "--name", required=True, type=validate_name, help="Local node name created as /tmp/<name>")
|
|
52
|
+
parser.add_argument("--remote-tty", default=DEFAULT_REMOTE_TTY, help=f"Remote UART device path (default: {DEFAULT_REMOTE_TTY})")
|
|
53
|
+
parser.add_argument("--baud", default=DEFAULT_BAUD, help=f"Remote UART baud rate for stty (default: {DEFAULT_BAUD})")
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--cmd-before",
|
|
56
|
+
action="append",
|
|
57
|
+
help=f"Remote shell command before bridging; repeatable (default: {DEFAULT_CMD_BEFORE!r})",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--cmd-after",
|
|
61
|
+
action="append",
|
|
62
|
+
help=f"Remote shell command during teardown; repeatable (default: {DEFAULT_CMD_AFTER!r} when G2 logging was enabled)",
|
|
63
|
+
)
|
|
64
|
+
return parser.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_adb_args(serial: str, *args: str) -> list[str]:
|
|
68
|
+
return ["adb", "-s", serial, *args]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def list_adb_device_serials() -> list[str]:
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
["adb", "devices"],
|
|
74
|
+
check=True,
|
|
75
|
+
text=True,
|
|
76
|
+
stdout=subprocess.PIPE,
|
|
77
|
+
stderr=subprocess.PIPE,
|
|
78
|
+
)
|
|
79
|
+
serials = []
|
|
80
|
+
for line in result.stdout.splitlines():
|
|
81
|
+
line = line.strip()
|
|
82
|
+
if not line or line.startswith("List of devices attached"):
|
|
83
|
+
continue
|
|
84
|
+
fields = line.split()
|
|
85
|
+
if len(fields) >= 2 and fields[1] == "device":
|
|
86
|
+
serials.append(fields[0])
|
|
87
|
+
return serials
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resolve_serial(serial: str | None) -> str:
|
|
91
|
+
if serial:
|
|
92
|
+
return serial
|
|
93
|
+
try:
|
|
94
|
+
serials = list_adb_device_serials()
|
|
95
|
+
except subprocess.CalledProcessError as exc:
|
|
96
|
+
detail = exc.stderr.strip() or exc.stdout.strip() or str(exc)
|
|
97
|
+
raise RuntimeError(f"failed to run adb devices: {detail}") from exc
|
|
98
|
+
if len(serials) == 1:
|
|
99
|
+
return serials[0]
|
|
100
|
+
if not serials:
|
|
101
|
+
raise RuntimeError("no adb devices in state 'device'; pass --serial explicitly")
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
"multiple adb devices in state 'device' ("
|
|
104
|
+
+ ", ".join(serials)
|
|
105
|
+
+ "); pass --serial explicitly"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_stty_command(remote_tty: str, baud: str) -> str:
|
|
110
|
+
return f"stty -F {remote_tty} {baud} raw -echo -crtscts cs8 -parenb -cstopb min 1 time 0"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_remote_command(remote_tty: str = DEFAULT_REMOTE_TTY, baud: str = DEFAULT_BAUD) -> str:
|
|
114
|
+
return (
|
|
115
|
+
f"{build_stty_command(remote_tty, baud)} || exit $?; "
|
|
116
|
+
"trap 'kill \"$rx\" 2>/dev/null; wait \"$rx\" 2>/dev/null' EXIT HUP INT TERM; "
|
|
117
|
+
f"cat {remote_tty} & rx=$!; "
|
|
118
|
+
f"cat > {remote_tty}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def termios_baud_name(fd):
|
|
123
|
+
speed = termios.tcgetattr(fd)[5]
|
|
124
|
+
return TERMIOS_TO_BAUD.get(speed)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def set_fd_baud(fd, baud: str) -> bool:
|
|
128
|
+
speed = BAUD_TO_TERMIOS.get(str(baud))
|
|
129
|
+
if speed is None:
|
|
130
|
+
return False
|
|
131
|
+
attrs = termios.tcgetattr(fd)
|
|
132
|
+
attrs[4] = speed
|
|
133
|
+
attrs[5] = speed
|
|
134
|
+
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def sync_pty_baud_if_changed(slave_fd, adb, remote_tty, last_baud):
|
|
139
|
+
current_baud = termios_baud_name(slave_fd)
|
|
140
|
+
if current_baud is None or current_baud == last_baud:
|
|
141
|
+
return last_baud
|
|
142
|
+
adb.shell(build_stty_command(remote_tty, current_baud))
|
|
143
|
+
return current_baud
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class BaudSynchronizer:
|
|
147
|
+
def __init__(self, slave_fd, adb, remote_tty, initial_baud, poll_ms=BAUD_SYNC_POLL_MS):
|
|
148
|
+
self.slave_fd = slave_fd
|
|
149
|
+
self.adb = adb
|
|
150
|
+
self.remote_tty = remote_tty
|
|
151
|
+
self.last_baud = initial_baud
|
|
152
|
+
self.poll_interval = poll_ms / 1000.0
|
|
153
|
+
self.next_poll = 0.0
|
|
154
|
+
|
|
155
|
+
def sync_if_due(self):
|
|
156
|
+
now = time.monotonic()
|
|
157
|
+
if now < self.next_poll:
|
|
158
|
+
return
|
|
159
|
+
self.next_poll = now + self.poll_interval
|
|
160
|
+
self.last_baud = sync_pty_baud_if_changed(self.slave_fd, self.adb, self.remote_tty, self.last_baud)
|
|
161
|
+
|
|
162
|
+
def sync_now(self):
|
|
163
|
+
self.next_poll = time.monotonic() + self.poll_interval
|
|
164
|
+
self.last_baud = sync_pty_baud_if_changed(self.slave_fd, self.adb, self.remote_tty, self.last_baud)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class BridgePaths:
|
|
168
|
+
def __init__(self, name, tmp_dir="/tmp"):
|
|
169
|
+
safe_name = validate_name(name)
|
|
170
|
+
self.node_path = os.path.join(tmp_dir, safe_name)
|
|
171
|
+
self.lock_path = os.path.join(tmp_dir, safe_name + ".lock")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_symlink(node_path, slave_path, lock=None):
|
|
175
|
+
if os.path.lexists(node_path):
|
|
176
|
+
if not os.path.islink(node_path):
|
|
177
|
+
raise FileExistsError(f"refusing to overwrite non-symlink: {node_path}")
|
|
178
|
+
current_target = os.readlink(node_path)
|
|
179
|
+
if os.path.exists(node_path):
|
|
180
|
+
raise FileExistsError(f"refusing to overwrite live symlink: {node_path} -> {current_target}")
|
|
181
|
+
os.unlink(node_path)
|
|
182
|
+
os.symlink(slave_path, node_path)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def remove_symlink_if_owned(node_path, slave_path):
|
|
186
|
+
if not os.path.islink(node_path):
|
|
187
|
+
return
|
|
188
|
+
if os.readlink(node_path) == slave_path:
|
|
189
|
+
os.unlink(node_path)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class LocalLock:
|
|
193
|
+
def __init__(self, lock_path):
|
|
194
|
+
self.lock_path = lock_path
|
|
195
|
+
self.fd = None
|
|
196
|
+
|
|
197
|
+
def acquire(self):
|
|
198
|
+
self.fd = os.open(self.lock_path, os.O_RDWR | os.O_CREAT, 0o600)
|
|
199
|
+
try:
|
|
200
|
+
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
201
|
+
except BlockingIOError as exc:
|
|
202
|
+
os.close(self.fd)
|
|
203
|
+
self.fd = None
|
|
204
|
+
raise RuntimeError(f"lock already held: {self.lock_path}") from exc
|
|
205
|
+
|
|
206
|
+
def is_held(self):
|
|
207
|
+
return self.fd is not None
|
|
208
|
+
|
|
209
|
+
def release(self):
|
|
210
|
+
if self.fd is None:
|
|
211
|
+
return
|
|
212
|
+
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
|
213
|
+
os.close(self.fd)
|
|
214
|
+
self.fd = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Adb:
|
|
218
|
+
def __init__(self, serial):
|
|
219
|
+
self.serial = serial
|
|
220
|
+
|
|
221
|
+
def run(self, *args, check=True):
|
|
222
|
+
return subprocess.run(
|
|
223
|
+
build_adb_args(self.serial, *args),
|
|
224
|
+
check=check,
|
|
225
|
+
text=True,
|
|
226
|
+
stdout=subprocess.PIPE,
|
|
227
|
+
stderr=subprocess.PIPE,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def shell(self, command, check=True):
|
|
231
|
+
return self.run("shell", command, check=check)
|
|
232
|
+
|
|
233
|
+
def popen_shell_t(self, command):
|
|
234
|
+
return subprocess.Popen(
|
|
235
|
+
build_adb_args(self.serial, "shell", "-T", command),
|
|
236
|
+
stdin=subprocess.PIPE,
|
|
237
|
+
stdout=subprocess.PIPE,
|
|
238
|
+
stderr=subprocess.PIPE,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def run_remote_commands(adb, commands):
|
|
243
|
+
for command in commands:
|
|
244
|
+
adb.shell(command)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_rtos_uart_state(adb):
|
|
248
|
+
result = adb.shell("getprop persist.rtoslogd.uart")
|
|
249
|
+
return result.stdout.strip()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def set_rtos_uart_state(adb, state):
|
|
253
|
+
adb.shell(f"setprop persist.rtoslogd.uart {state}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def restore_rtos_logging(adb, startup_state):
|
|
257
|
+
if startup_state != "1":
|
|
258
|
+
return True
|
|
259
|
+
try:
|
|
260
|
+
set_rtos_uart_state(adb, "1")
|
|
261
|
+
return True
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
warn_stderr(f"failed to restore RTOS logging: {exc}")
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def run_cmd_after(adb, startup_state, commands):
|
|
268
|
+
try:
|
|
269
|
+
if commands is None:
|
|
270
|
+
return restore_rtos_logging(adb, startup_state)
|
|
271
|
+
run_remote_commands(adb, commands)
|
|
272
|
+
return True
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
warn_stderr(f"failed to run teardown commands: {exc}")
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
COPY_SIZE = 65536
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def read_chunk(fd):
|
|
282
|
+
while True:
|
|
283
|
+
try:
|
|
284
|
+
return os.read(fd, COPY_SIZE)
|
|
285
|
+
except InterruptedError:
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def write_all(fd, data):
|
|
290
|
+
view = memoryview(data)
|
|
291
|
+
offset = 0
|
|
292
|
+
while offset < len(view):
|
|
293
|
+
try:
|
|
294
|
+
written = os.write(fd, view[offset:])
|
|
295
|
+
except InterruptedError:
|
|
296
|
+
continue
|
|
297
|
+
if written == 0:
|
|
298
|
+
raise OSError(errno.EPIPE, "write returned zero")
|
|
299
|
+
offset += written
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def copy_available(src_fd, dst_fd):
|
|
303
|
+
data = read_chunk(src_fd)
|
|
304
|
+
if not data:
|
|
305
|
+
raise EOFError("input closed")
|
|
306
|
+
write_all(dst_fd, data)
|
|
307
|
+
return len(data)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def drain_to_eof(src_fd, dst_fd):
|
|
311
|
+
while True:
|
|
312
|
+
data = read_chunk(src_fd)
|
|
313
|
+
if not data:
|
|
314
|
+
return
|
|
315
|
+
write_all(dst_fd, data)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def forward_loop(pty_fd, adb_proc, baud_sync=None):
|
|
319
|
+
adb_stdout_fd = adb_proc.stdout.fileno()
|
|
320
|
+
adb_stdin_fd = adb_proc.stdin.fileno()
|
|
321
|
+
adb_stderr_fd = adb_proc.stderr.fileno()
|
|
322
|
+
poller = select.poll()
|
|
323
|
+
poller.register(pty_fd, select.POLLIN | select.POLLERR | select.POLLHUP)
|
|
324
|
+
poller.register(adb_stdout_fd, select.POLLIN | select.POLLERR | select.POLLHUP)
|
|
325
|
+
poller.register(adb_stderr_fd, select.POLLIN | select.POLLERR | select.POLLHUP)
|
|
326
|
+
|
|
327
|
+
def drain_adb_pipes():
|
|
328
|
+
drain_to_eof(adb_stdout_fd, pty_fd)
|
|
329
|
+
drain_to_eof(adb_stderr_fd, sys.stderr.fileno())
|
|
330
|
+
|
|
331
|
+
while True:
|
|
332
|
+
if adb_proc.poll() is not None:
|
|
333
|
+
drain_adb_pipes()
|
|
334
|
+
return
|
|
335
|
+
if baud_sync is not None:
|
|
336
|
+
baud_sync.sync_if_due()
|
|
337
|
+
events = poller.poll(BAUD_SYNC_POLL_MS if baud_sync is not None else 1000)
|
|
338
|
+
for fd, event in events:
|
|
339
|
+
if adb_proc.poll() is not None:
|
|
340
|
+
drain_adb_pipes()
|
|
341
|
+
return
|
|
342
|
+
if fd == pty_fd:
|
|
343
|
+
if event & select.POLLIN:
|
|
344
|
+
if baud_sync is not None:
|
|
345
|
+
baud_sync.sync_now()
|
|
346
|
+
copy_available(pty_fd, adb_stdin_fd)
|
|
347
|
+
elif event & (select.POLLERR | select.POLLHUP):
|
|
348
|
+
raise EOFError(f"fd closed: {fd}")
|
|
349
|
+
elif fd == adb_stdout_fd:
|
|
350
|
+
if event & select.POLLIN:
|
|
351
|
+
copy_available(adb_stdout_fd, pty_fd)
|
|
352
|
+
elif event & (select.POLLERR | select.POLLHUP):
|
|
353
|
+
raise EOFError(f"fd closed: {fd}")
|
|
354
|
+
elif fd == adb_stderr_fd:
|
|
355
|
+
if event & select.POLLIN:
|
|
356
|
+
copy_available(adb_stderr_fd, sys.stderr.fileno())
|
|
357
|
+
elif event & (select.POLLERR | select.POLLHUP):
|
|
358
|
+
raise EOFError(f"fd closed: {fd}")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class BridgeRuntime:
|
|
362
|
+
def __init__(self, adb, node_path, slave_path, lock, startup_rtos_state, cmd_after=None):
|
|
363
|
+
self.adb = adb
|
|
364
|
+
self.node_path = node_path
|
|
365
|
+
self.slave_path = slave_path
|
|
366
|
+
self.lock = lock
|
|
367
|
+
self.startup_rtos_state = startup_rtos_state
|
|
368
|
+
self.cmd_after = cmd_after
|
|
369
|
+
self.adb_proc = None
|
|
370
|
+
self.pty_fd = None
|
|
371
|
+
self.slave_fd = None
|
|
372
|
+
self.restore_ok = None
|
|
373
|
+
self._torn_down = False
|
|
374
|
+
|
|
375
|
+
def teardown(self, reason):
|
|
376
|
+
if self._torn_down:
|
|
377
|
+
return self.restore_ok
|
|
378
|
+
self._torn_down = True
|
|
379
|
+
if self.adb_proc is not None and self.adb_proc.poll() is None:
|
|
380
|
+
try:
|
|
381
|
+
self.adb_proc.terminate()
|
|
382
|
+
except Exception as exc:
|
|
383
|
+
warn_stderr(f"failed to terminate adb bridge: {exc}")
|
|
384
|
+
try:
|
|
385
|
+
self.adb_proc.wait(timeout=2)
|
|
386
|
+
except subprocess.TimeoutExpired:
|
|
387
|
+
try:
|
|
388
|
+
self.adb_proc.kill()
|
|
389
|
+
except Exception as exc:
|
|
390
|
+
warn_stderr(f"failed to kill adb bridge: {exc}")
|
|
391
|
+
try:
|
|
392
|
+
self.adb_proc.wait(timeout=2)
|
|
393
|
+
except subprocess.TimeoutExpired as exc:
|
|
394
|
+
warn_stderr(f"adb bridge did not exit after kill: {exc}")
|
|
395
|
+
except Exception as exc:
|
|
396
|
+
warn_stderr(f"failed waiting for killed adb bridge: {exc}")
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
warn_stderr(f"failed waiting for adb bridge: {exc}")
|
|
399
|
+
if self.pty_fd is not None:
|
|
400
|
+
try:
|
|
401
|
+
os.close(self.pty_fd)
|
|
402
|
+
except OSError:
|
|
403
|
+
pass
|
|
404
|
+
self.pty_fd = None
|
|
405
|
+
if self.slave_fd is not None:
|
|
406
|
+
try:
|
|
407
|
+
os.close(self.slave_fd)
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
self.slave_fd = None
|
|
411
|
+
remove_symlink_if_owned(self.node_path, self.slave_path)
|
|
412
|
+
self.restore_ok = run_cmd_after(self.adb, self.startup_rtos_state, self.cmd_after)
|
|
413
|
+
self.lock.release()
|
|
414
|
+
print(f"teardown complete: {reason}", file=sys.stderr)
|
|
415
|
+
return self.restore_ok
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
_active_runtime = None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def set_raw(fd):
|
|
422
|
+
tty.setraw(fd, when=termios.TCSANOW)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class PrepareRemoteUartError(RuntimeError):
|
|
426
|
+
def __init__(self, message, startup_state):
|
|
427
|
+
super().__init__(message)
|
|
428
|
+
self.startup_state = startup_state
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def prepare_remote_uart(adb, cmd_before=None, cmd_after=None):
|
|
432
|
+
adb.run("wait-for-device")
|
|
433
|
+
adb.run("root")
|
|
434
|
+
adb.run("wait-for-device")
|
|
435
|
+
startup_state = ""
|
|
436
|
+
if cmd_after is None:
|
|
437
|
+
startup_state = get_rtos_uart_state(adb)
|
|
438
|
+
try:
|
|
439
|
+
if cmd_before is None:
|
|
440
|
+
set_rtos_uart_state(adb, "0")
|
|
441
|
+
else:
|
|
442
|
+
run_remote_commands(adb, cmd_before)
|
|
443
|
+
except BaseException as exc:
|
|
444
|
+
exc.startup_state = startup_state
|
|
445
|
+
raise
|
|
446
|
+
return startup_state
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _signal_handler(signum, frame):
|
|
450
|
+
if _active_runtime is not None:
|
|
451
|
+
_active_runtime.teardown(f"signal {signum}")
|
|
452
|
+
raise SystemExit(128 + signum)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def install_signal_handlers():
|
|
456
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
457
|
+
signal.signal(signal.SIGTERM, _signal_handler)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _fd_is_tty(fd):
|
|
461
|
+
try:
|
|
462
|
+
return os.isatty(fd)
|
|
463
|
+
except OSError:
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def close_fd_if_owned(fd, owned):
|
|
468
|
+
if not owned or fd is None:
|
|
469
|
+
return
|
|
470
|
+
try:
|
|
471
|
+
os.close(fd)
|
|
472
|
+
except OSError:
|
|
473
|
+
pass
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def cleanup_before_runtime(paths, lock, adb, startup_state, cmd_after, master_fd, master_fd_owned, slave_fd, slave_fd_owned, slave_path):
|
|
477
|
+
close_fd_if_owned(master_fd, master_fd_owned)
|
|
478
|
+
close_fd_if_owned(slave_fd, slave_fd_owned)
|
|
479
|
+
if slave_path is not None:
|
|
480
|
+
remove_symlink_if_owned(paths.node_path, slave_path)
|
|
481
|
+
run_cmd_after(adb, startup_state, cmd_after)
|
|
482
|
+
lock.release()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def run_bridge(args, tmp_dir="/tmp"):
|
|
486
|
+
global _active_runtime
|
|
487
|
+
paths = BridgePaths(args.name, tmp_dir=tmp_dir)
|
|
488
|
+
lock = LocalLock(paths.lock_path)
|
|
489
|
+
lock.acquire()
|
|
490
|
+
adb = Adb(args.serial)
|
|
491
|
+
startup_state = ""
|
|
492
|
+
master_fd = None
|
|
493
|
+
master_fd_owned = False
|
|
494
|
+
slave_fd = None
|
|
495
|
+
slave_fd_owned = False
|
|
496
|
+
slave_path = None
|
|
497
|
+
runtime = None
|
|
498
|
+
try:
|
|
499
|
+
startup_state = prepare_remote_uart(adb, args.cmd_before, args.cmd_after)
|
|
500
|
+
master_fd, slave_fd = pty.openpty()
|
|
501
|
+
slave_path = os.ttyname(slave_fd)
|
|
502
|
+
slave_fd_owned = _fd_is_tty(slave_fd)
|
|
503
|
+
master_fd_owned = _fd_is_tty(master_fd)
|
|
504
|
+
if master_fd_owned:
|
|
505
|
+
set_raw(master_fd)
|
|
506
|
+
if slave_fd_owned and not set_fd_baud(slave_fd, args.baud):
|
|
507
|
+
warn_stderr(f"cannot observe local PTY baud {args.baud}; remote baud sync disabled until a supported rate is set")
|
|
508
|
+
create_symlink(paths.node_path, slave_path, lock=lock)
|
|
509
|
+
runtime = BridgeRuntime(adb, paths.node_path, slave_path, lock, startup_state, args.cmd_after)
|
|
510
|
+
runtime.pty_fd = master_fd if master_fd_owned else None
|
|
511
|
+
runtime.slave_fd = slave_fd if slave_fd_owned else None
|
|
512
|
+
baud_sync = BaudSynchronizer(slave_fd, adb, args.remote_tty, args.baud) if slave_fd_owned else None
|
|
513
|
+
runtime.adb_proc = adb.popen_shell_t(build_remote_command(args.remote_tty, args.baud))
|
|
514
|
+
_active_runtime = runtime
|
|
515
|
+
print(f"PTY ready: {paths.node_path}", file=sys.stderr)
|
|
516
|
+
print(f"Target: adb -s {args.serial} {args.remote_tty}", file=sys.stderr)
|
|
517
|
+
print(f"Use another terminal: minicom -D {paths.node_path} -b {args.baud}", file=sys.stderr)
|
|
518
|
+
print("Press Ctrl-C here to teardown.", file=sys.stderr)
|
|
519
|
+
forward_loop(master_fd, runtime.adb_proc, baud_sync=baud_sync)
|
|
520
|
+
adb_returncode = runtime.adb_proc.poll()
|
|
521
|
+
restore_ok = runtime.teardown("forward loop ended")
|
|
522
|
+
if adb_returncode not in (None, 0):
|
|
523
|
+
return 1
|
|
524
|
+
if restore_ok is False:
|
|
525
|
+
return 1
|
|
526
|
+
return 0
|
|
527
|
+
except PrepareRemoteUartError as exc:
|
|
528
|
+
startup_state = exc.startup_state
|
|
529
|
+
run_cmd_after(adb, startup_state, args.cmd_after)
|
|
530
|
+
lock.release()
|
|
531
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
532
|
+
return 1
|
|
533
|
+
except Exception as exc:
|
|
534
|
+
startup_state = getattr(exc, "startup_state", startup_state)
|
|
535
|
+
if runtime is not None:
|
|
536
|
+
runtime.teardown(str(exc))
|
|
537
|
+
else:
|
|
538
|
+
cleanup_before_runtime(
|
|
539
|
+
paths, lock, adb, startup_state, args.cmd_after,
|
|
540
|
+
master_fd, master_fd_owned, slave_fd, slave_fd_owned, slave_path,
|
|
541
|
+
)
|
|
542
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
543
|
+
return 1
|
|
544
|
+
except BaseException as exc:
|
|
545
|
+
startup_state = getattr(exc, "startup_state", startup_state)
|
|
546
|
+
if runtime is not None:
|
|
547
|
+
runtime.teardown(str(exc))
|
|
548
|
+
else:
|
|
549
|
+
cleanup_before_runtime(
|
|
550
|
+
paths, lock, adb, startup_state, args.cmd_after,
|
|
551
|
+
master_fd, master_fd_owned, slave_fd, slave_fd_owned, slave_path,
|
|
552
|
+
)
|
|
553
|
+
raise
|
|
554
|
+
finally:
|
|
555
|
+
_active_runtime = None
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def main(argv: list[str] | None = None) -> int:
|
|
559
|
+
install_signal_handlers()
|
|
560
|
+
args = parse_args(argv)
|
|
561
|
+
try:
|
|
562
|
+
args.serial = resolve_serial(args.serial)
|
|
563
|
+
except RuntimeError as exc:
|
|
564
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
565
|
+
return 1
|
|
566
|
+
return run_bridge(args)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
if __name__ == "__main__":
|
|
570
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from adbuart import cli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_parse_args_keeps_required_name_and_defaults():
|
|
9
|
+
args = cli.parse_args(["-n", "g2air"])
|
|
10
|
+
|
|
11
|
+
assert args.name == "g2air"
|
|
12
|
+
assert args.serial is None
|
|
13
|
+
assert args.remote_tty == cli.DEFAULT_REMOTE_TTY
|
|
14
|
+
assert args.baud == cli.DEFAULT_BAUD
|
|
15
|
+
assert args.cmd_before is None
|
|
16
|
+
assert args.cmd_after is None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_args_accepts_existing_options():
|
|
20
|
+
args = cli.parse_args([
|
|
21
|
+
"-s",
|
|
22
|
+
"d7a6340b",
|
|
23
|
+
"-n",
|
|
24
|
+
"g2air",
|
|
25
|
+
"--remote-tty",
|
|
26
|
+
"/dev/ttyHS2",
|
|
27
|
+
"--baud",
|
|
28
|
+
"115200",
|
|
29
|
+
"--cmd-before",
|
|
30
|
+
"echo before",
|
|
31
|
+
"--cmd-before",
|
|
32
|
+
"echo before2",
|
|
33
|
+
"--cmd-after",
|
|
34
|
+
"echo after",
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
assert args.serial == "d7a6340b"
|
|
38
|
+
assert args.name == "g2air"
|
|
39
|
+
assert args.remote_tty == "/dev/ttyHS2"
|
|
40
|
+
assert args.baud == "115200"
|
|
41
|
+
assert args.cmd_before == ["echo before", "echo before2"]
|
|
42
|
+
assert args.cmd_after == ["echo after"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("bad_name", ["", ".", "..", "bad/name", " bad", "bad ", "bad:name"])
|
|
46
|
+
def test_validate_name_rejects_unsafe_names(bad_name):
|
|
47
|
+
with pytest.raises(ValueError):
|
|
48
|
+
cli.validate_name(bad_name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_validate_name_accepts_safe_names():
|
|
52
|
+
assert cli.validate_name("g2air") == "g2air"
|
|
53
|
+
assert cli.validate_name("g2-air_1.0") == "g2-air_1.0"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_missing_required_name_exits_parser():
|
|
57
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
58
|
+
cli.parse_args([])
|
|
59
|
+
|
|
60
|
+
assert excinfo.value.code == 2
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_build_remote_command_keeps_existing_shape():
|
|
64
|
+
command = cli.build_remote_command("/dev/ttyHS2", "115200")
|
|
65
|
+
|
|
66
|
+
assert "stty -F /dev/ttyHS2 115200 raw -echo -crtscts cs8 -parenb -cstopb min 1 time 0" in command
|
|
67
|
+
assert "cat /dev/ttyHS2 & rx=$!" in command
|
|
68
|
+
assert "cat > /dev/ttyHS2" in command
|