Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 72 additions & 9 deletions .github/scripts/android-native-crash.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
#!/usr/bin/env bash
# Asserts a native crash produces a minidump carrying an attribute set after init.
# INSTALL=apk installs the universal APK, INSTALL=split installs the device's APK set from the bundle, like Google Play.
set -euo pipefail

INSTALL="${INSTALL:-apk}"
PACKAGE="com.reactnative"
ACTIVITY="$PACKAGE/.MainActivity"
APK="examples/sdk/reactNative/android/app/build/outputs/apk/release/app-release.apk"
MARKER="ci-marker-$(date +%s)"
OUTPUTS="examples/sdk/reactNative/android/app/build/outputs"
APK="$OUTPUTS/apk/release/app-release.apk"
AAB="$OUTPUTS/bundle/release/app-release.aab"
KEYSTORE="examples/sdk/reactNative/android/app/debug.keystore"
BUNDLETOOL_JAR="${BUNDLETOOL_JAR:-bundletool.jar}"
MARKER="ci-marker-$INSTALL-$(date +%s)"
TRIGGER_URL="backtrace-example://ci-native-crash?marker=$MARKER"
DUMP_OUT="/tmp/native-crash-$INSTALL.dmp"
LOGCAT_OUT="/tmp/logcat-$INSTALL.txt"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK="$(mktemp -d)"
ADB="$(command -v adb)"

case "$INSTALL" in
apk|split) ;;
*) echo "::error::INSTALL must be apk or split, got '$INSTALL'"; exit 2 ;;
esac

wait_for_log() {
local pattern="$1" deadline="$2"
Expand All @@ -23,12 +38,52 @@ wait_for_log() {
return 1
}

install_apk() {
adb install -r "$APK"
}

install_split() {
if [ ! -f "$BUNDLETOOL_JAR" ]; then
echo "::error::bundletool jar not found at $BUNDLETOOL_JAR"
exit 1
fi

java -jar "$BUNDLETOOL_JAR" build-apks --bundle="$AAB" --output="$WORK/app.apks" --overwrite \
--connected-device --adb="$ADB" ${ANDROID_SERIAL:+--device-id="$ANDROID_SERIAL"} \
--ks="$KEYSTORE" --ks-key-alias=androiddebugkey --ks-pass=pass:android --key-pass=pass:android

unzip -o -q "$WORK/app.apks" "splits/base-master.apk" "splits/base-$ABI_US.apk" -d "$WORK"
if unzip -l "$WORK/splits/base-master.apk" | grep -q "libbacktrace-native.so"; then
echo "::error::base-master.apk carries libbacktrace-native.so, this is not a Play-style split install"
exit 1
fi
ABIS="$ABI" bash "$HERE/verify-jni-symbols.sh" "$WORK/splits/base-$ABI_US.apk"

java -jar "$BUNDLETOOL_JAR" install-apks --apks="$WORK/app.apks" --adb="$ADB" ${ANDROID_SERIAL:+--device-id="$ANDROID_SERIAL"}

local installed
installed="$(adb shell pm path "$PACKAGE" | tr -d '\r')"
echo "$installed"
if ! grep -q "split_config.$ABI_US.apk" <<<"$installed"; then
echo "::error::split_config.$ABI_US.apk is not installed"
exit 1
fi
}

adb wait-for-device
adb install -r "$APK"
ABI="$(adb shell getprop ro.product.cpu.abi | tr -d '\r')"
ABI_US="${ABI//-/_}"
echo "install mode: $INSTALL, device abi: $ABI, device abis: $(adb shell getprop ro.product.cpu.abilist | tr -d '\r')"

adb uninstall "$PACKAGE" >/dev/null 2>&1 || true
case "$INSTALL" in
apk) install_apk ;;
split) install_split ;;
esac
adb shell pm clear "$PACKAGE" >/dev/null
adb logcat -c
trap 'adb logcat -d > "$LOGCAT_OUT" 2>/dev/null || true; rm -rf "$WORK"' EXIT

echo "device abis: $(adb shell getprop ro.product.cpu.abilist | tr -d '\r')"
adb shell am start -n "$ACTIVITY" >/dev/null
echo "app abi:$(adb shell dumpsys package "$PACKAGE" | grep -m1 primaryCpuAbi | tr -d '\r' | cut -d= -f2)"

