diff --git a/CHANGELOG.md b/CHANGELOG.md index b067ae1d9..faaefb85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add WinGDK (Gaming.Desktop.x64) platform support ([#1631](https://github.com/getsentry/sentry-native/pull/1631)) - Track discarded events via client reports. ([#1549](https://github.com/getsentry/sentry-native/pull/1549)) +- Android: allow Sentry.NET to preload the NDK integration to install signal handlers before the .NET runtime. ([#1613](https://github.com/getsentry/sentry-native/pull/1613)) ## 0.13.5 diff --git a/include/sentry.h b/include/sentry.h index 01de1ff6f..f33635edc 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1869,7 +1869,10 @@ SENTRY_API int sentry_flush(uint64_t timeout); * * Note that this does not uninstall any crash handler installed by our * backends, which will still process crashes after `sentry_close()`, except - * when using `crashpad` on Linux or the `inproc` backend. + * when using `crashpad` on Linux or the `inproc` backend. The Android + * preload mode of `inproc` is a special case: a lightweight signal-chain + * placeholder may remain installed across `sentry_close()` to preserve + * ordering relative to the managed runtime until a later re-init. * * Further note that this function will block the thread it was called from * until the sentry background worker has finished its work, or it timed out, diff --git a/ndk/lib/api/sentry-native-ndk.api b/ndk/lib/api/sentry-native-ndk.api index 4997d0444..9d6c41fc3 100644 --- a/ndk/lib/api/sentry-native-ndk.api +++ b/ndk/lib/api/sentry-native-ndk.api @@ -104,5 +104,16 @@ public final class io/sentry/ndk/SentryNdk { public static fun close ()V public static fun init (Lio/sentry/ndk/NdkOptions;)V public static fun loadNativeLibraries ()V + public static fun preload ()V +} + +public final class io/sentry/ndk/SentryNdkPreloadProvider : android/content/ContentProvider { + public fun ()V + public fun delete (Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I + public fun getType (Landroid/net/Uri;)Ljava/lang/String; + public fun insert (Landroid/net/Uri;Landroid/content/ContentValues;)Landroid/net/Uri; + public fun onCreate ()Z + public fun query (Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor; + public fun update (Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I } diff --git a/ndk/lib/src/main/AndroidManifest.xml b/ndk/lib/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4139b2a1b --- /dev/null +++ b/ndk/lib/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java index 72046377f..2369c1104 100644 --- a/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java +++ b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java @@ -10,6 +10,8 @@ public final class SentryNdk { private SentryNdk() {} + private static native void preloadSentryNative(); + /** * Initializes sentry-native and returns 0 on success, non-zero on failure. * @@ -20,6 +22,24 @@ private SentryNdk() {} private static native void shutdown(); + /** + * Preloads sentry-native into the process signal chain before full + * initialization. + * + *

Intended for downstream SDK/runtime integrations on Android. This does + * not initialize sentry-native, configure a database path, or start the + * inproc handler thread; it only establishes signal-handler ordering ahead of + * the managed runtime. + * + *

This is intended to be used by downstream integrations that gate the + * preload flow to supported runtimes and then call {@link #init(NdkOptions)} + * with the normal DEFAULT handler strategy. + */ + public static void preload() { + loadNativeLibraries(); + preloadSentryNative(); + } + /** * Init the NDK integration * diff --git a/ndk/lib/src/main/java/io/sentry/ndk/SentryNdkPreloadProvider.java b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdkPreloadProvider.java new file mode 100644 index 000000000..274b56f63 --- /dev/null +++ b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdkPreloadProvider.java @@ -0,0 +1,88 @@ +package io.sentry.ndk; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Preloads the NDK integration before the .NET runtime provider on Android. + * + *

This is intended for downstream SDK integrations that run with CoreCLR on + * Android. By installing sentry-native before the managed runtime registers + * its own signal handlers, native crash signals can chain from the runtime + * back to the Native SDK, while runtime-generated fault signals can still be + * consumed by the runtime for managed exception handling. + * + *

This is the preload alternative to CHAIN_AT_START. Mono on Android + * continues to use CHAIN_AT_START. + * + *

Enabled by setting {@code io.sentry.ndk.preload} to {@code true} in the + * app manifest metadata. The high {@code initOrder} ensures this runs before + * the runtime provider emitted by dotnet/android. + */ +public final class SentryNdkPreloadProvider extends ContentProvider { + + @Override + public boolean onCreate() { + final Context context = getContext(); + if (context == null) { + return false; + } + try { + final ApplicationInfo info = + context + .getPackageManager() + .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + final Bundle metadata = info.metaData; + if (metadata != null && metadata.getBoolean("io.sentry.ndk.preload", false)) { + android.util.Log.d("sentry", "io.sentry.ndk.preload read: true"); + SentryNdk.preload(); + android.util.Log.d("sentry", "SentryNdk.preload() completed"); + } + } catch (Throwable e) { + android.util.Log.e("sentry", "SentryNdk.preload() failed", e); + } + return true; + } + + @Override + public @Nullable Cursor query( + @NotNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + return null; + } + + @Override + public @Nullable String getType(@NotNull Uri uri) { + return null; + } + + @Override + public @Nullable Uri insert(@NotNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete(@NotNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NotNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } +} diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index eac279959..c64f00565 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -302,6 +302,15 @@ static void send_envelope(sentry_envelope_t *envelope, void *data) { sentry_envelope_free(envelope); } +// sentry_backend.h +extern void sentry__backend_preload(void); + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_SentryNdk_preloadSentryNative(JNIEnv *env, jclass cls) +{ + sentry__backend_preload(); +} + JNIEXPORT jint JNICALL Java_io_sentry_ndk_SentryNdk_initSentryNative( JNIEnv *env, diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index b6a5142b3..e57f13820 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -323,6 +323,11 @@ breakpad_backend_except( extern "C" { +void +sentry__backend_preload(void) +{ +} + sentry_backend_t * sentry__backend_new(void) { diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 4397052d0..f0fda4bef 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -1029,6 +1029,11 @@ crashpad_backend_remove_attachment( } #endif +void +sentry__backend_preload(void) +{ +} + sentry_backend_t * sentry__backend_new(void) { diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 40616ccd3..9da1fce26 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -200,6 +200,14 @@ static volatile long g_handler_has_work = 0; #define CRASH_STATE_DONE 2 static volatile long g_crash_handling_state = CRASH_STATE_IDLE; +// Set once handlers were installed via sentry__backend_preload(). In this +// mode sentry may keep its chain position across sentry_close() until either +// full init reuses it, or a pre-init/post-close signal falls through, and +// consumes the preload state. +#ifdef SENTRY_PLATFORM_UNIX +static volatile long g_preloaded = 0; +#endif + // trigger/schedule primitives that block the other side until this side is done #ifdef SENTRY_PLATFORM_UNIX static int g_handler_pipe[2] = { -1, -1 }; @@ -468,6 +476,37 @@ invoke_signal_handler(int signum, siginfo_t *info, void *user_context) } } +static int +install_signal_handlers(void) +{ + if (sentry__atomic_fetch(&g_preloaded)) { + return 0; + } + + memset(g_previous_handlers, 0, sizeof(g_previous_handlers)); + for (size_t i = 0; i < SIGNAL_COUNT; ++i) { + if (sigaction( + SIGNAL_DEFINITIONS[i].signum, NULL, &g_previous_handlers[i]) + == -1) { + return 1; + } + } + + setup_sigaltstack(&g_signal_stack, "init"); + + sigemptyset(&g_sigaction.sa_mask); + g_sigaction.sa_sigaction = handle_signal; + // SA_NODEFER allows the signal to be delivered while the handler is + // running. This is needed for recursive crash detection to work - + // without it, a crash during crash handling would block the signal + // and leave the process in an undefined state. + g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; + for (size_t i = 0; i < SIGNAL_COUNT; ++i) { + sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); + } + return 0; +} + static int startup_inproc_backend( sentry_backend_t *backend, const sentry_options_t *options) @@ -503,30 +542,7 @@ startup_inproc_backend( return 1; } - // save the old signal handlers - memset(g_previous_handlers, 0, sizeof(g_previous_handlers)); - for (size_t i = 0; i < SIGNAL_COUNT; ++i) { - if (sigaction( - SIGNAL_DEFINITIONS[i].signum, NULL, &g_previous_handlers[i]) - == -1) { - return 1; - } - } - - setup_sigaltstack(&g_signal_stack, "init"); - - // install our own signal handler - sigemptyset(&g_sigaction.sa_mask); - g_sigaction.sa_sigaction = handle_signal; - // SA_NODEFER allows the signal to be delivered while the handler is - // running. This is needed for recursive crash detection to work - - // without it, a crash during crash handling would block the signal - // and leave the process in an undefined state. - g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; - for (size_t i = 0; i < SIGNAL_COUNT; ++i) { - sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); - } - return 0; + return install_signal_handlers(); } static void @@ -534,8 +550,19 @@ shutdown_inproc_backend(sentry_backend_t *backend) { stop_handler_thread(); - teardown_sigaltstack(&g_signal_stack); - reset_signal_handlers(); + // In Android preload mode, we intentionally keep our signal handlers + // installed across shutdown. The handler thread is torn down, but our + // position in the signal chain must remain stable so that a later + // sentry_init() can reactivate full crash handling without losing the + // ordering relative to the managed runtime. + // + // While in this state, full inproc crash processing is inactive and any + // signal we still observe will fall through to the previously installed + // handler. + if (!sentry__atomic_fetch(&g_preloaded)) { + teardown_sigaltstack(&g_signal_stack); + reset_signal_handlers(); + } if (backend) { backend->data = NULL; @@ -1643,6 +1670,34 @@ process_ucontext(const sentry_ucontext_t *uctx) "multiple recursive crashes detected, bailing out"); goto cleanup; } + + // If we were only preloaded (before sentry_init()) or were closed after + // preload (handlers still installed, but no handler thread), do not attempt + // full crash capture here. In this state, preload only serves as a + // placeholder in the signal chain. + // + // We therefore remove our placeholder from the chain and forward the + // signal to the previously installed handler set. + // + // This path is expected to be terminal for real crash signals. If it + // returns, preload mode is consumed for the remainder of the process + // lifetime until a later sentry_init() reinstalls handlers from the + // then-current chain. + // + // This also covers the window after preload but before the managed runtime + // has installed its own handlers: a signal seen in that window is forwarded + // to the pre-preload handler set rather than being captured by Sentry. + if (sentry__atomic_fetch(&g_preloaded) + && !sentry__atomic_fetch(&g_handler_thread_ready)) { + SENTRY_SIGNAL_SAFE_LOG( + "handler thread not ready, falling through to previous handler"); + reset_signal_handlers(); + sentry__atomic_store(&g_preloaded, 0); + sentry__leave_signal_handler(); + invoke_signal_handler( + uctx->signum, uctx->siginfo, (void *)uctx->user_context); + return; + } #endif if (!g_backend_config.enable_logging_when_crashed) { @@ -1742,6 +1797,16 @@ handle_except(sentry_backend_t *UNUSED(backend), const sentry_ucontext_t *uctx) process_ucontext(uctx); } +void +sentry__backend_preload(void) +{ +#ifdef SENTRY_PLATFORM_UNIX + if (install_signal_handlers() == 0) { + sentry__atomic_store(&g_preloaded, 1); + } +#endif +} + sentry_backend_t * sentry__backend_new(void) { diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index f0460e867..32328fe49 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -919,6 +919,11 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } } +void +sentry__backend_preload(void) +{ +} + /** * Create native backend */ diff --git a/src/backends/sentry_backend_none.c b/src/backends/sentry_backend_none.c index 8cc1ad736..08efcd5dc 100644 --- a/src/backends/sentry_backend_none.c +++ b/src/backends/sentry_backend_none.c @@ -1,5 +1,10 @@ #include "sentry_backend.h" +void +sentry__backend_preload(void) +{ +} + sentry_backend_t * sentry__backend_new(void) { diff --git a/src/sentry_backend.h b/src/sentry_backend.h index fc80bd975..a55fc507a 100644 --- a/src/sentry_backend.h +++ b/src/sentry_backend.h @@ -32,6 +32,28 @@ struct sentry_backend_s { bool can_capture_after_shutdown; }; +/** + * Backend-specific pre-initialization that can be called before sentry_init(). + * + * Currently used by downstream SDKs on Android to preload the inproc backend + * before the .NET runtime installs its own signal handlers. This is the + * preload alternative to CHAIN_AT_START for runtimes where the handler order + * can be established at process startup (e.g., CoreCLR on Android). + * + * Preload only establishes signal-chain order. Full crash processing becomes + * active once sentry_init() starts the handler thread. + * + * If a crash/signal occurs before sentry_init() is called, or after a + * subsequent sentry_close() while the preload chain position is still kept, + * the preloaded handler will fall through to the previously installed handler. + * + * This API is intended for downstream SDK/runtime integrations, not as a + * general app-level initialization entry point. Callers are expected to gate + * this to supported runtime configurations (e.g., no preload together with + * CHAIN_AT_START). + */ +SENTRY_API void sentry__backend_preload(void); + /** * This will free a previously allocated backend. */ diff --git a/tests/fixtures/dotnet_signal/Platforms/Android/MainActivity.cs b/tests/fixtures/dotnet_signal/Platforms/Android/MainActivity.cs index a6b35ceba..d1a63ccc4 100644 --- a/tests/fixtures/dotnet_signal/Platforms/Android/MainActivity.cs +++ b/tests/fixtures/dotnet_signal/Platforms/Android/MainActivity.cs @@ -9,20 +9,33 @@ namespace dotnet_signal; [Activity(Name = "dotnet_signal.MainActivity", MainLauncher = true)] public class MainActivity : Activity { + static MainActivity() + { + Java.Lang.JavaSystem.LoadLibrary("sentry"); + Java.Lang.JavaSystem.LoadLibrary("crash"); + } + protected override void OnResume() { base.OnResume(); var arg = Intent?.GetStringExtra("arg"); + var strategy = Intent?.GetStringExtra("strategy") ?? ""; + var reinit = Intent?.GetStringExtra("reinit") ?? ""; if (!string.IsNullOrEmpty(arg)) { var databasePath = FilesDir?.AbsolutePath + "/.sentry-native"; + var args = new List { arg }; + if (!string.IsNullOrEmpty(strategy)) + args.Add(strategy); + if (!string.IsNullOrEmpty(reinit)) + args.Add("reinit"); // Post to the message queue so the activity finishes starting // before the crash test runs. Without this, "am start -W" may hang. new Handler(Looper.MainLooper!).Post(() => { - Program.RunTest(new[] { arg }, databasePath); + Program.RunTest(args.ToArray(), databasePath); FinishAndRemoveTask(); Java.Lang.JavaSystem.Exit(0); }); diff --git a/tests/fixtures/dotnet_signal/Platforms/Android/PreloadManifest.xml b/tests/fixtures/dotnet_signal/Platforms/Android/PreloadManifest.xml new file mode 100644 index 000000000..06edeb191 --- /dev/null +++ b/tests/fixtures/dotnet_signal/Platforms/Android/PreloadManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/dotnet_signal/Program.cs b/tests/fixtures/dotnet_signal/Program.cs index c21e10ef7..390db7fb9 100644 --- a/tests/fixtures/dotnet_signal/Program.cs +++ b/tests/fixtures/dotnet_signal/Program.cs @@ -26,6 +26,9 @@ class Program [DllImport("sentry", EntryPoint = "sentry_init")] static extern int sentry_init(IntPtr options); + [DllImport("sentry", EntryPoint = "sentry_close")] + static extern void sentry_close(); + public static void RunTest(string[] args, string? databasePath = null) { var githubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") ?? string.Empty; @@ -39,7 +42,11 @@ public static void RunTest(string[] args, string? databasePath = null) // setup minimal sentry-native var options = sentry_options_new(); - sentry_options_set_handler_strategy(options, 1); + // Preload replaces CHAIN_AT_START in the CoreCLR path. If preload is + // active, run sentry-native with DEFAULT strategy so the runtime can + // chain native crashes back to us. + int strategy = args.Contains("default-strategy") ? 0 : 1; + sentry_options_set_handler_strategy(options, strategy); sentry_options_set_debug(options, 1); if (databasePath != null) { @@ -47,6 +54,17 @@ public static void RunTest(string[] args, string? databasePath = null) } sentry_init(options); + if (args.Contains("reinit")) + { + sentry_close(); + options = sentry_options_new(); + sentry_options_set_handler_strategy(options, strategy); + sentry_options_set_debug(options, 1); + if (databasePath != null) + sentry_options_set_database_path(options, databasePath); + sentry_init(options); + } + if (args.Contains("native-crash")) { native_crash(); diff --git a/tests/fixtures/dotnet_signal/test_dotnet.csproj b/tests/fixtures/dotnet_signal/test_dotnet.csproj index e266400d0..48b4680af 100644 --- a/tests/fixtures/dotnet_signal/test_dotnet.csproj +++ b/tests/fixtures/dotnet_signal/test_dotnet.csproj @@ -17,7 +17,13 @@ - + + + + + + + diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index bede1d44f..0741de87d 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -276,7 +276,7 @@ def wait_for(condition, timeout=10, interval=0.5): return condition() -def run_android(args=None, timeout=30): +def run_android(args=None, strategy=None, reinit=False, timeout=30): if args is None: args = [] adb("logcat", "-c") @@ -287,6 +287,10 @@ def run_android(args=None, timeout=30): intent_args = [] for arg in args: intent_args += ["--es", "arg", arg] + if strategy: + intent_args += ["--es", "strategy", strategy] + if reinit: + intent_args += ["--es", "reinit", "true"] try: adb( "shell", @@ -317,26 +321,54 @@ def run_android(args=None, timeout=30): return adb(*logcat_args, capture_output=True, text=True).stdout -def run_android_managed_exception(): - return run_android(["managed-exception"]) +def run_android_managed_exception(strategy=None, reinit=False): + return run_android(["managed-exception"], strategy=strategy, reinit=reinit) -def run_android_unhandled_managed_exception(): - return run_android(["unhandled-managed-exception"]) +def run_android_unhandled_managed_exception(strategy=None): + return run_android(["unhandled-managed-exception"], strategy=strategy) -def run_android_native_crash(): - return run_android(["native-crash"]) +def run_android_native_crash(strategy=None, reinit=False): + return run_android(["native-crash"], strategy=strategy, reinit=reinit) + + +ndk_aar_path = ( + pathlib.Path(__file__).parent.parent + / "ndk" + / "lib" + / "build" + / "outputs" + / "aar" + / "sentry-native-ndk-release.aar" +) @pytest.mark.skipif( not is_android or int(is_android) < 26, reason="needs Android API 26+ (tombstoned)", ) -def test_android_signals_inproc(cmake): +# Mono on Android keeps using CHAIN_AT_START. Preload is the CoreCLR path, +# where sentry-native can enter the signal chain before the runtime installs +# its own handlers. +@pytest.mark.parametrize( + "runtime,strategy", + [ + ("mono", "chain-at-start"), + ("coreclr", "preload"), + ], +) +def test_android_signals_inproc(cmake, runtime, strategy): if shutil.which("dotnet") is None: pytest.skip("dotnet is not installed") + if strategy == "preload": + ndk_dir = pathlib.Path(__file__).parent.parent / "ndk" + subprocess.run(["./gradlew", "assembleRelease"], cwd=ndk_dir, check=True) + + use_mono = runtime == "mono" + is_preload = strategy == "preload" + arch = os.environ.get("ANDROID_ARCH", "x86_64") rid_map = { "x86_64": "android-x64", @@ -370,7 +402,6 @@ def test_android_signals_inproc(cmake): ) native_lib_dir = project_fixture_path / "native" / arch native_lib_dir.mkdir(parents=True, exist_ok=True) - shutil.copy2(tmp_path / "libsentry.so", native_lib_dir / "libsentry.so") subprocess.run( [ ndk_clang, @@ -385,18 +416,26 @@ def test_android_signals_inproc(cmake): check=True, ) + # for chain-at-start, bundle the cmake-built libsentry.so directly + # (preload uses the AAR which brings its own libsentry.so) + if not is_preload: + shutil.copy2(tmp_path / "libsentry.so", native_lib_dir / "libsentry.so") + # build and install the APK - subprocess.run( - [ - "dotnet", - "build", - "-f:net10.0-android", - "-p:RuntimeIdentifier={}".format(rid_map[arch]), - "-p:Configuration=Release", - ], - cwd=project_fixture_path, - check=True, - ) + build_args = [ + "dotnet", + "build", + "-f:net10.0-android", + "-p:RuntimeIdentifier={}".format(rid_map[arch]), + "-p:Configuration=Release", + "-p:UseMonoRuntime={}".format(str(use_mono).lower()), + ] + if is_preload: + build_args += [ + "-p:EnablePreload=true", + "-p:NdkAarPath={}".format(ndk_aar_path), + ] + subprocess.run(build_args, cwd=project_fixture_path, check=True) apk_dir = ( project_fixture_path / "bin" / "Release" / "net10.0-android" / rid_map[arch] ) @@ -424,8 +463,13 @@ def has_envelope(): ) return bool(result.stdout.strip()) + # Preload replaces CHAIN_AT_START in the CoreCLR path. Once the runtime + # chains native crashes back to sentry-native, the app-side handler strategy + # should be DEFAULT rather than CHAIN_AT_START. + app_strategy = "default-strategy" if is_preload else None + # managed exception: handled, no crash - logcat = run_android_managed_exception() + logcat = run_android_managed_exception(strategy=app_strategy) assert not ( "NullReferenceException" in logcat ), f"Managed exception leaked.\nlogcat:\n{logcat}" @@ -433,10 +477,10 @@ def has_envelope(): assert not file_exists(db + "/last_crash"), "A crash was registered" assert not has_envelope(), "Unexpected envelope found" - # unhandled managed exception: Mono calls exit(1), the native SDK - # should not register a crash (sentry-dotnet handles this at the + # unhandled managed exception: the runtime calls exit(1), the native + # SDK should not register a crash (sentry-dotnet handles this at the # managed layer via UnhandledExceptionRaiser) - logcat = run_android_unhandled_managed_exception() + logcat = run_android_unhandled_managed_exception(strategy=app_strategy) assert ( "NullReferenceException" in logcat ), f"Expected NullReferenceException.\nlogcat:\n{logcat}" @@ -445,10 +489,30 @@ def has_envelope(): assert not has_envelope(), "Unexpected envelope found" # native crash - run_android_native_crash() + run_android_native_crash(strategy=app_strategy) assert wait_for(lambda: file_exists(db + "/last_crash")), "Crash marker missing" assert wait_for(has_envelope), "Crash envelope is missing" + if is_preload: + # after close/reinit, managed exceptions must still be handled by + # the runtime (not caught as native crashes by sentry) + logcat = run_android_managed_exception(strategy=app_strategy, reinit=True) + assert not ( + "NullReferenceException" in logcat + ), f"Managed exception leaked after reinit.\nlogcat:\n{logcat}" + assert wait_for(lambda: dir_exists(db)), "No database-path exists" + assert not file_exists( + db + "/last_crash" + ), "A crash was registered after reinit" + assert not has_envelope(), "Unexpected envelope found after reinit" + + # after close/reinit, native crashes must still be captured through + # the preloaded chain position + run_android_native_crash(strategy=app_strategy, reinit=True) + assert wait_for( + lambda: file_exists(db + "/last_crash") + ), "Crash marker missing after reinit" + assert wait_for(has_envelope), "Crash envelope is missing after reinit" finally: shutil.rmtree(project_fixture_path / "native", ignore_errors=True) shutil.rmtree(project_fixture_path / "bin", ignore_errors=True)