From 72b41971bffa2ba1d446d52aeb98ab294071f8f1 Mon Sep 17 00:00:00 2001 From: JP Cottin Date: Sun, 26 Jul 2026 17:37:13 -0700 Subject: [PATCH 1/2] Preview multi-run: never relaunch the app, and bound the adb polls Two problems, both in the preview multi-run job. The job issued `am start` unconditionally on every cycle, including after a snapshot restore. Whether the app comes back by itself is the question this job exists to answer, so launching it there answers it for the emulator. It happened to be invisible because the preview emulator does restore the app, so the launch was a no-op ("Activity not started, intent has been delivered to currently running top-most instance") -- but had a restore ever dropped the app, the relaunch would have produced a healthy-looking screenshot and hidden it. Now only cycle 1 launches; later cycles report "app survived restore: YES/NO" and are left alone, matching the Android CLI multi-run job so the two are directly comparable. Input is only driven while the app is actually running, so a dead app cannot leave taps landing on the home screen. The polling loops also called adb without a timeout. adb blocks indefinitely against a wedged device, so the loops never iterated, the nominal 180s and 480s budgets never applied, and the "boot timeout" branch never ran. One run sat silent for 23 minutes after "Snapshot 'default_boot' loaded" and was then killed by the step timeout with no diagnostics. Each adb call is now bounded and the failure paths dump the tail of the emulator log, so a hang reports within its budget and says something useful. The hang itself looks intermittent -- twelve runs of this job passed before it and the run straight after it passed too -- and is not addressed here. --- .github/workflows/ci.yml | 63 ++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8978bf5..a390ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -606,17 +606,26 @@ jobs: fi tail -F "${{ github.workspace }}/emulator_run$N.txt" & TAIL_PID=$! + # Every adb call here is bounded. Against a wedged device adb blocks + # indefinitely, so an unbounded call makes these loops never iterate: + # the budgets below would not apply and the step would sit silent + # until the job timeout, with no diagnostics. for _ in $(seq 1 36); do - kill -0 "$EMU_PID" 2>/dev/null || { echo "ERROR: emulator process exited early (run $N)"; exit 1; } - adb get-state >/dev/null 2>&1 && break + kill -0 "$EMU_PID" 2>/dev/null || { echo "ERROR: emulator process exited early (run $N)"; tail -40 "${{ github.workspace }}/emulator_run$N.txt"; exit 1; } + timeout 10 adb get-state >/dev/null 2>&1 && break sleep 5 done - adb get-state >/dev/null 2>&1 || { echo "ERROR: no device after 180s (run $N)"; exit 1; } + timeout 10 adb get-state >/dev/null 2>&1 || { echo "ERROR: no device after 180s (run $N)"; tail -40 "${{ github.workspace }}/emulator_run$N.txt"; exit 1; } for _ in $(seq 1 48); do - [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] && break + [ "$(timeout 15 adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] && break sleep 10 done - [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] || { echo "===== boot timeout (run $N) ====="; exit 1; } + if [ "$(timeout 15 adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; then + echo "===== boot timeout (run $N): the emulator never finished booting =====" + echo "----- last 60 lines of the emulator log -----" + tail -60 "${{ github.workspace }}/emulator_run$N.txt" || true + exit 1 + fi kill "$TAIL_PID" 2>/dev/null || true T_RESUME=$(date +%s) @@ -639,22 +648,38 @@ jobs: echo "RUN $N entry screenshot: $(stat -c %s "${{ github.workspace }}/screenshots/run$N-entry.png" 2>/dev/null || echo 0) bytes" fi - echo "RUN $N app pid after boot/restore (pre am-start): $(adb shell pidof "$PKG" 2>/dev/null | tr -d '\r' || echo none)" - # Start (or re-foreground, after a restored snapshot) the app; the - # TITLE screen needs one tap to start a round — auto-play only - # pilots during PLAYING. - adb shell am start -n "$PKG/android.app.NativeActivity" || true - for _ in $(seq 1 30); do - adb shell dumpsys window 2>/dev/null | grep -qi "ocus.*vulkanspaceinvaders" && break - sleep 1 - done + if [ "$N" = "1" ]; then + # The only launch in the whole job. The TITLE screen needs one tap + # to start a round -- auto-play only pilots during PLAYING. + adb shell am start -n "$PKG/android.app.NativeActivity" || true + for _ in $(seq 1 30); do + timeout 15 adb shell dumpsys window 2>/dev/null | grep -qi "ocus.*vulkanspaceinvaders" && break + sleep 1 + done + APP_PID="$(timeout 15 adb shell pidof "$PKG" 2>/dev/null | tr -d '\r')" + echo "RUN $N app pid after launch: ${APP_PID:-none}" + else + # Deliberately NOT relaunched. The question this job asks is + # whether the snapshot brings the app back by itself, so starting + # it here would answer it for the emulator. + APP_PID="$(timeout 15 adb shell pidof "$PKG" 2>/dev/null | tr -d '\r')" + if [ -n "$APP_PID" ]; then + echo "RUN $N app survived restore: YES (pid $APP_PID)" + else + echo "RUN $N app survived restore: NO (process gone; left as-is)" + fi + fi sleep 3 T_FIRST_TAP=$(date +%s) - for _ in 1 2 3 4; do - adb shell input tap 540 1500 || true - sleep 2 - done - sleep 5 + # Only drive the game while it is actually running; tapping a home + # screen would just launch something at random. + if [ -n "${APP_PID:-}" ]; then + for _ in 1 2 3 4; do + adb shell input tap 540 1500 || true + sleep 2 + done + sleep 5 + fi # Diagnostics and the liveness probe run BEFORE the exit screenshot, # so that screenshot is the last thing captured and sits ~1s from the # freeze. (It used to come first, leaving 15-20s of auto-play between From 697f7073f4f5a121ca112b6fce032a2bb632c352 Mon Sep 17 00:00:00 2001 From: JP Cottin Date: Sun, 26 Jul 2026 17:59:10 -0700 Subject: [PATCH 2/2] Retry pidof after the launch in cycle 1 The new tap guard reads the app pid to decide whether to drive input, but a single pidof query races the launch and can come back empty while the process is still starting. That suppressed the taps in cycle 1, so the game stayed on the TITLE screen -- auto-play only pilots during PLAYING -- and the cycle played ~6s against ~25s for the others. Poll for the pid the way the Android CLI multi-run job already does. --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a390ca5..2035e58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -656,7 +656,14 @@ jobs: timeout 15 adb shell dumpsys window 2>/dev/null | grep -qi "ocus.*vulkanspaceinvaders" && break sleep 1 done - APP_PID="$(timeout 15 adb shell pidof "$PKG" 2>/dev/null | tr -d '\r')" + # pidof races a launch: a single query can come back empty while + # the process is still starting, which would then suppress the + # taps below and leave the game sitting on the TITLE screen. + for _ in $(seq 1 15); do + APP_PID="$(timeout 15 adb shell pidof "$PKG" 2>/dev/null | tr -d '\r')" + [ -n "$APP_PID" ] && break + sleep 1 + done echo "RUN $N app pid after launch: ${APP_PID:-none}" else # Deliberately NOT relaunched. The question this job asks is