Expand All @@ -45,9 +100,9 @@ PULLED=""
# exec-out, not shell: a pty mangles binary and would corrupt the minidump.
for _ in $(seq 1 180); do
DUMP="$(adb shell run-as "$PACKAGE" find files/backtrace/native -name '*.dmp' 2>/dev/null | tr -d '\r' | head -1 || true)"
if [ -n "$DUMP" ] && adb exec-out run-as "$PACKAGE" cat "$DUMP" > /tmp/native-crash.dmp 2>/dev/null; then
if [ -n "$DUMP" ] && adb exec-out run-as "$PACKAGE" cat "$DUMP" > "$DUMP_OUT" 2>/dev/null; then
ON_DEVICE_SIZE="$(adb shell run-as "$PACKAGE" stat -c %s "$DUMP" 2>/dev/null | tr -d '\r' || true)"
PULLED_SIZE="$(wc -c < /tmp/native-crash.dmp | tr -d ' ')"
PULLED_SIZE="$(wc -c < "$DUMP_OUT" | tr -d ' ')"
if [ -n "$ON_DEVICE_SIZE" ] && [ "$ON_DEVICE_SIZE" = "$PULLED_SIZE" ]; then
PULLED=1
break
Expand All @@ -63,7 +118,7 @@ if [ -z "$PULLED" ]; then
echo "::error::the app is still running, so the crash never fired"
fi
adb shell run-as "$PACKAGE" ls -R files/backtrace 2>&1 || true
adb logcat -d | grep -iE "backtrace|crashpad|SIGSEGV|BT_CI_DRIVER" | tail -30
adb logcat -d | grep -iE "backtrace|crashpad|SIGSEGV|BT_CI_DRIVER|AndroidRuntime|nativeloader" | tail -40
else
echo "::error::could not pull a stable copy of $DUMP"
fi
Expand All @@ -72,6 +127,14 @@ fi
echo "minidump: $DUMP"
echo "minidump pulled: $PULLED_SIZE bytes"

adb logcat -d > /tmp/logcat.txt
MARKER="$MARKER" python3 "$HERE/check-minidump-annotations.py" "$DUMP_OUT"
Comment thread
KishanPRao marked this conversation as resolved.

MARKER="$MARKER" python3 "$HERE/check-minidump-annotations.py" /tmp/native-crash.dmp
if [ "$INSTALL" = "split" ]; then
LOADED_FROM_SPLIT="BT_NATIVE_HANDLER_LOADED /.*/split_config\.$ABI_US\.apk!/lib/$ABI/libbacktrace-native\.so"
if ! wait_for_log "$LOADED_FROM_SPLIT" 15; then
echo "::error::missing BT_NATIVE_HANDLER_LOADED for split_config.$ABI_US.apk"
adb logcat -d -s BacktraceCrashHandlerRunner:I '*:S' | tail -20
exit 1
fi
adb logcat -d -v raw -s BacktraceCrashHandlerRunner:I '*:S' | grep -E "^BT_NATIVE_HANDLER_LOADED"
fi
2 changes: 1 addition & 1 deletion .github/scripts/verify-jni-symbols.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -euo pipefail

APK="${1:?usage: $0 <apk>}"
ABIS=${ABIS:-"arm64-v8a armeabi-v7a x86 x86_64"}
SYMBOLS_BOUND_AT_RUNTIME="Java_backtraceio_library_nativeCalls_BacktraceCrashHandler_initializeJavaCrashHandler Java_backtraceio_library_nativeCalls_BacktraceCrashHandler_handleCrash Java_backtraceio_library_BacktraceDatabase_addAttribute Java_backtraceio_library_base_BacktraceBase_crash"
SYMBOLS_BOUND_AT_RUNTIME="Java_backtraceio_library_nativeCalls_BacktraceCrashHandler_initializeJavaCrashHandler Java_backtraceio_library_nativeCalls_BacktraceCrashHandler_handleCrash Java_backtraceio_library_BacktraceDatabase_addAttribute Java_backtraceio_library_base_BacktraceBase_crash Java_backtraceio_library_models_nativeHandler_CrashHandlerConfiguration_resolveLoadedLibraryPath"
SYMBOLS_PROVING_LIBRARY_VERSION="Java_backtraceio_library_BacktraceDatabase_addAttachment"
SYMBOLS=${SYMBOLS:-"$SYMBOLS_BOUND_AT_RUNTIME $SYMBOLS_PROVING_LIBRARY_VERSION"}

