#!/usr/bin/env bash set -euo pipefail SSH_HOST="${REDROID_SSH_HOST:-192.168.2.179}" SSH_USER="${REDROID_SSH_USER:-eric}" REMOTE_REPO="${REDROID_REMOTE_REPO:-/home/eric/redroid-android13}" REDROID_IMAGE_OVERRIDE="${REDROID_IMAGE:-}" REDROID_IMAGE_X86="${REDROID_IMAGE_X86:-redroid/redroid:13.0.0_mindthegapps_houdini_magisk}" REDROID_IMAGE_ARM64="${REDROID_IMAGE_ARM64:-redroid/redroid:13.0.0_mindthegapps_magisk}" REDROID_ARCH_PROFILE="${REDROID_ARCH_PROFILE:-auto}" DEVICE_PROFILE="${REDROID_DEVICE_PROFILE:-pixel-7-pro}" PORTAINER_PORT="${REDROID_PORTAINER_PORT:-9000}" DEFAULT_PORT="${REDROID_DEFAULT_PORT:-5570}" DATA_PREFIX="${REDROID_DATA_PREFIX:-/home/eric/data-redroid13}" PORT_STEP="${REDROID_PORT_STEP:-1}" INSTANCE_PREFIX="${REDROID_INSTANCE_PREFIX:-redroid13}" DEFAULT_COUNT="${REDROID_DEFAULT_COUNT:-3}" GPU_MODE="${REDROID_GPU_MODE:-guest}" ADB_INSECURE="${REDROID_ADB_INSECURE:-1}" BOOT_TIMEOUT="${REDROID_BOOT_TIMEOUT:-120}" ROOT_TIMEOUT="${REDROID_ROOT_TIMEOUT:-60}" WAIT_TCP="${REDROID_WAIT_TCP:-120}" RESOLVED_IMAGE="" RESOLVED_ARCH="" if ! command -v python3 >/dev/null 2>&1; then echo "python3 is required." >&2 exit 1 fi if ! python3 - <<'PY' >/dev/null 2>&1 import importlib.util raise SystemExit(0 if importlib.util.find_spec("paramiko") else 1) PY then echo "python3 paramiko is required." >&2 exit 1 fi read -r -s -p "SSH password for ${SSH_USER}@${SSH_HOST}: " SSH_PASSWORD echo sq() { printf '%q' "$1" } remote_exec() { local remote_cmd="$1" SSH_HOST="$SSH_HOST" \ SSH_USER="$SSH_USER" \ SSH_PASSWORD="$SSH_PASSWORD" \ SSH_CMD="$remote_cmd" \ python3 - <<'PY' import os import sys import paramiko host = os.environ["SSH_HOST"] user = os.environ["SSH_USER"] password = os.environ["SSH_PASSWORD"] cmd = os.environ["SSH_CMD"] client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect( hostname=host, username=user, password=password, timeout=20, banner_timeout=20, auth_timeout=20, ) stdin, stdout, stderr = client.exec_command(cmd, get_pty=True) out = stdout.read().decode(errors="replace") err = stderr.read().decode(errors="replace") if out: sys.stdout.write(out) if err: sys.stderr.write(err) status = stdout.channel.recv_exit_status() client.close() sys.exit(status) PY } remote_repo_cmd() { local inner="$1" remote_exec "cd $(sq "$REMOTE_REPO") && ${inner}" } normalize_arch() { case "${1,,}" in x86_64|amd64) printf '%s' "x86_64" ;; aarch64|arm64) printf '%s' "arm64" ;; *) printf '%s' "unknown" ;; esac } detect_remote_arch() { remote_exec "uname -m | tr -d '\r'" } resolve_image() { if [[ -n "$RESOLVED_IMAGE" ]]; then printf '%s' "$RESOLVED_IMAGE" return 0 fi if [[ -n "$REDROID_IMAGE_OVERRIDE" ]]; then RESOLVED_IMAGE="$REDROID_IMAGE_OVERRIDE" RESOLVED_ARCH="custom" printf '%s' "$RESOLVED_IMAGE" return 0 fi local arch="${REDROID_ARCH_PROFILE}" if [[ "$arch" == "auto" ]]; then arch="$(normalize_arch "$(detect_remote_arch)")" else arch="$(normalize_arch "$arch")" fi case "$arch" in x86_64) RESOLVED_IMAGE="$REDROID_IMAGE_X86" RESOLVED_ARCH="x86_64" ;; arm64) RESOLVED_IMAGE="$REDROID_IMAGE_ARM64" RESOLVED_ARCH="arm64" ;; *) RESOLVED_IMAGE="$REDROID_IMAGE_X86" RESOLVED_ARCH="x86_64" ;; esac printf '%s' "$RESOLVED_IMAGE" } resolve_arch() { if [[ -z "$RESOLVED_IMAGE" ]]; then resolve_image >/dev/null fi printf '%s' "${RESOLVED_ARCH:-x86_64}" } remote_docker_ps() { remote_exec "docker ps --format 'table {{.Names}}\\t{{.Image}}\\t{{.Ports}}\\t{{.Status}}'" } remote_redroid_ps() { local image image="$(resolve_image)" remote_exec "docker ps --format '{{.Names}} {{.Image}} {{.Ports}} {{.Status}}' | awk -v image=$(sq "$image") '\$2 == image || index(\$2, \"redroid/redroid:\") == 1 { print }'" } remote_redroid_names() { local image image="$(resolve_image)" remote_exec "docker ps --format '{{.Names}} {{.Image}}' | awk -v image=$(sq "$image") '\$2 == image || index(\$2, \"redroid/redroid:\") == 1 {print \$1}'" } show_runtime_info() { local image arch image="$(resolve_image)" arch="$(resolve_arch)" printf 'Remote arch profile: %s\n' "$arch" printf 'Resolved image: %s\n' "$image" printf 'Device profile: %s\n' "$DEVICE_PROFILE" printf 'GPU mode: %s\n' "$GPU_MODE" printf 'ADB insecure: %s\n' "$ADB_INSECURE" } build_image() { remote_repo_cmd "env -u REDROID_MAGISK_APK -u REDROID_MAGISK_URL REDROID_OFFLINE=1 REDROID_MAGISK_BOOT_COMPLETE=1 python3 redroid.py -a 13.0.0 -mtg -i -m --offline --no-compat-check" } make_instance_name() { local prefix="$1" local index="$2" printf '%s-%s' "$prefix" "$index" } make_data_dir() { local prefix="$1" local name="$2" printf '%s-%s' "$prefix" "$name" } start_single_instance() { local name="$1" local port="$2" local data_dir="$3" local image arch local adb_arg="" if [[ "$ADB_INSECURE" != "0" ]]; then adb_arg="--adb-insecure" fi image="$(resolve_image)" arch="$(resolve_arch)" remote_repo_cmd "python3 run_redroid.py --image $(sq "$image") --bridge auto --device-profile $(sq "$DEVICE_PROFILE") --gpu-mode $(sq "$GPU_MODE") --data-dir $(sq "$data_dir") --port $(sq "$port") --name $(sq "$name") --replace --keep-container --wait-tcp $(sq "$WAIT_TCP") ${adb_arg}" bootstrap_instance "$name" echo "Local ADB connect:" echo " adb connect ${SSH_HOST}:${port}" echo "Resolved arch/image: ${arch} / ${image}" } start_instance() { local name port data_dir read -r -p "Instance name [${INSTANCE_PREFIX}-1]: " name name="${name:-${INSTANCE_PREFIX}-1}" read -r -p "ADB port [${DEFAULT_PORT}]: " port port="${port:-$DEFAULT_PORT}" read -r -p "Data dir [${DATA_PREFIX}-${name}]: " data_dir data_dir="${data_dir:-${DATA_PREFIX}-${name}}" start_single_instance "$name" "$port" "$data_dir" } start_batch_instances() { local count start_port prefix data_prefix read -r -p "How many instances [${DEFAULT_COUNT}]: " count count="${count:-$DEFAULT_COUNT}" read -r -p "Start ADB port [${DEFAULT_PORT}]: " start_port start_port="${start_port:-$DEFAULT_PORT}" read -r -p "Name prefix [${INSTANCE_PREFIX}]: " prefix prefix="${prefix:-$INSTANCE_PREFIX}" read -r -p "Data prefix [${DATA_PREFIX}]: " data_prefix data_prefix="${data_prefix:-$DATA_PREFIX}" local i name port data_dir for ((i = 1; i <= count; i++)); do name="$(make_instance_name "$prefix" "$i")" port="$((start_port + (i - 1) * PORT_STEP))" data_dir="$(make_data_dir "$data_prefix" "$name")" echo "Starting ${name} on port ${port}..." start_single_instance "$name" "$port" "$data_dir" echo done local open_panel read -r -p "Start Portainer web panel now? [Y/n]: " open_panel case "${open_panel:-Y}" in n|N|no|NO|No) ;; *) start_portainer ;; esac } bootstrap_instance() { local name="${1:?instance name required}" remote_repo_cmd "python3 bootstrap_redroid.py --container-name $(sq "$name") --container-runtime docker --device-profile $(sq "$DEVICE_PROFILE") --proxy-host none --timeout $(sq "$BOOT_TIMEOUT") --root-timeout $(sq "$ROOT_TIMEOUT")" } bootstrap_all_instances() { local names names="$(remote_redroid_names)" if [[ -z "$names" ]]; then echo "No running redroid containers found." return 0 fi local name while IFS= read -r name; do [[ -z "$name" ]] && continue echo "Bootstrapping ${name}..." bootstrap_instance "$name" echo done <<< "$names" } check_instance() { local port read -r -p "ADB port [${DEFAULT_PORT}]: " port port="${port:-$DEFAULT_PORT}" remote_exec " set +e adb connect 127.0.0.1:${port} >/dev/null 2>&1 adb -s 127.0.0.1:${port} shell getprop sys.boot_completed adb -s 127.0.0.1:${port} shell settings get global device_provisioned adb -s 127.0.0.1:${port} shell settings get secure user_setup_complete adb -s 127.0.0.1:${port} shell settings get global http_proxy adb -s 127.0.0.1:${port} shell getprop ro.build.fingerprint adb -s 127.0.0.1:${port} shell getprop ro.product.model adb -s 127.0.0.1:${port} shell su root id 2>/dev/null || adb -s 127.0.0.1:${port} shell su 0 id 2>/dev/null adb -s 127.0.0.1:${port} shell cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.LAUNCHER io.github.huskydg.magisk 2>/dev/null | tail -n 2 || true adb -s 127.0.0.1:${port} shell pm path io.github.huskydg.magisk 2>/dev/null || true adb -s 127.0.0.1:${port} shell pm list packages 2>/dev/null | grep -E 'com.google.android.gms|com.google.android.setupwizard|io.github.huskydg.magisk' | sort || true adb -s 127.0.0.1:${port} shell dumpsys package com.google.android.gms 2>/dev/null | grep -E 'versionName=|pkg=|enabled=' | head -n 10 || true " } start_portainer() { remote_exec " docker volume create portainer_data >/dev/null docker rm -f portainer >/dev/null 2>&1 || true docker run -d \ --name portainer \ --restart unless-stopped \ -p ${PORTAINER_PORT}:9000 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data \ portainer/portainer-ce:latest " echo "Portainer: http://${SSH_HOST}:${PORTAINER_PORT}" } stop_instance() { local name read -r -p "Container name [redroid13-1]: " name name="${name:-redroid13-1}" remote_exec "docker rm -f $(sq "$name")" } menu() { while true; do cat <" >&2 exit 1 fi bootstrap_instance "$2" ;; list) remote_docker_ps ;; redroid-list|rlist) remote_redroid_ps ;; info|runtime) show_runtime_info ;; check) check_instance ;; status) check_instance ;; portainer) start_portainer ;; panel|web) start_portainer ;; stop) if [[ $# -lt 2 ]]; then echo "Usage: $0 stop " >&2 exit 1 fi remote_exec "docker rm -f $(sq "$2")" ;; bootstrap-all|ball) bootstrap_all_instances ;; ""|menu) menu ;; *) cat <|bootstrap-all|ball|list|redroid-list|check|status|portainer|info|runtime|stop ] EOF exit 1 ;; esac