428 lines
11 KiB
Bash
428 lines
11 KiB
Bash
#!/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 <<EOF
|
|
redroid13 menu
|
|
1) Build Android 13 image
|
|
2) Start redroid instance
|
|
3) Start multiple instances
|
|
4) Bootstrap instance
|
|
5) List containers
|
|
6) Check instance health
|
|
7) Start Portainer web panel
|
|
8) Stop instance
|
|
9) Bootstrap all redroid containers
|
|
10) Show runtime info
|
|
11) Exit
|
|
EOF
|
|
read -r -p "Choose: " choice
|
|
case "$choice" in
|
|
1) build_image ;;
|
|
2) start_instance ;;
|
|
3) start_batch_instances ;;
|
|
4)
|
|
local name
|
|
read -r -p "Container name [${INSTANCE_PREFIX}-1]: " name
|
|
name="${name:-${INSTANCE_PREFIX}-1}"
|
|
bootstrap_instance "$name"
|
|
;;
|
|
5) remote_docker_ps ;;
|
|
6) check_instance ;;
|
|
7) start_portainer ;;
|
|
8) stop_instance ;;
|
|
9) bootstrap_all_instances ;;
|
|
10) show_runtime_info ;;
|
|
11) exit 0 ;;
|
|
*) echo "Invalid choice." ;;
|
|
esac
|
|
echo
|
|
done
|
|
}
|
|
|
|
case "${1:-}" in
|
|
build)
|
|
build_image
|
|
;;
|
|
start)
|
|
start_instance
|
|
;;
|
|
batch|multi)
|
|
start_batch_instances
|
|
;;
|
|
bootstrap)
|
|
if [[ $# -lt 2 ]]; then
|
|
echo "Usage: $0 bootstrap <container-name>" >&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 <container-name>" >&2
|
|
exit 1
|
|
fi
|
|
remote_exec "docker rm -f $(sq "$2")"
|
|
;;
|
|
bootstrap-all|ball)
|
|
bootstrap_all_instances
|
|
;;
|
|
""|menu)
|
|
menu
|
|
;;
|
|
*)
|
|
cat <<EOF
|
|
Usage: $0 [build|start|batch|multi|bootstrap <name>|bootstrap-all|ball|list|redroid-list|check|status|portainer|info|runtime|stop <name>]
|
|
EOF
|
|
exit 1
|
|
;;
|
|
esac
|