Expand Down
20 changes: 15 additions & 5 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,17 @@ jobs:

- name: Build example app
working-directory: examples/sdk/reactNative/android
run: ./gradlew assembleRelease -PbtCiDebuggable -PbtCiMinify -PnewArchEnabled=${{ matrix.new-arch }} -x uploadSourceMapsToBacktrace --console=plain
run: ./gradlew assembleRelease bundleRelease -PbtCiDebuggable -PbtCiMinify -PnewArchEnabled=${{ matrix.new-arch }} -x uploadSourceMapsToBacktrace --console=plain

- name: Verify native libraries and JNI symbols
run: bash .github/scripts/verify-jni-symbols.sh examples/sdk/reactNative/android/app/build/outputs/apk/release/app-release.apk

- name: Download bundletool
run: |
curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$RUNNER_TEMP/bundletool.jar" \
https://github.com/google/bundletool/releases/download/1.17.2/bundletool-all-1.17.2.jar
echo "2d4ad908faea64047c1cc9cb747e6aa667c6ab192e09607bd16b67246a8cd6ae $RUNNER_TEMP/bundletool.jar" | sha256sum -c -

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
Expand Down Expand Up @@ -107,24 +113,28 @@ jobs:
disable-animations: false
script: echo "Generated AVD snapshot for caching."

- name: Capture a native crash on the emulator
- name: Capture a native crash on the emulator, single APK then Play-style split install
uses: reactivecircus/android-emulator-runner@v2
env:
BUNDLETOOL_JAR: ${{ runner.temp }}/bundletool.jar
with:
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.arch }}
target: google_apis
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: bash .github/scripts/android-native-crash.sh
script: |
INSTALL=apk bash .github/scripts/android-native-crash.sh
INSTALL=split bash .github/scripts/android-native-crash.sh

- name: Upload crash evidence
if: failure()
uses: actions/upload-artifact@v4
with:
name: native-crash-api${{ matrix.api-level }}-${{ matrix.arch }}-newarch-${{ matrix.new-arch }}
path: |
/tmp/native-crash.dmp
/tmp/logcat.txt
/tmp/native-crash-*.dmp
/tmp/logcat-*.txt
if-no-files-found: warn
retention-days: 7
Original file line number Diff line number Diff line change
@@ -1,14 +1,70 @@
package backtraceio.library.common;

import android.os.Build;
import android.os.Process;

import androidx.annotation.RequiresApi;

public class AbiHelper {
@SuppressWarnings("deprecation")
public static String getCurrentAbi() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
// on newer Android versions, we'll return only the most important Abi version
return Build.SUPPORTED_ABIS[0];
// CPU_ABI follows the bitness of the running process; SUPPORTED_ABIS[0] is only the device preference.
String processAbi = normalize(Build.CPU_ABI);
if (processAbi != null) {
return processAbi;
}

String bitnessAbi = firstValidAbi(getProcessBitnessAbis());
if (bitnessAbi != null) {
return bitnessAbi;
}

String supportedAbi = firstValidAbi(Build.SUPPORTED_ABIS);
if (supportedAbi != null) {
return supportedAbi;
}

throw new IllegalStateException("Unable to determine the current process ABI");
}

private static String[] getProcessBitnessAbis() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return null;
}
return Api23Impl.getProcessBitnessAbis();
}

@RequiresApi(api = Build.VERSION_CODES.M)
private static final class Api23Impl {
private Api23Impl() {
}

static String[] getProcessBitnessAbis() {
return Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS;
}
}

private static String firstValidAbi(String[] abis) {
if (abis == null) {
return null;
}
for (String abi : abis) {
String normalizedAbi = normalize(abi);
if (normalizedAbi != null) {
return normalizedAbi;
}
}
return null;
}

private static String normalize(String abi) {
if (abi == null) {
return null;
}
String normalizedAbi = abi.trim();
if (normalizedAbi.isEmpty() || Build.UNKNOWN.equals(normalizedAbi)) {
return null;
}
// on pre-Lollip versions, we got only one Abi
return Build.CPU_ABI;
return normalizedAbi;
}
}
Loading
Loading