Add standalone repro for the gioui activity-recreate hang

scripts/repro_gioui_hang.sh builds the stock gioui.org/example 'hello'
app (v0.10.3, unmodified source), installs it on an emulator, recreates
the activity with a real uiMode toggle, and checks for NO_INPUT_CHANNEL
after a tap. Exit 0 = bug reproduced, 1 = clean, 2 = setup failure.

The bug: gioui v0.10.x app/os_android.go onCreateView#2 sends the
window-creation ack (mainWindow.windows <- struct{}{}) on the UI thread
with no receiver (Window.init never re-runs on Android), blocking the UI
thread forever -> no input channel, black unresponsive app. The fix
lives in our gioui fork (only send the ack on first window creation).
This commit is contained in:
Greg Pomerantz 2026-10-12 17:44:58 -04:00
parent 7bffc7ffda
commit 8bb5038cdb

102
scripts/repro_gioui_hang.sh Executable file
View File

@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Reproduce the gioui Android activity-recreation hang with a STOCK gioui
# example app (the "hello" example from gioui.org/example).
#
# What it does:
# 1. Builds gioui.org/example's hello app (unmodified) with gogio.
# 2. Installs it on an emulator (boots one if needed).
# 3. Recreates the activity via `cmd uimode night yes` (uiMode is not in
# gioui's configChanges mask, so Android destroys/recreates the
# activity in the same process).
# 4. Taps the app and checks logcat for NO_INPUT_CHANNEL.
#
# The bug (gioui v0.10.x, app/os_android.go onCreateView): on re-attach the
# second onCreateView sends the window-creation ack
# (mainWindow.windows <- struct{}{}) on the UI thread, but its only
# receiver (newWindow in Window.init) never re-runs on Android — the driver
# is never reset, so w.Event() never returns to re-init. The UI thread
# blocks forever, the activity never attaches an input channel, and the app
# is black and unresponsive (process still alive).
#
# Usage:
# scripts/repro_gioui_hang.sh [avd-name] [path-to-apk]
#
# Exit codes: 0 = bug REPRODUCED, 1 = not reproduced, 2 = setup failure.
set -euo pipefail
AVD=${1:-pad_avd}
APK=${2:-}
SDK=${ANDROID_HOME:-$HOME/android-sdk}
export PATH=$PATH:$HOME/go/bin:$SDK/platform-tools:$SDK/build-tools/35.0.0
say() { printf '\n=== %s\n' "$*"; }
die() { say "FAIL: $*"; exit 2; }
# --- 1. Build the stock hello example, unless an APK was supplied ---------
if [ -z "$APK" ]; then
SRC=$(mktemp -d)
trap 'rm -rf "$SRC"' EXIT
say "building stock gioui.org/example hello app (v0.10.3)"
go mod download gioui.org/example@v0.10.3
cp "$HOME/go/pkg/mod/gioui.org/example@v0.10.3/hello/hello.go" "$SRC/main.go"
cat > "$SRC/go.mod" <<'EOF'
module hello
go 1.23
require gioui.org v0.10.3
EOF
(cd "$SRC" && go mod tidy && gogio -target android -targetsdk 34 -arch amd64 -o /tmp/hello-gioui-repro.apk .)
APK=/tmp/hello-gioui-repro.apk
fi
[ -f "$APK" ] || die "APK not found: $APK"
# --- 2. Emulator: reuse a booted one or start $AVD ------------------------
say "waiting for emulator (avd $AVD)"
EMU=""
for _ in $(seq 1 60); do
EMU=$(adb devices | awk '$1 ~ /^emulator-/ {print $1; exit}')
[ -n "$EMU" ] && break
emulator -avd "$AVD" -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect >/dev/null 2>&1 &
sleep 5
done
[ -n "$EMU" ] || die "no emulator available"
for _ in $(seq 1 60); do
bc=$(adb -s "$EMU" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
[ "$bc" = "1" ] && break
sleep 5
done
[ "$(adb -s "$EMU" shell getprop sys.boot_completed | tr -d '\r')" = "1" ] || die "emulator did not boot"
say "emulator: $EMU"
# --- 3. Install + launch ---------------------------------------------------
APPID=localhost.hello # gogio's default app id for module "hello"
adb -s "$EMU" install -r "$APK" >/dev/null
adb -s "$EMU" shell am force-stop "$APPID" >/dev/null 2>&1 || true
adb -s "$EMU" logcat -c
adb -s "$EMU" shell am start -n "$APPID/org.gioui.GioActivity" >/dev/null
sleep 5
adb -s "$EMU" shell pidof "$APPID" >/dev/null || die "app did not start"
say "baseline: app running"
# --- 4. Recreate the activity ----------------------------------------------
# uiMode is not in gioui's configChanges mask -> Android recreates the
# activity. Force a real state change (a no-op toggle recreates nothing).
cur=$(adb -s "$EMU" shell cmd uimode night | tr -d '\r') # "Night mode: yes/no"
case $cur in
*yes*) adb -s "$EMU" shell cmd uimode night no >/dev/null ;;
*) adb -s "$EMU" shell cmd uimode night yes >/dev/null ;;
esac
sleep 5
adb -s "$EMU" logcat -c
adb -s "$EMU" shell input tap 540 1200
sleep 2
# --- 5. Verdict -------------------------------------------------------------
adb -s "$EMU" exec-out screencap -p > /tmp/gioui-hang-repro.png
if adb -s "$EMU" logcat -d | grep -q NO_INPUT_CHANNEL; then
say "REPRODUCED: activity recreation left the window with NO_INPUT_CHANNEL (see /tmp/gioui-hang-repro.png)"
exit 0
fi
say "NOT reproduced: no NO_INPUT_CHANNEL after recreation (gioui handles re-attach correctly)"
exit 1