├── .gitignore ├── CHANGELOG.MD ├── CHANGELOG.MD.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Scripts.meta ├── Scripts ├── Editor.meta └── Editor │ ├── BrunoMikoski.SmartSymbolicate.Editor.asmdef │ ├── BrunoMikoski.SmartSymbolicate.Editor.asmdef.meta │ ├── HyperlinkInterceptor.cs │ ├── HyperlinkInterceptor.cs.meta │ ├── SmartSymbolicateWindow.cs │ └── SmartSymbolicateWindow.cs.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Asset meta data should only be ignored when the corresponding asset is also ignored 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | [Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | # Visual Studio cache directory 23 | .vs/ 24 | 25 | # Gradle cache directory 26 | .gradle/ 27 | 28 | # Autogenerated VS/MD/Consulo solution and project files 29 | ExportedObj/ 30 | .consulo/ 31 | *.csproj 32 | *.unityproj 33 | *.sln 34 | *.suo 35 | *.tmp 36 | *.user 37 | *.userprefs 38 | *.pidb 39 | *.booproj 40 | *.svd 41 | *.pdb 42 | *.mdb 43 | *.opendb 44 | *.VC.db 45 | 46 | # Unity3D generated meta files 47 | *.pidb.meta 48 | *.pdb.meta 49 | *.mdb.meta 50 | 51 | # Unity3D generated file on crash reports 52 | sysinfo.txt 53 | 54 | # Builds 55 | *.apk 56 | *.unitypackage 57 | 58 | # Crashlytics generated file 59 | crashlytics-build.properties 60 | 61 | -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## [1.1.1] 10 | ### Changed 11 | - Fixed formatting to expose all previous information as well, since can hide valueable information 12 | - Improved Hyperlink generator to find target classes 13 | 14 | ## [1.1.0] 15 | ### Changed 16 | - Better output format 17 | 18 | ### Add 19 | - Added new parser that will try to find that file and line number of `libil2cpp` errors 20 | 21 | ## [1.0.3] 22 | ### Changed 23 | - Allowed custom symbols to be used besides the know ones 24 | 25 | ### Fix 26 | - Fixed an issue that when parsing crashes would still use the unity symbols from unity installation instead of the project ones when Strip Engine Code is enabled 27 | 28 | 29 | ## [1.0.2] 30 | ### Changed 31 | - Moved the Menu to `Window/Analysis/Smart Symbolicate` 32 | 33 | ## [1.0.1] 34 | ### Add 35 | - Added a symbol selection option 36 | 37 | ## [1.0.0] 38 | ### Add 39 | - Initial implementation of the Smart Symbolicate 40 | 41 | [1.1.1]: https://github.com/brunomikoski/UnitySmartSymbolicate/releases/tag/v1.1.1 42 | [1.1.0]: https://github.com/brunomikoski/UnitySmartSymbolicate/releases/tag/v1.1.0 43 | [1.0.3]: https://github.com/brunomikoski/UnitySmartSymbolicate/releases/tag/v1.0.3 44 | [1.0.2]: https://github.com/brunomikoski/UnitySmartSymbolicate/releases/tag/v1.0.2 45 | [1.0.1]: https://github.com/brunomikoski/UnitySmartSymbolicate/releases/tag/v1.0.1 46 | [1.0.0]: https://github.com/brunomikoski/UnitySmartSymbolicate/releases/tag/v1.0.0 47 | -------------------------------------------------------------------------------- /CHANGELOG.MD.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2cc98d63797d66245bb7867fcb950530 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bruno Mikoski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec045a84b366e304b80921485115eba0 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Smart Symbolicate 2 | 3 | image 4 | 5 | 6 |

7 | 8 | GitHub license 9 | 10 | 11 |

12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 | GitHub issues 20 | 21 | 22 | 23 | GitHub pull requests 24 | 25 | 26 | GitHub last commit 27 |

28 | 29 |

30 | 31 | GitHub followers 32 | 33 | 34 | Twitter Follow 35 | 36 |

37 | 38 | 39 |

40 | 41 |

42 | 43 | 44 | ## Features 45 | - Automatically identify Google Play Crash reports information and settings 46 | 47 | ## How to use? 48 | - Define unity installation folder (It should be the folder where all your unity installations are) 49 | - Define your project [symbols](https://docs.unity3d.com/2020.3/Documentation/Manual/android-symbols.html) folder 50 | - Paste google play crash rates on the input 51 | - Validate information 52 | - Press Parse 53 | 54 | ## FAQ 55 | 56 | ## System Requirements 57 | Unity 2018.4.0 or later versions 58 | 59 | 60 | ## How to install 61 | 62 | 63 | 64 |
65 | Add from OpenUPM | via scoped registry, recommended 66 | 67 | This package is available on OpenUPM: https://openupm.com/packages/com.brunomikoski.animationsequencer 68 | 69 | To add it the package to your project: 70 | 71 | - open `Edit/Project Settings/Package Manager` 72 | - add a new Scoped Registry: 73 | ``` 74 | Name: OpenUPM 75 | URL: https://package.openupm.com/ 76 | Scope(s): com.brunomikoski 77 | ``` 78 | - click Save 79 | - open Package Manager 80 | - click + 81 | - select Add from Git URL 82 | - paste `com.brunomikoski.unitysmartsymbolicate` 83 | - click Add 84 |
85 | 86 |
87 | Add from GitHub | not recommended, no updates :( 88 | 89 | You can also add it directly from GitHub on Unity 2019.4+. Note that you won't be able to receive updates through Package Manager this way, you'll have to update manually. 90 | 91 | - open Package Manager 92 | - click + 93 | - select Add from Git URL 94 | - paste `https://github.com/brunomikoski/UnitySmartSymbolicate.git` 95 | - click Add 96 |
97 | 98 | 99 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7cf8fddf6294fdd4fbcd08c342a569a9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10cd6ef00afd26641b0c86de5f4b1015 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff3ad93ba63dab447b53541b2c32ca26 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor/BrunoMikoski.SmartSymbolicate.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BrunoMikoski.SmartSymbolicate.Editor", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Scripts/Editor/BrunoMikoski.SmartSymbolicate.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bc1e7b1da4c62a54091335e9a9be11d8 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts/Editor/HyperlinkInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using Unity.CodeEditor; 6 | using UnityEditor; 7 | 8 | namespace BrunoMikoski.SmartSymbolicate 9 | { 10 | public static class HyperlinkInterceptor 11 | { 12 | private static EventInfo cachedHyperLinkClickedEvent; 13 | private static EventInfo HyperLinkClickedEvent 14 | { 15 | get 16 | { 17 | if (cachedHyperLinkClickedEvent == null) 18 | { 19 | cachedHyperLinkClickedEvent = typeof(EditorGUI).GetEvent("hyperLinkClicked", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); 20 | } 21 | return cachedHyperLinkClickedEvent; 22 | } 23 | } 24 | 25 | private static Delegate cachedHyperLinkClickedHandler; 26 | private static Delegate HyperLinkClickedHandler 27 | { 28 | get 29 | { 30 | if (cachedHyperLinkClickedHandler == null) 31 | { 32 | #if UNITY_2021_2_OR_NEWER 33 | Action handler = (_, args) => OnProcessClickData(args.hyperLinkData); 34 | cachedHyperLinkClickedHandler = handler; 35 | #else 36 | if (HyperLinkClickedEvent != null) 37 | { 38 | var method = typeof(HyperlinkInterceptor).GetMethod("OnClicked", BindingFlags.Static | BindingFlags.NonPublic| BindingFlags.Public); 39 | if (method != null) 40 | { 41 | cachedHyperLinkClickedHandler = Delegate.CreateDelegate(HyperLinkClickedEvent.EventHandlerType, method); 42 | } 43 | } 44 | #endif 45 | } 46 | return cachedHyperLinkClickedHandler; 47 | } 48 | } 49 | 50 | public static void Enable() 51 | { 52 | if (HyperLinkClickedHandler == null) 53 | throw new NotSupportedException(); 54 | 55 | #if UNITY_2021_2_OR_NEWER 56 | EditorGUI.hyperLinkClicked += (Action)HyperLinkClickedHandler; 57 | #else 58 | HyperLinkClickedEvent.AddMethod.Invoke(null, new object[] { HyperLinkClickedHandler }); 59 | #endif 60 | } 61 | public static void Disable() 62 | { 63 | if (HyperLinkClickedHandler == null) 64 | throw new NotSupportedException(); 65 | 66 | #if UNITY_2021_2_OR_NEWER 67 | EditorGUI.hyperLinkClicked -= (Action)HyperLinkClickedHandler; 68 | #else 69 | HyperLinkClickedEvent.RemoveMethod.Invoke(null, new object[] { HyperLinkClickedHandler }); 70 | #endif 71 | } 72 | 73 | static PropertyInfo property; 74 | static void OnClicked(object sender, EventArgs args) 75 | { 76 | if (!EditorWindow.HasOpenInstances()) 77 | return; 78 | 79 | if (property == null) 80 | { 81 | property = args.GetType().GetProperty("hyperlinkInfos", BindingFlags.Instance | BindingFlags.Public); 82 | if (property == null) return; 83 | } 84 | var infos = property.GetValue(args); 85 | if (infos is Dictionary) 86 | { 87 | OnProcessClickData((Dictionary)infos); 88 | } 89 | } 90 | 91 | static void OnProcessClickData(Dictionary infos) 92 | { 93 | if (infos == null) return; 94 | if (!infos.TryGetValue("href", out var path)) return; 95 | infos.TryGetValue("line", out var line); 96 | infos.TryGetValue("column", out var column); 97 | 98 | if (File.Exists(path)) 99 | { 100 | int.TryParse(line, out var _line); 101 | int.TryParse(column, out var _column); 102 | OpenFileInIDE(path, _line, _column); 103 | } 104 | } 105 | 106 | public static bool OpenFileInIDE(string filepath, int line, int column) 107 | { 108 | return CodeEditor.CurrentEditor.OpenProject(filepath, line, column); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /Scripts/Editor/HyperlinkInterceptor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 27f5b855b2e24cd0a4e0f9981a6b9b1e 3 | timeCreated: 1652950101 -------------------------------------------------------------------------------- /Scripts/Editor/SmartSymbolicateWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using UnityEditor; 9 | using UnityEngine; 10 | using Debug = UnityEngine.Debug; 11 | 12 | namespace BrunoMikoski.SmartSymbolicate 13 | { 14 | public class SmartSymbolicateWindow : EditorWindow 15 | { 16 | private const string UNITY_PATH_STORAGE_KEY = "SmartSymbolicate.UnityPathStorageKey"; 17 | private const string PROJECT_SYMBOLS_PATH_STORAGE_KEY = "SmartSymbolicate.ProjectSymbolsStorageKey"; 18 | 19 | private const string LIB_IL2CPP_NAME = "libil2cpp"; 20 | private const string LIB_UNITY_NAME = "libunity"; 21 | 22 | private const string DEBUG_EXTENSION = "dbg.so"; 23 | private const string SYM_EXTENSION = "sym.so"; 24 | private const string DEFAULT_EXTENSION = "so"; 25 | private const string SMART_SYMBOLICATE_TITLE_WINDOW = "Smart Symbolicate"; 26 | 27 | private string Addr2Line32Path 28 | { 29 | get 30 | { 31 | if (NDKVersion >= new Version(23, 1)) 32 | { 33 | if (Application.platform == RuntimePlatform.WindowsEditor) 34 | return @"Editor\Data\PlaybackEngines\AndroidPlayer\NDK\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-addr2line.exe"; 35 | return @"PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line"; 36 | } 37 | else 38 | { 39 | if (Application.platform == RuntimePlatform.WindowsEditor) 40 | return @"Editor\Data\PlaybackEngines\AndroidPlayer\NDK\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-addr2line.exe"; 41 | return @"PlaybackEngines/AndroidPlayer/NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line"; 42 | } 43 | } 44 | } 45 | 46 | private string Addr2Line64Path 47 | { 48 | get 49 | { 50 | if (NDKVersion >= new Version(23, 1)) 51 | { 52 | if (Application.platform == RuntimePlatform.WindowsEditor) 53 | return @"Editor\Data\PlaybackEngines\AndroidPlayer\NDK\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-addr2line.exe"; 54 | return @"PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line"; 55 | } 56 | else 57 | { 58 | if (Application.platform == RuntimePlatform.WindowsEditor) 59 | return @"Editor\Data\PlaybackEngines\AndroidPlayer\NDK\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin\aarch64-linux-android-addr2line.exe"; 60 | return @"PlaybackEngines/AndroidPlayer/NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line"; 61 | } 62 | } 63 | } 64 | 65 | private Version NDKVersion 66 | { 67 | get 68 | { 69 | var ndkPath = Application.platform == RuntimePlatform.WindowsEditor ? @"Editor\Data\PlaybackEngines\AndroidPlayer\NDK" : @"PlaybackEngines/AndroidPlayer/NDK"; 70 | var sourceProperties = Path.Combine(unityHubPath, unityVersion, ndkPath, "source.properties"); 71 | if (!File.Exists(sourceProperties)) 72 | throw new Exception($"Couldn't acquire NDK version, '{sourceProperties}' was not found"); 73 | 74 | var contents = File.ReadAllText(sourceProperties); 75 | var regex = new Regex(@"Pkg\.Revision\s*=\s*(?\S+)"); 76 | var match = regex.Match(contents); 77 | if (match.Success) 78 | { 79 | var versionText = match.Groups["version"].Value; 80 | if (!Version.TryParse(versionText, out var version)) 81 | throw new Exception($"Couldn't resolve version from '{sourceProperties}', the value was '{versionText}'"); 82 | return version; 83 | } 84 | else 85 | { 86 | throw new Exception($"Couldn't not find NDK version inside '{sourceProperties}', file contents\n:{contents}"); 87 | } 88 | } 89 | } 90 | 91 | private static string DefaultUnityInstallationFolder 92 | { 93 | get 94 | { 95 | if (Application.platform == RuntimePlatform.OSXEditor) 96 | return @"/Applications/Unity/Hub/Editor"; 97 | return @"C:\Program Files\Unity\Hub\Editor"; 98 | } 99 | } 100 | 101 | private static string UnityVariationsPath 102 | { 103 | get 104 | { 105 | if (Application.platform == RuntimePlatform.WindowsEditor) 106 | return @"Editor\Data\PlaybackEngines\AndroidPlayer\Variations"; 107 | return @"PlaybackEngines/AndroidPlayer/Variations"; 108 | } 109 | } 110 | 111 | 112 | private enum ReleaseType 113 | { 114 | Release = 0, 115 | Development = 1 116 | } 117 | 118 | private enum ScriptingBackendType 119 | { 120 | il2cpp = 0, 121 | mono = 1 122 | } 123 | 124 | private enum CPUType 125 | { 126 | arm64_v8a = 0, 127 | armeabi_v7a = 1 128 | } 129 | 130 | private enum SymbolsType 131 | { 132 | Auto, 133 | libil2cpp, 134 | libunity 135 | } 136 | 137 | private string unityHubPath 138 | { 139 | get => EditorPrefs.GetString(UNITY_PATH_STORAGE_KEY, DefaultUnityInstallationFolder); 140 | set => EditorPrefs.SetString(UNITY_PATH_STORAGE_KEY, value); 141 | } 142 | 143 | private string projectSymbolsPath 144 | { 145 | get => EditorPrefs.GetString(PROJECT_SYMBOLS_PATH_STORAGE_KEY, string.Empty); 146 | set => EditorPrefs.SetString(PROJECT_SYMBOLS_PATH_STORAGE_KEY, value); 147 | } 148 | 149 | private string crashInput; 150 | private string unityVersion; 151 | private string output; 152 | 153 | private ReleaseType releaseType = ReleaseType.Release; 154 | private ScriptingBackendType scriptingBackendType = ScriptingBackendType.il2cpp; 155 | private CPUType cpuType = CPUType.arm64_v8a; 156 | private SymbolsType symbolsType = SymbolsType.Auto; 157 | 158 | private string[] releaseTypeDisplayNames = Array.Empty(); 159 | private string[] scriptingBackendTypeNames = Array.Empty(); 160 | private string[] symbolTypeNames = Array.Empty(); 161 | private string[] cpuTypeNames = Array.Empty(); 162 | private string[] availableUnityVersions = Array.Empty(); 163 | 164 | private bool validUnityHubFolder; 165 | private bool isMissingUnityVersion; 166 | private string desiredUnityVersion; 167 | private bool isMissingCPUType; 168 | private string desiredCPUType; 169 | 170 | private Vector2 outputScrollView; 171 | private GUIStyle outputTextFieldStyle; 172 | private Vector2 inputScrollView; 173 | private bool printCommands; 174 | 175 | private static HashSet unknowLibs = new HashSet(); 176 | 177 | [MenuItem("Window/Analysis/Smart Symbolicate", false, 1000)] 178 | public static void ShowExample() 179 | { 180 | SmartSymbolicateWindow wnd = GetWindow(); 181 | wnd.titleContent = new GUIContent(SMART_SYMBOLICATE_TITLE_WINDOW); 182 | } 183 | 184 | private void OnEnable() 185 | { 186 | GenerateDisplayNames(); 187 | ValidateUnityHubPath(unityHubPath); 188 | HyperlinkInterceptor.Enable(); 189 | } 190 | 191 | private void OnDisable() 192 | { 193 | HyperlinkInterceptor.Disable(); 194 | } 195 | 196 | private void GenerateDisplayNames() 197 | { 198 | releaseTypeDisplayNames = Enum.GetNames(typeof(ReleaseType)); 199 | scriptingBackendTypeNames = Enum.GetNames(typeof(ScriptingBackendType)); 200 | cpuTypeNames = Enum.GetNames(typeof(CPUType)); 201 | symbolTypeNames = Enum.GetNames(typeof(SymbolsType)); 202 | 203 | for (int i = 0; i < cpuTypeNames.Length; i++) 204 | cpuTypeNames[i] = cpuTypeNames[i].Replace("_", "-"); 205 | } 206 | 207 | private void OnGUI() 208 | { 209 | if (outputTextFieldStyle == null) 210 | outputTextFieldStyle = new GUIStyle(EditorStyles.textArea) { wordWrap = true, richText = true }; 211 | 212 | DrawPaths(); 213 | DrawInput(); 214 | DrawSettings(); 215 | DrawOutput(); 216 | } 217 | 218 | private void DrawOutput() 219 | { 220 | EditorGUILayout.BeginVertical("HelpBox"); 221 | EditorGUILayout.LabelField("Output", EditorStyles.foldoutHeader); 222 | EditorGUILayout.Separator(); 223 | 224 | outputScrollView = EditorGUILayout.BeginScrollView(outputScrollView, false, true, GUILayout.ExpandHeight(true)); 225 | EditorGUILayout.TextArea(output, outputTextFieldStyle, GUILayout.ExpandHeight(true), 226 | GUILayout.ExpandWidth(true)); 227 | EditorGUILayout.EndScrollView(); 228 | 229 | EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(crashInput) || string.IsNullOrEmpty(unityHubPath) || string.IsNullOrEmpty(projectSymbolsPath)); 230 | if (GUILayout.Button("Parse")) 231 | { 232 | ParseInput(); 233 | } 234 | 235 | EditorGUI.EndDisabledGroup(); 236 | 237 | EditorGUILayout.EndVertical(); 238 | } 239 | 240 | private void ParseInput() 241 | { 242 | unknowLibs.Clear(); 243 | 244 | string targetADDR2 = GetTargetAddr2line(); 245 | if (!File.Exists(targetADDR2)) 246 | { 247 | Debug.LogError($"Failed to find ADDR2Line at path {targetADDR2}"); 248 | return; 249 | } 250 | 251 | StringBuilder parsedResults = new StringBuilder(); 252 | string[] lines = crashInput.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); 253 | 254 | for (int i = 0; i < lines.Length; i++) 255 | { 256 | string line = lines[i]; 257 | 258 | Match memoryAddress = Regex.Match(line, @"0[xX][0-9a-fA-F]+"); 259 | if (!memoryAddress.Success) 260 | { 261 | Regex removeEmptyLines = new Regex(@"\s+"); 262 | if (removeEmptyLines.Replace(line, "").Length < 3) 263 | continue; 264 | 265 | parsedResults.AppendLine(line); 266 | continue; 267 | } 268 | 269 | string targetLib = string.Empty; 270 | if (symbolsType == SymbolsType.Auto) 271 | { 272 | Match libMatch = Regex.Match(line, @"(?<=at )(.*)(?=\.)"); 273 | if (!libMatch.Success) 274 | continue; 275 | 276 | targetLib = libMatch.Value; 277 | } 278 | else if (symbolsType == SymbolsType.libunity) 279 | { 280 | targetLib = Path.GetFileNameWithoutExtension(LIB_UNITY_NAME); 281 | } 282 | else if (symbolsType == SymbolsType.libil2cpp) 283 | { 284 | targetLib = Path.GetFileNameWithoutExtension(LIB_IL2CPP_NAME); 285 | } 286 | 287 | string memoryAddressValue = memoryAddress.Value; 288 | if (!TryGetLibPath(targetLib, out string knowPath)) 289 | { 290 | parsedResults.AppendLine($"Unknow lib named {targetLib} :: {memoryAddressValue}"); 291 | continue; 292 | } 293 | 294 | if (!File.Exists(knowPath)) 295 | { 296 | parsedResults.AppendLine($"Failed to find lib {targetLib} at Path: {knowPath}"); 297 | continue; 298 | } 299 | 300 | if (printCommands) 301 | { 302 | parsedResults.AppendLine($"Executing Command: {targetADDR2} -f -C -e \"{knowPath}\" {memoryAddressValue}"); 303 | } 304 | 305 | EditorUtility.DisplayProgressBar("Processing Symbols", $"Processing {memoryAddressValue} against {targetLib}", (float)i / lines.Length); 306 | 307 | using (Process process = new Process()) 308 | { 309 | process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; 310 | process.StartInfo.FileName = targetADDR2; 311 | process.StartInfo.Arguments = $"-f -C -e \"{knowPath}\" {memoryAddressValue}"; 312 | process.StartInfo.UseShellExecute = false; 313 | process.StartInfo.CreateNoWindow = true; 314 | process.StartInfo.RedirectStandardOutput = true; 315 | process.StartInfo.RedirectStandardInput = true; 316 | process.StartInfo.RedirectStandardError = true; 317 | process.Start(); 318 | 319 | string processOutput = process.StandardOutput.ReadToEnd().Replace("??:?", string.Empty); 320 | 321 | if (targetLib.IndexOf(LIB_IL2CPP_NAME, StringComparison.Ordinal) > -1) 322 | { 323 | TryGenerateCodeHyperlinkForOutput(ref processOutput); 324 | } 325 | 326 | parsedResults.AppendLine($" at {GetLibraryDisplay(targetLib)}.{memoryAddressValue} => {processOutput}"); 327 | 328 | string error = process.StandardError.ReadToEnd(); 329 | if (!string.IsNullOrEmpty(error)) 330 | parsedResults.AppendLine($"[Error] {memoryAddressValue} => {error}"); 331 | 332 | process.WaitForExit(); 333 | } 334 | } 335 | 336 | output = Regex.Replace(parsedResults.ToString(), @"^\s+$[\r\n]*", string.Empty, RegexOptions.Multiline); 337 | 338 | EditorUtility.ClearProgressBar(); 339 | } 340 | 341 | private string GetLibraryDisplay(string targetLib) 342 | { 343 | string color = "green"; 344 | if (string.Equals(targetLib, "libunity", StringComparison.Ordinal)) 345 | color = "yellow"; 346 | if (unknowLibs.Contains(targetLib)) 347 | color = "red"; 348 | 349 | return $"{targetLib}"; 350 | } 351 | 352 | private void TryGenerateCodeHyperlinkForOutput(ref string outputLine) 353 | { 354 | try 355 | { 356 | if (string.IsNullOrEmpty(outputLine)) 357 | return; 358 | 359 | string[] splitResults = outputLine.Split(new[] { "_" }, StringSplitOptions.RemoveEmptyEntries); 360 | if (splitResults.Length < 2) 361 | return; 362 | 363 | string className = splitResults[0]; 364 | string methodName = splitResults[1]; 365 | 366 | string[] classGUIDs = AssetDatabase.FindAssets($"{className} t:TextAsset"); 367 | if (classGUIDs.Length == 0) 368 | return; 369 | 370 | string classPath = string.Empty; 371 | 372 | for (int i = 0; i < classGUIDs.Length; i++) 373 | { 374 | string possibleClass = AssetDatabase.GUIDToAssetPath(classGUIDs[i]); 375 | string possibleClassName = Path.GetFileNameWithoutExtension(possibleClass); 376 | if (string.Equals(possibleClassName, className, StringComparison.Ordinal)) 377 | { 378 | classPath = possibleClass; 379 | break; 380 | } 381 | } 382 | 383 | if (string.IsNullOrEmpty(classPath)) 384 | return; 385 | 386 | string[] lines = File.ReadAllLines(Path.GetFullPath(classPath)); 387 | 388 | int hitCounts = 0; 389 | int targetLine = -1; 390 | 391 | for (int i = 0; i < lines.Length; i++) 392 | { 393 | string line = lines[i]; 394 | 395 | if (line.IndexOf(methodName, StringComparison.Ordinal) == -1) 396 | continue; 397 | 398 | //If is assigning the value somewhere, probably its not the method declaration 399 | if (line.IndexOf("=", StringComparison.OrdinalIgnoreCase) > -1) 400 | continue; 401 | 402 | //If has less than 1 space, probably is a method invocation 403 | if (line.Split(' ').Length < 1) 404 | continue; 405 | 406 | //Ignore . 407 | if (line.IndexOf(".", StringComparison.OrdinalIgnoreCase) > -1) 408 | continue; 409 | 410 | hitCounts++; 411 | targetLine = i + 1; 412 | } 413 | 414 | if (hitCounts <= 0) 415 | return; 416 | 417 | outputLine = outputLine.Replace("\n", string.Empty); 418 | if (hitCounts == 1) 419 | outputLine = $"{outputLine} (at {classPath}:{targetLine} This is a guess :) ) "; 420 | else 421 | outputLine = $"{outputLine} (at {classPath} This is a guess :) ) "; 422 | } 423 | catch (Exception e) 424 | { 425 | Debug.LogException(e); 426 | } 427 | } 428 | 429 | private bool TryGetLibPath(string targetLibName, out string libKnowPath) 430 | { 431 | string targetProjectSymbolsPath = projectSymbolsPath; 432 | targetProjectSymbolsPath = Path.Combine(targetProjectSymbolsPath, cpuTypeNames[(int)cpuType]); 433 | 434 | DirectoryInfo projectPathDirectory = new DirectoryInfo(targetProjectSymbolsPath); 435 | 436 | FileInfo[] targetLibFiles = projectPathDirectory.GetFiles($"{targetLibName}*", SearchOption.AllDirectories); 437 | SortedDictionary prioritySymbols = new SortedDictionary(); 438 | for (int i = 0; i < targetLibFiles.Length; i++) 439 | { 440 | FileInfo fileInfo = targetLibFiles[i]; 441 | 442 | if (!prioritySymbols.ContainsKey(DEBUG_EXTENSION)) 443 | { 444 | if (fileInfo.FullName.IndexOf(DEBUG_EXTENSION, StringComparison.OrdinalIgnoreCase) > -1) 445 | { 446 | prioritySymbols.Add(DEBUG_EXTENSION, fileInfo); 447 | continue; 448 | } 449 | } 450 | 451 | if (!prioritySymbols.ContainsKey(SYM_EXTENSION)) 452 | { 453 | if (fileInfo.FullName.IndexOf(SYM_EXTENSION, StringComparison.OrdinalIgnoreCase) > -1) 454 | { 455 | prioritySymbols.Add(SYM_EXTENSION, fileInfo); 456 | continue; 457 | } 458 | } 459 | 460 | if (!prioritySymbols.ContainsKey(DEFAULT_EXTENSION)) 461 | { 462 | if (fileInfo.FullName.IndexOf(DEFAULT_EXTENSION, StringComparison.OrdinalIgnoreCase) > -1) 463 | { 464 | prioritySymbols.Add(DEFAULT_EXTENSION, fileInfo); 465 | } 466 | } 467 | } 468 | 469 | if (prioritySymbols.TryGetValue(DEBUG_EXTENSION, out FileInfo resultFileInfo)) 470 | { 471 | libKnowPath = resultFileInfo.FullName; 472 | return true; 473 | } 474 | 475 | if (prioritySymbols.TryGetValue(SYM_EXTENSION, out resultFileInfo)) 476 | { 477 | libKnowPath = resultFileInfo.FullName; 478 | return true; 479 | } 480 | 481 | if (prioritySymbols.TryGetValue(DEFAULT_EXTENSION, out resultFileInfo)) 482 | { 483 | libKnowPath = resultFileInfo.FullName; 484 | return true; 485 | } 486 | 487 | 488 | if (targetLibName.Equals(LIB_UNITY_NAME, StringComparison.Ordinal)) 489 | { 490 | string targetPath = Path.Combine(unityHubPath, unityVersion); 491 | targetPath = Path.Combine(targetPath, UnityVariationsPath); 492 | 493 | targetPath = Path.Combine(targetPath, scriptingBackendTypeNames[(int)releaseType]); 494 | targetPath = Path.Combine(targetPath, releaseTypeDisplayNames[(int)releaseType]); 495 | targetPath = Path.Combine(targetPath, "Symbols"); 496 | targetPath = Path.Combine(targetPath, cpuTypeNames[(int)cpuType]); 497 | 498 | 499 | targetPath = Path.Combine(targetPath, $"{LIB_UNITY_NAME}.{SYM_EXTENSION}"); 500 | libKnowPath = targetPath; 501 | return true; 502 | } 503 | 504 | 505 | libKnowPath = string.Empty; 506 | unknowLibs.Add(targetLibName); 507 | return false; 508 | } 509 | 510 | private string GetTargetAddr2line() 511 | { 512 | if (cpuType == CPUType.arm64_v8a) 513 | return Path.Combine(Path.Combine(unityHubPath, unityVersion), Addr2Line64Path); 514 | 515 | return Path.Combine(Path.Combine(unityHubPath, unityVersion), Addr2Line32Path); 516 | } 517 | 518 | private void DrawSettings() 519 | { 520 | EditorGUILayout.BeginVertical("HelpBox"); 521 | EditorGUILayout.LabelField("Settings", EditorStyles.foldoutHeader); 522 | EditorGUILayout.Separator(); 523 | EditorGUI.indentLevel++; 524 | 525 | EditorGUILayout.BeginHorizontal(); 526 | 527 | EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(100), GUILayout.Height(68)); 528 | EditorGUILayout.LabelField("Unity Version", EditorStyles.boldLabel); 529 | EditorGUI.BeginChangeCheck(); 530 | 531 | if (validUnityHubFolder) 532 | { 533 | if (isMissingUnityVersion) 534 | { 535 | EditorGUILayout.HelpBox($"Crash Reports needs Unity version {desiredUnityVersion}", 536 | MessageType.Error); 537 | } 538 | else 539 | { 540 | int selectedUnityVersion = Mathf.Clamp(Array.IndexOf(availableUnityVersions, unityVersion), 0, 541 | availableUnityVersions.Length); 542 | unityVersion = availableUnityVersions[EditorGUILayout.Popup(selectedUnityVersion, availableUnityVersions)]; 543 | printCommands = EditorGUILayout.ToggleLeft("Print Commands", printCommands); 544 | } 545 | } 546 | else 547 | { 548 | EditorGUILayout.HelpBox("Invalid Unity Hub Folder", MessageType.Error); 549 | } 550 | 551 | EditorGUILayout.EndVertical(); 552 | 553 | EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(100)); 554 | EditorGUILayout.LabelField("Release Type", EditorStyles.boldLabel); 555 | releaseType = (ReleaseType)GUILayout.SelectionGrid((int)releaseType, releaseTypeDisplayNames, 1, EditorStyles.radioButton); 556 | EditorGUILayout.EndVertical(); 557 | 558 | EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(100)); 559 | EditorGUILayout.LabelField("Scripting Backend", EditorStyles.boldLabel); 560 | scriptingBackendType = (ScriptingBackendType)GUILayout.SelectionGrid((int)scriptingBackendType, scriptingBackendTypeNames, 1, EditorStyles.radioButton); 561 | EditorGUILayout.EndVertical(); 562 | 563 | EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(100)); 564 | EditorGUILayout.LabelField("CPU", EditorStyles.boldLabel); 565 | if (!isMissingCPUType) 566 | { 567 | cpuType = (CPUType)GUILayout.SelectionGrid((int)cpuType, cpuTypeNames, 1, EditorStyles.radioButton); 568 | } 569 | else 570 | { 571 | EditorGUILayout.HelpBox($"Missing CPU Type {desiredCPUType}", MessageType.Error); 572 | } 573 | EditorGUILayout.EndVertical(); 574 | 575 | 576 | EditorGUILayout.BeginVertical("HelpBox", GUILayout.Width(100)); 577 | EditorGUILayout.LabelField("Symbols", EditorStyles.boldLabel); 578 | symbolsType = (SymbolsType)GUILayout.SelectionGrid((int)symbolsType, symbolTypeNames, 2, EditorStyles.radioButton); 579 | EditorGUILayout.EndVertical(); 580 | 581 | EditorGUILayout.EndHorizontal(); 582 | 583 | 584 | EditorGUI.indentLevel--; 585 | EditorGUILayout.EndVertical(); 586 | } 587 | 588 | private void DrawInput() 589 | { 590 | EditorGUILayout.BeginVertical("HelpBox"); 591 | EditorGUILayout.LabelField("Crash Input", EditorStyles.foldoutHeader); 592 | EditorGUILayout.Separator(); 593 | 594 | inputScrollView = EditorGUILayout.BeginScrollView(inputScrollView, false, true, GUILayout.Height(200)); 595 | EditorGUI.BeginChangeCheck(); 596 | crashInput = EditorGUILayout.TextArea(crashInput, GUILayout.ExpandHeight(true)); 597 | if (EditorGUI.EndChangeCheck()) 598 | TryGetInformationFromCrashInput(); 599 | 600 | EditorGUILayout.EndScrollView(); 601 | 602 | EditorGUILayout.EndVertical(); 603 | } 604 | 605 | private void TryGetInformationFromCrashInput() 606 | { 607 | if (string.IsNullOrEmpty(crashInput)) 608 | { 609 | isMissingCPUType = false; 610 | isMissingUnityVersion = false; 611 | return; 612 | } 613 | 614 | TryGetScriptingBackendFromCrashInput(); 615 | TryGetUnityVersionFromCrashInput(); 616 | TryGetCPUFromCrashInput(); 617 | TryGetBuildType(); 618 | } 619 | 620 | private void TryGetBuildType() 621 | { 622 | Match buildTypeMatch = Regex.Match(crashInput, pattern: @"(?<=Build type ')(.*?)[^']*"); 623 | if (buildTypeMatch.Success) 624 | { 625 | int targetBuildType = Array.IndexOf(releaseTypeDisplayNames, buildTypeMatch.Value); 626 | if (targetBuildType != -1) 627 | { 628 | releaseType = (ReleaseType)targetBuildType; 629 | } 630 | } 631 | } 632 | 633 | private void TryGetCPUFromCrashInput() 634 | { 635 | Match cpuMatch = Regex.Match(crashInput, pattern: @"(?<=CPU ')(.*?)[^']*"); 636 | if (cpuMatch.Success) 637 | { 638 | int targetCPUIndex = Array.IndexOf(cpuTypeNames, cpuMatch.Value); 639 | if (targetCPUIndex == -1) 640 | { 641 | isMissingCPUType = true; 642 | desiredCPUType = cpuMatch.Value; 643 | } 644 | else 645 | { 646 | cpuType = (CPUType)targetCPUIndex; 647 | isMissingCPUType = false; 648 | } 649 | } 650 | else 651 | { 652 | isMissingCPUType = false; 653 | } 654 | } 655 | 656 | private void TryGetUnityVersionFromCrashInput() 657 | { 658 | Match unityVersionMatch = Regex.Match(crashInput, pattern: @"(?<=Version ')(.*?)[^ ']*"); 659 | if (unityVersionMatch.Success) 660 | { 661 | int targetUnityVersionIndex = Array.IndexOf(availableUnityVersions, unityVersionMatch.Value); 662 | if (targetUnityVersionIndex == -1) 663 | { 664 | isMissingUnityVersion = true; 665 | desiredUnityVersion = unityVersionMatch.Value; 666 | } 667 | else 668 | { 669 | isMissingUnityVersion = false; 670 | unityVersion = availableUnityVersions[targetUnityVersionIndex]; 671 | } 672 | } 673 | else 674 | { 675 | isMissingUnityVersion = false; 676 | desiredUnityVersion = "Unknow"; 677 | } 678 | } 679 | 680 | private void TryGetScriptingBackendFromCrashInput() 681 | { 682 | Match scriptingBackendNameMatch = Regex.Match(crashInput, @"(?<=\Scripting Backend ')(.*?)[^']*"); 683 | if (scriptingBackendNameMatch.Success) 684 | { 685 | int newScriptingBackend = Array.IndexOf(scriptingBackendTypeNames, scriptingBackendNameMatch.Value); 686 | if (newScriptingBackend != -1) 687 | scriptingBackendType = (ScriptingBackendType)newScriptingBackend; 688 | } 689 | } 690 | 691 | 692 | private void DrawPaths() 693 | { 694 | EditorGUILayout.BeginVertical("HelpBox"); 695 | EditorGUILayout.LabelField("Paths", EditorStyles.foldoutHeader); 696 | EditorGUILayout.Separator(); 697 | 698 | EditorGUI.indentLevel++; 699 | 700 | EditorGUILayout.BeginHorizontal(); 701 | EditorGUI.BeginChangeCheck(); 702 | string newUnityPath = EditorGUILayout.TextField("Unity Hub Path", unityHubPath); 703 | if (EditorGUI.EndChangeCheck()) 704 | ValidateUnityHubPath(newUnityPath); 705 | 706 | if (GUILayout.Button("Browse", GUILayout.Width(70))) 707 | { 708 | newUnityPath = EditorUtility.OpenFolderPanel("Select Unity Hub Root Folder", unityHubPath, ""); 709 | if (!string.Equals(newUnityPath, unityHubPath)) 710 | ValidateUnityHubPath(newUnityPath); 711 | } 712 | 713 | EditorGUILayout.EndHorizontal(); 714 | 715 | EditorGUILayout.BeginHorizontal(); 716 | projectSymbolsPath = EditorGUILayout.TextField("Project Symbols", projectSymbolsPath); 717 | 718 | if (GUILayout.Button("Browse", GUILayout.Width(70))) 719 | { 720 | projectSymbolsPath = EditorUtility.OpenFolderPanel("Select Unity Hub Root Folder", projectSymbolsPath, ""); 721 | } 722 | 723 | EditorGUILayout.EndHorizontal(); 724 | 725 | 726 | EditorGUI.indentLevel--; 727 | EditorGUILayout.EndVertical(); 728 | } 729 | 730 | private void ValidateUnityHubPath(string targetUnityPath) 731 | { 732 | unityHubPath = targetUnityPath; 733 | 734 | string[] childDirectories = Directory.GetDirectories(targetUnityPath); 735 | 736 | List validUnityPaths = new List(); 737 | 738 | for (int i = 0; i < childDirectories.Length; i++) 739 | { 740 | string folderName = Path.GetFileName(childDirectories[i]); 741 | if (!Regex.Match(folderName, @"^[1-9]\d*(\.[1-9]\d*)*[a-z]*[1-9]").Success) 742 | continue; 743 | 744 | validUnityPaths.Add(folderName); 745 | } 746 | availableUnityVersions = validUnityPaths.ToArray(); 747 | validUnityHubFolder = availableUnityVersions.Length > 0; 748 | } 749 | } 750 | } 751 | -------------------------------------------------------------------------------- /Scripts/Editor/SmartSymbolicateWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6c8efce964139ee4191926b9eb7aa475 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.brunomikoski.unitysmartsymbolicate", 3 | "displayName": "Smart Symbolicate", 4 | "version": "1.1.1", 5 | "unity": "2018.4", 6 | "description": "A tool to help you symbolicate android crashes", 7 | "keywords": [ 8 | "symbolicate", 9 | "google play", 10 | "mobile", 11 | "crash", 12 | "il2cpp" 13 | ], 14 | "category": "editor extensions", 15 | "homepage": "https://github.com/brunomikoski/UnitySmartSymbolicate", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/brunomikoski/UnitySmartSymbolicate.git" 19 | }, 20 | "bugs": "https://github.com/brunomikoski/UnitySmartSymbolicate/issues", 21 | "author": { 22 | "name": "Bruno Mikoski", 23 | "url": "https://www.brunomikoski.com" 24 | }, 25 | "type": "library" 26 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc8b54ed970180344a6acbd1c796e468 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------