diff --git a/Android/lib/arm64-v8a/libbacktrace-native.so b/Android/lib/arm64-v8a/libbacktrace-native.so index 5f4b1db7..9a75b74f 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/arm64-v8a/libcrashpad_handler.so b/Android/lib/arm64-v8a/libcrashpad_handler.so index 21077759..46f76e74 100644 Binary files a/Android/lib/arm64-v8a/libcrashpad_handler.so and b/Android/lib/arm64-v8a/libcrashpad_handler.so differ diff --git a/Android/lib/armeabi-v7a/libbacktrace-native.so b/Android/lib/armeabi-v7a/libbacktrace-native.so index fbb64323..b60ee285 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/armeabi-v7a/libcrashpad_handler.so b/Android/lib/armeabi-v7a/libcrashpad_handler.so index d41bd662..273c2b84 100644 Binary files a/Android/lib/armeabi-v7a/libcrashpad_handler.so and b/Android/lib/armeabi-v7a/libcrashpad_handler.so differ diff --git a/Android/lib/x86/libbacktrace-native.so b/Android/lib/x86/libbacktrace-native.so index 4b01a7ba..c8da04c5 100644 Binary files a/Android/lib/x86/libbacktrace-native.so and b/Android/lib/x86/libbacktrace-native.so differ diff --git a/Editor/BacktraceConfigurationEditor.cs b/Editor/BacktraceConfigurationEditor.cs index 0bdead12..cea84eaf 100644 --- a/Editor/BacktraceConfigurationEditor.cs +++ b/Editor/BacktraceConfigurationEditor.cs @@ -64,7 +64,6 @@ public override void OnInspectorGUI() serializedObject.FindProperty("SendUnhandledGameCrashesOnGameStartup"), new GUIContent(BacktraceConfigurationLabels.LABEL_SEND_UNHANDLED_GAME_CRASHES_ON_STARTUP)); #endif - EditorGUILayout.PropertyField( serializedObject.FindProperty("ReportFilterType"), new GUIContent(BacktraceConfigurationLabels.LABEL_REPORT_FILTER)); @@ -93,6 +92,13 @@ public override void OnInspectorGUI() EditorGUILayout.HelpBox("Please insert value greater or equal -1", MessageType.Error); } } + +#if UNITY_ANDROID || UNITY_IOS + EditorGUILayout.PropertyField( + serializedObject.FindProperty("AttachmentPaths"), + new GUIContent(BacktraceConfigurationLabels.LABEL_REPORT_ATTACHMENTS)); +#endif + #if !UNITY_SWITCH SerializedProperty enabled = serializedObject.FindProperty("Enabled"); EditorGUILayout.PropertyField(enabled, new GUIContent(BacktraceConfigurationLabels.LABEL_ENABLE_DATABASE)); diff --git a/Editor/BacktraceConfigurationLabels.cs b/Editor/BacktraceConfigurationLabels.cs index 666930de..96a90f1b 100644 --- a/Editor/BacktraceConfigurationLabels.cs +++ b/Editor/BacktraceConfigurationLabels.cs @@ -18,6 +18,7 @@ internal static class BacktraceConfigurationLabels "(Early access) Send Out of memory exceptions to Backtrace"; #endif #endif + internal static string LABEL_REPORT_ATTACHMENTS = "Report attachment paths"; internal static string CAPTURE_NATIVE_CRASHES = "Capture native crashes"; internal static string LABEL_REPORT_FILTER = "Filter reports"; internal static string LABEL_NUMBER_OF_LOGS = "Collect last n game logs"; diff --git a/Runtime/BacktraceClient.cs b/Runtime/BacktraceClient.cs index 664a60a9..8fd718f7 100644 --- a/Runtime/BacktraceClient.cs +++ b/Runtime/BacktraceClient.cs @@ -31,6 +31,11 @@ public class BacktraceClient : MonoBehaviour, IBacktraceClient internal readonly Stack BackgroundExceptions = new Stack(); + /// + /// Client report attachments + /// + private List _clientReportAttachments; + /// /// Attribute object accessor /// @@ -50,6 +55,26 @@ public string this[string index] } } + /// + /// Add attachment to managed reports. + /// Note: this option won't add attachment to your native reports. You can add attachments to + /// native reports only on BacktraceClient initialization. + /// + /// Path to attachment + public void AddAttachment(string pathToAttachment) + { + _clientReportAttachments.Add(pathToAttachment); + } + + /// + /// Returns list of defined path to attachments stored by Backtrace client. + /// + /// List of client attachments + public List GetAttachments() + { + return _clientReportAttachments; + } + /// /// Set client attributes that will be included in every report /// @@ -271,6 +296,7 @@ internal ReportLimitWatcher ReportLimitWatcher /// /// Backtrace configuration scriptable object /// Client side attributes + /// param name="attachments">List of attachments /// game object name /// Backtrace client public static BacktraceClient Initialize(BacktraceConfiguration configuration, Dictionary attributes = null, string gameObjectName = "BacktraceClient") @@ -313,9 +339,24 @@ public static BacktraceClient Initialize(BacktraceConfiguration configuration, D /// game object name /// Backtrace client public static BacktraceClient Initialize(string url, string databasePath, Dictionary attributes = null, string gameObjectName = "BacktraceClient") + { + return Initialize(url, databasePath, attributes, null, gameObjectName); + } + + /// + /// Initialize new Backtrace integration with database path. Note - database path will be auto created by Backtrace Unity plugin + /// + /// Server url + /// Database path + /// Client side attributes + /// Paths to attachments that Backtrace client will include in managed/native reports + /// game object name + /// Backtrace client + public static BacktraceClient Initialize(string url, string databasePath, Dictionary attributes = null, string[] attachments = null, string gameObjectName = "BacktraceClient") { var configuration = ScriptableObject.CreateInstance(); configuration.ServerUrl = url; + configuration.AttachmentPaths = attachments; configuration.Enabled = true; configuration.DatabasePath = databasePath; configuration.CreateDatabase = true; @@ -330,9 +371,23 @@ public static BacktraceClient Initialize(string url, string databasePath, Dictio /// game object name /// Backtrace client public static BacktraceClient Initialize(string url, Dictionary attributes = null, string gameObjectName = "BacktraceClient") + { + return Initialize(url, attributes, new string[0], gameObjectName); + } + + /// + /// Initialize new Backtrace integration + /// + /// Server url + /// Client side attributes + /// Paths to attachments that Backtrace client will include in managed/native reports + /// game object name + /// Backtrace client + public static BacktraceClient Initialize(string url, Dictionary attributes = null, string[] attachments = null, string gameObjectName = "BacktraceClient") { var configuration = ScriptableObject.CreateInstance(); configuration.ServerUrl = url; + configuration.AttachmentPaths = attachments; configuration.Enabled = false; return Initialize(configuration, attributes, gameObjectName); } @@ -358,7 +413,7 @@ public void Refresh() _current = Thread.CurrentThread; CaptureUnityMessages(); _reportLimitWatcher = new ReportLimitWatcher(Convert.ToUInt32(Configuration.ReportPerMin)); - + _clientReportAttachments = Configuration.GetAttachmentPaths(); BacktraceApi = new BacktraceApi( credentials: new BacktraceCredentials(Configuration.GetValidServerUrl()), @@ -668,6 +723,7 @@ private BacktraceData SetupBacktraceData(BacktraceReport report) : _backtraceLogManager.ToSourceCode(); report.AssignSourceCodeToReport(sourceCode); + report.AttachmentPaths.AddRange(_clientReportAttachments); // pass copy of dictionary to prevent overriding client attributes var result = report.ToBacktraceData(null, GameObjectDepth); diff --git a/Runtime/Common/ClientPathHelper.cs b/Runtime/Common/ClientPathHelper.cs new file mode 100644 index 00000000..0e248645 --- /dev/null +++ b/Runtime/Common/ClientPathHelper.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using UnityEngine; + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] +namespace Backtrace.Unity.Common +{ + internal static class ClientPathHelper + { + /// + /// Parse Backtrace client path string to full path + /// + /// Backtrace path with interpolated string and other Backtrace's improvements + /// Full path to Backtrace file/directory + internal static string GetFulLPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return string.Empty; + } + + return path.ParseInterpolatedString().GetFullPath(); + } + + private static string ParseInterpolatedString(this string path) + { + // check if string has any interpolated substring + var interpolationStart = path.IndexOf("${"); + if (interpolationStart == -1) + { + return path; + } + var interpolationEnd = path.IndexOf('}', interpolationStart); + if (interpolationEnd == -1) + { + return path; + } + var interpolationValue = path.Substring(interpolationStart, interpolationEnd - interpolationStart + 1); + if (string.IsNullOrEmpty(interpolationValue)) + { + return path; + } + + switch (interpolationValue.ToLower()) + { + case "${application.persistentdatapath}": + return path.Replace(interpolationValue, Application.persistentDataPath); + case "${application.datapath}": + return path.Replace(interpolationValue, Application.dataPath); + default: + return path; + } + + } + + private static string GetFullPath(this string path) + { + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(Application.persistentDataPath, path); + } + return Path.GetFullPath(path); + + } + } +} diff --git a/Runtime/Common/DatabasePathHelper.cs.meta b/Runtime/Common/ClientPathHelper.cs.meta similarity index 83% rename from Runtime/Common/DatabasePathHelper.cs.meta rename to Runtime/Common/ClientPathHelper.cs.meta index 7f9c2ab9..e103c163 100644 --- a/Runtime/Common/DatabasePathHelper.cs.meta +++ b/Runtime/Common/ClientPathHelper.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 96136aa964005a24283d081dd833dee1 +guid: 63d4546acbb289c4ea8c6bde7c01f8f0 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Common/DatabasePathHelper.cs b/Runtime/Common/DatabasePathHelper.cs deleted file mode 100644 index 6784a66c..00000000 --- a/Runtime/Common/DatabasePathHelper.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.IO; -using UnityEngine; - -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Backtrace.Unity.Tests.Runtime")] -namespace Backtrace.Unity.Common -{ - internal static class DatabasePathHelper - { - /// - /// Parse Backtrace database path string to full path - /// - /// Backtrace database path - /// Full path to Backtrace database - internal static string GetFullDatabasePath(string databasePath) - { - if (string.IsNullOrEmpty(databasePath)) - { - return string.Empty; - } - - return databasePath.ParseInterpolatedString().GetFullPath(); - } - - private static string ParseInterpolatedString(this string databasePath) - { - // check if string has any interpolated substring - var interpolationStart = databasePath.IndexOf("${"); - if (interpolationStart == -1) - { - return databasePath; - } - var interpolationEnd = databasePath.IndexOf('}', interpolationStart); - if(interpolationEnd == -1) - { - return databasePath; - } - var interpolationValue = databasePath.Substring(interpolationStart, interpolationEnd - interpolationStart +1); - if(string.IsNullOrEmpty(interpolationValue)) - { - return databasePath; - } - - switch (interpolationValue.ToLower()) - { - case "${application.persistentdatapath}": - return databasePath.Replace(interpolationValue, Application.persistentDataPath); - case "${application.datapath}": - return databasePath.Replace(interpolationValue, Application.dataPath); - default: - return databasePath; - } - - } - - private static string GetFullPath(this string databasePath) - { - if (!Path.IsPathRooted(databasePath)) - { - databasePath = Path.Combine(Application.persistentDataPath, databasePath); - } - return Path.GetFullPath(databasePath); - - } - } -} diff --git a/Runtime/Model/BacktraceConfiguration.cs b/Runtime/Model/BacktraceConfiguration.cs index 99867a79..426beec6 100644 --- a/Runtime/Model/BacktraceConfiguration.cs +++ b/Runtime/Model/BacktraceConfiguration.cs @@ -1,6 +1,7 @@ using Backtrace.Unity.Common; using Backtrace.Unity.Types; using System; +using System.Collections.Generic; using System.IO; using UnityEngine; @@ -161,7 +162,13 @@ public class BacktraceConfiguration : ScriptableObject /// Generate game screen shot when exception happen /// [Tooltip("Generate and attach screenshot of frame as exception occurs")] - public bool GenerateScreenshotOnException = false; + public bool GenerateScreenshotOnException = false; + + /// + /// List of path to attachments that Backtrace client will include in the native and managed reports. + /// + [Tooltip("List of path to attachments that Backtrace client will include in the native and managed reports.")] + public string[] AttachmentPaths; /// /// Directory path where reports and minidumps are stored @@ -213,7 +220,7 @@ public class BacktraceConfiguration : ScriptableObject /// /// Maximum number of retries - [Tooltip("If the database is unable to send its record, this setting specifies the maximum number of retries before the system gives up")] + [Tooltip("If the database is unable to send its record, this setting specifies the maximum number of retries before the system gives up.")] public int RetryLimit = 3; /// @@ -222,9 +229,28 @@ public class BacktraceConfiguration : ScriptableObject [Tooltip("This specifies in which order records are sent to the Backtrace server.")] public RetryOrder RetryOrder; + /// + /// Get full paths to attachments added by client + /// + /// List of absolute path to attachments + public List GetAttachmentPaths() + { + var result = new List(); + if (AttachmentPaths == null || AttachmentPaths.Length == 0) + { + return result; + } + + foreach (var path in AttachmentPaths) + { + result.Add(ClientPathHelper.GetFulLPath(path)); + } + return result; + } + public string GetFullDatabasePath() { - return DatabasePathHelper.GetFullDatabasePath(DatabasePath); + return ClientPathHelper.GetFulLPath(DatabasePath); } public string CrashpadDatabasePath { diff --git a/Runtime/Native/Android/NativeClient.cs b/Runtime/Native/Android/NativeClient.cs index 64eb2f1a..e2075075 100644 --- a/Runtime/Native/Android/NativeClient.cs +++ b/Runtime/Native/Android/NativeClient.cs @@ -34,13 +34,13 @@ internal class NativeClient : INativeClient private Thread _anrThread; [DllImport("backtrace-native")] - private static extern bool Initialize(IntPtr submissionUrl, IntPtr databasePath, IntPtr handlerPath, IntPtr keys, IntPtr values); + private static extern bool Initialize(IntPtr submissionUrl, IntPtr databasePath, IntPtr handlerPath, IntPtr keys, IntPtr values, IntPtr attachments); [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 static extern bool NativeReport(IntPtr message, bool setMainThreadAsFaultingThread); /// /// Native client built-in specific attributes @@ -206,9 +206,10 @@ private void HandleNativeCrashes() return; } // get default built-in Backtrace-Unity attributes - var backtraceAttributes = new BacktraceAttributes(null, null, true); + var backtraceAttributes = new BacktraceAttributes(null, null, true).Attributes; var minidumpUrl = new BacktraceCredentials(_configuration.GetValidServerUrl()).GetMinidumpSubmissionUrl().ToString(); + var attachments = _configuration.GetAttachmentPaths().ToArray(); // reassign to captureNativeCrashes // to avoid doing anything on crashpad binary, when crashpad @@ -217,12 +218,14 @@ private void HandleNativeCrashes() AndroidJNI.NewStringUTF(minidumpUrl), AndroidJNI.NewStringUTF(databasePath), AndroidJNI.NewStringUTF(crashpadHandlerPath), - AndroidJNIHelper.ConvertToJNIArray(backtraceAttributes.Attributes.Keys.ToArray()), - AndroidJNIHelper.ConvertToJNIArray(backtraceAttributes.Attributes.Values.ToArray())); + AndroidJNIHelper.ConvertToJNIArray(backtraceAttributes.Keys.ToArray()), + AndroidJNIHelper.ConvertToJNIArray(backtraceAttributes.Values.ToArray()), + AndroidJNIHelper.ConvertToJNIArray(attachments)); if (!_captureNativeCrashes) { Debug.LogWarning("Backtrace native integration status: Cannot initialize Crashpad client"); } + // add exception type to crashes handled by crashpad - all exception handled by crashpad // by default we setting this option here, to set error.type when unexpected crash happen (so attribute will present) // otherwise in other methods - ANR detection, OOM handler, we're overriding it and setting it back to "crash" @@ -335,7 +338,7 @@ public void HandleAnr(string gameObjectName, string callbackName) AndroidJNI.NewStringUTF("error.type"), AndroidJNI.NewStringUTF("Hang")); - NativeReport(AndroidJNI.NewStringUTF("ANRException: Blocked thread detected.")); + NativeReport(AndroidJNI.NewStringUTF("ANRException: Blocked thread detected."), true); // update error.type attribute in case when crash happen SetAttribute("error.type", "Crash"); } diff --git a/Runtime/Native/INativeClient.cs b/Runtime/Native/INativeClient.cs index e7f30c34..e3647549 100644 --- a/Runtime/Native/INativeClient.cs +++ b/Runtime/Native/INativeClient.cs @@ -1,50 +1,50 @@ -using System.Collections.Generic; - -namespace Backtrace.Unity.Runtime.Native -{ - /// - /// Backtrace native client definition - /// - internal interface INativeClient - { - /// - /// Handle ANR - Application not responding events - /// - void HandleAnr(string gameObjectName, string callbackName); - - /// - /// 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(); - +using System.Collections.Generic; + +namespace Backtrace.Unity.Runtime.Native +{ + /// + /// Backtrace native client definition + /// + internal interface INativeClient + { + /// + /// Handle ANR - Application not responding events + /// + void HandleAnr(string gameObjectName, string callbackName); + + /// + /// 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(); + /// /// Pause ANR thread /// - /// True if should pause, otherwise false. - void PauseAnrThread(bool state); - } -} + /// True if should pause, otherwise false. + void PauseAnrThread(bool state); + } +} diff --git a/Runtime/Native/NativeClientFactory.cs b/Runtime/Native/NativeClientFactory.cs index a2878fa5..aa76c12d 100644 --- a/Runtime/Native/NativeClientFactory.cs +++ b/Runtime/Native/NativeClientFactory.cs @@ -1,5 +1,6 @@ using Backtrace.Unity.Model; - +using System.Collections.Generic; + namespace Backtrace.Unity.Runtime.Native { internal static class NativeClientFactory @@ -12,7 +13,7 @@ internal static INativeClient GetNativeClient(BacktraceConfiguration configurati #if UNITY_ANDROID return new Android.NativeClient(gameObjectName, configuration); #elif UNITY_IOS - return new iOS.NativeClient(gameObjectName, configuration); + return new iOS.NativeClient(configuration); #else return null; #endif diff --git a/Runtime/Native/iOS/NativeClient.cs b/Runtime/Native/iOS/NativeClient.cs index 1adcb249..59b67bf6 100644 --- a/Runtime/Native/iOS/NativeClient.cs +++ b/Runtime/Native/iOS/NativeClient.cs @@ -1,268 +1,272 @@ -#if UNITY_IOS -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using Backtrace.Unity.Model; -using UnityEngine; - -namespace Backtrace.Unity.Runtime.Native.iOS -{ - /// - /// iOS native client - /// - internal class NativeClient : INativeClient - { - // Last Backtrace client update time - volatile internal float _lastUpdateTime; - - /// - /// Determine if the ANR background thread should be disabled or not - /// for some period of time. - /// This option will be used by the native client implementation - /// once application goes to background/foreground - /// - volatile internal bool _preventAnr = false; - - /// - /// Determine if ANR thread should exit - /// - volatile internal bool _stopAnr = false; - - private Thread _anrThread; - - // NSDictinary entry used only for iOS native integration - internal struct Entry - { - public string Key; - public string Value; - } - - [DllImport("__Internal", EntryPoint = "StartBacktraceIntegration")] - private static extern void Start(string plCrashReporterUrl, string[] attributeKeys, string[] attributeValues, int size, bool enableOomSupport); - - [DllImport("__Internal", EntryPoint = "NativeReport")] - private static extern void NativeReport(string message); - - [DllImport("__Internal", EntryPoint = "Crash")] - private static extern string Crash(); - - [DllImport("__Internal", EntryPoint = "GetAttributes")] - private static extern void GetNativeAttributes(out IntPtr attributes, out int keysCount); - - [DllImport("__Internal", EntryPoint = "AddAttribute")] - private static extern void AddAttribute(string key, string value); - - private static bool INITIALIZED = false; - - /// - /// Determine if ios integration should be enabled - /// - private readonly bool _enabled = -#if UNITY_IOS && !UNITY_EDITOR - true; -#else - false; - -#endif - - public NativeClient(string gameObjectName, BacktraceConfiguration configuration) - { - if (INITIALIZED || !_enabled) - { - return; - } - if (configuration.CaptureNativeCrashes) - { - HandleNativeCrashes(configuration); - INITIALIZED = true; - } - if (configuration.HandleANR) - { - HandleAnr(gameObjectName, string.Empty); - } - } - - - /// - /// Start crashpad process to handle native Android crashes - /// - - private void HandleNativeCrashes(BacktraceConfiguration configuration) - { - var databasePath = configuration.GetFullDatabasePath(); - // make sure database is enabled - if (string.IsNullOrEmpty(databasePath) || !Directory.Exists(databasePath)) - { - Debug.LogWarning("Backtrace native integration status: database path doesn't exist"); - return; - } - - var plcrashreporterUrl = new BacktraceCredentials(configuration.GetValidServerUrl()).GetPlCrashReporterSubmissionUrl(); - var backtraceAttributes = new Model.JsonData.BacktraceAttributes(null, null, true); - - // add exception.type attribute to PLCrashReporter reports - // The library will send PLCrashReporter crashes to Backtrace - // only when Crash occured - backtraceAttributes.Attributes["error.type"] = "Crash"; - var attributeKeys = backtraceAttributes.Attributes.Keys.ToArray(); - var attributeValues = backtraceAttributes.Attributes.Values.ToArray(); - - Start(plcrashreporterUrl.ToString(), attributeKeys, attributeValues, attributeValues.Length, configuration.OomReports); - } - - /// - /// Retrieve Backtrace Attributes from the Android native code. - /// - /// Backtrace Attributes from the Android build - public void GetAttributes(Dictionary result) - { - if (!_enabled) - { - return; - } - GetNativeAttributes(out IntPtr pUnmanagedArray, out int keysCount); - - for (int i = 0; i < keysCount; i++) - { - var address = pUnmanagedArray + i * 16; - Entry entry = Marshal.PtrToStructure(address); - result.Add(entry.Key, entry.Value); - } - - Marshal.FreeHGlobal(pUnmanagedArray); - } - - /// - /// Setup iOS ANR support and set callback function when ANR happened. - /// - public void HandleAnr(string gameObjectName, string callbackName) - { - // 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 (_anrThread.IsAlive && _stopAnr == false) - { - if (!_preventAnr) - { - if (lastUpdatedCache == 0) - { - lastUpdatedCache = _lastUpdateTime; - } - else if (lastUpdatedCache == _lastUpdateTime) - { - if (!reported) - { - // set temporary attribute to "Hang" - SetAttribute("error.type", "Hang"); - NativeReport("ANRException: Blocked thread detected."); - // update error.type attribute in case when crash happen - SetAttribute("error.type", "Crash"); - reported = true; - } - } - else - { - reported = false; - } - - - lastUpdatedCache = _lastUpdateTime; - } - else if (lastUpdatedCache != 0) - { - // make sure when ANR happened just after going to foreground - // we won't false positive ANR report - lastUpdatedCache = 0; - } - Thread.Sleep(5000); - - } - }); - _anrThread.IsBackground = true; - _anrThread.Start(); - } - - - - /// - /// Add attribute to native crash - /// - /// attribute name - /// attribute value - public void SetAttribute(string key, string value) - { - // if INITIALIZED is equal to false, we don't need to set - // attributes or store them. AddAttibutes call to objective-c code - // is usefull ONLY when we initialized PLCrashReporter integration - if (!_enabled || INITIALIZED == false) - { - return; - } - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) - { - return; - } - 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; - } - // oom support will be handled by native plugin - this will prevent - // false positive reports - // to avoid reporting low memory warning when application didn't crash - // native plugin will analyse previous application session - 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) - { - _stopAnr = true; - } - } - - /// - /// Pause ANR detection - /// - /// True - if native client should pause ANR detection" - public void PauseAnrThread(bool stopAnr) - { - _preventAnr = stopAnr; - } - } -} -#endif +#if UNITY_IOS +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using Backtrace.Unity.Model; +using UnityEngine; + +namespace Backtrace.Unity.Runtime.Native.iOS +{ + /// + /// iOS native client + /// + internal class NativeClient : INativeClient + { + // Last Backtrace client update time + volatile internal float _lastUpdateTime; + + /// + /// Determine if the ANR background thread should be disabled or not + /// for some period of time. + /// This option will be used by the native client implementation + /// once application goes to background/foreground + /// + volatile internal bool _preventAnr = false; + + /// + /// Determine if ANR thread should exit + /// + volatile internal bool _stopAnr = false; + + private Thread _anrThread; + + // NSDictinary entry used only for iOS native integration + internal struct Entry + { + public string Key; + public string Value; + } + + [DllImport("__Internal", EntryPoint = "StartBacktraceIntegration")] + private static extern void Start(string plCrashReporterUrl, string[] attributeKeys, string[] attributeValues, int attributesSize, bool enableOomSupport, string[] attachments, int attachmentSize); + + [DllImport("__Internal", EntryPoint = "NativeReport")] + private static extern void NativeReport(string message, bool setMainThreadAsFaultingThread); + + [DllImport("__Internal", EntryPoint = "Crash")] + private static extern string Crash(); + + [DllImport("__Internal", EntryPoint = "GetAttributes")] + private static extern void GetNativeAttributes(out IntPtr attributes, out int keysCount); + + [DllImport("__Internal", EntryPoint = "AddAttribute")] + private static extern void AddAttribute(string key, string value); + + private static bool INITIALIZED = false; + + /// + /// Determine if ios integration should be enabled + /// + private readonly bool _enabled = +#if UNITY_IOS && !UNITY_EDITOR + true; +#else + false; + +#endif + + public NativeClient(BacktraceConfiguration configuration) + { + if (INITIALIZED || !_enabled) + { + return; + } + if (configuration.CaptureNativeCrashes) + { + HandleNativeCrashes(configuration); + INITIALIZED = true; + } + if (configuration.HandleANR) + { + // iOS integration doesn't require to pass game object name or callback function + // it's required by android to know which one object to call when ANR was detected + // in Java. In this situation we simply ignore them. + HandleAnr(); + } + } + + + /// + /// Start crashpad process to handle native Android crashes + /// + + private void HandleNativeCrashes(BacktraceConfiguration configuration) + { + var databasePath = configuration.GetFullDatabasePath(); + // make sure database is enabled + if (string.IsNullOrEmpty(databasePath) || !Directory.Exists(databasePath)) + { + Debug.LogWarning("Backtrace native integration status: database path doesn't exist"); + return; + } + + var plcrashreporterUrl = new BacktraceCredentials(configuration.GetValidServerUrl()).GetPlCrashReporterSubmissionUrl(); + var backtraceAttributes = new Model.JsonData.BacktraceAttributes(null, null, true); + + // add exception.type attribute to PLCrashReporter reports + // The library will send PLCrashReporter crashes to Backtrace + // only when Crash occured + backtraceAttributes.Attributes["error.type"] = "Crash"; + var attributeKeys = backtraceAttributes.Attributes.Keys.ToArray(); + var attributeValues = backtraceAttributes.Attributes.Values.ToArray(); + var attachments = configuration.GetAttachmentPaths().ToArray(); + + Start(plcrashreporterUrl.ToString(), attributeKeys, attributeValues, attributeValues.Length, configuration.OomReports, attachments, attachments.Length); + } + + /// + /// Retrieve Backtrace Attributes from the Android native code. + /// + /// Backtrace Attributes from the Android build + public void GetAttributes(Dictionary result) + { + if (!_enabled) + { + return; + } + GetNativeAttributes(out IntPtr pUnmanagedArray, out int keysCount); + + for (int i = 0; i < keysCount; i++) + { + var address = pUnmanagedArray + i * 16; + Entry entry = Marshal.PtrToStructure(address); + result.Add(entry.Key, entry.Value); + } + + Marshal.FreeHGlobal(pUnmanagedArray); + } + + /// + /// Setup iOS ANR support and set callback function when ANR happened. + /// + public void HandleAnr(string gameObjectName = "", string callbackName = "") + { + // 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 (_anrThread.IsAlive && _stopAnr == false) + { + if (!_preventAnr) + { + if (lastUpdatedCache == 0) + { + lastUpdatedCache = _lastUpdateTime; + } + else if (lastUpdatedCache == _lastUpdateTime) + { + if (!reported) + { + // set temporary attribute to "Hang" + SetAttribute("error.type", "Hang"); + NativeReport("ANRException: Blocked thread detected.", true); + // update error.type attribute in case when crash happen + SetAttribute("error.type", "Crash"); + reported = true; + } + } + else + { + reported = false; + } + + + lastUpdatedCache = _lastUpdateTime; + } + else if (lastUpdatedCache != 0) + { + // make sure when ANR happened just after going to foreground + // we won't false positive ANR report + lastUpdatedCache = 0; + } + Thread.Sleep(5000); + + } + }); + _anrThread.IsBackground = true; + _anrThread.Start(); + } + + + + /// + /// Add attribute to native crash + /// + /// attribute name + /// attribute value + public void SetAttribute(string key, string value) + { + // if INITIALIZED is equal to false, we don't need to set + // attributes or store them. AddAttibutes call to objective-c code + // is usefull ONLY when we initialized PLCrashReporter integration + if (!_enabled || INITIALIZED == false) + { + return; + } + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) + { + return; + } + 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; + } + // oom support will be handled by native plugin - this will prevent + // false positive reports + // to avoid reporting low memory warning when application didn't crash + // native plugin will analyse previous application session + 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) + { + _stopAnr = true; + } + } + + /// + /// Pause ANR detection + /// + /// True - if native client should pause ANR detection" + public void PauseAnrThread(bool stopAnr) + { + _preventAnr = stopAnr; + } + } +} +#endif diff --git a/Tests/Runtime/Database/DatabasePathTests.cs b/Tests/Runtime/Database/DatabasePathTests.cs index 6c337eb5..86241ab5 100644 --- a/Tests/Runtime/Database/DatabasePathTests.cs +++ b/Tests/Runtime/Database/DatabasePathTests.cs @@ -11,7 +11,7 @@ public class DatabasePathTests [Test] public void TestDbPath_EmptyPathToDatabase_PathShouldBeEmpty() { - Assert.IsEmpty(DatabasePathHelper.GetFullDatabasePath(string.Empty)); + Assert.IsEmpty(ClientPathHelper.GetFulLPath(string.Empty)); } [Test] @@ -21,7 +21,7 @@ public void TestDbPath_ShouldReplaceInterpolationWithDataPath_PathShouldntBeEmpt var expectedDatabasePath = Path.Combine(Application.dataPath, "foo", "bar"); var testedDatabasePath = "${Application.dataPath}/foo/bar"; - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedDatabasePath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedDatabasePath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -33,7 +33,7 @@ public void TestDbPath_ShouldReplaceInterpolationWithPersistentDataPathDataPath_ var expectedDatabasePath = Path.Combine(Application.persistentDataPath, "foo", "bar"); var testedDatabasePath = "${Application.persistentDataPath}/foo/bar"; - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedDatabasePath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedDatabasePath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -42,7 +42,7 @@ public void TestDbPath_ShouldtTryToParseInterpolatedString_PathShouldntBeEmpty() { var expectedDatabasePath = Path.Combine(Application.persistentDataPath, "foo", "bar"); - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(expectedDatabasePath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(expectedDatabasePath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -51,7 +51,7 @@ public void TestDbPath_ShouldTryToEscapeRootedDir_PathShouldntBeEmpty() { var testedPath = "./test"; var expectedDatabasePath = Path.Combine(Application.persistentDataPath, testedPath); - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedPath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedPath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -65,7 +65,7 @@ public void TestDbPath_ShouldCorrectlyGenerateFullpath_PathShouldntBeEmpty() "C:/users/user/Backtrace/database/path"; #endif - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(expectedDatabasePath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(expectedDatabasePath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -75,7 +75,7 @@ public void TestDbPath_ShouldParseCorrectlyInterpolatedStringWithUpperCaseChar_P var expectedDatabasePath = Path.Combine(Application.persistentDataPath, "foo", "bar"); var testedDatabasePath = "${Application.PersistentDataPath}/foo/bar"; - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedDatabasePath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedDatabasePath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -85,7 +85,7 @@ public void TestDbPath_ShouldParseCorrectlyInterpolatedStringWithLowerCaseChar_P var expectedDatabasePath = Path.Combine(Application.persistentDataPath, "foo", "bar"); var testedDatabasePath = "${application.persistentDataPath}/foo/bar"; - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedDatabasePath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedDatabasePath); Assert.AreEqual(new DirectoryInfo(expectedDatabasePath).FullName, actualDatabasePath); } @@ -97,7 +97,7 @@ public void TestDbPath_ShouldHandleIncorrectInterpolationClosingValue_ShouldRetu // beacuse ${ will point to root dir of project, we will try to extend this path // with application.dataPath var expectedInvalidPath = Path.Combine(Application.persistentDataPath, testedInvalidPath); - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedInvalidPath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedInvalidPath); Assert.AreEqual(Path.GetFullPath(expectedInvalidPath), actualDatabasePath); } @@ -108,7 +108,7 @@ public void TestDbPath_ShouldHandleIncorrectInterpolationStartingValue_ShouldRet // beacuse ${ will point to root dir of project, we will try to extend this path // with application.dataPath var expectedInvalidPath = Path.Combine(Application.persistentDataPath, testedInvalidPath); - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedInvalidPath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedInvalidPath); Assert.AreEqual(Path.GetFullPath(expectedInvalidPath), actualDatabasePath); } @@ -120,7 +120,7 @@ public void TestDbPath_ShouldHandleIncorrectInterpolationValue_ShouldReturnInval // beacuse ${ will point to root dir of project, we will try to extend this path // with application.dataPath var expectedInvalidPath = Path.Combine(Application.persistentDataPath, testedInvalidPath); - var actualDatabasePath = DatabasePathHelper.GetFullDatabasePath(testedInvalidPath); + var actualDatabasePath = ClientPathHelper.GetFulLPath(testedInvalidPath); Assert.AreEqual(Path.GetFullPath(expectedInvalidPath), actualDatabasePath); } } diff --git a/iOS/libBacktrace-Unity-Cocoa.a b/iOS/libBacktrace-Unity-Cocoa.a index 119ada25..98600b92 100644 Binary files a/iOS/libBacktrace-Unity-Cocoa.a and b/iOS/libBacktrace-Unity-Cocoa.a differ