diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e6b36fd..fcdcc21ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- The SDK no longer crashes on Android versions 5 and 6 with native support enabled ([#1652](https://github.com/getsentry/sentry-unity/pull/1652)) + ### Dependencies - Bump Cocoa SDK from v8.25.2 to v8.26.0 ([#1648](https://github.com/getsentry/sentry-unity/pull/1648)) diff --git a/src/Sentry.Unity.Android/AndroidJavaScopeObserver.cs b/src/Sentry.Unity.Android/AndroidJavaScopeObserver.cs index 92eaf8ee6..013c80833 100644 --- a/src/Sentry.Unity.Android/AndroidJavaScopeObserver.cs +++ b/src/Sentry.Unity.Android/AndroidJavaScopeObserver.cs @@ -8,69 +8,85 @@ namespace Sentry.Unity.Android /// public class AndroidJavaScopeObserver : ScopeObserver { - public AndroidJavaScopeObserver(SentryOptions options) : base("Android", options) { } + private readonly JniExecutor _jniExecutor; + + public AndroidJavaScopeObserver(SentryOptions options, JniExecutor jniExecutor) : base("Android", options) + { + _jniExecutor = jniExecutor; + } private AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry"); public override void AddBreadcrumbImpl(Breadcrumb breadcrumb) { - AndroidJNI.AttachCurrentThread(); - using var sentry = GetSentryJava(); - using var javaBreadcrumb = new AndroidJavaObject("io.sentry.Breadcrumb"); - javaBreadcrumb.Set("message", breadcrumb.Message); - javaBreadcrumb.Set("type", breadcrumb.Type); - javaBreadcrumb.Set("category", breadcrumb.Category); - using var javaLevel = breadcrumb.Level.ToJavaSentryLevel(); - javaBreadcrumb.Set("level", javaLevel); - sentry.CallStatic("addBreadcrumb", javaBreadcrumb, null); + _jniExecutor.Run(() => + { + using var sentry = GetSentryJava(); + using var javaBreadcrumb = new AndroidJavaObject("io.sentry.Breadcrumb"); + javaBreadcrumb.Set("message", breadcrumb.Message); + javaBreadcrumb.Set("type", breadcrumb.Type); + javaBreadcrumb.Set("category", breadcrumb.Category); + using var javaLevel = breadcrumb.Level.ToJavaSentryLevel(); + javaBreadcrumb.Set("level", javaLevel); + sentry.CallStatic("addBreadcrumb", javaBreadcrumb, null); + }); } public override void SetExtraImpl(string key, string? value) { - AndroidJNI.AttachCurrentThread(); - using var sentry = GetSentryJava(); - sentry.CallStatic("setExtra", key, value); + _jniExecutor.Run(() => + { + using var sentry = GetSentryJava(); + sentry.CallStatic("setExtra", key, value); + }); } public override void SetTagImpl(string key, string value) { - AndroidJNI.AttachCurrentThread(); - using var sentry = GetSentryJava(); - sentry.CallStatic("setTag", key, value); + _jniExecutor.Run(() => + { + using var sentry = GetSentryJava(); + sentry.CallStatic("setTag", key, value); + }); } public override void UnsetTagImpl(string key) { - AndroidJNI.AttachCurrentThread(); - using var sentry = GetSentryJava(); - sentry.CallStatic("removeTag", key); + _jniExecutor.Run(() => + { + using var sentry = GetSentryJava(); + sentry.CallStatic("removeTag", key); + }); } public override void SetUserImpl(SentryUser user) { - AndroidJNI.AttachCurrentThread(); - - AndroidJavaObject? javaUser = null; - try + _jniExecutor.Run(() => { - javaUser = new AndroidJavaObject("io.sentry.protocol.User"); - javaUser.Set("email", user.Email); - javaUser.Set("id", user.Id); - javaUser.Set("username", user.Username); - javaUser.Set("ipAddress", user.IpAddress); - using var sentry = GetSentryJava(); - sentry.CallStatic("setUser", javaUser); - } - finally - { - javaUser?.Dispose(); - } + AndroidJavaObject? javaUser = null; + try + { + javaUser = new AndroidJavaObject("io.sentry.protocol.User"); + javaUser.Set("email", user.Email); + javaUser.Set("id", user.Id); + javaUser.Set("username", user.Username); + javaUser.Set("ipAddress", user.IpAddress); + using var sentry = GetSentryJava(); + sentry.CallStatic("setUser", javaUser); + } + finally + { + javaUser?.Dispose(); + } + }); } public override void UnsetUserImpl() { - AndroidJNI.AttachCurrentThread(); - using var sentry = GetSentryJava(); - sentry.CallStatic("setUser", null); + _jniExecutor.Run(() => + { + using var sentry = GetSentryJava(); + sentry.CallStatic("setUser", null); + }); } } } diff --git a/src/Sentry.Unity.Android/JniExecutor.cs b/src/Sentry.Unity.Android/JniExecutor.cs new file mode 100644 index 000000000..39791f910 --- /dev/null +++ b/src/Sentry.Unity.Android/JniExecutor.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace Sentry.Unity.Android +{ + public class JniExecutor + { + private readonly CancellationTokenSource _shutdownSource; + private readonly AutoResetEvent _taskEvent; + private Delegate _currentTask = null!; // The current task will always be set together with the task event + + private TaskCompletionSource? _taskCompletionSource; + + public JniExecutor() + { + _taskEvent = new AutoResetEvent(false); + _shutdownSource = new CancellationTokenSource(); + + new Thread(DoWork) { IsBackground = true }.Start(); + } + + private void DoWork() + { + AndroidJNI.AttachCurrentThread(); + + var waitHandles = new[] { _taskEvent, _shutdownSource.Token.WaitHandle }; + + while (true) + { + var index = WaitHandle.WaitAny(waitHandles); + if (index > 0) + { + // We only care about the _taskEvent + break; + } + + try + { + // Matching the type of the `_currentTask` exactly. The result gets cast to the expected type + // when returning from the blocking call. + switch (_currentTask) + { + case Action action: + { + action.Invoke(); + _taskCompletionSource?.SetResult(null); + break; + } + case Func func1: + { + var result = func1.Invoke(); + _taskCompletionSource?.SetResult(result); + break; + } + case Func func2: + { + var result = func2.Invoke(); + _taskCompletionSource?.SetResult(result); + break; + } + default: + throw new ArgumentException("Invalid type for _currentTask."); + } + } + catch (Exception e) + { + _taskCompletionSource?.SetException(e); + } + } + + AndroidJNI.DetachCurrentThread(); + } + + public TResult? Run(Func jniOperation) + { + _taskCompletionSource = new TaskCompletionSource(); + _currentTask = jniOperation; + _taskEvent.Set(); + + try + { + return (TResult?)_taskCompletionSource.Task.GetAwaiter().GetResult(); + } + catch (Exception e) + { + Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}"); + } + + return default; + } + + public void Run(Action jniOperation) + { + _taskCompletionSource = new TaskCompletionSource(); + _currentTask = jniOperation; + _taskEvent.Set(); + + try + { + _taskCompletionSource.Task.Wait(); + } + catch (Exception e) + { + Debug.unityLogger.Log(LogType.Exception, UnityLogger.LogTag, $"Error during JNI execution: {e}"); + } + } + + public void Dispose() + { + _shutdownSource.Cancel(); + _taskEvent.Dispose(); + } + } +} diff --git a/src/Sentry.Unity.Android/NativeContextWriter.cs b/src/Sentry.Unity.Android/NativeContextWriter.cs index 64d0c74de..a65628aee 100644 --- a/src/Sentry.Unity.Android/NativeContextWriter.cs +++ b/src/Sentry.Unity.Android/NativeContextWriter.cs @@ -4,6 +4,13 @@ namespace Sentry.Unity.Android { internal class NativeContextWriter : ContextWriter { + private readonly JniExecutor _jniExecutor; + + public NativeContextWriter(JniExecutor jniExecutor) + { + _jniExecutor = jniExecutor; + } + protected override void WriteScope( string? AppStartTime, string? AppBuildType, @@ -44,6 +51,7 @@ protected override void WriteScope( // the "unity" context, but it doesn't seem so useful and the effort to do is larger because there's no // class for it in Java - not sure how we could add a generic context object in Java... SentryJava.WriteScope( + _jniExecutor, GpuId, GpuName, GpuVendorName, diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs index 4fdc9a361..e922ae60e 100644 --- a/src/Sentry.Unity.Android/SentryJava.cs +++ b/src/Sentry.Unity.Android/SentryJava.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using UnityEngine; namespace Sentry.Unity.Android @@ -13,17 +14,15 @@ namespace Sentry.Unity.Android /// internal static class SentryJava { - internal static string? GetInstallationId() + internal static string? GetInstallationId(JniExecutor jniExecutor) { - if (!Attach()) + return jniExecutor.Run(() => { - return null; - } - - using var sentry = GetSentryJava(); - using var hub = sentry.CallStatic("getCurrentHub"); - using var options = hub?.Call("getOptions"); - return options?.Call("getDistinctId"); + using var sentry = GetSentryJava(); + using var hub = sentry.CallStatic("getCurrentHub"); + using var options = hub?.Call("getOptions"); + return options?.Call("getDistinctId"); + }); } /// @@ -36,30 +35,29 @@ internal static class SentryJava /// True if the last run terminated in a crash. No otherwise. /// If the SDK wasn't able to find this information, null is returned. /// - public static bool? CrashedLastRun() + public static bool? CrashedLastRun(JniExecutor jniExecutor) { - if (!Attach()) + return jniExecutor.Run(() => { - return null; - } - using var sentry = GetSentryJava(); - using var jo = sentry.CallStatic("isCrashedLastRun"); - return jo?.Call("booleanValue"); + using var sentry = GetSentryJava(); + using var jo = sentry.CallStatic("isCrashedLastRun"); + return jo?.Call("booleanValue"); + }); } - public static void Close() + public static void Close(JniExecutor jniExecutor) { - if (Attach()) + jniExecutor.Run(() => { using var sentry = GetSentryJava(); sentry.CallStatic("close"); - } + }); } - private static bool Attach() => AndroidJNI.AttachCurrentThread() == 0; private static AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry"); public static void WriteScope( + JniExecutor jniExecutor, int? GpuId, string? GpuName, string? GpuVendorName, @@ -76,17 +74,17 @@ public static void WriteScope( bool? GpuMultiThreadedRendering, string? GpuGraphicsShaderLevel) { - if (Attach()) + jniExecutor.Run(() => { using var gpu = new AndroidJavaObject("io.sentry.protocol.Gpu"); gpu.SetIfNotNull("name", GpuName); gpu.SetIfNotNull("id", GpuId); - int intVendorId; - if (GpuVendorId is not null && int.TryParse(GpuVendorId, out intVendorId) && intVendorId != 0) + if (GpuVendorId is not null && int.TryParse(GpuVendorId, out var intVendorId) && intVendorId != 0) { using var integer = new AndroidJavaObject("java.lang.Integer", intVendorId); gpu.Set("vendorId", integer); } + gpu.SetIfNotNull("vendorName", GpuVendorName); gpu.SetIfNotNull("memorySize", GpuMemorySize); gpu.SetIfNotNull("apiType", GpuApiType); @@ -100,7 +98,7 @@ public static void WriteScope( using var contexts = scope.Call("getContexts"); contexts.Call("setGpu", gpu); })); - } + }); } private static void SetIfNotNull(this AndroidJavaObject javaObject, string property, T? value, string? valueClass = null) diff --git a/src/Sentry.Unity.Android/SentryNativeAndroid.cs b/src/Sentry.Unity.Android/SentryNativeAndroid.cs index 76d94abf2..761aca8a0 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -1,7 +1,6 @@ using System; using Sentry.Extensibility; -using Sentry.Unity.Integrations; -using Sentry.Unity.NativeUtils; +using UnityEngine; namespace Sentry.Unity.Android { @@ -10,53 +9,59 @@ namespace Sentry.Unity.Android /// public static class SentryNativeAndroid { + private static JniExecutor? JniExecutor; + /// /// Configures the native Android support. /// /// The Sentry Unity options to use. public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentryUnityInfo) { - if (options.AndroidNativeSupportEnabled) + if (!options.AndroidNativeSupportEnabled) { - options.NativeContextWriter = new NativeContextWriter(); - options.ScopeObserver = new AndroidJavaScopeObserver(options); - options.EnableScopeSync = true; - options.CrashedLastRun = () => - { - var crashedLastRun = SentryJava.CrashedLastRun(); - if (crashedLastRun is null) - { - // Could happen if the Android SDK wasn't initialized before the .NET layer. - options.DiagnosticLogger? - .LogWarning("Unclear from the native SDK if the previous run was a crash. Assuming it was not."); - crashedLastRun = false; - } - else - { - options.DiagnosticLogger? - .LogDebug("Native Android SDK reported: 'crashedLastRun': '{0}'", crashedLastRun); - } + return; + } - return crashedLastRun.Value; - }; + JniExecutor = new JniExecutor(); - try + options.NativeContextWriter = new NativeContextWriter(JniExecutor); + options.ScopeObserver = new AndroidJavaScopeObserver(options, JniExecutor); + options.EnableScopeSync = true; + options.CrashedLastRun = () => + { + var crashedLastRun = SentryJava.CrashedLastRun(JniExecutor); + if (crashedLastRun is null) { - options.DiagnosticLogger?.LogDebug("Reinstalling native backend."); - - // At this point Unity has taken the signal handler and will not invoke the original handler (Sentry) - // So we register our backend once more to make sure user-defined data is available in the crash report. - SentryNative.ReinstallBackend(); + // Could happen if the Android SDK wasn't initialized before the .NET layer. + options.DiagnosticLogger? + .LogWarning("Unclear from the native SDK if the previous run was a crash. Assuming it was not."); + crashedLastRun = false; } - catch (Exception e) + else { - options.DiagnosticLogger?.LogError( - e, "Failed to reinstall backend. Captured native crashes will miss scope data and tag."); + options.DiagnosticLogger? + .LogDebug("Native Android SDK reported: 'crashedLastRun': '{0}'", crashedLastRun); } - options.NativeSupportCloseCallback = () => Close(options.DiagnosticLogger); - options.DefaultUserId = SentryJava.GetInstallationId(); + return crashedLastRun.Value; + }; + + try + { + options.DiagnosticLogger?.LogDebug("Reinstalling native backend."); + + // At this point Unity has taken the signal handler and will not invoke the original handler (Sentry) + // So we register our backend once more to make sure user-defined data is available in the crash report. + SentryNative.ReinstallBackend(); + } + catch (Exception e) + { + options.DiagnosticLogger?.LogError( + e, "Failed to reinstall backend. Captured native crashes will miss scope data and tag."); } + + options.NativeSupportCloseCallback = () => Close(options.DiagnosticLogger); + options.DefaultUserId = SentryJava.GetInstallationId(JniExecutor); } /// @@ -66,7 +71,12 @@ public static void Close(IDiagnosticLogger? logger = null) { // Sentry Native is initialized and closed by the Java SDK, no need to call into it directly logger?.LogDebug("Closing the sentry-java SDK"); - SentryJava.Close(); + + // This is an edge-case where the Android SDK has been enabled and setup during build-time but is being + // shut down at runtime. In this case Configure() has not been called and there is no JniExecutor yet + JniExecutor ??= new JniExecutor(); + SentryJava.Close(JniExecutor); + JniExecutor.Dispose(); } } }