From a48db482a89e649cadd5bf031e9b36a118c3ce34 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 4 Apr 2026 16:18:52 -0500 Subject: [PATCH] 's' --- README.md | 47 ++++++++-- requirements.txt | 3 +- run_redroid.py | 73 +++++++++++++++- tools/compat.py | 26 ++++-- tools/ssh_remote.py | 205 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 tools/ssh_remote.py diff --git a/README.md b/README.md index 01c22f4..8956ac2 100644 --- a/README.md +++ b/README.md @@ -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 ... ``` diff --git a/requirements.txt b/requirements.txt index 3d1b66b..5e98b13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests==2.28.1 -tqdm==4.64.1 \ No newline at end of file +tqdm==4.64.1 +paramiko>=3.4.0,<4 \ No newline at end of file diff --git a/run_redroid.py b/run_redroid.py index 63e3c78..3a8195f 100644 --- a/run_redroid.py +++ b/run_redroid.py @@ -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), diff --git a/tools/compat.py b/tools/compat.py index b7db1e4..7f49afc 100644 --- a/tools/compat.py +++ b/tools/compat.py @@ -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) diff --git a/tools/ssh_remote.py b/tools/ssh_remote.py new file mode 100644 index 0000000..9f7e8bd --- /dev/null +++ b/tools/ssh_remote.py @@ -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()