's'
This commit is contained in:
@@ -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
205
tools/ssh_remote.py
Normal 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()
|
||||
Reference in New Issue
Block a user