scripts: emu.sh — reliable emulator lifecycle + on-device debug helpers
Replaces the ad-hoc adb/nohup sequences used throughout verification with one self-contained script: up/down/status, app start|stop|restart, shot, log, tap, type, cmd (one-shot editor debug command), perf on|off|pull, push/pull of Notes test files. Encodes the machine-local paths (Notes/, PadPerf/ profiler + cmd file) in one auditable place. Reliability findings baked in (each hit during testing): - The AVD auto-saves a 'default_boot' instant-boot snapshot on clean shutdown; a corrupted one makes the emulator segfault during restore (device never comes online). up() detects it (process death / no adb device within PAD_ONLINE_TIMEOUT) and falls back to a cold boot with -no-snapshot-load; the next clean down() re-saves a good snapshot, so this self-heals. down() is the only sanctioned stop (clean kill saves the snapshot; SIGKILL corrupts it). - The emulator process identity is ambiguous (launcher vs qemu-system-*-headless), and crashpad/netsimd children carry the AVD name in their command lines. Liveness and kill therefore use a tight signature: comm matches qemu-system-* AND cmdline contains ' -avd <AVD>' (never a broad pkill -f pattern — one matched and killed the calling shell during testing). - Before any launch, orphaned qemu instances are cleared (they hold the AVD lock and make new launches fail silently); before starting a replacement, the old one must be fully gone (lingering device/lock causes false success). - Timeouts: short online window for the snapshot attempt, full BOOT_TIMEOUT for the fallback cold boot (~20 s). Tested: idempotent up, 10 s snapshot-restore boot, forced fallback (30 s end-to-end, verified correct instance), orphan cleanup, perf/cmd/ push/pull/shot/log on-device. doc/README.md now documents it.
This commit is contained in:
parent
3a39fb8126
commit
b1a1719f17
|
|
@ -56,28 +56,38 @@ What it does, and why (gogio cannot inject manifest permissions):
|
|||
5. `apksigner sign` with the debug key → `cmd/pad/pad-emu.apk`
|
||||
6. `adb install -r` (skipped with `--no-install`)
|
||||
|
||||
Run the emulator (headless, AVD `pad_avd`, API 35):
|
||||
## Emulator + on-device debug: `scripts/emu.sh`
|
||||
|
||||
```
|
||||
nohup emulator -avd pad_avd -no-window -no-audio -no-boot-anim \
|
||||
-gpu swiftshader_indirect >/tmp/emulator.log 2>&1 &
|
||||
```
|
||||
All emulator lifecycle and on-device debug operations go through
|
||||
`scripts/emu.sh` (headless AVD `pad_avd`, API 35): `up` / `down` /
|
||||
`status`, `app start|stop|restart`, `shot`, `log`, `tap X Y`, `type TEXT`,
|
||||
`cmd <top|bottom|frac F|dp N>` (one-shot editor debug command),
|
||||
`perf on|off|pull` (in-app profiler), `push`/`pull` (Notes files). See the
|
||||
script header for the full list and the device paths it encodes.
|
||||
|
||||
Snapshot behavior: the AVD auto-saves an instant-boot snapshot on clean
|
||||
`down` (restore ~10 s vs ~20 s cold boot). A corrupted snapshot makes the
|
||||
emulator segfault during restore; `up` detects that and falls back to a
|
||||
cold boot, and the next `down` re-saves a good snapshot (self-healing).
|
||||
Always stop with `down` — SIGKILLing the emulator corrupts the snapshot.
|
||||
|
||||
App package/activity: `pad.pad / org.gioui.GioActivity`. Default root
|
||||
directory: `/storage/emulated/0/Notes` (push test files there with
|
||||
`adb push file /storage/emulated/0/Notes/`).
|
||||
directory: `/storage/emulated/0/Notes`.
|
||||
|
||||
## On-device observation loop (quick reference)
|
||||
|
||||
Gio renders into one GL surface, so the authoritative debug signals are
|
||||
data, not pixels:
|
||||
data, not pixels (use `scripts/emu.sh log|shot|perf|cmd` for the first
|
||||
four):
|
||||
|
||||
- `adb logcat -s pad.pad` — app log (IME commits, open-file timing, errors).
|
||||
- `adb shell input tap|swipe|text` — drive the UI (see `development_plan.md`
|
||||
for the tap coordinates and IME-tap cadence that works).
|
||||
- `logcat -s pad.pad` — app log (errors, limits, recovery; the normal path
|
||||
is quiet by design).
|
||||
- `input tap|swipe|text` — drive the UI (see `development_plan.md` for the
|
||||
tap coordinates and IME-tap cadence that works). The first tap right
|
||||
after launch/open is sometimes swallowed — re-tap.
|
||||
- Autosave debounce is 1 s: **wait ~1.6 s before reading a file back from
|
||||
disk** after typing.
|
||||
- Memory: `adb shell dumpsys meminfo pad.pad` (watch PSS/RSS; the 3.8 GB
|
||||
emulator OOMs the app above ~2.5 GB RSS).
|
||||
- Screenshots (`adb exec-out screencap -p > /tmp/x.png`) are an auxiliary
|
||||
check only — state, logcat, and file diffs are authoritative.
|
||||
- Screenshots are an auxiliary check only — state, logcat, and file diffs
|
||||
are authoritative.
|
||||
|
|
|
|||
267
scripts/emu.sh
Executable file
267
scripts/emu.sh
Executable file
|
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env bash
|
||||
# Emulator + on-device debug helpers for Pad (machine-local workstation).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/emu.sh up # start headless AVD, wait for boot
|
||||
# scripts/emu.sh down # stop the emulator (clean; saves snapshot)
|
||||
# scripts/emu.sh status # adb / boot / top-activity state
|
||||
# scripts/emu.sh app start|stop|restart
|
||||
# scripts/emu.sh shot [FILE] # screenshot (default /tmp/pad_shot_<ts>.png)
|
||||
# scripts/emu.sh log [LINES] # last N lines of app logcat (default 20)
|
||||
# scripts/emu.sh tap X Y # tap (note: the first tap right after
|
||||
# # launch/open is sometimes swallowed —
|
||||
# # re-tap if nothing happens)
|
||||
# scripts/emu.sh type TEXT # type into the focused field (spaces ok)
|
||||
# scripts/emu.sh cmd <top|bottom|frac F|dp N> # one-shot editor debug command
|
||||
# scripts/emu.sh perf on|off # enable/disable the in-app profiler
|
||||
# scripts/emu.sh perf pull [FILE] # pull logic_frames.csv (default ./logic_frames.csv)
|
||||
# scripts/emu.sh push FILE # push FILE -> /storage/emulated/0/Notes/
|
||||
# scripts/emu.sh pull NAME # pull Notes/NAME -> ./
|
||||
#
|
||||
# Machine-local facts encoded here (see doc/README.md for context):
|
||||
# - AVD pad_avd (Pixel 6 profile, API 35, x86_64); headless + swiftshader.
|
||||
# Boot is fast (snapshot restore ~10 s, cold boot ~20-30 s warm).
|
||||
# - App pad.pad / org.gioui.GioActivity.
|
||||
# - Profiler: enable marker + CSV live under /storage/emulated/0/PadPerf/;
|
||||
# the CSV is truncated on app relaunch, so pull it before restarting the
|
||||
# app if you are accumulating data.
|
||||
# - One-shot editor debug commands are read from /storage/emulated/0/PadPerf/cmd
|
||||
# (scroll: top | bottom | "frac 0.5" | "dp 1234").
|
||||
# - Test files go under /storage/emulated/0/Notes/.
|
||||
# - The emulator OOMs above ~2.5 GB RSS on this VM; kill it if it wedges.
|
||||
#
|
||||
# Snapshots: the AVD auto-saves a "default_boot" instant-boot snapshot on
|
||||
# clean shutdown. A corrupted snapshot makes the emulator segfault during
|
||||
# restore (device never comes online). `up` detects that (process death or
|
||||
# no adb device within PAD_ONLINE_TIMEOUT seconds) and falls back to a cold
|
||||
# boot with -no-snapshot-load; the next clean `down` re-saves a healthy
|
||||
# snapshot, so this self-heals. Always stop with `down` (clean kill saves
|
||||
# the snapshot) — SIGKILLing the emulator corrupts the snapshot.
|
||||
#
|
||||
# Env overrides: PAD_AVD, PAD_ONLINE_TIMEOUT (default 60 s),
|
||||
# PAD_BOOT_TIMEOUT (default 300 s), EMU_LOG path via ANDROID_HOME-adjacent.
|
||||
set -euo pipefail
|
||||
|
||||
AVD=${PAD_AVD:-pad_avd}
|
||||
PKG=pad.pad
|
||||
ACTIVITY=org.gioui.GioActivity
|
||||
NOTES=/storage/emulated/0/Notes
|
||||
PERFDIR=/storage/emulated/0/PadPerf
|
||||
EMU_LOG=${PAD_EMU_LOG:-/tmp/emulator.log}
|
||||
ONLINE_TIMEOUT=${PAD_ONLINE_TIMEOUT:-60}
|
||||
BOOT_TIMEOUT=${PAD_BOOT_TIMEOUT:-300}
|
||||
|
||||
export ANDROID_HOME=${ANDROID_HOME:-$HOME/android-sdk}
|
||||
export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
adb_state() { adb get-state 2>/dev/null || echo offline; }
|
||||
|
||||
booted() {
|
||||
[ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]
|
||||
}
|
||||
|
||||
EMU_PID=""
|
||||
|
||||
# Find the actual qemu process for this AVD: comm is qemu-system-* and the
|
||||
# command line contains " -avd $AVD". The comm check excludes the emulator's
|
||||
# crashpad/netsimd children (their command lines carry the AVD name too) and
|
||||
# any unrelated process that merely mentions the AVD. Prints the pid.
|
||||
# Returns 0 if found, 1 otherwise.
|
||||
find_emu_qemu() {
|
||||
local p
|
||||
for p in $(pgrep -f " -avd $AVD" 2>/dev/null); do
|
||||
case "$(ps -o comm= -p "$p" 2>/dev/null)" in
|
||||
qemu-system-*) echo "$p"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
start_emulator() {
|
||||
nohup emulator -avd "$AVD" -no-window -no-audio -no-boot-anim \
|
||||
-gpu swiftshader_indirect "$@" >"$EMU_LOG" 2>&1 &
|
||||
EMU_PID=$!
|
||||
}
|
||||
|
||||
# Wait until the adb device is online.
|
||||
# Returns: 0 online, 1 emulator process died, 2 timeout.
|
||||
wait_device_online() {
|
||||
local deadline=${1:-$ONLINE_TIMEOUT} i=0
|
||||
while [ "$i" -lt "$deadline" ]; do
|
||||
[ "$(adb_state)" = "device" ] && return 0
|
||||
# Give the launcher time to exec into qemu before declaring death.
|
||||
if [ "$i" -ge 10 ] && ! find_emu_qemu >/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
sleep 3
|
||||
i=$((i + 3))
|
||||
done
|
||||
return 2
|
||||
}
|
||||
|
||||
# Wait for boot_completed on an already-online device.
|
||||
# Returns: 0 booted, 1 not booted (device lost or timeout).
|
||||
wait_boot_completed() {
|
||||
local deadline=${1:-$BOOT_TIMEOUT} i=0
|
||||
while [ "$i" -lt "$deadline" ]; do
|
||||
booted && return 0
|
||||
[ "$(adb_state)" = "device" ] || return 1
|
||||
sleep 5
|
||||
i=$((i + 5))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_up() {
|
||||
if [ "$(adb_state)" = "device" ] && booted; then
|
||||
echo "emulator already up and booted"
|
||||
return 0
|
||||
fi
|
||||
if [ "$(adb_state)" = "device" ]; then
|
||||
echo "emulator present; waiting for boot..."
|
||||
wait_boot_completed "$BOOT_TIMEOUT" || die "boot failed; see $EMU_LOG"
|
||||
echo "booted"
|
||||
return 0
|
||||
fi
|
||||
echo "starting AVD $AVD (headless)..."
|
||||
# Clear any orphaned qemu left by a previously force-killed run: it would
|
||||
# hold the AVD lock and make the new launch fail silently.
|
||||
local qp
|
||||
if qp=$(find_emu_qemu); then
|
||||
echo "WARN: orphaned emulator (pid $qp) still running; killing it first"
|
||||
kill -9 "$qp" 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
start_emulator
|
||||
local rc=0
|
||||
wait_device_online "$ONLINE_TIMEOUT" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if [ "$rc" -eq 1 ]; then
|
||||
echo "WARN: emulator process died during startup (corrupt snapshot?) — $EMU_LOG"
|
||||
else
|
||||
echo "WARN: device never came online within ${ONLINE_TIMEOUT}s — $EMU_LOG"
|
||||
fi
|
||||
echo "retrying with -no-snapshot-load (cold boot; next 'down' re-saves a good snapshot)..."
|
||||
# Kill the old instance (launcher and/or qemu). SIGKILL: the snapshot
|
||||
# is already broken, there is nothing to save, and a half-written
|
||||
# snapshot is harmless (next clean 'down' re-saves).
|
||||
qp=$(find_emu_qemu || true)
|
||||
if [ -n "$qp" ]; then kill -9 "$qp" 2>/dev/null || true; fi
|
||||
kill -9 "$EMU_PID" 2>/dev/null || true
|
||||
# Wait until it is truly gone: the old device and AVD lock must be
|
||||
# released before the replacement starts, or the new waits can latch
|
||||
# onto the dying instance and report false success.
|
||||
local j=0
|
||||
while [ "$j" -lt 15 ]; do
|
||||
find_emu_qemu >/dev/null 2>&1 || break
|
||||
sleep 1
|
||||
j=$((j + 1))
|
||||
done
|
||||
start_emulator -no-snapshot-load
|
||||
# Note: full BOOT_TIMEOUT here — a cold boot legitimately takes far
|
||||
# longer than the ONLINE_TIMEOUT used for the first (snapshot) attempt.
|
||||
wait_device_online "$BOOT_TIMEOUT" || { tail -n 5 "$EMU_LOG" >&2; die "device never came online (cold boot); see $EMU_LOG"; }
|
||||
fi
|
||||
wait_boot_completed "$BOOT_TIMEOUT" || die "boot failed; see $EMU_LOG"
|
||||
echo "booted"
|
||||
}
|
||||
|
||||
cmd_down() {
|
||||
adb emu kill 2>/dev/null || die "no emulator running"
|
||||
echo "stopped"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo "adb: $(adb_state)"
|
||||
if [ "$(adb_state)" = "device" ]; then
|
||||
echo "boot_completed: $(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
|
||||
local top
|
||||
top=$(adb shell dumpsys activity activities 2>/dev/null | grep -m1 "topResumedActivity" || true)
|
||||
echo "top: ${top:-none}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_app() {
|
||||
case "${1:-}" in
|
||||
start) adb shell am start -n "$PKG/$ACTIVITY" >/dev/null; echo "started" ;;
|
||||
stop) adb shell am force-stop "$PKG"; echo "stopped" ;;
|
||||
restart)
|
||||
adb shell am force-stop "$PKG"
|
||||
sleep 1
|
||||
adb shell am start -n "$PKG/$ACTIVITY" >/dev/null
|
||||
echo "restarted"
|
||||
;;
|
||||
*) die "app: start|stop|restart" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_shot() {
|
||||
local out="${1:-/tmp/pad_shot_$(date +%Y%m%d_%H%M%S).png}"
|
||||
adb exec-out screencap -p >"$out"
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
cmd_log() {
|
||||
adb logcat -d -s "$PKG" | tail -n "${1:-20}"
|
||||
}
|
||||
|
||||
cmd_tap() {
|
||||
[ $# -eq 2 ] || die "tap X Y"
|
||||
adb shell input tap "$1" "$2"
|
||||
}
|
||||
|
||||
cmd_type() {
|
||||
[ $# -ge 1 ] || die "type TEXT"
|
||||
# input text takes one arg; encode spaces as %s.
|
||||
local text=${*// /%s}
|
||||
adb shell input text "$text"
|
||||
}
|
||||
|
||||
cmd_cmd() {
|
||||
# Commands are constrained (top|bottom|frac F|dp N) — no shell metachars.
|
||||
[ $# -ge 1 ] || die "cmd <top|bottom|frac F|dp N>"
|
||||
adb shell "mkdir -p $PERFDIR && printf '%s\n' \"$*\" > $PERFDIR/cmd"
|
||||
echo "sent editor debug command: $*"
|
||||
}
|
||||
|
||||
cmd_perf() {
|
||||
case "${1:-}" in
|
||||
on) adb shell "mkdir -p $PERFDIR && touch $PERFDIR/enable"; echo "profiler on" ;;
|
||||
off) adb shell "rm -f $PERFDIR/enable"; echo "profiler off" ;;
|
||||
pull)
|
||||
local out="${2:-./logic_frames.csv}"
|
||||
adb pull "$PERFDIR/logic_frames.csv" "$out"
|
||||
;;
|
||||
*) die "perf on|off|pull [FILE]" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_push() {
|
||||
[ $# -ge 1 ] || die "push FILE"
|
||||
adb push "$1" "$NOTES/"
|
||||
}
|
||||
|
||||
cmd_pull() {
|
||||
[ $# -ge 1 ] || die "pull NAME"
|
||||
adb pull "$NOTES/$1" "./"
|
||||
}
|
||||
|
||||
[ $# -ge 1 ] || die "no subcommand (see header)"
|
||||
main="${1}"; shift
|
||||
case "$main" in
|
||||
up) cmd_up "$@" ;;
|
||||
down) cmd_down "$@" ;;
|
||||
status) cmd_status "$@" ;;
|
||||
app) cmd_app "$@" ;;
|
||||
shot) cmd_shot "$@" ;;
|
||||
log) cmd_log "$@" ;;
|
||||
tap) cmd_tap "$@" ;;
|
||||
type) cmd_type "$@" ;;
|
||||
cmd) cmd_cmd "$@" ;;
|
||||
perf) cmd_perf "$@" ;;
|
||||
push) cmd_push "$@" ;;
|
||||
pull) cmd_pull "$@" ;;
|
||||
*) die "unknown subcommand: $main" ;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user