diff --git a/Android/lib/arm64-v8a/libbacktrace-native.so b/Android/lib/arm64-v8a/libbacktrace-native.so index d3feac38..5f4b1db7 100644 Binary files a/Android/lib/arm64-v8a/libbacktrace-native.so and b/Android/lib/arm64-v8a/libbacktrace-native.so differ diff --git a/Android/lib/armeabi-v7a/libbacktrace-native.so b/Android/lib/armeabi-v7a/libbacktrace-native.so index 089c9263..fbb64323 100644 Binary files a/Android/lib/armeabi-v7a/libbacktrace-native.so and b/Android/lib/armeabi-v7a/libbacktrace-native.so differ diff --git a/Android/lib/x86/libbacktrace-native.so b/Android/lib/x86/libbacktrace-native.so index beffaba6..4b01a7ba 100644 Binary files a/Android/lib/x86/libbacktrace-native.so and b/Android/lib/x86/libbacktrace-native.so differ diff --git a/Editor/BacktraceClientConfigurationEditor.cs b/Editor/BacktraceClientConfigurationEditor.cs index ff0c4ff4..ec1803aa 100644 --- a/Editor/BacktraceClientConfigurationEditor.cs +++ b/Editor/BacktraceClientConfigurationEditor.cs @@ -26,7 +26,7 @@ public override void OnInspectorGUI() #else settings.IgnoreSslValidation = false; #endif -#if UNITY_ANDROID +#if UNITY_ANDROID || UNITY_IOS settings.HandleANR = EditorGUILayout.Toggle(BacktraceConfigurationLabels.LABEL_HANDLE_ANR, settings.HandleANR); #endif settings.GameObjectDepth = EditorGUILayout.IntField(BacktraceConfigurationLabels.LABEL_GAME_OBJECT_DEPTH, settings.GameObjectDepth); diff --git a/Editor/BacktraceConfigurationEditor.cs b/Editor/BacktraceConfigurationEditor.cs index 2bb4fd10..fb81e73a 100644 --- a/Editor/BacktraceConfigurationEditor.cs +++ b/Editor/BacktraceConfigurationEditor.cs @@ -41,15 +41,15 @@ public override void OnInspectorGUI() serializedObject.FindProperty("IgnoreSslValidation"), new GUIContent(BacktraceConfigurationLabels.LABEL_IGNORE_SSL_VALIDATION)); #endif -#if UNITY_ANDROID +#if UNITY_ANDROID || UNITY_IOS EditorGUILayout.PropertyField( serializedObject.FindProperty("HandleANR"), new GUIContent(BacktraceConfigurationLabels.LABEL_HANDLE_ANR)); -#if UNITY_2019_2_OR_NEWER - EditorGUILayout.PropertyField( - serializedObject.FindProperty("SymbolsUploadToken"), - new GUIContent(BacktraceConfigurationLabels.LABEL_SYMBOLS_UPLOAD_TOKEN)); +#if UNITY_2019_2_OR_NEWER && UNITY_ANDROID + EditorGUILayout.PropertyField( + serializedObject.FindProperty("SymbolsUploadToken"), + new GUIContent(BacktraceConfigurationLabels.LABEL_SYMBOLS_UPLOAD_TOKEN)); #endif #endif EditorGUILayout.PropertyField( diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index ac47f3aa..4500aa97 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -288,10 +288,12 @@ public static BacktraceClient Initialize(BacktraceConfiguration configuration, D } var backtrackGameObject = new GameObject(gameObjectName, typeof(BacktraceClient), typeof(BacktraceDatabase)); BacktraceClient backtraceClient = backtrackGameObject.GetComponent(); - BacktraceDatabase backtraceDatabase = backtrackGameObject.GetComponent(); - - backtraceDatabase.Configuration = configuration; backtraceClient.Configuration = configuration; + if (configuration.Enabled) + { + BacktraceDatabase backtraceDatabase = backtrackGameObject.GetComponent(); + backtraceDatabase.Configuration = configuration; + } backtrackGameObject.SetActive(true); backtraceClient.Refresh(); backtraceClient.SetAttributes(attributes); @@ -371,12 +373,15 @@ public void Refresh() DontDestroyOnLoad(gameObject); _instance = this; } - Database = GetComponent(); - if (Database != null) + if (Configuration.Enabled) { - Database.Reload(); - Database.SetApi(BacktraceApi); - Database.SetReportWatcher(_reportLimitWatcher); + Database = GetComponent(); + if (Database != null) + { + Database.Reload(); + Database.SetApi(BacktraceApi); + Database.SetReportWatcher(_reportLimitWatcher); + } } _nativeClient = NativeClientFactory.GetNativeClient(Configuration, name); @@ -400,10 +405,23 @@ private void Awake() Refresh(); } + /// + /// Update native client internal ANR timer. + /// + private void Update() + { + _nativeClient?.UpdateClientTime(Time.time); + } + private void OnDestroy() { Enabled = false; Application.logMessageReceived -= HandleUnityMessage; +#if UNITY_ANDROID || UNITY_IOS + Application.lowMemory -= HandleLowMemory; + _nativeClient?.Disable(); +#endif + } /// @@ -520,7 +538,7 @@ private IEnumerator CollectDataAndSend(BacktraceReport report, Action /// Catch Unity logger data and create Backtrace reports for log type that represents exception or error /// diff --git a/Runtime/BacktraceDatabase.cs b/Runtime/BacktraceDatabase.cs index 4b1d3b6e..4b6f46ec 100644 --- a/Runtime/BacktraceDatabase.cs +++ b/Runtime/BacktraceDatabase.cs @@ -220,6 +220,15 @@ public void SetApi(IBacktraceApi backtraceApi) BacktraceApi = backtraceApi; } + /// + /// Validate if BacktraceDatabase is enabled + /// + /// true if BacktraceDatabase is enabled. Otherwise false. + public bool Enabled() + { + return Enable; + } + /// /// Get settings /// diff --git a/Runtime/Interfaces/IBacktraceDatabase.cs b/Runtime/Interfaces/IBacktraceDatabase.cs index 69f98f8f..905fe48b 100644 --- a/Runtime/Interfaces/IBacktraceDatabase.cs +++ b/Runtime/Interfaces/IBacktraceDatabase.cs @@ -83,5 +83,11 @@ public interface IBacktraceDatabase /// Lock report - default true /// Backtrace record BacktraceDatabaseRecord Add(BacktraceData data, bool @lock = true); + + /// + /// Validate if BacktraceDatabase is enabled + /// + /// true if BacktraceDatabase is enabled. Otherwise false. + bool Enabled(); } } diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs index b63f3299..1e076c4f 100644 --- a/Runtime/Model/BacktraceConfiguration.cs +++ b/Runtime/Model/BacktraceConfiguration.cs @@ -97,7 +97,7 @@ public class BacktraceConfiguration : ScriptableObject /// [Tooltip("Capture native NDK Crashes (ANDROID API 21+)")] #elif UNITY_IOS - /// + /// /// Capture native iOS Crashes. /// [Tooltip("Capture native Crashes")] @@ -106,7 +106,7 @@ public class BacktraceConfiguration : ScriptableObject public bool CaptureNativeCrashes = true; #endif -#if UNITY_ANDROID +#if UNITY_ANDROID || UNITY_IOS /// /// Handle ANR events - Application not responding /// diff --git a/Runtime/Model/BacktraceStackFrame.cs b/Runtime/Model/BacktraceStackFrame.cs index 5705197d..1d6a2afd 100644 --- a/Runtime/Model/BacktraceStackFrame.cs +++ b/Runtime/Model/BacktraceStackFrame.cs @@ -30,7 +30,7 @@ public string FileName return string.IsNullOrEmpty(Library) ? GetFileNameFromFunctionName() : Library.IndexOfAny(Path.GetInvalidPathChars()) == -1 && Path.HasExtension(Path.GetFileName(Library)) - ? Path.GetFileName(Library).Trim() + ? GetFileNameFromLibraryName() : GetFileNameFromFunctionName(); } } @@ -186,6 +186,31 @@ private string GetMethodName(MethodBase method) return string.Format("{0}.{1}()", method.DeclaringType == null ? null : method.DeclaringType.ToString(), methodName); } + private string GetFileNameFromLibraryName() + { + var libraryName = Path.GetFileName(Library).Trim(); + + // detect namespace + var lastSeparatorIndex = libraryName.LastIndexOf("."); + if (lastSeparatorIndex == -1 || libraryName.IndexOf(".") == lastSeparatorIndex) + { + // detected full path to source code + return libraryName; + } + + // omit '.' character that substring will return based on lastSeparatorIndex + libraryName = libraryName.Substring(lastSeparatorIndex + 1); + switch (StackFrameType) + { + case BacktraceStackFrameType.Dotnet: + return string.Format("{0}.cs", libraryName); + case BacktraceStackFrameType.Android: + return string.Format("{0}.java", libraryName); + default: + return libraryName; + } + } + /// /// Generate file name based on full functiom name /// @@ -219,7 +244,21 @@ private string GetFileNameFromFunctionName() } var libraryPath = FunctionName.Substring(0, separatorIndex).Split(new char[] { '.' }); - var fileName = libraryPath[libraryPath.Length - 1]; + // handle situation when function name is a constructor path or specific module path + var currentIndex = libraryPath.Length - 1; + string fileName = libraryPath[currentIndex]; + + while (string.IsNullOrEmpty(fileName) && currentIndex > 0) + { + fileName = libraryPath[currentIndex - 1]; + currentIndex--; + + } + if (string.IsNullOrEmpty(fileName)) + { + return Library; + } + if (fileName.IndexOfAny(Path.GetInvalidPathChars()) == -1 && Path.HasExtension(fileName) || StackFrameType == BacktraceStackFrameType.Unknown) { return fileName; diff --git a/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs b/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs index 870bb23d..e0e9a3c0 100644 --- a/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs +++ b/Runtime/Model/Database/BacktraceDatabaseAttachmentManager.cs @@ -1,5 +1,6 @@ using Backtrace.Unity.Common; using Backtrace.Unity.Types; +using System; using System.Collections.Generic; using System.IO; using UnityEngine; diff --git a/Runtime/Model/JsonData/BacktraceAttributes.cs b/Runtime/Model/JsonData/BacktraceAttributes.cs index 03f8cf29..c87ab9f3 100644 --- a/Runtime/Model/JsonData/BacktraceAttributes.cs +++ b/Runtime/Model/JsonData/BacktraceAttributes.cs @@ -194,10 +194,15 @@ internal void SetExceptionAttributes(BacktraceReport report) } if (report.Exception is BacktraceUnhandledException) { - if ((report.Exception as BacktraceUnhandledException).Classifier == "ANRException") + var classifier = (report.Exception as BacktraceUnhandledException).Classifier; + if (classifier == "ANRException") { Attributes[errorType] = "Hang"; } + else if (classifier == "OOMException") + { + Attributes[errorType] = "oom"; + } else { Attributes[errorType] = "Unhandled exception"; diff --git a/Runtime/Native/Android/NativeClient.cs b/Runtime/Native/Android/NativeClient.cs index 24d9f228..dcfabc67 100644 --- a/Runtime/Native/Android/NativeClient.cs +++ b/Runtime/Native/Android/NativeClient.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using UnityEngine; namespace Backtrace.Unity.Runtime.Native.Android @@ -14,12 +15,22 @@ namespace Backtrace.Unity.Runtime.Native.Android /// internal class NativeClient : INativeClient { + // Last Backtrace client update time + internal float _lastUpdateTime; + + private Thread _anrThread; + [DllImport("backtrace-native")] private static extern bool Initialize(IntPtr submissionUrl, IntPtr databasePath, IntPtr handlerPath, IntPtr keys, IntPtr values); [DllImport("backtrace-native")] private static extern bool AddAttribute(IntPtr key, IntPtr value); + [DllImport("backtrace-native", EntryPoint = "DumpWithoutCrash")] + private static extern bool NativeReport(IntPtr message); + + + private readonly BacktraceConfiguration _configuration; // Android native interface paths private const string _namespace = "backtrace.io.backtrace_unity_android_plugin"; @@ -51,10 +62,9 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration) return; } #if UNITY_ANDROID - _captureNativeCrashes = _configuration.CaptureNativeCrashes; _handlerANR = _configuration.HandleANR; - HandleAnr(gameObjectName, "OnAnrDetected"); HandleNativeCrashes(); + HandleAnr(gameObjectName, "OnAnrDetected"); #endif } @@ -65,7 +75,13 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration) private void HandleNativeCrashes() { // make sure database is enabled - if (!_captureNativeCrashes || !_configuration.Enabled) + var integrationDisabled = +#if UNITY_ANDROID + !_configuration.CaptureNativeCrashes || !_configuration.Enabled; +#else + true; +#endif + if (integrationDisabled) { Debug.LogWarning("Backtrace native integration status: Disabled NDK integration"); return; @@ -176,6 +192,46 @@ public void HandleAnr(string gameObjectName, string callbackName) Debug.LogWarning(string.Format("Cannot initialize ANR watchdog - reason: {0}", e.Message)); _enabled = false; } + + if (!_captureNativeCrashes) + { + return; + } + + bool reported = false; + var mainThreadId = Thread.CurrentThread.ManagedThreadId; + _anrThread = new Thread(() => + { + float lastUpdatedCache = 0; + while (true) + { + if (lastUpdatedCache == 0) + { + lastUpdatedCache = _lastUpdateTime; + } + else if (lastUpdatedCache == _lastUpdateTime) + { + if (!reported) + { + if (AndroidJNI.AttachCurrentThread() == 0) + { + NativeReport(AndroidJNI.NewStringUTF("ANRException: Blocked thread detected.")); + } + reported = true; + } + } + else + { + reported = false; + } + + lastUpdatedCache = _lastUpdateTime; + Thread.Sleep(5000); + + } + }); + + _anrThread.Start(); } /// @@ -199,5 +255,40 @@ public void SetAttribute(string key, string value) AndroidJNI.NewStringUTF(key), AndroidJNI.NewStringUTF(value)); } + + /// + /// Report OOM via Backtrace native android library. + /// + /// true - if native crash reprorter is enabled. Otherwise false. + public bool OnOOM() + { + if (!_enabled || _captureNativeCrashes) + { + return false; + } + + NativeReport(AndroidJNI.NewStringUTF("OOMException: Out of memory detected.")); + return true; + } + + /// + /// Update native client internal timer. + /// + /// Current time + public void UpdateClientTime(float time) + { + _lastUpdateTime = time; + } + + /// + /// Disable native client integration + /// + public void Disable() + { + if (_anrThread != null) + { + _anrThread.Abort(); + } + } } } \ No newline at end of file diff --git a/Runtime/Native/INativeClient.cs b/Runtime/Native/INativeClient.cs index 812a5ad5..2fea969c 100644 --- a/Runtime/Native/INativeClient.cs +++ b/Runtime/Native/INativeClient.cs @@ -2,13 +2,43 @@ namespace Backtrace.Unity.Runtime.Native { + /// + /// Backtrace native client definition + /// internal interface INativeClient { -#if UNITY_ANDROID || UNITY_IOS + /// + /// Handle ANR - Application not responding events + /// void HandleAnr(string gameObjectName, string callbackName); -#endif + + /// + /// Set native attributes in attributes dictionary + /// + /// Attributes dictionary void GetAttributes(Dictionary data); + /// + /// Set native attribute + /// + /// attribute key + /// attribute value void SetAttribute(string key, string value); + + /// + /// Report OOM via Backtrace native library. + /// + /// true - if native crash reprorter is enabled. Otherwise false. + bool OnOOM(); + + /// + /// Update native client internal ANR timer. + /// + void UpdateClientTime(float time); + + /// + /// Disable native integration + /// + void Disable(); } } diff --git a/Runtime/Native/iOS/NativeClient.cs b/Runtime/Native/iOS/NativeClient.cs index a6e5db6c..ce842eaa 100644 --- a/Runtime/Native/iOS/NativeClient.cs +++ b/Runtime/Native/iOS/NativeClient.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Backtrace.Unity.Model; using UnityEngine; @@ -12,8 +13,13 @@ namespace Backtrace.Unity.Runtime.Native.iOS /// /// iOS native client /// - public class NativeClient : INativeClient + internal class NativeClient : INativeClient { + // Last Backtrace client update time + internal float _lastUpdateTime; + + private Thread _anrThread; + // NSDictinary entry used only for iOS native integration internal struct Entry { @@ -24,6 +30,9 @@ internal struct Entry [DllImport("__Internal", EntryPoint = "StartBacktraceIntegration")] private static extern void Start(string plCrashReporterUrl, string[] attributeKeys, string[] attributeValues, int size); + [DllImport("__Internal", EntryPoint = "NativeReport")] + public static extern void NativeReport(string message); + [DllImport("__Internal", EntryPoint = "Crash")] public static extern string Crash(); @@ -57,6 +66,10 @@ public NativeClient(string gameObjectName, BacktraceConfiguration configuration) HandleNativeCrashes(configuration); INITIALIZED = true; } + if (configuration.HandleANR) + { + HandleAnr(gameObjectName, string.Empty); + } } @@ -112,13 +125,50 @@ public void GetAttributes(Dictionary result) /// /// Setup iOS ANR support and set callback function when ANR happened. /// - /// Backtrace game object name - /// Callback function name public void HandleAnr(string gameObjectName, string callbackName) { - Debug.Log("ANR support on iOS is unsupported."); + // if INITIALIZED is equal to false, plcrashreporter instance is disabled + // so we can't generate native report + if (!_enabled || INITIALIZED == false) + { + return; + } + + bool reported = false; + var mainThreadId = Thread.CurrentThread.ManagedThreadId; + _anrThread = new Thread(() => + { + float lastUpdatedCache = 0; + while (true) + { + if (lastUpdatedCache == 0) + { + lastUpdatedCache = _lastUpdateTime; + } + else if (lastUpdatedCache == _lastUpdateTime) + { + if (!reported) + { + NativeReport("ANRException: Blocked thread detected."); + reported = true; + } + } + else + { + reported = false; + } + + lastUpdatedCache = _lastUpdateTime; + Thread.Sleep(5000); + + } + }); + + _anrThread.Start(); } + + /// /// Add attribute to native crash /// @@ -139,6 +189,41 @@ public void SetAttribute(string key, string value) } AddAttribute(key, value); } + /// + /// Report OOM via PlCrashReporter report. + /// + /// true - if native crash reprorter is enabled. Otherwise false. + public bool OnOOM() + { + // if INITIALIZED is equal to false, plcrashreporter instance is disabled + // so we can't generate native report + if (!_enabled || INITIALIZED == false) + { + return false; + } + NativeReport("OOMException: Out of memory detected."); + return true; + } + + /// + /// Update native client internal timer. + /// + /// Current time + public void UpdateClientTime(float time) + { + _lastUpdateTime = time; + } + + /// + /// Disable native client integration + /// + public void Disable() + { + if (_anrThread != null) + { + _anrThread.Abort(); + } + } } } #endif diff --git a/Runtime/Services/BacktraceApi.cs b/Runtime/Services/BacktraceApi.cs index 58773c2f..01bd42de 100644 --- a/Runtime/Services/BacktraceApi.cs +++ b/Runtime/Services/BacktraceApi.cs @@ -300,7 +300,7 @@ public IEnumerator Send(string json, List attachments, Dictionary