1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
#!/usr/bin/env bash
#
# Determine whether an Android app actually launches and stays up, and surface
# the exception if it does not.
#
# Why this exists: `adb shell monkey -p <pkg> -c android.intent.category.LAUNCHER 1`
# followed by polling `pidof` is not a reliable check. In practice it reported a
# healthy app as dead and a dead app as healthy, in the same session, on the same
# emulator — monkey's launch can land on whatever activity happens to be in the
# foreground, and a single `pidof` sample races app startup and restart.
#
# This script instead:
# * resolves the launcher activity from the package manager rather than guessing
# * starts a continuous logcat capture BEFORE launching, so the crash window is
# never missed (the `-b crash` buffer was empty even for real FATAL crashes)
# * launches with `am start -W` and checks its reported status
# * samples the pid repeatedly over a settle period, so a process that starts
# and then dies is reported as a failure, not a success
# * greps the captured log for FATAL EXCEPTION and prints the stack
#
# Usage:
# android-launch-check <package> [--serial <adb-serial>] [--settle <seconds>]
#
# Exit: 0 = launched and stayed up with no fatal exception, 1 = did not.
set -euo pipefail
die() { printf '%s: %s\n' "$(basename "$0")" "$*" >&2; exit 2; }
[ $# -ge 1 ] || die "usage: $(basename "$0") <package> [--serial <s>] [--settle <secs>]"
pkg=$1; shift
serial=""
settle=20
apk=""
while [ $# -gt 0 ]; do
case "$1" in
--serial) serial=${2:-}; shift 2 ;;
--settle) settle=${2:-20}; shift 2 ;;
--apk) apk=${2:-}; shift 2 ;;
*) die "unknown argument: $1" ;;
esac
done
adb_bin=$(command -v adb || echo /opt/android-sdk/platform-tools/adb)
[ -x "$adb_bin" ] || die "adb not found"
ADB=("$adb_bin")
[ -n "$serial" ] && ADB+=(-s "$serial")
devices=$("${ADB[@]}" devices | awk 'NR>1 && $2=="device" {print $1}')
[ -n "$devices" ] || die "no adb device in 'device' state"
[ "$(printf '%s\n' "$devices" | wc -l)" -eq 1 ] || [ -n "$serial" ] \
|| die "multiple devices attached; pass --serial. Found: $(echo $devices)"
printf '=== android-launch-check ===\n'
printf 'package : %s\n' "$pkg"
printf 'device : %s\n' "$(echo $devices | tr '\n' ' ')"
if [ -n "$apk" ]; then
[ -f "$apk" ] || die "apk not found: $apk"
# adb rejects any filename not ending .apk/.apex, and prints that refusal to
# stdout while still exiting 0 in some versions — so check the text as well as
# the status. Discarding this output is how a "reinstall" can silently not
# happen and leave the previous build under test.
case "$apk" in
*.apk|*.apex) : ;;
*) die "adb will refuse this filename (must end .apk/.apex): $apk" ;;
esac
printf 'apk : %s\n' "$apk"
install_out=$("${ADB[@]}" install -r "$apk" 2>&1 | tr -d '\r' || true)
if ! printf '%s' "$install_out" | grep -q '^Success$'; then
printf 'result : FAIL — install did not succeed\n'
printf '%s\n' "$install_out" | sed 's/^/ /'
exit 1
fi
printf 'install : Success\n'
fi
# Membership test done in pure shell, not `... | grep -q`. Under `set -o pipefail`
# a `grep -q` that exits on its first match can SIGPIPE the upstream command, and
# the pipeline then reports failure even though the match succeeded — which made
# this check report an installed package as missing.
pkg_list=$("${ADB[@]}" shell pm list packages 2>/dev/null | tr -d '\r' || true)
case $'\n'"$pkg_list"$'\n' in
*$'\n'"package:$pkg"$'\n'*) : ;;
*) die "package not installed: $pkg" ;;
esac
# Resolve rather than assume. Guessing ".MainActivity" when the manifest says
# ".ui.MainActivity" produces "Activity class does not exist", which is an
# operator error that can easily be misread as an app crash.
activity=$("${ADB[@]}" shell cmd package resolve-activity --brief "$pkg" 2>/dev/null \
| tr -d '\r' | tail -1)
case "$activity" in
"$pkg"/*) : ;;
*) die "could not resolve launcher activity for $pkg (got: '$activity')" ;;
esac
printf 'activity: %s\n' "$activity"
logfile=$(mktemp); trap 'rm -f "$logfile"' EXIT
"${ADB[@]}" shell am force-stop "$pkg" >/dev/null 2>&1 || true
sleep 2
"${ADB[@]}" logcat -c >/dev/null 2>&1 || true
# Capture starts first and runs across the whole window.
"${ADB[@]}" logcat -v threadtime > "$logfile" 2>&1 &
logcat_pid=$!
trap 'kill "$logcat_pid" 2>/dev/null || true; rm -f "$logfile"' EXIT
sleep 2
start_out=$("${ADB[@]}" shell am start -W -n "$activity" 2>&1 | tr -d '\r')
start_status=$(printf '%s\n' "$start_out" | awk -F': *' '/^Status:/ {print $2}')
printf 'am start: %s\n' "${start_status:-<none>}"
# Sample repeatedly. One sample cannot distinguish "not started yet" from
# "started and already died".
alive_samples=0; total_samples=0; last_pid=""
interval=5
elapsed=0
while [ "$elapsed" -lt "$settle" ]; do
sleep "$interval"
elapsed=$((elapsed + interval))
# `|| true` is load-bearing: pidof exits non-zero when the process is gone,
# which under `set -e` would abort this script at exactly the moment it is
# supposed to be detecting a crash.
pid=$("${ADB[@]}" shell pidof "$pkg" 2>/dev/null | tr -d '\r' | awk '{print $1}' || true)
total_samples=$((total_samples + 1))
if [ -n "$pid" ]; then alive_samples=$((alive_samples + 1)); last_pid=$pid; fi
printf ' t=%-3ss pid=%s\n' "$elapsed" "${pid:-<none>}"
done
sleep 1
kill "$logcat_pid" 2>/dev/null || true
wait "$logcat_pid" 2>/dev/null || true
fatal_count=$(grep -c "FATAL EXCEPTION" "$logfile" 2>/dev/null || true)
fatal_count=${fatal_count:-0}
printf 'samples : %d/%d alive\n' "$alive_samples" "$total_samples"
printf 'fatal : %s\n' "$fatal_count"
if [ "$fatal_count" -gt 0 ]; then
printf '\n--- first fatal exception ---\n'
grep -A 25 -m 1 "FATAL EXCEPTION" "$logfile" | sed 's/^/ /'
printf '\nresult : FAIL — app crashed\n'
exit 1
fi
if [ "$alive_samples" -ne "$total_samples" ]; then
printf '\nresult : FAIL — process was not alive for the whole settle window\n'
printf 'hint : started but died, or never started. No FATAL in logcat, so\n'
printf ' check for a native crash or an early System.exit.\n'
exit 1
fi
printf 'pid : %s\n' "$last_pid"
printf 'result : PASS — launched and stayed up %ss with no fatal exception\n' "$settle"
exit 0
|