diff --git a/src/Tools/dotnet-stack/Program.cs b/src/Tools/dotnet-stack/Program.cs index 34f9a61b07..f06f08c838 100644 --- a/src/Tools/dotnet-stack/Program.cs +++ b/src/Tools/dotnet-stack/Program.cs @@ -16,6 +16,7 @@ public static Task Main(string[] args) var parser = new CommandLineBuilder() .AddCommand(ReportCommandHandler.ReportCommand()) .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that traces can be collected")) + .AddCommand(SymbolicateHandler.SymbolicateCommand()) .UseDefaults() .Build(); diff --git a/src/Tools/dotnet-stack/Symbolicate.cs b/src/Tools/dotnet-stack/Symbolicate.cs new file mode 100644 index 0000000000..1c88d94a70 --- /dev/null +++ b/src/Tools/dotnet-stack/Symbolicate.cs @@ -0,0 +1,334 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Tools.Common; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Binding; +using System.CommandLine.IO; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.Tools.Stack +{ + internal static class SymbolicateHandler + { + private static readonly Regex s_regex = new Regex(@" at (?[\w+\.?]+)\.(?\w+)\((?.*)\) in (?[\w+\.?]+):token (?0x\d+)\+(?0x\d+)", RegexOptions.Compiled); + private static readonly Dictionary s_assemblyFilePathDictionary = new Dictionary(); + private static readonly Dictionary s_metadataReaderDictionary = new Dictionary(); + + delegate void SymbolicateDelegate(IConsole console, FileInfo inputPath, DirectoryInfo[] searchDir, FileInfo output, bool stdout); + + /// + /// Get the line number from the Method Token and IL Offset in a stacktrace + /// + /// + /// Path to the stacktrace text file + /// Path of multiple directories with assembly and pdb where the exception occurred + /// Output directly to a file + /// Output directly to a console + /// + private static void Symbolicate(IConsole console, FileInfo inputPath, DirectoryInfo[] searchDir, FileInfo output, bool stdout) + { + try + { + if (output == null) + { + output = new FileInfo(inputPath.FullName + ".symbolicated"); + } + + SetAssemblyFilePathDictionary(console, searchDir); + + CreateSymbolicateFile(console, inputPath.FullName, output.FullName, stdout); + } + catch (Exception e) + { + console.Error.WriteLine(e.Message); + } + } + + private static void SetAssemblyFilePathDictionary(IConsole console, DirectoryInfo[] searchDir) + { + try + { + List searchPaths = new List + { + Directory.GetCurrentDirectory() + }; + foreach (var path in searchDir) + { + searchPaths.Add(path.FullName); + } + + List peFiles = GrabFiles(searchPaths, "*.dll"); + if (peFiles.Count == 0) + { + throw new FileNotFoundException("Assembly file not found\n"); + } + peFiles = peFiles.Distinct().ToList(); + peFiles.Sort(); + + List pdbFiles = GrabFiles(searchPaths, "*.pdb"); + if (pdbFiles.Count == 0) + { + throw new FileNotFoundException("PDB file not found\n"); + } + pdbFiles = pdbFiles.Distinct().ToList(); + pdbFiles.Sort(); + + int pdbCnt = 0; + for (int peCnt = 0; peCnt < peFiles.Count; peCnt++) + { + if (peFiles[peCnt].Contains(".ni.dll")) + { + continue; + } + int compare = string.Compare(Path.GetFileNameWithoutExtension(peFiles[peCnt]), Path.GetFileNameWithoutExtension(pdbFiles[pdbCnt]), StringComparison.OrdinalIgnoreCase); + if (compare == 0) + { + s_assemblyFilePathDictionary.Add(Path.GetFileNameWithoutExtension(peFiles[peCnt]), peFiles[peCnt]); + } + else if (compare > 0) + { + pdbCnt++; + peCnt--; + } + if (pdbCnt == pdbFiles.Count) break; + } + } + catch (Exception e) + { + console.Error.WriteLine(e.Message); + } + } + + private static List GrabFiles(List paths, string searchPattern) + { + try + { + List files = new List(); + foreach (var assemDir in paths) + { + if (Directory.Exists(assemDir)) + { + files.AddRange(Directory.GetFiles(assemDir, searchPattern, SearchOption.AllDirectories)); + } + } + return files; + } + catch + { + return new List(); + } + } + + private static void CreateSymbolicateFile(IConsole console, string inputPath, string outputPath, bool isStdout) + { + try + { + using StreamWriter fileStreamWriter = new StreamWriter(new FileStream(outputPath, FileMode.Create, FileAccess.Write)); + using StreamReader fileStreamReader = new StreamReader(new FileStream(inputPath, FileMode.Open, FileAccess.Read)); + while (!fileStreamReader.EndOfStream) + { + string ret = TrySymbolicateLine(fileStreamReader.ReadLine()); + fileStreamWriter?.WriteLine(ret); + if (isStdout) console.Out.WriteLine(ret); + } + console.Out.WriteLine($"\nOutput: {outputPath}\n"); + } + catch (Exception e) + { + console.Error.WriteLine(e.Message); + } + } + + internal sealed class StackTraceInfo + { + public string Type; + public string Method; + public string Param; + public string Filename; + public string Assembly; + public string Pdb; + public string Token; + public string Offset; + } + + private static string TrySymbolicateLine(string line) + { + Match match = s_regex.Match(line); + if (!match.Success) + { + return line; + } + + StackTraceInfo stInfo = new StackTraceInfo() + { + Type = match.Groups["type"].Value, + Method = match.Groups["method"].Value, + Param = match.Groups["params"].Value, + Assembly = match.Groups["filename"].Value, + Token = match.Groups["token"].Value, + Offset = match.Groups["offset"].Value + }; + if (stInfo.Assembly.Contains(".ni.dll")) + { + stInfo.Filename = stInfo.Assembly.Replace(".ni.dll", ""); + } + else + { + stInfo.Filename = stInfo.Assembly.Replace(".dll", ""); + } + stInfo.Pdb = stInfo.Filename + ".pdb"; + + return GetLineFromMetadata(TryGetMetadataReader(stInfo.Filename), line, stInfo); + } + + private static MetadataReader TryGetMetadataReader(string assemblyName) + { + MetadataReader reader = null; + try + { + if (s_assemblyFilePathDictionary.TryGetValue(assemblyName, out string filePath)) + { + if (s_metadataReaderDictionary.TryGetValue(filePath, out reader)) + { + return reader; + } + s_metadataReaderDictionary.Add(filePath, SetMetadataReader(filePath)); + return s_metadataReaderDictionary[filePath]; + } + return reader; + } + catch + { + return reader; + } + } + + private static MetadataReader SetMetadataReader(string filePath) + { + MetadataReader reader = null; + try + { + MetadataReaderProvider provider = null; + static Stream streamProvider(string sp) => new FileStream(sp, FileMode.Open, FileAccess.Read); + using Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + if (stream != null) + { + if (filePath.Contains(".dll")) + { + using PEReader peReader = new PEReader(stream); + if (!peReader.TryOpenAssociatedPortablePdb(filePath, streamProvider, out provider, out string pdbPath)) + { + return reader; + } + } + /*else if (filePath.Contains(".pdb")) + { + provider = MetadataReaderProvider.FromPortablePdbStream(stream); + }*/ + else + { + return reader; + } + } + return provider?.GetMetadataReader(); + } + catch + { + return reader; + } + } + + private static string GetLineFromMetadata(MetadataReader reader, string line, StackTraceInfo stInfo) + { + try + { + if (reader != null) + { + Handle handle = MetadataTokens.Handle(Convert.ToInt32(stInfo.Token, 16)); + if (handle.Kind == HandleKind.MethodDefinition) + { + MethodDebugInformationHandle methodDebugHandle = ((MethodDefinitionHandle)handle).ToDebugInformationHandle(); + MethodDebugInformation methodInfo = reader.GetMethodDebugInformation(methodDebugHandle); + if (!methodInfo.SequencePointsBlob.IsNil) + { + SequencePointCollection sequencePoints = methodInfo.GetSequencePoints(); + SequencePoint? bestPointSoFar = null; + foreach (SequencePoint point in sequencePoints) + { + if (point.Offset > Convert.ToInt64(stInfo.Offset, 16)) + break; + + if (point.StartLine != SequencePoint.HiddenLine) + bestPointSoFar = point; + } + + if (bestPointSoFar.HasValue) + { + string sourceFile = reader.GetString(reader.GetDocument(bestPointSoFar.Value.Document).Name); + int sourceLine = bestPointSoFar.Value.StartLine; + string pattern = stInfo.Assembly + @":token " + stInfo.Token + @"\+" + stInfo.Offset; + string replacement = sourceFile + @":line " + sourceLine; + return Regex.Replace(line, pattern, replacement); + } + } + } + } + return line; + } + catch + { + return line; + } + } + + public static Command SymbolicateCommand() => + new Command( + name: "symbolicate", description: "Get the line number from the Method Token and IL Offset in a stacktrace") + { + // Handler + HandlerDescriptor.FromDelegate((SymbolicateDelegate)Symbolicate).GetCommandHandler(), + // Arguments and Options + InputFileArgument(), + SearchDirectoryOption(), + OutputFileOption(), + StandardOutOption() + }; + + public static Argument InputFileArgument() => + new Argument(name: "input-path") + { + Description = "Path to the stacktrace text file", + Arity = ArgumentArity.ExactlyOne + }.ExistingOnly(); + + public static Option SearchDirectoryOption() => + new Option(new[] { "-d", "--search-dir" }, "Path of multiple directories with assembly and pdb") + { + Argument = new Argument(name: "directory1 directory2 ...", getDefaultValue: () => new DirectoryInfo(Directory.GetCurrentDirectory()).GetDirectories()) + { + Arity = ArgumentArity.ZeroOrMore + }.ExistingOnly() + }; + + public static Option OutputFileOption() => + new Option(new[] { "-o", "--output" }, "Output directly to a file (Default: .symbolicated)") + { + Argument = new Argument(name: "output-path") + { + Arity = ArgumentArity.ZeroOrOne + } + }; + + public static Option StandardOutOption() => + new Option(new[] { "-c", "--stdout" }, getDefaultValue: () => false, "Output directly to a console"); + } +} \ No newline at end of file