// ---------------------------------------- // // BuglyAgent.cs // // Author: // bugly, // // Copyright (c) 2023 Bugly, Tencent. All rights reserved. // // ---------------------------------------- // using UnityEngine; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Runtime.InteropServices; using AOT; // We dont use the LogType enum in Unity as the numerical order doesnt suit our purposes /// /// Log severity. /// { Log, LogDebug, LogInfo, LogWarning, LogAssert, LogError, LogException } /// public enum LogSeverity { Log, LogDebug, LogInfo, LogWarning, LogAssert, LogError, LogException } /// /// Bugly agent. /// public sealed class BuglyAgent { // Define delegate support multicasting to replace the 'Application.LogCallback' public delegate void LogCallbackDelegate (string condition,string stackTrace,LogType type); /// /// Configs the type of the crash reporter and customized log level to upload /// /// Type. Default=0, 1=Bugly v2.x MSDK=2 /// Log level. Off=0,Error=1,Warn=2,Info=3,Debug=4 public static void ConfigCrashReporter(int type, int logLevel){ BuglyCommon.SetBuglyReporterType (type); _SetCrashReporterLogLevel (logLevel); } /// /// Init sdk with the specified appId. /// This will initialize sdk to report native exception such as obj-c, c/c++, java exceptions, and also enable c# exception handler to report c# exception logs /// /// App identifier. public static void InitWithAppId (string appId, string appKey) { if (IsInitialized) { DebugLog (null, "BuglyAgent has already been initialized."); return; } if (string.IsNullOrEmpty (appId) || string.IsNullOrEmpty(appKey)) { return; } // init the sdk with appid and appkey BuglyCommon.InitBuglyAgent (appId, appKey); DebugLog (null, "Initialized with app id: {0} app key: {1}", appId, appKey); // Register the LogCallbackHandler by Application.RegisterLogCallback(Application.LogCallback) _RegisterExceptionHandler (); } /// /// Only Enable the C# exception handler. /// /// /// You can call it when you do not call the 'InitWithAppId(string)', but you must make sure initialized the sdk in elsewhere, /// such as the native code in associated Android or iOS project. /// /// /// /// Default Level is LogError, so the LogError, LogException will auto report. /// /// /// /// You can call the method BuglyAgent.ConfigAutoReportLogLevel(LogSeverity) /// to change the level to auto report if you known what are you doing. /// /// /// public static void EnableExceptionHandler () { if (IsInitialized) { DebugLog (null, "BuglyAgent has already been initialized."); return; } DebugLog (null, "Only enable the exception handler, please make sure you has initialized the sdk in the native code in associated Android or iOS project."); // Register the LogCallbackHandler by Application.RegisterLogCallback(Application.LogCallback) _RegisterExceptionHandler (); } /// /// Registers the log callback handler. /// /// If you need register logcallback using Application.RegisterLogCallback(LogCallback), /// you can call this method to replace it. /// /// /// /// Handler. public static void RegisterLogCallback (LogCallbackDelegate handler) { if (handler != null) { DebugLog (null, "Add log callback handler: {0}", handler); _LogCallbackEventHandler += handler; } } /// /// Sets the log callback extras handler. /// /// Handler. public static void SetLogCallbackExtrasHandler(Func> handler){ if (handler != null) { BuglyCommon.BuglyLogCallbackExtrasHandler = handler; DebugLog(null, "Add log callback extra data handler : {0}", handler); } } /// /// Reports the exception. /// /// Exception. /// Message. public static void ReportException (System.Exception e, string message) { if (!IsInitialized) { return; } DebugLog (null, "Report exception: {0}\n------------\n{1}\n------------", message, e); _HandleException (e, message, false); } /// /// Reports the exception. /// /// Name. /// Message. /// Stack trace. public static void ReportException (string name, string message, string stackTrace) { if (!IsInitialized) { return; } DebugLog (null, "Report exception: {0} {1} \n{2}", name, message, stackTrace); _HandleException (LogSeverity.LogException, name, message, stackTrace, false); } /// /// Unregisters the log callback. /// /// Handler. public static void UnregisterLogCallback (LogCallbackDelegate handler) { if (handler != null) { DebugLog (null, "Remove log callback handler"); _LogCallbackEventHandler -= handler; } } /// /// Sets the user identifier. /// /// User identifier. public static void SetUserId (string userId) { if (!IsInitialized) { return; } DebugLog (null, "Set user id: {0}", userId); BuglyCommon.SetBuglyUserId(userId); } public static void SetDeviceId(string deviceId) { if (!IsInitialized) { return; } DebugLog (null, "Set device id: {0}", deviceId); BuglyCommon.SetBuglyDeviceId(deviceId); } /// /// set Addtional Attachment Paths to uplaod /// /// Additional Path Array public static void SetCrashAttachmentPaths(string[] pathArray){ if (!IsInitialized) { return; } BuglyCommon.SetBuglyAdditionalAttachmentPaths(pathArray); } /// /// Set crash handle callback. /// /// listener. public static void SetCrashListener (BuglyCallback.ListenerAdapter listener) { DebugLog (null, "Sets crash handle callback"); BuglyCommon.SetBuglyCrashHandlerListener (listener); } /// /// Adds the scene data. /// /// Key. /// Value. public static void AddSceneData (string key, string value) { if (!IsInitialized) { return; } DebugLog (null, "Add scene data: [{0}, {1}]", key, value); BuglyCommon.AddBuglyKeyAndValueInScene (key, value); } public static void ConfigAutoReportInterval(int interval) { _autoReportInterval = interval; } /// /// Configs the auto quit application. /// /// If set to true auto quit. public static void ConfigAutoQuitApplication (bool autoQuit) { _autoQuitApplicationAfterReport = autoQuit; } /// /// Configs the auto report log level. Default is LogSeverity.LogError. /// /// LogSeverity { Log, LogDebug, LogInfo, LogWarning, LogAssert, LogError, LogException } /// /// /// /// Level. public static void ConfigAutoReportLogLevel (LogSeverity level) { _autoReportLogLevel = level; } /// /// Configs the pluginArray. /// /// pluginArray. public static void ConfigPluginArray (string[] pluginArray) { BuglyCommon.BuglyPluginArray = pluginArray; } /// /// Configs the default. /// /// Channel. /// Version. /// User. /// Delay. public static void ConfigDefault (string channel, string version, string buildNum, string user, string model) { DebugLog (null, "Config default channel:{0}, version:{1}, buildNum:{2}, user:{3}, delay:{4}", channel, version, buildNum, user, model); BuglyCommon.ConfigBuglyDefaultBeforeInit (channel, version, buildNum, user, model); } /// /// Logs the debug. /// /// Tag. /// Format. /// Arguments. public static void DebugLog (string tag, string format, params object[] args) { if(!BuglyCommon.BuglyDebugMode) { return; } if (string.IsNullOrEmpty (format)) { return; } Console.WriteLine ("[BuglyAgent] - {0} : {1}", tag, string.Format (format, args)); } /// /// Prints the log. /// /// Level. /// Format. /// Arguments. public static void PrintLog (LogSeverity level, string format, params object[] args) { if (string.IsNullOrEmpty (format)) { return; } BuglyCommon.LogBuglyRecord (level, string.Format (format, args)); } #region Privated Fields and Methods private static event LogCallbackDelegate _LogCallbackEventHandler; private static bool _isInitialized = false; private static LogSeverity _autoReportLogLevel = LogSeverity.LogError; private static int _autoReportInterval = -1; // 不限频率 private static float _autoReportTime = 0; // 上次上报的时间 #if UNITY_ANDROID // The crash reporter package name, default is 'com.tencent.bugly' private static string _crashReporterPackage = "com.tencent.bugly"; // The Bugly version mode, value in 'Unknown', 'Debug', 'Gray', 'Release' private static string _versionMode = "Unknown"; #endif #pragma warning disable 414 private static bool _autoQuitApplicationAfterReport = false; private static readonly int EXCEPTION_TYPE_UNCAUGHT = 1; private static readonly int EXCEPTION_TYPE_CAUGHT = 2; private static readonly string _pluginVersion = "1.7.0"; public static string PluginVersion { get { return _pluginVersion; } } public static bool IsInitialized { get { return _isInitialized; } } public static bool AutoQuitApplicationAfterReport { get { return _autoQuitApplicationAfterReport; } } private static void _SetCrashReporterLogLevel(int logLevel){ #if UNITY_IPHONE || UNITY_IOS BuglyCommon.BuglyReproterCustomizedLogLevel = logLevel; #endif } private static void _RegisterExceptionHandler () { try { // hold only one instance #if UNITY_5 Application.logMessageReceived += _OnLogCallbackHandler; #else Application.RegisterLogCallback (_OnLogCallbackHandler); #endif AppDomain.CurrentDomain.UnhandledException += _OnUncaughtExceptionHandler; _isInitialized = true; DebugLog (null, "Register the log callback in Unity {0}", Application.unityVersion); } catch { } BuglyCommon.SetSDKUnityVersion (); } private static void _UnregisterExceptionHandler () { try { #if UNITY_5 Application.logMessageReceived -= _OnLogCallbackHandler; #else Application.RegisterLogCallback (null); #endif System.AppDomain.CurrentDomain.UnhandledException -= _OnUncaughtExceptionHandler; DebugLog (null, "Unregister the log callback in unity {0}", Application.unityVersion); } catch { } } private static void _OnLogCallbackHandler (string condition, string stackTrace, LogType type) { if (_LogCallbackEventHandler != null) { _LogCallbackEventHandler (condition, stackTrace, type); } if (!IsInitialized) { return; } if (!string.IsNullOrEmpty (condition) && condition.Contains ("[BuglyAgent] ")) { return; } if (_uncaughtAutoReportOnce) { return; } // convert the log level LogSeverity logLevel = LogSeverity.Log; switch (type) { case LogType.Exception: logLevel = LogSeverity.LogException; break; case LogType.Error: logLevel = LogSeverity.LogError; break; case LogType.Assert: logLevel = LogSeverity.LogAssert; break; case LogType.Warning: logLevel = LogSeverity.LogWarning; break; case LogType.Log: logLevel = LogSeverity.LogDebug; break; default: break; } if (LogSeverity.Log == logLevel) { return; } _HandleException (logLevel, null, condition, stackTrace, true); } private static void _OnUncaughtExceptionHandler (object sender, System.UnhandledExceptionEventArgs args) { if (args == null || args.ExceptionObject == null) { return; } try { if (args.ExceptionObject.GetType () != typeof(System.Exception)) { return; } } catch { if (UnityEngine.Debug.isDebugBuild == true) { UnityEngine.Debug.Log ("BuglyAgent: Failed to report uncaught exception"); } return; } if (!IsInitialized) { return; } if (_uncaughtAutoReportOnce) { return; } _HandleException ((System.Exception)args.ExceptionObject, null, true); } private static void _HandleException (System.Exception e, string message, bool uncaught) { if (e == null) { return; } if (!IsInitialized) { return; } // 频率限制 float curTime = Time.realtimeSinceStartup; if (_autoReportInterval > 0 && (curTime - _autoReportTime) < _autoReportInterval) { return; } _autoReportTime = curTime; string name = e.GetType ().Name; string reason = e.Message; if (!string.IsNullOrEmpty (message)) { reason = string.Format ("{0}{1}***{2}", reason, Environment.NewLine, message); } StringBuilder stackTraceBuilder = new StringBuilder (""); StackTrace stackTrace = new StackTrace (e, true); int count = stackTrace.FrameCount; for (int i = 0; i < count; i++) { StackFrame frame = stackTrace.GetFrame (i); stackTraceBuilder.AppendFormat ("{0}.{1}", frame.GetMethod ().DeclaringType.Name, frame.GetMethod ().Name); ParameterInfo[] parameters = frame.GetMethod ().GetParameters (); if (parameters == null || parameters.Length == 0) { stackTraceBuilder.Append (" () "); } else { stackTraceBuilder.Append (" ("); int pcount = parameters.Length; ParameterInfo param = null; for (int p = 0; p < pcount; p++) { param = parameters [p]; stackTraceBuilder.AppendFormat ("{0} {1}", param.ParameterType.Name, param.Name); if (p != pcount - 1) { stackTraceBuilder.Append (", "); } } param = null; stackTraceBuilder.Append (") "); } string fileName = frame.GetFileName (); if (!string.IsNullOrEmpty (fileName) && !fileName.ToLower ().Equals ("unknown")) { fileName = fileName.Replace ("\\", "/"); int loc = fileName.ToLower ().IndexOf ("/assets/"); if (loc < 0) { loc = fileName.ToLower ().IndexOf ("assets/"); } if (loc > 0) { fileName = fileName.Substring (loc); } stackTraceBuilder.AppendFormat ("(at {0}:{1})", fileName, frame.GetFileLineNumber ()); } stackTraceBuilder.AppendLine (); } // report _reportException (uncaught, name, reason, stackTraceBuilder.ToString ()); } private static void _reportException (bool uncaught, string name, string reason, string stackTrace) { if (string.IsNullOrEmpty (name)) { return; } if (string.IsNullOrEmpty (stackTrace)) { stackTrace = StackTraceUtility.ExtractStackTrace (); } if (string.IsNullOrEmpty (stackTrace)) { stackTrace = "Empty"; } else { try { string[] frames = stackTrace.Split ('\n'); if (frames != null && frames.Length > 0) { StringBuilder trimFrameBuilder = new StringBuilder (); string frame = null; int count = frames.Length; for (int i = 0; i < count; i++) { frame = frames [i]; if (string.IsNullOrEmpty (frame) || string.IsNullOrEmpty (frame.Trim ())) { continue; } frame = frame.Trim (); // System.Collections.Generic if (frame.StartsWith ("System.Collections.Generic.") || frame.StartsWith ("ShimEnumerator")) { continue; } if (frame.StartsWith ("Bugly")) { continue; } if (frame.Contains ("..ctor")) { continue; } int start = frame.ToLower ().IndexOf ("(at"); int end = frame.ToLower ().IndexOf ("/assets/"); if (start > 0 && end > 0) { trimFrameBuilder.AppendFormat ("{0}(at {1}", frame.Substring (0, start).Replace (":", "."), frame.Substring (end)); } else { trimFrameBuilder.Append (frame.Replace (":", ".")); } trimFrameBuilder.AppendLine (); } stackTrace = trimFrameBuilder.ToString (); } } catch { PrintLog(LogSeverity.LogWarning,"{0}", "Error to parse the stack trace"); } } PrintLog (LogSeverity.LogError, "ReportException: {0} {1}\n*********\n{2}\n*********", name, reason, stackTrace); _uncaughtAutoReportOnce = uncaught && _autoQuitApplicationAfterReport; BuglyCommon.ReportBuglyException (uncaught ? EXCEPTION_TYPE_UNCAUGHT : EXCEPTION_TYPE_CAUGHT, name, reason, stackTrace, uncaught && _autoQuitApplicationAfterReport); } private static void _HandleException (LogSeverity logLevel, string name, string message, string stackTrace, bool uncaught) { if (!IsInitialized) { DebugLog (null, "It has not been initialized."); return; } if (logLevel == LogSeverity.Log) { return; } if ((uncaught && logLevel < _autoReportLogLevel)) { DebugLog (null, "Not report exception for level {0}", logLevel.ToString ()); return; } // 频率限制 float curTime = Time.realtimeSinceStartup; if (_autoReportInterval > 0 && (curTime - _autoReportTime) < _autoReportInterval) { return; } _autoReportTime = curTime; string type = null; string reason = null; if (!string.IsNullOrEmpty (message)) { try { if ((LogSeverity.LogException == logLevel) && message.Contains ("Exception")) { Match match = new Regex (@"^(?\S+):\s*(?.*)", RegexOptions.Singleline).Match (message); if (match.Success) { type = match.Groups ["errorType"].Value.Trim(); reason = match.Groups ["errorMessage"].Value.Trim (); } } else if ((LogSeverity.LogError == logLevel) && message.StartsWith ("Unhandled Exception:")) { Match match = new Regex (@"^Unhandled\s+Exception:\s*(?\S+):\s*(?.*)", RegexOptions.Singleline).Match(message); if (match.Success) { string exceptionName = match.Groups ["exceptionName"].Value.Trim(); string exceptionDetail = match.Groups ["exceptionDetail"].Value.Trim (); // int dotLocation = exceptionName.LastIndexOf("."); if (dotLocation > 0 && dotLocation != exceptionName.Length) { type = exceptionName.Substring(dotLocation + 1); } else { type = exceptionName; } int stackLocation = exceptionDetail.IndexOf(" at "); if (stackLocation > 0) { // reason = exceptionDetail.Substring(0, stackLocation); // substring after " at " string callStacks = exceptionDetail.Substring(stackLocation + 3).Replace(" at ", "\n").Replace("in :0","").Replace("[0x00000]",""); // stackTrace = string.Format("{0}\n{1}", stackTrace, callStacks.Trim()); } else { reason = exceptionDetail; } // for LuaScriptException if(type.Equals("LuaScriptException") && exceptionDetail.Contains(".lua") && exceptionDetail.Contains("stack traceback:")) { stackLocation = exceptionDetail.IndexOf("stack traceback:"); if(stackLocation > 0) { reason = exceptionDetail.Substring(0, stackLocation); // substring after "stack traceback:" string callStacks = exceptionDetail.Substring(stackLocation + 16).Replace(" [", " \n["); // stackTrace = string.Format("{0}\n{1}", stackTrace, callStacks.Trim()); } } } } } catch { } if (string.IsNullOrEmpty (reason)) { reason = message; } } if (string.IsNullOrEmpty (name)) { if (string.IsNullOrEmpty (type)) { type = string.Format ("Unity{0}", logLevel.ToString ()); } } else { type = name; } _reportException (uncaught, type, reason, stackTrace); } private static bool _uncaughtAutoReportOnce = false; #endregion }