feat(dev): an Android failure looks exactly like a success
The APK installs and launches. It also dies six milliseconds later, and finding that out cost a cycle for three reasons that have nothing to do with the bug itself: **Go's stdout does not reach logcat.** An Android app's fd 1 and 2 go to /dev/null, so every slog line -- including the one naming the error the app is about to exit on -- is discarded. `setprop log.redirect-stdio true` does not help: that redirects the Java runtime's System.out, and our code is a c-shared native library. **os.Exit leaves no evidence.** No panic, no AndroidRuntime stack, nothing in /data/tombstones, nothing in `logcat -b crash` or dropbox. All three places anyone would look are empty, and the one signal that is present -- "Zygote: exited due to signal 9" -- reads as "the system killed it" and sends you after the low-memory killer. **ActivityManager restarts it faster than you can observe.** pidof always answers and `am start` always reports Status: ok, so a crash-looping app looks alive. "Did it start" is the wrong question; `make android-smoke` asks whether it is the *same pid* N seconds later, and prints the filtered logcat plus how to read it when it is not. The tell, once known: "I/WailsBridge: Wails bridge initialized" followed immediately by a new pid doing the same thing. scripts/android-emulator.sh follows dev-headless.sh's shape -- background start, saved-PID stop, filtered log tail, never pkill -f. Two scaffold tasks are deliberately not wrapped: `android:logs` greps logcat for (Wails|yellowjacket), which catches the WailsBridge tag but misses the app's own process tag (app.yellowjacket is lowercase) and misses ActivityManager's "has died" line, which is the one that says it crashed; and `ensure-emulator` boots whatever `-list-avds | tail -1` returns, with no pidfile and no boot wait, so it cannot be sequenced. One environment note that is not obvious on Arch: Gradle needs a platform and /opt/android-sdk has none, so ANDROID_SDK defaults to ~/Android/Sdk while ANDROID_NDK points at /opt/android-ndk. Two SDKs, one for each half of the build.
This commit is contained in:
@@ -151,6 +151,7 @@ only climb when it cannot.
|
|||||||
| Something you cannot predict — exploring | `make dev-headless SEED=default` + `playwright-cli` | interactive |
|
| Something you cannot predict — exploring | `make dev-headless SEED=default` + `playwright-cli` | interactive |
|
||||||
| Something whose answer is a *number*, not a pass | `make perf` against a bulk-seeded app | ~1 min + setup |
|
| Something whose answer is a *number*, not a pass | `make perf` against a bulk-seeded app | ~1 min + setup |
|
||||||
| A `.sql` or `.templ` file | `make generate`, then the checklist in [references/schema-change.md](references/schema-change.md) | |
|
| A `.sql` or `.templ` file | `make generate`, then the checklist in [references/schema-change.md](references/schema-change.md) | |
|
||||||
|
| Anything that has to survive on a phone | `make android-smoke` against a booted emulator | ~1 min + setup |
|
||||||
|
|
||||||
Two targets are once-per-clone prerequisites that are **not**
|
Two targets are once-per-clone prerequisites that are **not**
|
||||||
dependencies of the targets needing them, so on a fresh checkout each
|
dependencies of the targets needing them, so on a fresh checkout each
|
||||||
@@ -426,3 +427,9 @@ fails the build otherwise, including in files no lint pass compiles.
|
|||||||
and what breaks in it.
|
and what breaks in it.
|
||||||
- [schema-change.md](references/schema-change.md) — the two-file
|
- [schema-change.md](references/schema-change.md) — the two-file
|
||||||
schema/migration checklist.
|
schema/migration checklist.
|
||||||
|
- [android-tier.md](references/android-tier.md) — the emulator tier,
|
||||||
|
and the three reasons a failure there looks like a success. **Read
|
||||||
|
its first section before running anything on Android**: Go's stdout
|
||||||
|
does not reach logcat, `os.Exit` leaves no panic and no tombstone,
|
||||||
|
and ActivityManager restarts a dying app fast enough that `pidof`
|
||||||
|
always answers.
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# The Android tier
|
||||||
|
|
||||||
|
A sixth tier, and the only one where **the app failing looks exactly
|
||||||
|
like the app working**. Read the first section before you run anything;
|
||||||
|
it is the difference between a diagnosis and an afternoon.
|
||||||
|
|
||||||
|
This tier answers "does the phone build run", nothing else. It is not a
|
||||||
|
spec tier, it does not run in CI, and the app is not a usable Android
|
||||||
|
player yet (plan 015 says why, at length).
|
||||||
|
|
||||||
|
## Three facts that make failure invisible
|
||||||
|
|
||||||
|
**Go's stdout does not reach logcat.** An Android app's fd 1 and 2 go to
|
||||||
|
`/dev/null`. Every `slog` line the app writes is discarded — including
|
||||||
|
the one naming the error it is about to exit on. `setprop
|
||||||
|
log.redirect-stdio true` does not help: it redirects the *Java*
|
||||||
|
runtime's `System.out`, and the Go code is a c-shared native library.
|
||||||
|
|
||||||
|
**`os.Exit` is a silent death.** `main()` ends several failure paths in
|
||||||
|
`os.Exit(1)`. From Android's side that is a process that vanished:
|
||||||
|
`ActivityManager: Process com.wails.app has died`, `Zygote: exited due
|
||||||
|
to signal 9`, and **no** panic, **no** `AndroidRuntime` stack, **no**
|
||||||
|
tombstone under `/data/tombstones` and nothing in `logcat -b crash` or
|
||||||
|
dropbox. All three of the places you would look are empty, and the one
|
||||||
|
signal that is present — SIGKILL — reads as "the system killed it",
|
||||||
|
which is the wrong hypothesis.
|
||||||
|
|
||||||
|
**ActivityManager restarts it, so a dead app looks alive.** A
|
||||||
|
crash-looping app is respawned several times a second, so `pidof` always
|
||||||
|
answers and `am start` always reports `Status: ok`. "Did it start" is
|
||||||
|
the wrong question. `make android-smoke` asks the right one — is it the
|
||||||
|
*same pid* a few seconds later.
|
||||||
|
|
||||||
|
The tell, once you know it: `I/WailsBridge: Wails bridge initialized`
|
||||||
|
followed immediately by a new pid doing the same thing. That means the
|
||||||
|
native library loaded, the JNI bridge came up, Go's `main()` ran, and
|
||||||
|
`main()` left. Work backwards through its `os.Exit(1)` paths.
|
||||||
|
|
||||||
|
## What to run
|
||||||
|
|
||||||
|
One-time, ~3.5 GB:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make android-setup # SDK pieces + the yj-test AVD, idempotent
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make android # fat APK (arm64 + x86_64) -> bin/yellowjacket.apk
|
||||||
|
make android-emulator # boot headless in the background, wait for boot
|
||||||
|
make android-install # adb install -r
|
||||||
|
make android-smoke # launch, then assert the same pid survives 10s
|
||||||
|
make android-logs # filtered logcat, follow
|
||||||
|
make android-emulator-stop # console kill, then the saved PID
|
||||||
|
```
|
||||||
|
|
||||||
|
`make android-smoke SECONDS=30` for a longer window. On failure it
|
||||||
|
prints the last 40 app-relevant logcat lines and how to read them.
|
||||||
|
|
||||||
|
Never `pkill -f emulator` — the pattern matches the invoking shell's own
|
||||||
|
command line and kills it, silently dropping the rest of your compound
|
||||||
|
command. The emulator is addressed by its saved pid in
|
||||||
|
`.dev/emulator.pid`, same discipline as `make dev-stop`.
|
||||||
|
|
||||||
|
## Things that cost a cycle
|
||||||
|
|
||||||
|
- **`ANDROID_HOME` must carry a platform, and Arch's does not.**
|
||||||
|
`/opt/android-sdk` (the `android-sdk` package) has an NDK and
|
||||||
|
build-tools but `platforms/` is *empty*, so Gradle fails with a
|
||||||
|
compileSdk error that reads like a version mismatch. The Makefile
|
||||||
|
defaults `ANDROID_SDK` to `~/Android/Sdk` (user-owned, writable,
|
||||||
|
where sdkmanager puts things) and `ANDROID_NDK` to `/opt/android-ndk`
|
||||||
|
separately, because the Go half wants the NDK and the Gradle half
|
||||||
|
wants the platform and they are in different places.
|
||||||
|
- **The NDK is pinned to r26d** (`26.3.11579264`, Arch's
|
||||||
|
`android-ndk-26`). Newer NDKs have broken the Wails Android build
|
||||||
|
before. CI pins the same one.
|
||||||
|
- **Without KVM the emulator still works and is unusably slow** — a 30 s
|
||||||
|
boot becomes tens of minutes, which reads as a hung target rather than
|
||||||
|
a slow one. `make android-setup` checks and warns.
|
||||||
|
- **`-no-snapshot` is deliberate.** A snapshot-resumed emulator carries
|
||||||
|
the previous run's app state, and a smoke result that depends on what
|
||||||
|
the last run left behind is not a result.
|
||||||
|
- **The logcat filter is not optional.** The emulator emits thousands of
|
||||||
|
lines a second, nearly all WindowManager transitions; an unfiltered
|
||||||
|
`adb logcat` buries the six lines that matter. `make android-logs`
|
||||||
|
filters to `WailsBridge`, the app's own tag, `GoLog`, `AndroidRuntime`,
|
||||||
|
`DEBUG` and `libc:F`.
|
||||||
|
- **`run-as` does not work on a release-signed APK** (`package not
|
||||||
|
debuggable`), so you cannot read the app's data directory or its
|
||||||
|
environment that way. Ask the device instead, or build a debug variant.
|
||||||
|
- **The `google_apis` system image, not `default`.** This app is a
|
||||||
|
WebView app; `google_apis` ships the Chrome-based WebView that
|
||||||
|
actually renders it.
|
||||||
|
|
||||||
|
## The current state of the build
|
||||||
|
|
||||||
|
`make android-smoke` **fails today, and the cause is known.**
|
||||||
|
`backend/system`'s `buildUserDirPath` switches on `runtime.GOOS` with
|
||||||
|
cases for darwin, linux and windows, and a `default:` that returns
|
||||||
|
`errUnsupportedOS`. `runtime.GOOS` is `"android"`, so it takes the
|
||||||
|
default, `NewYellowJacketApp` fails, and `main()` calls `os.Exit(1)` —
|
||||||
|
about 6 ms after the bridge initialises, which is exactly the signature
|
||||||
|
described above.
|
||||||
|
|
||||||
|
**The fix is a documented Wails API and needs no build tags.**
|
||||||
|
`application.Mobile.StoragePath()` returns the app's private files
|
||||||
|
directory and returns `""` on desktop (`mobile_stub.go`), and
|
||||||
|
`resolveUserDirPath` already lets `YJ_HOME` override the path on every
|
||||||
|
OS — so setting that override from `StoragePath()` early in `main()`,
|
||||||
|
when it is non-empty, is the whole change. Do *not* import
|
||||||
|
`pkg/application` into `backend/system`: that package is deliberately
|
||||||
|
Wails-free, which is what the `indexbuild` tag split is protecting.
|
||||||
|
|
||||||
|
It is the *first* thing that stops it, not the only one. MPRIS is
|
||||||
|
compiled in (`android` implies the `linux` build tag, so
|
||||||
|
`mpris_linux.go` is in the build and will look for a session bus that
|
||||||
|
does not exist), and the desktop shell is still a desktop shell. Fixing
|
||||||
|
one and re-running the smoke is how you find the next.
|
||||||
|
|
||||||
|
**And one that no amount of porting will fix:** open-*directory*
|
||||||
|
dialogs return an error on Android — the Storage Access Framework gives
|
||||||
|
tree URIs, not filesystem paths — as do save-file dialogs. This app's
|
||||||
|
first run is "choose your music folder" and its library model is
|
||||||
|
filesystem paths, so that is a design question, not a port.
|
||||||
|
|
||||||
|
## The scaffold's own tasks
|
||||||
|
|
||||||
|
`build/android/Taskfile.yml` ships more than the Makefile wraps, and
|
||||||
|
they are the right thing to reach for when you want something one-off:
|
||||||
|
|
||||||
|
```
|
||||||
|
wails3 task android:run # debug build + emulator install + launch
|
||||||
|
wails3 task android:run:device # same, first connected physical device
|
||||||
|
wails3 task android:deploy-device # production APK to a device
|
||||||
|
wails3 task android:bundle:fat # AAB, for a Play Store upload
|
||||||
|
wails3 task android:studio # open build/android/ in Android Studio
|
||||||
|
wails3 task android:device:list
|
||||||
|
wails3 task android:logs:all
|
||||||
|
wails3 task android:clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Two are deliberately **not** wrapped. `android:logs` greps logcat for
|
||||||
|
`(Wails|yellowjacket)`, which catches the `WailsBridge` tag but misses
|
||||||
|
the app's own process tag (`app.yellowjacket` — lowercase, so `Wails`
|
||||||
|
does not match it) and misses `ActivityManager`'s "has died" line, which
|
||||||
|
is the one that tells you it crashed; `make android-logs` filters by tag
|
||||||
|
instead. And `ensure-emulator` boots whatever `-list-avds | tail -1`
|
||||||
|
returns, with no pidfile and no boot wait, so it cannot be stopped or
|
||||||
|
sequenced.
|
||||||
|
|
||||||
|
## The identity is declared twice
|
||||||
|
|
||||||
|
`applicationId` in `build/android/app/build.gradle` is what Gradle
|
||||||
|
installs. `APP_ID` in `build/android/Taskfile.yml` is what every
|
||||||
|
adb-driven task uninstalls, launches and filters. **Nothing enforces
|
||||||
|
that they agree**, and `ANDROID.md`'s advice to set `APP_ID` in
|
||||||
|
`build/config.yml` does not work in beta.8 — `wails3 task` never reads
|
||||||
|
that file (verified with `--dry`), and even when set it feeds only the
|
||||||
|
adb commands, never Gradle. Change both or the official `run`/`deploy`
|
||||||
|
tasks address a package that is not installed.
|
||||||
|
|
||||||
|
Related, and it will bite once: the launcher activity is
|
||||||
|
`com.wails.app.MainActivity` and the applicationId is
|
||||||
|
`app.yellowjacket`. `am start -n app.yellowjacket/.MainActivity`
|
||||||
|
resolves the leading dot against the *applicationId* and fails with a
|
||||||
|
class-not-found that reads like a broken build. Always the
|
||||||
|
fully-qualified form.
|
||||||
@@ -38,6 +38,46 @@ dev-stop: ## Stop the headless app (SIGTERM, so shutdown hooks run)
|
|||||||
dev-logs: ## Tail the headless app log
|
dev-logs: ## Tail the headless app log
|
||||||
@tail -f .dev/app.log
|
@tail -f .dev/app.log
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
# The Android tier. See .pi/skills/yellowjacket-dev/references/ #
|
||||||
|
# android-tier.md for which of these to reach for and why a failure #
|
||||||
|
# here looks like nothing at all. #
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# The NDK is pinned: r26d is what the pipeline is built and checked
|
||||||
|
# against, and newer NDKs have broken Wails' Android build before.
|
||||||
|
# ANDROID_HOME must carry a *platform*, which Arch's /opt/android-sdk
|
||||||
|
# does not — hence the separate default.
|
||||||
|
ANDROID_SDK ?= $(HOME)/Android/Sdk
|
||||||
|
ANDROID_NDK ?= /opt/android-ndk
|
||||||
|
ANDROID_ENV := ANDROID_HOME=$(ANDROID_SDK) ANDROID_SDK_ROOT=$(ANDROID_SDK) ANDROID_NDK_HOME=$(ANDROID_NDK)
|
||||||
|
|
||||||
|
android: build-frontend ## Build the fat APK (arm64 + x86_64) into bin/
|
||||||
|
@$(ANDROID_ENV) PATH="$(TOOLBIN):$$PATH" go tool wails3 task android:package:fat
|
||||||
|
|
||||||
|
android-setup: ## Install the SDK pieces and create the AVD (once, ~3.5GB)
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh setup
|
||||||
|
|
||||||
|
android-emulator: ## Boot the emulator headless in the background and wait for it
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh start
|
||||||
|
|
||||||
|
android-emulator-stop: ## Shut the emulator down (console kill, then saved PID)
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh stop
|
||||||
|
|
||||||
|
android-install: ## Install bin/yellowjacket.apk onto the running emulator
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh install
|
||||||
|
|
||||||
|
android-launch: ## Force-stop, clear logcat, and start the app
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh launch
|
||||||
|
|
||||||
|
android-logs: ## Tail logcat, filtered to the app's own tags
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh logs
|
||||||
|
|
||||||
|
# "Did it start" is the wrong question — a crash-looping app starts
|
||||||
|
# several times a second. This asserts the *same pid* is still there.
|
||||||
|
android-smoke: ## Launch and assert the app is still alive (SECONDS=<n>)
|
||||||
|
@$(ANDROID_ENV) ./scripts/android-emulator.sh smoke $(if $(SECONDS),$(SECONDS),10)
|
||||||
|
|
||||||
# Seeds are produced by *running the app* — driving the real AddLibrary
|
# Seeds are produced by *running the app* — driving the real AddLibrary
|
||||||
# binding and waiting for the real scan — never by hand-writing a
|
# binding and waiting for the real scan — never by hand-writing a
|
||||||
# config.toml and DB rows. A hand-built seed is a second description
|
# config.toml and DB rows. A hand-built seed is a second description
|
||||||
|
|||||||
Executable
+235
@@ -0,0 +1,235 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# The Android tier: an emulator, an APK, and a way to find out why the
|
||||||
|
# app died.
|
||||||
|
#
|
||||||
|
# This is the phone equivalent of `dev-headless.sh`, and it is
|
||||||
|
# deliberately shaped like it — start in the background and return,
|
||||||
|
# stop by saved state, tail a log — because the operating pattern is
|
||||||
|
# the one this repo already has. What is different is *what a failure
|
||||||
|
# looks like*, and that is the whole reason this script exists rather
|
||||||
|
# than a paragraph telling you to run adb.
|
||||||
|
#
|
||||||
|
# **Go's stdout does not reach logcat.** An Android app's fd 1 and 2 go
|
||||||
|
# to /dev/null, so every `slog` line the app writes — including the one
|
||||||
|
# naming the error it is about to exit on — is discarded. There is no
|
||||||
|
# flag for this: `setprop log.redirect-stdio true` redirects the *Java*
|
||||||
|
# runtime's System.out and does nothing for a c-shared Go library.
|
||||||
|
#
|
||||||
|
# **And `os.Exit` is a silent death.** `main()` ends several failure
|
||||||
|
# paths in `os.Exit(1)`; from Android's side that is a process that
|
||||||
|
# vanished, reported as "has died: fg TOP" and signal 9, with no panic,
|
||||||
|
# no `AndroidRuntime` stack and no tombstone — the three places anyone
|
||||||
|
# would look. ActivityManager then restarts it, so `pidof` answers with
|
||||||
|
# a pid and the app looks alive while crash-looping several times a
|
||||||
|
# second.
|
||||||
|
#
|
||||||
|
# `smoke` exists because of those two facts together: the honest test
|
||||||
|
# is not "did it start" but "is the same pid still there a few seconds
|
||||||
|
# later", and the useful output is the app's own logcat tags plus a
|
||||||
|
# named guess at which `os.Exit` it took.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
AVD="${YJ_AVD:-yj-test}"
|
||||||
|
SDK="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}"
|
||||||
|
PKG="${YJ_ANDROID_PKG:-app.yellowjacket}"
|
||||||
|
# **Not "$PKG/.MainActivity".** A leading-dot activity is resolved
|
||||||
|
# relative to the *applicationId*, and the scaffold's activity lives in
|
||||||
|
# the Java package `com.wails.app`, which is deliberately not the
|
||||||
|
# applicationId (see app/build.gradle). The short form silently
|
||||||
|
# resolves to app.yellowjacket.MainActivity, which does not exist, and
|
||||||
|
# `am start` fails with a class-not-found that reads like a broken
|
||||||
|
# build rather than a wrong name.
|
||||||
|
ACTIVITY="${YJ_ANDROID_ACTIVITY:-com.wails.app.MainActivity}"
|
||||||
|
IMAGE="${YJ_ANDROID_IMAGE:-system-images;android-35;google_apis;x86_64}"
|
||||||
|
DEVDIR=".dev"
|
||||||
|
PIDFILE="$DEVDIR/emulator.pid"
|
||||||
|
LOGFILE="$DEVDIR/emulator.log"
|
||||||
|
|
||||||
|
ADB="$SDK/platform-tools/adb"
|
||||||
|
EMULATOR="$SDK/emulator/emulator"
|
||||||
|
SDKMANAGER="$SDK/cmdline-tools/latest/bin/sdkmanager"
|
||||||
|
AVDMANAGER="$SDK/cmdline-tools/latest/bin/avdmanager"
|
||||||
|
|
||||||
|
die() { echo "android: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
need_sdk() {
|
||||||
|
[ -x "$ADB" ] || die "no adb at $ADB — set ANDROID_SDK_ROOT, or run 'make android-setup'"
|
||||||
|
[ -x "$EMULATOR" ] || die "no emulator at $EMULATOR — run 'make android-setup'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The emulator is the only long-lived process here, and it is addressed
|
||||||
|
# by its saved pid. Never by name: `pkill -f emulator` matches this
|
||||||
|
# script's own command line and kills the shell running it, which is
|
||||||
|
# the same trap dev-stop.sh documents.
|
||||||
|
running() {
|
||||||
|
[ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_setup() {
|
||||||
|
[ -x "$SDKMANAGER" ] || die "no sdkmanager at $SDKMANAGER; install the Android command line tools first"
|
||||||
|
|
||||||
|
# Each piece is installed only when missing. sdkmanager is itself
|
||||||
|
# idempotent but still spends minutes verifying, so the guards are
|
||||||
|
# what make this cheap to re-run.
|
||||||
|
for want in "platform-tools" "platforms;android-35" "build-tools;34.0.0" "$IMAGE"; do
|
||||||
|
dir="$SDK/$(printf '%s' "$want" | tr ';' '/')"
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
echo " $want: present"
|
||||||
|
else
|
||||||
|
echo " $want: installing"
|
||||||
|
yes | "$SDKMANAGER" --install "$want" >/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if "$EMULATOR" -list-avds 2>/dev/null | grep -qx "$AVD"; then
|
||||||
|
echo " avd $AVD: present"
|
||||||
|
else
|
||||||
|
echo " avd $AVD: creating"
|
||||||
|
echo no | "$AVDMANAGER" create avd -n "$AVD" -k "$IMAGE" -d pixel_6 --force >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A dependency with a requirement, checked like one. Without KVM the
|
||||||
|
# emulator falls back to full software emulation and a boot that
|
||||||
|
# takes 30 s takes 20 minutes — which reads as a hung target.
|
||||||
|
if ! "$EMULATOR" -accel-check 2>&1 | grep -q "is installed and usable"; then
|
||||||
|
echo
|
||||||
|
echo " WARNING: KVM is not usable. The emulator will run under software"
|
||||||
|
echo " emulation and boot times go from ~30s to tens of minutes."
|
||||||
|
echo " Check /dev/kvm exists and that you are in the kvm group."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
need_sdk
|
||||||
|
mkdir -p "$DEVDIR"
|
||||||
|
|
||||||
|
if running; then
|
||||||
|
echo "emulator already running (pid $(cat "$PIDFILE"))"
|
||||||
|
else
|
||||||
|
"$EMULATOR" -list-avds 2>/dev/null | grep -qx "$AVD" ||
|
||||||
|
die "no AVD named '$AVD' — run 'make android-setup'"
|
||||||
|
|
||||||
|
# -no-window because there is no display and does not need one;
|
||||||
|
# -no-snapshot so a run starts from the same state every time,
|
||||||
|
# which is what makes a smoke result mean something.
|
||||||
|
nohup "$EMULATOR" -avd "$AVD" \
|
||||||
|
-no-window -no-boot-anim -no-snapshot \
|
||||||
|
-gpu swiftshader_indirect \
|
||||||
|
-netdelay none -netspeed full \
|
||||||
|
>"$LOGFILE" 2>&1 &
|
||||||
|
echo $! >"$PIDFILE"
|
||||||
|
echo "emulator starting (pid $(cat "$PIDFILE")), log: $LOGFILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n "waiting for boot"
|
||||||
|
"$ADB" wait-for-device >/dev/null 2>&1 || die "device never appeared; see $LOGFILE"
|
||||||
|
for _ in $(seq 1 150); do
|
||||||
|
if [ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; then
|
||||||
|
echo " ok"
|
||||||
|
"$ADB" shell getprop ro.build.version.release |
|
||||||
|
sed 's/^/ android /'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -n .
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
die "boot did not complete in 300s; see $LOGFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
if running; then
|
||||||
|
pid=$(cat "$PIDFILE")
|
||||||
|
# The emulator's own console command shuts the guest down
|
||||||
|
# cleanly; the saved pid is the fallback and the guarantee.
|
||||||
|
"$ADB" emu kill >/dev/null 2>&1 || true
|
||||||
|
for _ in $(seq 1 15); do
|
||||||
|
kill -0 "$pid" 2>/dev/null || break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true
|
||||||
|
echo "emulator stopped"
|
||||||
|
else
|
||||||
|
echo "emulator not running"
|
||||||
|
fi
|
||||||
|
rm -f "$PIDFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
need_sdk
|
||||||
|
[ -f bin/yellowjacket.apk ] || die "no bin/yellowjacket.apk — run 'make android' first"
|
||||||
|
"$ADB" get-state >/dev/null 2>&1 || die "no device — run 'make android-emulator' first"
|
||||||
|
"$ADB" install -r bin/yellowjacket.apk
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_launch() {
|
||||||
|
need_sdk
|
||||||
|
"$ADB" shell am force-stop "$PKG"
|
||||||
|
"$ADB" logcat -c
|
||||||
|
"$ADB" shell am start -n "$PKG/$ACTIVITY" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
need_sdk
|
||||||
|
# The app's own tags plus the two that report its death. Chasing a
|
||||||
|
# raw logcat here is hopeless: the emulator emits thousands of lines
|
||||||
|
# a second, almost all of them WindowManager transitions.
|
||||||
|
"$ADB" logcat -v time \
|
||||||
|
WailsBridge:V "$PKG":V GoLog:V AndroidRuntime:E DEBUG:V libc:F ActivityManager:I '*:S'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the app and assert it is *still the same process* a few seconds
|
||||||
|
# later. "It started" is not the question — a crash-looping app starts
|
||||||
|
# continuously.
|
||||||
|
cmd_smoke() {
|
||||||
|
need_sdk
|
||||||
|
local wait_s="${1:-10}"
|
||||||
|
|
||||||
|
cmd_launch
|
||||||
|
sleep 3
|
||||||
|
local first
|
||||||
|
first=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}')
|
||||||
|
sleep "$wait_s"
|
||||||
|
local second
|
||||||
|
second=$("$ADB" shell pidof "$PKG" 2>/dev/null | tr -d '\r' | awk '{print $1}')
|
||||||
|
|
||||||
|
if [ -n "$first" ] && [ "$first" = "$second" ]; then
|
||||||
|
echo "PASS: $PKG alive as pid $first after ${wait_s}s"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "FAIL: $PKG is not stable (pid was '${first:-none}', now '${second:-none}')"
|
||||||
|
if [ -n "$first" ] && [ -n "$second" ]; then
|
||||||
|
echo " The pid changed: it is crash-looping, not running."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "--- last 40 app-relevant logcat lines ---"
|
||||||
|
"$ADB" logcat -d -v time \
|
||||||
|
WailsBridge:V "$PKG":V GoLog:V AndroidRuntime:E DEBUG:V libc:F '*:S' 2>/dev/null |
|
||||||
|
tail -40
|
||||||
|
echo
|
||||||
|
echo "--- reading this ---"
|
||||||
|
echo "If the last line is 'Wails bridge initialized' and nothing follows,"
|
||||||
|
echo "the Go side reached main() and left it. There will be no panic and"
|
||||||
|
echo "no tombstone, because that is os.Exit, not a crash. Go's stdout"
|
||||||
|
echo "does not reach logcat, so the slog line naming the error is gone."
|
||||||
|
echo "Work backwards through main()'s os.Exit(1) paths instead."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
setup) cmd_setup ;;
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
install) cmd_install ;;
|
||||||
|
launch) cmd_launch ;;
|
||||||
|
logs) cmd_logs ;;
|
||||||
|
smoke) cmd_smoke "${2:-10}" ;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 {setup|start|stop|install|launch|logs|smoke [seconds]}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user