This commit is contained in:
eric
2026-04-04 16:18:52 -05:00
parent 9672f63cd9
commit a48db482a8
5 changed files with 340 additions and 14 deletions

View File

@@ -18,6 +18,7 @@ It keeps only the parts that were actually used:
- `bootstrap_redroid.py`
- `verify_offline_bundle.py`
- `tools/compat.py` (host / GPU / runtime checks)
- `tools/ssh_remote.py` (Paramiko SSH: remote `docker` / `adb` diagnostics)
- `offline/README.md`
- `offline/manifest.json`
- `stuff/mindthegapps.py`
@@ -75,15 +76,15 @@ The project probes the **host** before critical steps (no separate manual checkl
**GPU mode (`run_redroid.py`):**
- **`--gpu-mode auto` (default):** uses **`host`** when DRI devices exist and automatically adds `docker run --device …` for each `/dev/dri/card*` / `renderD*` node; otherwise **`guest`** (software GLES).
- **`--gpu-mode guest` (default):** software GLES — most stable on headless / server GPUs where host EGL inside the guest often breaks boot or **adbd**.
- **`--gpu-mode auto`:** stays **`guest`** unless **`REDROID_USE_HOST_GPU=1`** (or `true`/`yes`/`on`) **and** `/dev/dri` nodes exist; then **`host`** and `docker run --device …` per DRI node.
- **`--gpu-mode host`:** pass-through when DRI exists; otherwise **falls back to `guest`** with a warning.
- **`--gpu-mode guest`:** force software rendering (common black-screen case on headless servers).
Quick probe only:
```bash
python tools/compat.py
python tools/compat.py -c podman --gpu-mode auto
python tools/compat.py -c podman --gpu-mode guest
```
## Build the 5570 image
@@ -131,6 +132,12 @@ python run_redroid.py \
--adb-insecure
```
On the **same machine as Docker**, one-shot status + logs + `logcat` after start:
```bash
python run_redroid.py ... --adb-insecure --diagnose --diagnose-settle 20
```
Then:
```bash
@@ -145,6 +152,36 @@ If you still see `unauthorized` after an upgrade, stop the container, remove per
rm -rf ~/data-redroid13/misc/adb
```
## Remote diagnostics over SSH (Paramiko)
When redroid runs on **another Linux machine**, you can drive checks from your PC with **`tools/ssh_remote.py`** (uses [Paramiko](https://www.paramiko.org/), declared in `requirements.txt`).
**Do not put passwords in Git.** Use a key file or a one-time prompt:
```bash
pip install -r requirements.txt
export REDROID_SSH_HOST=192.168.2.179
export REDROID_SSH_USER=eric
python tools/ssh_remote.py --password-prompt diagnose
```
Key-based:
```bash
export REDROID_SSH_HOST=... REDROID_SSH_USER=eric REDROID_SSH_KEY=$HOME/.ssh/id_ed25519
python tools/ssh_remote.py diagnose
```
Optional env: **`REDROID_CONTAINER`** (default `redroid_13.0.0_mindthegapps_houdini_magisk`), **`REDROID_ADB_PORT`** (default `5570`). You can also pass **`--container-name`** / **`--adb-port`**.
Arbitrary remote command (note the **`--`** before the shell snippet):
```bash
python tools/ssh_remote.py -H 192.168.2.179 -U eric --password-prompt exec -- bash -lc 'docker ps -a | head -20'
```
The `shell` subcommand is a minimal pseudo-shell only; for a full TTY, use normal OpenSSH **`ssh`**.
## ADB `failed to connect` / connection refused
This means **nothing is listening on the host port** (not the same as `unauthorized`). Typical causes:
@@ -158,11 +195,11 @@ This means **nothing is listening on the host port** (not the same as `unauthori
## Black screen or flickering
With **`--gpu-mode guest`** (or **`auto`** when no `/dev/dri` exists), the display uses **software GLES** (SwiftShader). On many **headless servers**, SurfaceFlinger may show a **black screen**, **flicker**, or very low FPS. That is a **display stack** issue, not ADB. The default **`auto`** mode uses **host** GPU when DRI nodes are visible and injects **`--device`** flags automatically.
With **`--gpu-mode guest`** (the default), the display uses **software GLES** (SwiftShader). On many **headless servers**, SurfaceFlinger may still show **black screen** or **flicker** — try **`--soft-display`** and more RAM. Host GPU needs **`REDROID_USE_HOST_GPU=1`** with **`--gpu-mode auto`** or **`--gpu-mode host`** when DRI works inside the guest.
**What to try**
1. **GPU on the host:** **`--gpu-mode auto`** (default) enables **host** when `/dev/dri` exists. If the instance **crashes** or **ADB never connects**, host EGL may be incompatible — fall back to **`--gpu-mode guest`**. If you must pass devices manually, for example:
1. **GPU on the host:** set **`REDROID_USE_HOST_GPU=1`** and **`--gpu-mode auto`**, or **`--gpu-mode host`**. If **ADB never connects**, host EGL is likely broken — use default **`--gpu-mode guest`**. Manual devices example:
```bash
docker run ... --device /dev/dri ...
```

View File

@@ -1,2 +1,3 @@
requests==2.28.1
tqdm==4.64.1
tqdm==4.64.1
paramiko>=3.4.0,<4

View File

@@ -195,6 +195,58 @@ def wait_tcp_port(host, port, timeout_sec, interval=1.0):
return False
def local_post_diagnose(container_runtime, name, port, settle_sec):
"""On the Docker host: container status, logs, adb, short logcat."""
print_color(
"\n========== --diagnose (local): waiting {}s ==========".format(settle_sec),
bcolors.YELLOW,
)
time.sleep(settle_sec)
serial = "127.0.0.1:{}".format(port)
print_color("--- {} ps -a ---".format(container_runtime), bcolors.GREEN)
subprocess.run(
[container_runtime, "ps", "-a", "--filter", "name={}".format(name), "--no-trunc"],
check=False,
)
print_color("--- {} inspect ---".format(container_runtime), bcolors.GREEN)
subprocess.run(
[
container_runtime,
"inspect",
"--format",
"status={{.State.Status}} exit={{.State.ExitCode}} err={{.State.Error}}",
name,
],
check=False,
)
print_color("--- {} logs (tail 120) ---".format(container_runtime), bcolors.GREEN)
subprocess.run([container_runtime, "logs", "--tail", "120", name], check=False)
print_color("--- adb ---", bcolors.GREEN)
subprocess.run(["adb", "connect", serial], check=False)
subprocess.run(["adb", "devices", "-l"], check=False)
subprocess.run(["adb", "-s", serial, "shell", "getprop", "sys.boot_completed"], check=False)
print_color("--- adb logcat (display/GLES, last 80 lines) ---", bcolors.GREEN)
subprocess.run(
[
"adb",
"-s",
serial,
"logcat",
"-d",
"-t",
"80",
"*:S",
"SurfaceFlinger:V",
"EGL:V",
"OpenGLRenderer:V",
"redroid:V",
"AndroidRuntime:E",
],
check=False,
)
print_color("========== end --diagnose ==========\n", bcolors.YELLOW)
def main():
parser = argparse.ArgumentParser(
description="Launch a redroid container with a tested native bridge configuration."
@@ -240,10 +292,11 @@ def main():
)
parser.add_argument(
"--gpu-mode",
default="auto",
default="guest",
help=(
"androidboot.redroid_gpu_mode: auto=host if /dev/dri exists else guest (default); "
"guest=software GLES; host=GPU (adds --device for each DRI node when present)"
"androidboot.redroid_gpu_mode: default guest (stable on headless). "
"auto=guest unless REDROID_USE_HOST_GPU=1 and DRI exists, then host. "
"host=GPU (adds --device per DRI node when present)"
),
)
parser.add_argument(
@@ -307,6 +360,18 @@ def main():
"If it never opens, print container logs. Use 90 when debugging failed adb connect."
),
)
parser.add_argument(
"--diagnose",
action="store_true",
help="After start: wait, then print docker ps/inspect/logs + adb + logcat (same host as Docker)",
)
parser.add_argument(
"--diagnose-settle",
type=int,
default=15,
metavar="SEC",
help="Seconds to sleep before --diagnose probes (default 15)",
)
args = parser.parse_args()
image = args.image
@@ -407,6 +472,8 @@ def main():
subprocess.run(cmd, check=True)
print_color("Container started as {}".format(name), bcolors.GREEN)
if args.diagnose:
local_post_diagnose(args.container, name, args.port, args.diagnose_settle)
if args.wait_tcp > 0:
print_color(
"Waiting up to {}s for 127.0.0.1:{} (adbd TCP) ...".format(args.wait_tcp, args.port),

View File

@@ -90,11 +90,19 @@ def analyze_for_build(container_runtime: str) -> CompatReport:
def resolve_gpu_mode(requested: str, dri_devices: List[str]) -> Tuple[str, List[str]]:
"""
Map user --gpu-mode to an effective redroid mode and docker --device arguments.
auto -> host if DRI nodes exist, else guest.
auto -> guest by default (many servers have /dev/dri but host-EGL still breaks boot/adbd).
Set REDROID_USE_HOST_GPU=1 (or true/yes/on) with --gpu-mode auto to pass DRI + use host when nodes exist.
"""
req = (requested or "auto").strip().lower()
if req == "auto":
effective = "host" if dri_devices else "guest"
want_host = os.environ.get("REDROID_USE_HOST_GPU", "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
effective = "host" if (want_host and dri_devices) else "guest"
elif req == "host":
effective = "host" if dri_devices else "guest"
elif req == "guest":
@@ -126,7 +134,11 @@ def analyze_for_run(
report.warnings.extend(build.warnings)
report.notes.extend(build.notes)
if dri:
report.notes.append("DRI present: {} — GPU passthrough can be used.".format(", ".join(dri)))
report.notes.append(
"DRI present: {} — for host GPU use --gpu-mode host or REDROID_USE_HOST_GPU=1 with --gpu-mode auto.".format(
", ".join(dri)
)
)
elif platform.system() == "Linux":
report.warnings.append(
"No /dev/dri render nodes: software GLES (guest) often black-screens or flickers on headless servers."
@@ -140,7 +152,11 @@ def analyze_for_run(
"--gpu-mode host requested but no DRI devices found; falling back to guest."
)
if req == "auto" and not skip_check:
report.notes.append("Resolved --gpu-mode auto -> {}".format(effective))
report.notes.append(
"Resolved --gpu-mode auto -> {} (set REDROID_USE_HOST_GPU=1 to prefer host when DRI exists).".format(
effective
)
)
if effective == "host" and not skip_check:
report.notes.append(
"GPU host mode: if ADB cannot connect (connection refused), Android may not have finished boot "
@@ -164,7 +180,7 @@ if __name__ == "__main__":
ap = argparse.ArgumentParser(description="Host/runtime compatibility probe (same logic as run_redroid.py)")
ap.add_argument("-c", "--container", default="docker", help="docker or podman")
ap.add_argument("--gpu-mode", default="auto", help="guest, host, or auto")
ap.add_argument("--gpu-mode", default="guest", help="guest, host, or auto")
ns = ap.parse_args()
rep, gpu_eff, dev_args = analyze_for_run(ns.container, ns.gpu_mode, skip_check=False)
emit_compat_report(rep)

205
tools/ssh_remote.py Normal file
View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
SSH to a Linux host (where Docker + redroid run) and run diagnostics or shell commands.
Authentication (pick one): private key (--key-file / REDROID_SSH_KEY), password
(REDROID_SSH_PASSWORD), or --password-prompt. Do not commit secrets.
Examples:
REDROID_SSH_HOST=192.168.2.179 REDROID_SSH_USER=eric python tools/ssh_remote.py --password-prompt diagnose
python tools/ssh_remote.py -H 192.168.2.179 -U eric --key-file ~/.ssh/id_ed25519 diagnose
python tools/ssh_remote.py -H ... -U eric --password-prompt exec -- bash -lc 'docker ps -a | head'
"""
from __future__ import annotations
import argparse
import getpass
import os
import shlex
import sys
import threading
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
import paramiko
def connect_client(args: argparse.Namespace) -> paramiko.SSHClient:
host = (args.host or "").strip()
user = (args.user or "").strip()
if not host or not user:
print("Set -H/--host and -U/--user (or REDROID_SSH_HOST / REDROID_SSH_USER).", file=sys.stderr)
sys.exit(2)
password = (getattr(args, "password", None) or os.environ.get("REDROID_SSH_PASSWORD", "")).strip() or None
key_file = (args.key_file or os.environ.get("REDROID_SSH_KEY", "")).strip() or None
if getattr(args, "password_prompt", False):
password = getpass.getpass("SSH password: ") or None
if not password:
print("Empty password.", file=sys.stderr)
sys.exit(2)
if not password and not key_file:
print(
"Need auth: --key-file / REDROID_SSH_KEY, or REDROID_SSH_PASSWORD, or --password-prompt.",
file=sys.stderr,
)
sys.exit(2)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
kw: dict = {
"hostname": host,
"port": int(args.port),
"username": user,
"timeout": float(args.timeout),
"allow_agent": bool(key_file),
"look_for_keys": bool(key_file),
}
if key_file:
kw["key_filename"] = os.path.expanduser(key_file)
if password:
kw["password"] = password
client.connect(**kw)
return client
def run_remote(client: paramiko.SSHClient, bash_script: str, timeout: float = 300.0) -> int:
cmd = "bash -lc {}".format(shlex.quote(bash_script))
stdin, stdout, stderr = client.exec_command(cmd)
stdout.channel.settimeout(timeout)
out = stdout.read().decode("utf-8", errors="replace")
err = stderr.read().decode("utf-8", errors="replace")
if out:
sys.stdout.write(out)
if not out.endswith("\n"):
sys.stdout.write("\n")
if err:
sys.stderr.write(err)
if not err.endswith("\n"):
sys.stderr.write("\n")
return int(stdout.channel.recv_exit_status())
def cmd_diagnose(args: argparse.Namespace) -> int:
cname = args.container_name
port = int(args.adb_port)
qc = shlex.quote(cname)
script = """
set +e
echo "=== docker ps (name filter) ==="
docker ps -a --filter name={qc} --no-trunc 2>/dev/null | head -35
echo "=== docker inspect ==="
docker inspect --format 'status={{{{.State.Status}}}} exit={{{{.State.ExitCode}}}} err={{{{.State.Error}}}}' {qc} 2>/dev/null || echo "(no such container)"
echo "=== docker logs (tail 150) ==="
docker logs --tail 150 {qc} 2>/dev/null || echo "(no logs)"
echo "=== adb ==="
adb connect 127.0.0.1:{port}
adb devices -l
adb -s 127.0.0.1:{port} shell getprop sys.boot_completed 2>/dev/null || true
echo "=== adb logcat (display/GLES, last 80) ==="
adb -s 127.0.0.1:{port} logcat -d -t 80 '*:S' SurfaceFlinger:V EGL:V OpenGLRenderer:V redroid:V AndroidRuntime:E 2>/dev/null || true
""".format(
qc=qc,
port=port,
)
client = connect_client(args)
try:
return run_remote(client, script)
finally:
client.close()
def cmd_exec(args: argparse.Namespace) -> int:
remote = " ".join(args.remote_words).strip()
if not remote:
print("exec: provide a remote command after --", file=sys.stderr)
return 2
client = connect_client(args)
try:
return run_remote(client, remote, timeout=float(args.timeout))
finally:
client.close()
def cmd_shell(args: argparse.Namespace) -> int:
"""Minimal interactive shell (no full PTY); useful for quick typing. Ctrl+D to exit."""
client = connect_client(args)
try:
chan = client.invoke_shell()
chan.send("export TERM=dumb\n")
def read_loop():
try:
while True:
data = chan.recv(4096)
if not data:
break
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
except EOFError:
pass
t = threading.Thread(target=read_loop, daemon=True)
t.start()
try:
while True:
line = sys.stdin.readline()
if not line:
break
chan.send(line)
except KeyboardInterrupt:
chan.close()
return 0
finally:
client.close()
def main() -> None:
ap = argparse.ArgumentParser(description="SSH helper for remote Docker/adb diagnostics.")
ap.add_argument("-H", "--host", default=os.environ.get("REDROID_SSH_HOST", ""))
ap.add_argument("-U", "--user", default=os.environ.get("REDROID_SSH_USER", ""))
ap.add_argument("-P", "--port", type=int, default=int(os.environ.get("REDROID_SSH_PORT", "22")))
ap.add_argument("--key-file", default=os.environ.get("REDROID_SSH_KEY", ""))
ap.add_argument(
"--password",
default="",
help="Unsafe on shell history; prefer REDROID_SSH_PASSWORD or --password-prompt.",
)
ap.add_argument("--password-prompt", action="store_true")
ap.add_argument("--timeout", type=float, default=60.0, help="SSH connect / exec timeout (seconds)")
sub = ap.add_subparsers(dest="command", required=True)
d = sub.add_parser("diagnose", help="docker ps/inspect/logs + adb on the remote host")
d.add_argument(
"--container-name",
default=os.environ.get("REDROID_CONTAINER", "redroid_13.0.0_mindthegapps_houdini_magisk"),
)
d.add_argument("--adb-port", type=int, default=int(os.environ.get("REDROID_ADB_PORT", "5570")))
e = sub.add_parser("exec", help="Run one remote bash -lc string (pass command after --)")
e.add_argument("remote_words", nargs=argparse.REMAINDER, help="e.g. -- bash -lc 'docker ps'")
s = sub.add_parser("shell", help="Interactive SSH shell (basic; prefer OpenSSH ssh for full TTY)")
ns = ap.parse_args()
if ns.command == "diagnose":
sys.exit(cmd_diagnose(ns))
if ns.command == "exec":
sys.exit(cmd_exec(ns))
if ns.command == "shell":
sys.exit(cmd_shell(ns))
sys.exit(2)
if __name__ == "__main__":
main()