// ---------------------------------------- // // BuglyAgent.cs // // Author: // Yeelik, // // Copyright (c) 2015 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; // 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 { /// /// The SDK package name, default is 'com.tencent.bugly' /// private const string SDK_PACKAGE = "com.tencent.bugly.msdk"; private const int SDK_TYPE = 2; // Default=0,Bugly=1,MSDK=2 private const int SDK_LOG_LEVEL = 2; // Off=0,Error=1,Warn=2,Info=3,Debug=4 // Define delegate support multicasting to replace the 'Application.LogCallback' public delegate void LogCallbackDelegate (string condition,string stackTrace,LogType type); /// /// 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) { if (IsInitialized) { DebugLog (null, "BuglyAgent has already been initialized."); return; } if (string.IsNullOrEmpty (appId)) { return; } // init the sdk with app id InitBuglyAgent (appId); DebugLog (null, "Initialized with app id: {0}", appId); // 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) { _LogCallbackExtrasHandler = handler; DebugLog(null, "Add log callback extra data handler : {0}", handler); } } /// /// Reports the exception. /// /// E. /// 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); SetUserInfo (userId); } /// /// Sets the scene. /// /// Scene identifier. public static void SetScene (int sceneId) { if (!IsInitialized) { return; } DebugLog (null, "Set scene: {0}", sceneId); SetCurrentScene (sceneId); } /// /// 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); AddKeyAndValueInScene (key, value); } /// /// Configs the debug mode. /// /// If set to true debug mode. public static void ConfigDebugMode (bool enable) { EnableDebugMode (enable); DebugLog (null, "{0} the log message print to console", enable ? "Enable" : "Disable"); } /// /// 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 default. /// /// Channel. /// Version. /// User. /// Delay. public static void ConfigDefault (string channel, string version, string user, long delay) { DebugLog (null, "Config default channel:{0}, version:{1}, user:{2}, delay:{3}", channel, version, user, delay); ConfigDefaultBeforeInit (channel, version, user, delay); } /// /// Logs the debug. /// /// Tag. /// Format. /// Arguments. public static void DebugLog (string tag, string format, params object[] args) { if (string.IsNullOrEmpty (format)) { return; } if(!_debugMode) { 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; } LogToConsole (level, string.Format (format, args)); } #if UNITY_EDITOR || UNITY_STANDALONE #region Interface(Empty) in Editor private static void InitBuglyAgent (string appId) { } private static void ConfigDefaultBeforeInit(string channel, string version, string user, long delay){ } private static void EnableDebugMode(bool enable){ } private static void SetUserInfo(string userInfo){ } private static void ReportException (int type,string name, string message, string stackTrace, bool quitProgram) { } private static void SetCurrentScene(int sceneId) { } private static void AddKeyAndValueInScene(string key, string value){ } private static void AddExtraDataWithException(string key, string value) { // only impl for iOS } private static void LogToConsole(LogSeverity level, string message){ } private static void SetUnityVersion(){ } #endregion #elif UNITY_ANDROID // #if UNITY_ANDROID #region Interface for Android private static readonly string CLASS_UNITYAGENT = "com.tencent.bugly.unity.UnityAgent"; private static AndroidJavaObject _unityAgent; public static AndroidJavaObject UnityAgent { get { if (_unityAgent == null) { using (AndroidJavaClass clazz = new AndroidJavaClass(CLASS_UNITYAGENT)) { _unityAgent = clazz.CallStatic ("getInstance"); } } return _unityAgent; } } private static string _configChannel; private static string _configVersion; private static string _configUser; private static long _configDelayTime; private static void ConfigDefaultBeforeInit(string channel, string version, string user, long delay){ _configChannel = channel; _configVersion = version; _configUser = user; _configDelayTime = delay; } private static void InitBuglyAgent(string appId) { if (IsInitialized) { return; } try { UnityAgent.Call("setSDKPackagePrefixName", SDK_PACKAGE); } catch { } try { UnityAgent.Call("initWithConfiguration", appId, _configChannel, _configVersion, _configUser, _configDelayTime); _isInitialized = true; } catch { } } private static void EnableDebugMode(bool enable){ _debugMode = enable; try { UnityAgent.Call ("setLogEnable", enable); } catch { } } private static void SetUserInfo(string userInfo){ try { UnityAgent.Call ("setUserId", userInfo); } catch { } } private static void ReportException (int type, string name, string reason, string stackTrace, bool quitProgram) { try { UnityAgent.Call ("traceException", name, reason, stackTrace, quitProgram); } catch { } } private static void SetCurrentScene(int sceneId) { try { UnityAgent.Call ("setScene", sceneId); } catch { } } private static void SetUnityVersion(){ try { UnityAgent.Call ("setSdkConfig","UnityVersion", Application.unityVersion); } catch { } } private static void AddKeyAndValueInScene(string key, string value){ try { UnityAgent.Call ("addSceneValue", key, value); } catch { } } private static void AddExtraDataWithException(string key, string value) { // no impl } private static void LogToConsole(LogSeverity level, string message){ if (!_debugMode && LogSeverity.Log != level) { if (level < LogSeverity.LogWarning) { return; } } try { // UnityAgent.Call ("printLog", string.Format ("<{0}> - {1}", level.ToString (), message)); } catch { } } #endregion #elif UNITY_IPHONE || UNITY_IOS #region Interface for iOS private static bool _crashReporterTypeConfiged = false; private static void ConfigCrashReporterType(){ if (!_crashReporterTypeConfiged) { try { _BuglyConfigCrashReporterType(SDK_TYPE); // MSDK } catch { } } } private static void ConfigDefaultBeforeInit(string channel, string version, string user, long delay){ try { ConfigCrashReporterType(); _BuglyDefaultConfig(channel, version, user, null); } catch { } } private static void EnableDebugMode(bool enable){ _debugMode = enable; } private static void InitBuglyAgent (string appId) { if(!string.IsNullOrEmpty(appId)) { _BuglyConfigCrashReporterType(SDK_TYPE); // MSDK _BuglyInit(appId, _debugMode, SDK_LOG_LEVEL); // Log level } } private static void SetUnityVersion(){ ConfigCrashReporterType(); // _BuglySetExtraConfig("UnityVersion", Application.unityVersion); _BuglySetKeyValue("UnityVersion", Application.unityVersion); } private static void SetUserInfo(string userInfo){ if(!string.IsNullOrEmpty(userInfo)) { ConfigCrashReporterType(); _BuglySetUserId(userInfo); } } private static void ReportException (int type, string name, string reason, string stackTrace, bool quitProgram) { string extraInfo = ""; Dictionary extras = null; if (_LogCallbackExtrasHandler != null) { extras = _LogCallbackExtrasHandler(); } if (extras == null || extras.Count == 0) { extras = new Dictionary (); extras.Add ("UnityVersion", Application.unityVersion); } if (extras != null && extras.Count > 0) { if (!extras.ContainsKey("UnityVersion")) { extras.Add ("UnityVersion", Application.unityVersion); } StringBuilder builder = new StringBuilder(); foreach(KeyValuePair kvp in extras){ builder.Append(string.Format("\"{0}\" : \"{1}\"", kvp.Key, kvp.Value)).Append(" , "); } extraInfo = string.Format("{{ {0} }}", builder.ToString().TrimEnd(" , ".ToCharArray())); } ConfigCrashReporterType(); // 4 is C# exception _BuglyReportException(4, name, reason, stackTrace, extraInfo, quitProgram); } private static void SetCurrentScene(int sceneId) { ConfigCrashReporterType(); _BuglySetTag(sceneId); } private static void AddKeyAndValueInScene(string key, string value){ ConfigCrashReporterType(); _BuglySetKeyValue(key, value); } private static void AddExtraDataWithException(string key, string value) { } private static void LogToConsole(LogSeverity level, string message){ if (!_debugMode && LogSeverity.Log != level) { if (level < LogSeverity.LogWarning) { return; } } if (_debugMode ) { Console.WriteLine("[BuglyAgent] <{0}> - {1}", level.ToString(), message); } ConfigCrashReporterType(); // _BuglyLogMessage(LogSeverityToInt(level), null, message); } private static int LogSeverityToInt(LogSeverity logLevel){ int level = 5; switch(logLevel) { case LogSeverity.Log: level = 5; break; case LogSeverity.LogDebug: level = 4; break; case LogSeverity.LogInfo: level = 3; break; case LogSeverity.LogWarning: case LogSeverity.LogAssert: level = 2; break; case LogSeverity.LogError: case LogSeverity.LogException: level = 1; break; default: level = 0; break; } return level; } // --- dllimport start --- [DllImport("__Internal")] private static extern void _BuglyInit(string appId, bool debug, int level); [DllImport("__Internal")] private static extern void _BuglySetUserId(string userId); [DllImport("__Internal")] private static extern void _BuglySetTag(int tag); [DllImport("__Internal")] private static extern void _BuglySetKeyValue(string key, string value); [DllImport("__Internal")] private static extern void _BuglyReportException(int type, string name, string reason, string stackTrace, string extras, bool quit); [DllImport("__Internal")] private static extern void _BuglyDefaultConfig(string channel, string version, string user, string deviceId); [DllImport("__Internal")] private static extern void _BuglyLogMessage(int level, string tag, string log); [DllImport("__Internal")] private static extern void _BuglyConfigCrashReporterType(int type); [DllImport("__Internal")] private static extern void _BuglySetExtraConfig(string key, string value); // dllimport end #endregion #endif #region Privated Fields and Methods private static event LogCallbackDelegate _LogCallbackEventHandler; private static bool _isInitialized = false; private static LogSeverity _autoReportLogLevel = LogSeverity.LogError; #pragma warning disable 414 private static bool _debugMode = false; 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.4.2"; private static Func> _LogCallbackExtrasHandler; public static string PluginVersion { get { return _pluginVersion; } } public static bool IsInitialized { get { return _isInitialized; } } public static bool AutoQuitApplicationAfterReport { get { return _autoQuitApplicationAfterReport; } } 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 { } SetUnityVersion (); } 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; } 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 ("= new Vector3")) { 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 "); } } PrintLog (LogSeverity.LogError, "ReportException: {0} {1}\n*********\n{2}\n*********", name, reason, stackTrace); _uncaughtAutoReportOnce = uncaught && _autoQuitApplicationAfterReport; ReportException (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; } 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 }