The emulator in ~/Android/Sdk auto-updated to 37.2.12 on Oct 11 and since
then consistently fails its own KVM probe ("/dev/kvm is not found") and
segfaults on that failure path — while KVM is healthy: a manual
KVM_GET_API_VERSION ioctl returns the current API level 12, and both
/opt/android-sdk and the container-home emulator pass -accel-check on
the same kernel and device node. The script now prefers the documented
host SDK at /opt/android-sdk when no ANDROID_SDK_ROOT is set.
Once the home SDK is repaired (or the emulator regression disappears),
the fallback order can be revisited.
Co-Authored-By: Claude <noreply@anthropic.com>
177 lines
No EOL
6.9 KiB
Bash
Executable file
177 lines
No EOL
6.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# start-emulator.sh — boot the JTube TV test emulator on the HOST (not inside the
|
|
# agent container), with GPU acceleration, headless by default (drive it via
|
|
# adb/scrcpy). JTUBE_EMU_MODE=windowed shows the Qt window.
|
|
#
|
|
# Boots on console :5600 (adbd :5601); the agent container bridges
|
|
# rutherford:5556 -> 127.0.0.1:5601 with socat, so after this script the emulator is
|
|
# reachable from anywhere as:
|
|
#
|
|
# adb connect rutherford:5556 && scrcpy -s rutherford:5556
|
|
#
|
|
# An RSA authorization popup appears on the emulator window on first connect —
|
|
# click "Always allow"/OK.
|
|
set -euo pipefail
|
|
|
|
SDK="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}"
|
|
# Prefer the working host SDK at /opt/android-sdk: the emulator that
|
|
# ~/Android/Sdk auto-updated to on Oct 11 (37.2.12) fails its own KVM probe
|
|
# ("/dev/kvm is not found") with KVM perfectly healthy — /opt/android-sdk and
|
|
# the container-home copy both pass -accel-check on the same kernel, so that
|
|
# install is broken. /opt/android-sdk is the documented host SDK anyway.
|
|
if [ -z "${ANDROID_SDK_ROOT:-}" ] && [ -x /opt/android-sdk/emulator/emulator ]; then
|
|
SDK=/opt/android-sdk
|
|
fi
|
|
EMU="$SDK/emulator/emulator"
|
|
ADB="$SDK/platform-tools/adb"
|
|
SDKM="$SDK/cmdline-tools/latest/bin/sdkmanager"
|
|
AVDM="$SDK/cmdline-tools/latest/bin/avdmanager"
|
|
AVD="${1:-jtubesc}"
|
|
PORT=5600
|
|
IMAGE="system-images;android-36;android-tv;x86_64"
|
|
|
|
[ -x "$EMU" ] || { echo "emulator not found at $EMU — install Android SDK cmdline-tools first"; exit 1; }
|
|
|
|
# --- bootstrap: java, system image, AVD -------------------------------------
|
|
if ! command -v java >/dev/null; then
|
|
echo "no java on PATH — required by sdkmanager/AVD tools"
|
|
echo " install it (Arch): sudo pacman -S --noconfirm jdk21-openjdk"
|
|
echo " then re-run this script"
|
|
exit 1
|
|
fi
|
|
|
|
if ! "$SDKM" --list_installed 2>/dev/null | grep -q "$IMAGE"; then
|
|
echo "installing $IMAGE (~1.5 GB, once)…"
|
|
yes | "$SDKM" "$IMAGE" > /dev/null
|
|
echo "installed"
|
|
fi
|
|
|
|
if [ ! -f "$HOME/.android/avd/$AVD.ini" ]; then
|
|
echo "creating AVD '$AVD'…"
|
|
echo no | "$AVDM" create avd -n "$AVD" -k "$IMAGE" -d tv_1080p > /dev/null
|
|
fi
|
|
|
|
# --- free stale emulator instances holding our port --------------------------
|
|
for pid in $(ps aux | awk '/[q]emu-system-x86_64/ {print $2}' 2>/dev/null); do
|
|
if tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q "\-port $PORT"; then
|
|
echo "Killing stale emulator pid $pid"
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
fi
|
|
done
|
|
sleep 1
|
|
|
|
MODE="${JTUBE_EMU_MODE:-headless}" # headless (default) | windowed
|
|
WINDOWED=1
|
|
ACCEL=()
|
|
if [ "$MODE" = windowed ]; then
|
|
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then
|
|
echo "windowed (JTUBE_EMU_MODE=windowed) — emulator window + GPU host"
|
|
else
|
|
echo "JTUBE_EMU_MODE=windowed but no display detected — falling back to headless"
|
|
WINDOWED=0
|
|
ACCEL+=(-no-window -gpu swiftshader_indirect)
|
|
fi
|
|
else
|
|
WINDOWED=0
|
|
# Headless has no X display, so -gpu host/GLX can't init EGL ("Failed to get EGL
|
|
# display") and the emulator dies on launch — fall back to software rendering.
|
|
ACCEL+=(-no-window -gpu swiftshader_indirect)
|
|
echo "headless (default; JTUBE_EMU_MODE=windowed for a window) — no emulator window"
|
|
fi
|
|
if [ -e /dev/kvm ]; then
|
|
ACCEL+=(-accel on)
|
|
echo "/dev/kvm found — hardware acceleration on"
|
|
fi
|
|
|
|
# Pin the emulator process to the integrated GPU so the discrete card stays free
|
|
# for AI inference (windowed mode uses the host GL/Vulkan stack; headless runs on
|
|
# swiftshader — pure software — and never touches any GPU at all). Discovered
|
|
# from the PCI tree: first display device NOT driven by the nvidia driver.
|
|
IGPU=""
|
|
for dev in /sys/bus/pci/devices/*; do
|
|
class=$(cat "$dev/class" 2>/dev/null) || continue
|
|
case "$class" in
|
|
0x0300*|0x0380*) ;; # VGA / display controller
|
|
*) continue ;;
|
|
esac
|
|
if [ "$(basename "$(readlink "$dev/driver" 2>/dev/null)")" = nvidia ]; then continue; fi
|
|
IGPU=$(basename "$dev")
|
|
break
|
|
done
|
|
if [ -n "$IGPU" ]; then
|
|
# Mesa GL/GLES: force the Mesa vendors, then the Intel render device.
|
|
export __GLX_VENDOR_LIBRARY_NAME=mesa
|
|
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
|
|
export DRI_PRIME="pci-${IGPU//./_}"
|
|
# Vulkan: restrict the loader to the iGPU's ICD (both var names — loader
|
|
# version differences).
|
|
for icd in /usr/share/vulkan/icd.d/intel_icd.x86_64.json \
|
|
/usr/share/vulkan/icd.d/Intel_icd.x86_64.json; do
|
|
if [ -f "$icd" ]; then
|
|
export VK_ICD_FILENAMES="$icd"
|
|
export VK_DRIVER_FILES="$icd"
|
|
break
|
|
fi
|
|
done
|
|
echo "GPU pinned to iGPU $IGPU (Mesa/Intel env) — dGPU untouched"
|
|
else
|
|
echo "no iGPU found — leaving GPU selection to the driver (dGPU may be used)"
|
|
fi
|
|
|
|
# --- build combined adb vendor-keys file ------------------------------------
|
|
# The TV system image is a user build: adb push to /data/misc/adb/adb_keys is
|
|
# blocked, so remote keys are trusted via ADB_VENDOR_KEYS at launch instead.
|
|
# Drop extra public keys into /tmp/jtube-adbkeys/ (one per file), e.g. from the
|
|
# agent container: docker exec <ctr> sh -c 'cat ~/.android/adbkey.pub' > /tmp/jtube-adbkeys/agent-container.pub
|
|
mkdir -p /tmp/jtube-adbkeys
|
|
VKEYS=/tmp/jtube-adbkeys-combined
|
|
{
|
|
cat "$HOME/.android/adbkey.pub"
|
|
# Optional extra keys — must not fail the script under `set -e` when absent.
|
|
shopt -s nullglob
|
|
for k in /tmp/jtube-adbkeys/*.pub; do cat "$k"; done
|
|
shopt -u nullglob
|
|
} > "$VKEYS"
|
|
chmod 600 "$VKEYS"
|
|
|
|
echo "Booting AVD '$AVD' …"
|
|
ADB_VENDOR_KEYS="$VKEYS" setsid nohup "$EMU" -avd "$AVD" -port "$PORT" "${ACCEL[@]:-}" \
|
|
-no-snapshot > /tmp/jtube-emu.log 2>&1 < /dev/null &
|
|
echo "emulator PID $! — log: /tmp/jtube-emu.log"
|
|
|
|
for i in $(seq 1 90); do
|
|
# adb exits non-zero while the device is still booting — don't let
|
|
# pipefail+set -e kill the wait loop.
|
|
ok=$( ("$ADB" -s "emulator-$PORT" shell getprop sys.boot_completed 2>/dev/null || true) | tr -d '\r')
|
|
if [ "$ok" = "1" ]; then echo "BOOTED"; break; fi
|
|
sleep 5
|
|
done
|
|
|
|
# --- the bridge: advertise adbd on the LAN as :5556 ---------------------------
|
|
if ! ss -tln 2>/dev/null | grep -q ":5556"; then
|
|
if command -v socat > /dev/null; then
|
|
setsid nohup socat TCP-LISTEN:5556,bind=0.0.0.0,fork,reuseaddr TCP:127.0.0.1:$((PORT+1)) > /tmp/jtube-socat.log 2>&1 < /dev/null &
|
|
echo "socat bridge listening on :5556 (pid $!)"
|
|
else
|
|
echo "install socat for the remote bridge: sudo pacman -S --noconfirm socat"
|
|
fi
|
|
fi
|
|
|
|
# Keep the JTube TV app current + running so scrcpy lands on it.
|
|
APK="$HOME/Projects/jtube-tv/app/build/outputs/apk/debug/app-debug.apk"
|
|
if [ -f "$APK" ]; then
|
|
echo "Installing JTube TV…"
|
|
"$ADB" -s "emulator-$PORT" install -r "$APK" >/dev/null && echo "installed"
|
|
"$ADB" -s "emulator-$PORT" shell am start -n net.jeena.jtubetv/.MainActivity && echo "app started"
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
Ready. From any machine that can reach rutherford:
|
|
|
|
adb connect rutherford:5556
|
|
scrcpy -s rutherford:5556
|
|
|
|
If scrcpy says "unauthorized", click the RSA dialog on the emulator window.
|
|
EOF |