Add line number to extractor tool
[platform/core/dotnet/launcher.git] / tools / Extractor / dotnet-extractor / dotnet-extractor / LineNumber.cs
1 using Microsoft.DiaSymReader.Tools;\r
2 using System;\r
3 using System.Collections.Generic;\r
4 using System.IO;\r
5 using System.Text;\r
6 using System.Text.RegularExpressions;\r
7 using System.Xml;\r
8 \r
9 namespace Tizen.Runtime.Tools\r
10 {\r
11     public class LineNumber\r
12     {\r
13         private static readonly string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());\r
14 \r
15         public LineNumber()\r
16         {\r
17         }\r
18 \r
19         internal sealed class Args\r
20         {\r
21             public readonly string[] AssemblyPaths;\r
22             public readonly string[] PdbPaths;\r
23             public readonly string ExceptionPath;\r
24             public readonly string OutputPath;\r
25 \r
26             public Args(string[] assemblyPaths, string[] pdbPaths, string exceptionPath, string outputPath)\r
27             {\r
28                 AssemblyPaths = assemblyPaths;\r
29                 PdbPaths = pdbPaths;\r
30                 ExceptionPath = exceptionPath;\r
31                 OutputPath = outputPath;\r
32             }\r
33         }\r
34 \r
35         public static void UsageLine()\r
36         {\r
37             string UsageMsg = "Usage: dotnet-extractor line-number [Log] [Options]\n\n"\r
38                               + "Log:\n"\r
39                               + "<Filepath>                             Path to the exception log file (Filename extension: xxxxx.log)\n\n"\r
40                               //+ "<String>                               Input the log message directly\n\n"\r
41                               + "Options:\n"\r
42                               + " -h, --help                            Show this help message\n"\r
43                               + " -a, --assembly <Path1:Path2:...>      Multiple paths with assembly directories separated by colon(':')\n"\r
44                               + " -p, --pdb <Pdb path>                  Path to the pdb directory (Can be omitted if it is the same as the assembly directory path)\n"\r
45                               + " -o, --out <Output path>               Path to the output file (Default: Output to console. If omitted, the xxxxx.out file is created in the same location as the log file)\n\n"\r
46                               + "Example:\n"\r
47                               + "1. If both assembly and pdb are in the current directory\n"\r
48                               + " # dotnet extractor line-number /tmp/Exception1.log\n"\r
49                               + "2. If both assembly and pdb are in the same directory specified\n"\r
50                               + " # dotnet extractor line-number /tmp/Exception2.log --assembly /opt/usr/globalapps/org.tizen.example.TestApp/:/usr/share/dotnet.tizen/\n"\r
51                               + "3. If assembly and pdb are separated in each directory\n"\r
52                               + " # dotnet extractor line-number /tmp/Exception3.log --assembly /usr/share/dotnet.tizen/framework/ --pdb /tmp/pdbs/\n";\r
53             Console.WriteLine(UsageMsg);\r
54         }\r
55 \r
56         public void LineNumbers(string[] args)\r
57         {\r
58             try\r
59             {\r
60                 Args parsedArgs = ParseArgs(args);\r
61                 Extract(parsedArgs, Convert(parsedArgs));\r
62             }\r
63             catch (Exception e)\r
64             {\r
65                 UsageLine();\r
66                 Console.Error.WriteLine(e.Message);\r
67             }\r
68         }\r
69 \r
70         internal static Args ParseArgs(string[] args)\r
71         {\r
72             string[] assemblyPaths = null;\r
73             string[] pdbPaths = null;\r
74             string exceptionPath = null;\r
75             string outputPath = null;\r
76 \r
77             int i = 1;\r
78             while (i < args.Length)\r
79             {\r
80                 var arg = args[i++];\r
81                 string ReadValue() => (i < args.Length) ? args[i++] : throw new InvalidDataException(string.Format("Missing value for option '{0}'", arg));\r
82                 switch (arg)\r
83                 {\r
84                     case "-h":\r
85                     case "--help":\r
86                         UsageLine();\r
87                         Environment.Exit(0);\r
88                         break;\r
89                     case "-p":\r
90                     case "--pdb":\r
91                         pdbPaths = ReadValue().Split(":");\r
92                         break;\r
93                     case "-o":\r
94                     case "--out":\r
95                         outputPath = ReadValue();\r
96                         break;\r
97                     case "-a":\r
98                     case "--assembly":\r
99                         assemblyPaths = ReadValue().Split(":");\r
100                         break;\r
101                     default:\r
102                         if (arg.Contains("-"))\r
103                         {\r
104                             UsageLine();\r
105                             Console.WriteLine($"Unknown option [{arg}]\n");\r
106                             Environment.Exit(0);\r
107                             break;\r
108                         }\r
109                         exceptionPath ??= arg;\r
110                         if (!File.Exists(exceptionPath))\r
111                         {\r
112                             throw new FileNotFoundException("Log file not found\n");\r
113                         }\r
114                         break;\r
115                 }\r
116             }\r
117             if (exceptionPath == null)\r
118             {\r
119                 throw new InvalidDataException("Missing exception log path\n");\r
120             }\r
121             assemblyPaths ??= new string[] { Directory.GetCurrentDirectory() };\r
122             pdbPaths ??= assemblyPaths;\r
123             try\r
124             {\r
125                 outputPath ??= Path.ChangeExtension(exceptionPath, "out");\r
126             }\r
127             catch (Exception e)\r
128             {\r
129                 throw new InvalidDataException(e.Message);\r
130             }\r
131 \r
132             return new Args(\r
133                 assemblyPaths: assemblyPaths,\r
134                 pdbPaths: pdbPaths,\r
135                 exceptionPath: exceptionPath,\r
136                 outputPath: outputPath);\r
137         }\r
138 \r
139         private static List<string> GrabFiles(string[] paths, string searchPattern)\r
140         {\r
141             List<string> files = new List<string>();\r
142             foreach (var assemDir in paths)\r
143             {\r
144                 if (Directory.Exists(assemDir))\r
145                 {\r
146                     foreach (var peFile in Directory.GetFiles(assemDir, searchPattern, SearchOption.AllDirectories))\r
147                     {\r
148                         files.Add(peFile);\r
149                     }\r
150                 }\r
151             }\r
152             return files;\r
153         }\r
154 \r
155         /*\r
156         * Convert\r
157         */\r
158         private static List<string> Convert(Args args)\r
159         {\r
160             List<string> peFiles = GrabFiles(args.AssemblyPaths, "*.dll");\r
161             if (peFiles.Count == 0)\r
162             {\r
163                 throw new FileNotFoundException("Assembly file not found\n");\r
164             }\r
165 \r
166             List<string> pdbFiles = GrabFiles(args.PdbPaths, "*.pdb");\r
167             if (pdbFiles.Count == 0)\r
168             {\r
169                 throw new FileNotFoundException("PDB file not found\n");\r
170             }\r
171 \r
172             Console.Write("Converting pdb to xml...");\r
173             Console.SetCursorPosition(0, Console.CursorTop);\r
174 \r
175             Directory.CreateDirectory(tempDirectory);\r
176 \r
177             List<string> xmlList = new List<string>();\r
178             foreach (var pdbFile in pdbFiles)\r
179             {\r
180                 foreach (var peFile in peFiles)\r
181                 {\r
182                     if (Path.GetFileNameWithoutExtension(peFile) == Path.GetFileNameWithoutExtension(pdbFile))\r
183                     {\r
184                         string xmlPath = Path.ChangeExtension(peFile, "xml");\r
185                         if (xmlPath.Contains("/usr/share/dotnet"))\r
186                         {\r
187                             xmlPath = tempDirectory + "/" + Path.GetFileName(xmlPath);\r
188                         }\r
189                         GetXmlFromPdb(peFile, pdbFile, xmlPath);\r
190                         xmlList.Add(xmlPath);\r
191                         break;\r
192                     }\r
193                 }\r
194             }\r
195             return xmlList;\r
196         }\r
197 \r
198         private static void GetXmlFromPdb(string assemblyPath, string pdbPath, string xmlPath)\r
199         {\r
200             using var peStream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read);\r
201             using var pdbStream = new FileStream(pdbPath, FileMode.Open, FileAccess.Read);\r
202             using var dstFileStream = new FileStream(xmlPath, FileMode.Create, FileAccess.ReadWrite);\r
203             using var sw = new StreamWriter(dstFileStream, Encoding.UTF8);\r
204             PdbToXmlOptions options = PdbToXmlOptions.ResolveTokens | PdbToXmlOptions.IncludeModuleDebugInfo | PdbToXmlOptions.IncludeTokens;\r
205 \r
206             PdbToXmlConverter.ToXml(sw, pdbStream, peStream, options);\r
207         }\r
208 \r
209         /*\r
210          * Extract\r
211          */\r
212         private static void Extract(Args args, List<string> xmlList)\r
213         {\r
214             string logFile = args.ExceptionPath;\r
215             string outputPath = args.OutputPath;\r
216 \r
217             if (xmlList.Count == 0)\r
218             {\r
219                 throw new FileNotFoundException("Xml file not found\n");\r
220             }\r
221 \r
222             GetLineFromLog(logFile, xmlList, outputPath);\r
223 \r
224             if (Directory.Exists(tempDirectory))\r
225             {\r
226                 Directory.Delete(tempDirectory, true);\r
227             }\r
228         }\r
229 \r
230         internal sealed class StackTraceInfo\r
231         {\r
232             public string Type;\r
233             public string Method;\r
234             public string Param;\r
235             public string Assembly;\r
236             public string Token;\r
237             public string Offset;\r
238             public string Document;\r
239             public string Filepath;\r
240             public string Filename;\r
241             public string StartLine;\r
242             public string EndLine;\r
243         }\r
244 \r
245         private static void GetLineFromLog(string logPath, List<string> xmlList, string outputPath)\r
246         {\r
247             Console.WriteLine("Extraction result:      ");\r
248             try\r
249             {\r
250                 using StreamReader fsr = new StreamReader(new FileStream(logPath, FileMode.Open, FileAccess.Read));\r
251                 using StreamWriter fsw = new StreamWriter(new FileStream(outputPath, FileMode.Create, FileAccess.Write));\r
252 \r
253                 bool isParsed = false;\r
254                 while (!fsr.EndOfStream)\r
255                 {\r
256                     string line = fsr.ReadLine();\r
257                     if (!line.Contains(" at "))\r
258                     {\r
259                         continue;\r
260                     }\r
261                     string typeMethodStr = Regex.Match(line, " at (?<splitdata>.*?)\\(\\)").Groups["splitdata"].Value;\r
262                     string methodStr = typeMethodStr.Split(".")[^1];\r
263                     string typenameStr = typeMethodStr.Replace("." + methodStr, "");\r
264                     string assemblyStr = Regex.Match(line, " in (?<splitdata>.*?)\\: ").Groups["splitdata"].Value;\r
265                     string tokenStr = Regex.Match(line, " method_token\\((?<splitdata>.*?)\\)\\,").Groups["splitdata"].Value;\r
266                     string offsetStr = Regex.Match(line, " il_offset\\((?<splitdata>.*?)\\)").Groups["splitdata"].Value;\r
267                     string xmlStr = assemblyStr.Replace(".dll", ".xml");\r
268 \r
269                     StackTraceInfo stInfo = new StackTraceInfo() { Type = typenameStr, Method = methodStr, Assembly = assemblyStr, Token = tokenStr, Offset = offsetStr/*, Xml = xmlStr*/ };\r
270 \r
271                     foreach (var xmlPath in xmlList)\r
272                     {\r
273                         if (xmlPath.Contains(xmlStr))\r
274                         {\r
275                             isParsed = true;\r
276                             GetLineFromXml(xmlPath, stInfo);\r
277                             if (stInfo.Filepath == null || stInfo.StartLine == null)\r
278                             {\r
279                                 Console.WriteLine("    ===== PARSE ERROR FOR EXCEPTION LOG IN THIS LINE. PLEASE RECHECK THE EXCEPTION LOG =====");\r
280                                 break;\r
281                             }\r
282                             string ret = $" at {stInfo.Type}.{stInfo.Method}({stInfo.Param}) in {stInfo.Filepath}:line {stInfo.StartLine}";\r
283                             fsw.WriteLine(ret);\r
284                             Console.WriteLine(ret);\r
285                         }\r
286                     }\r
287                 }\r
288                 if (!isParsed)\r
289                 {\r
290                     Console.WriteLine(" There is no content matching the exception log.");\r
291                     Console.WriteLine(" Please recheck the assembly and pdb directory path.\n");\r
292                 }\r
293                 else\r
294                 {\r
295                     Console.WriteLine($"\nOutput: {outputPath}\n");\r
296                 }\r
297             }\r
298             catch (Exception e)\r
299             {\r
300                 Console.WriteLine(e);\r
301             }\r
302         }\r
303 \r
304         private static void GetLineFromXml(string xmlPath, StackTraceInfo stInfo)\r
305         {\r
306             try\r
307             {\r
308                 XmlDocument xmlDoc = new XmlDocument();\r
309                 xmlDoc.Load(xmlPath);\r
310                 XmlElement xRoot = xmlDoc.DocumentElement;\r
311                 XmlNodeList xnList = xRoot.ChildNodes;\r
312                 int xnCount = xnList.Count;\r
313                 if (xnCount > 0)\r
314                 {\r
315                     for (int i = xnCount - 1; i >= 0; i--)\r
316                     {\r
317                         XmlNode node = xnList[i];\r
318                         if (node.Name == "files")\r
319                         {\r
320                             ParseFile(node.ChildNodes, stInfo);\r
321                         }\r
322                         else if (node.Name == "methods")\r
323                         {\r
324                             ParseMethod(node.ChildNodes, stInfo);\r
325                         }\r
326                     }\r
327                 }\r
328             }\r
329             catch (ArgumentException e)\r
330             {\r
331                 Console.WriteLine(e);\r
332             }\r
333         }\r
334 \r
335         private static void ParseFile(XmlNodeList xn, StackTraceInfo stInfo)\r
336         {\r
337             try\r
338             {\r
339                 foreach (XmlNode node in xn)\r
340                 {\r
341                     if (stInfo.Document == node.Attributes["id"].Value)\r
342                     {\r
343                         stInfo.Filepath = node.Attributes["name"].Value;\r
344                         stInfo.Filename = Path.GetFileName(node.Attributes["name"].Value);\r
345                     }\r
346                 }\r
347             }\r
348             catch (ArgumentException e)\r
349             {\r
350                 Console.WriteLine(e);\r
351             }\r
352         }\r
353 \r
354         private static void ParseMethod(XmlNodeList xn, StackTraceInfo stInfo)\r
355         {\r
356             try\r
357             {\r
358                 foreach (XmlNode node in xn)\r
359                 {\r
360                     if (stInfo.Type == node.Attributes["containingType"].Value &&\r
361                         stInfo.Method == node.Attributes["name"].Value &&\r
362                         stInfo.Token == node.Attributes["token"].Value)\r
363                     {\r
364                         if (node.Attributes.Item(2).Name == "parameterNames")\r
365                         {\r
366                             stInfo.Param = node.Attributes["parameterNames"].Value;\r
367                         }\r
368                         ParseSequence(node.ChildNodes, stInfo);\r
369                     }\r
370                 }\r
371             }\r
372             catch (ArgumentException e)\r
373             {\r
374                 Console.WriteLine(e);\r
375             }\r
376         }\r
377 \r
378         private static void ParseSequence(XmlNodeList xn, StackTraceInfo stInfo)\r
379         {\r
380             try\r
381             {\r
382                 foreach (XmlNode node in xn)\r
383                 {\r
384                     if (node.Name == "sequencePoints")\r
385                     {\r
386                         ParseEntry(node.ChildNodes, stInfo);\r
387                     }\r
388                 }\r
389             }\r
390             catch (ArgumentException e)\r
391             {\r
392                 Console.WriteLine(e);\r
393             }\r
394         }\r
395 \r
396         private static void ParseEntry(XmlNodeList xn, StackTraceInfo stInfo)\r
397         {\r
398             try\r
399             {\r
400                 foreach (XmlNode node in xn)\r
401                 {\r
402                     if (stInfo.Offset == node.Attributes["offset"].Value)\r
403                     {\r
404                         if (node.Attributes.Item(1).Name == "startLine")\r
405                         {\r
406                             stInfo.StartLine = node.Attributes["startLine"].Value;\r
407                         }\r
408                         if (node.Attributes.Item(3).Name == "endLine")\r
409                         {\r
410                             stInfo.EndLine = node.Attributes["endLine"].Value;\r
411                         }\r
412                         stInfo.Document = node.Attributes["document"].Value;\r
413                     }\r
414                 }\r
415             }\r
416             catch (ArgumentException e)\r
417             {\r
418                 Console.WriteLine(e);\r
419             }\r
420         }\r
421     }\r
422 }\r