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