[Aa]rtifacts/
[Dd]ebug/
[Rr]elease/
-x64/
[Bb]in/
[Oo]bj/
+[Pp]ackages/
+x64/
.dotnet/
.packages/
.tools/
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-analyze", "src\Tools\dotnet-analyze\dotnet-analyze.csproj", "{1576314E-F823-4A24-BC90-22282AB33353}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-collect", "src\Tools\dotnet-collect\dotnet-collect.csproj", "{D9972D61-4B43-4007-B983-C02718DD8D33}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-trace", "src\Tools\dotnet-trace\dotnet-trace.csproj", "{718350FA-2DD9-4950-BA41-D7A7F66DAC91}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dump", "src\Tools\dotnet-dump\dotnet-dump.csproj", "{43D41DE9-7CCC-4DCB-A68A-B9099E538125}"
EndProject
{1576314E-F823-4A24-BC90-22282AB33353}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
{1576314E-F823-4A24-BC90-22282AB33353}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
{1576314E-F823-4A24-BC90-22282AB33353}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|Any CPU.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|Any CPU.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|ARM.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|ARM.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|ARM64.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|ARM64.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|x64.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|x64.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|x86.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Checked|x86.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|ARM.ActiveCfg = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|ARM.Build.0 = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|ARM64.Build.0 = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|x64.ActiveCfg = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|x64.Build.0 = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|x86.ActiveCfg = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Debug|x86.Build.0 = Debug|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|Any CPU.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|ARM.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|ARM.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|ARM64.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|ARM64.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|x64.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|x64.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|x86.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.Release|x86.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
- {D9972D61-4B43-4007-B983-C02718DD8D33}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
{43D41DE9-7CCC-4DCB-A68A-B9099E538125}.Checked|Any CPU.ActiveCfg = Release|Any CPU
{43D41DE9-7CCC-4DCB-A68A-B9099E538125}.Checked|Any CPU.Build.0 = Release|Any CPU
{43D41DE9-7CCC-4DCB-A68A-B9099E538125}.Checked|ARM.ActiveCfg = Release|Any CPU
{90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
{90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
{90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|Any CPU.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|ARM.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|ARM.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|ARM64.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|ARM64.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|x64.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|x64.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|x86.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Checked|x86.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|ARM.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|x64.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Debug|x86.Build.0 = Debug|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|ARM.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|ARM.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|ARM64.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|x64.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|x64.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|x86.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.Release|x86.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91}.RelWithDebInfo|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
{D52C65C4-2C7D-45E6-9F5C-6F3A96796018} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
{B62728C8-1267-4043-B46F-5537BBAEC692} = {19FAB78C-3351-4911-8F0C-8C6056401740}
{1576314E-F823-4A24-BC90-22282AB33353} = {B62728C8-1267-4043-B46F-5537BBAEC692}
- {D9972D61-4B43-4007-B983-C02718DD8D33} = {B62728C8-1267-4043-B46F-5537BBAEC692}
{43D41DE9-7CCC-4DCB-A68A-B9099E538125} = {B62728C8-1267-4043-B46F-5537BBAEC692}
{41F59D85-FC36-3015-861B-F177863252BC} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
{A9A7C879-C320-3327-BB84-16E1322E17AE} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
{41351955-16D5-48D7-AF4C-AF25F5FB2E78} = {B62728C8-1267-4043-B46F-5537BBAEC692}
{ED27F39F-DF5C-4E22-87E0-EC5B5873B503} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7}
{90CF2633-58F0-44EE-943B-D70207455F20} = {19FAB78C-3351-4911-8F0C-8C6056401740}
+ {718350FA-2DD9-4950-BA41-D7A7F66DAC91} = {B62728C8-1267-4043-B46F-5537BBAEC692}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0}
+++ /dev/null
-# .NET Diagnostics Tools
-
-## [dotnet-dump](src/dotnet-dump)
-
-A cross-platform tool to collect memory dumps of .NET processes:
-
-```
-Captures memory dumps of .NET processes
-
-Usage: dotnet-dump [options]
-
-Options:
- -p|--process-id <PROCESS_ID> The ID of the process to collect a memory dump for
- -o|--output <OUTPUT_DIRECTORY> The directory to write the dump to. Defaults to the current working directory.
- -?|-h|--help Show help information
-```
-
-This tool collects dumps of .NET processes. Currently supports Windows and Linux (using the `createdump` command). The tool currently supports capturing a dump immediately (when invoked). We plan to add support for "daemonizing" (running the tool the in the background) and having it capture a dump on certain conditions:
-
-* When the CPU usage goes over a certain amount
-* When memory usage goes over a certain amount
-* When a certain EventPipe event is emitted (this will inherently "lag" a bit since the events are buffered)
-
-## [dotnet-collect](src/dotnet-collect)
-
-A cross-platform tool for collecting data from Managed EventSources and .NET Runtime events using EventPipe
-
-```
-Collects Event Traces from .NET processes
-
-Usage: dotnet-collect [options]
-
-Options:
- -p|--process-id <PROCESS_ID> Filter to only the process with the specified process ID.
- -c|--config-path <CONFIG_PATH> The path of the EventPipe config file to write, must be named [AppName].eventpipeconfig and be in the base directory for a managed app.
- -o|--output <OUTPUT_DIRECTORY> The directory to write the trace to. Defaults to the current working directory.
- --buffer <BUFFER_SIZE_IN_MB> The size of the in-memory circular buffer in megabytes.
- --provider <PROVIDER_SPEC> An EventPipe provider to enable. A string in the form '<provider name>:<keywords>:<level>'.
- -?|-h|--help Show help information
- ```
-
- This tool collects EventPipe traces. Currently it is limited to using the file-based configuration added in .NET Core 2.2. To use it, you must manually provide the destination path for the `eventpipeconfig` file. For example:
-
- ```
- dotnet-collect -c ./path/to/my/app/MyApp.eventpipeconfig --provider Microsoft-Windows-DotNETRuntime
- ```
-
- The default behavior is to put traces in the directory from which you launched `dotnet-collect`. Traces are in files of the form `[appname].[processId].netperf` and can be viewed with [PerfView](https://github.com/Microsoft/PerfView).
-
-## [dotnet-analyze](src/dotnet-analyze)
-
-An SOS-like "REPL" for exploring .NET memory dumps (based on [CLRMD](https://github.com/Microsoft/clrmd)).
-
-```
-Inspect a crash dump using interactive commands
-
-Usage: dotnet-analyze [arguments] [options]
-
-Arguments:
-<DUMP> The path to the dump file to analyze.
-
-Options:
- -?|-h|--help Show help information
-```
-
-When you launch this command, a REPL prompt is provided:
-
-```
-Loading crash dump C:\Users\anurse\Desktop\dotnet-18956-20181012-153829-088.dmp...
-Ready to process analysis commands. Type 'help' to list available commands or 'help [command]' to get detailed help on a command.
-Type 'quit' or 'exit' to exit the analysis session.
->
-```
-
-The following commands are available:
-* `quit` (alias `exit`) - Exit the tool
-* `help` - List commands and help information (not yet implemented ;))
-* `threads` (alias `~`) - List thread
-* `DumpStack` - Dump managed stack trace for the current thread. A little like SOS's `!DumpStack`
-* `DumpHeap` - Dump information about objects on the heap, grouped by type. A little like SOS's `!DumpHeap -stat`
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.Tracing;
-using System.Linq;
-using System.Text;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public class CollectionConfiguration
- {
- public int? ProcessId { get; set; }
- public string OutputPath { get; set; }
- public int? CircularMB { get; set; }
- public IList<EventSpec> Providers { get; set; } = new List<EventSpec>();
- public IList<LoggerSpec> Loggers { get; set; } = new List<LoggerSpec>();
-
- internal string ToConfigString()
- {
- var builder = new StringBuilder();
- if (ProcessId != null)
- {
- builder.AppendLine($"ProcessId={ProcessId.Value}");
- }
- if (!string.IsNullOrEmpty(OutputPath))
- {
- builder.AppendLine($"OutputPath={OutputPath}");
- }
- if (CircularMB != null)
- {
- builder.AppendLine($"CircularMB={CircularMB}");
- }
- if (Providers != null && Providers.Count > 0)
- {
- builder.AppendLine($"Providers={SerializeProviders(Enumerable.Concat(Providers, GenerateLoggerSpec(Loggers)))}");
- }
- return builder.ToString();
- }
-
- public void AddProfile(CollectionProfile profile)
- {
- foreach (var provider in profile.EventSpecs)
- {
- Providers.Add(provider);
- }
-
- foreach (var logger in profile.LoggerSpecs)
- {
- Loggers.Add(logger);
- }
- }
-
- private string SerializeProviders(IEnumerable<EventSpec> providers) => string.Join(",", providers.Select(s => s.ToConfigString()));
-
- private IEnumerable<EventSpec> GenerateLoggerSpec(IList<LoggerSpec> loggers)
- {
- if (loggers.Count > 0)
- {
- var filterSpec = new StringBuilder();
- foreach (var logger in loggers)
- {
- if (string.IsNullOrEmpty(logger.Level))
- {
- filterSpec.Append($"{logger.Prefix}");
- }
- else
- {
- filterSpec.Append($"{logger.Prefix}:{logger.Level}");
- }
- filterSpec.Append(";");
- }
-
- // Remove trailing ';'
- filterSpec.Length -= 1;
-
- yield return new EventSpec(
- provider: "Microsoft-Extensions-Logging",
- keywords: 0x04 | 0x08, // FormattedMessage | JsonMessage (source: https://github.com/aspnet/Extensions/blob/aa7fa91cfc8f6ff078b020a428bcad71ae7a32ab/src/Logging/Logging.EventSource/src/LoggingEventSource.cs#L95)
- level: EventLevel.LogAlways,
- parameters: new Dictionary<string, string>() {
- { "FilterSpecs", filterSpec.ToString() }
- });
- }
- }
- }
-}
+++ /dev/null
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public class CollectionProfile
- {
- public static readonly string DefaultProfileName = "Default";
-
- public string Name { get; }
- public string Description { get; }
- public IReadOnlyList<EventSpec> EventSpecs { get; }
- public IReadOnlyList<LoggerSpec> LoggerSpecs { get; }
-
- public CollectionProfile(string name, string description, IEnumerable<EventSpec> eventSpecs, IEnumerable<LoggerSpec> loggerSpecs)
- {
- Name = name;
- Description = description;
- EventSpecs = eventSpecs.ToList();
- LoggerSpecs = loggerSpecs.ToList();
- }
- }
-}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- internal static class ConfigPathDetector
- {
- private static readonly HashSet<string> _managedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".exe", ".dll" };
-
- // Known .NET Platform Assemblies
- private static readonly HashSet<string> _platformAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "System.Private.CoreLib.dll",
- "clrjit.dll",
- };
-
- internal static string TryDetectConfigPath(int processId)
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return Windows.TryDetectConfigPath(processId);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- return Linux.TryDetectConfigPath(processId);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- return OSX.TryDetectConfigPath(processId);
- }
- return null;
- }
-
- private static class OSX
- {
- // This is defined in proc_info.h (https://opensource.apple.com/source/xnu/xnu-1228/bsd/sys/proc_info.h)
- private const int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
-
- /// <summary>
- /// Gets the full path to the executable file identified by the specified PID
- /// </summary>
- /// <param name="pid">The PID of the running process</param>
- /// <param name="buffer">A pointer to an allocated block of memory that will be filled with the process path</param>
- /// <param name="bufferSize">The size of the buffer, should be PROC_PIDPATHINFO_MAXSIZE</param>
- /// <returns>Returns the length of the path returned on success</returns>
- [DllImport("libproc.dylib", SetLastError = true)]
- private static extern unsafe int proc_pidpath(
- int pid,
- byte* buffer,
- uint bufferSize);
-
- /// <summary>
- /// Gets the full path to the executable file identified by the specified PID
- /// </summary>
- /// <param name="pid">The PID of the running process</param>
- /// <returns>Returns the full path to the process executable</returns>
- internal static unsafe string proc_pidpath(int pid)
- {
- // The path is a fixed buffer size, so use that and trim it after
- int result = 0;
- byte* pBuffer = stackalloc byte[PROC_PIDPATHINFO_MAXSIZE];
-
- // WARNING - Despite its name, don't try to pass in a smaller size than specified by PROC_PIDPATHINFO_MAXSIZE.
- // For some reason libproc returns -1 if you specify something that's NOT EQUAL to PROC_PIDPATHINFO_MAXSIZE
- // even if you declare your buffer to be smaller/larger than this size.
- result = proc_pidpath(pid, pBuffer, (uint)(PROC_PIDPATHINFO_MAXSIZE * sizeof(byte)));
- if (result <= 0)
- {
- throw new InvalidOperationException("Could not find procpath using libproc.");
- }
-
- // OS X uses UTF-8. The conversion may not strip off all trailing \0s so remove them here
- return System.Text.Encoding.UTF8.GetString(pBuffer, result);
- }
-
- public static string TryDetectConfigPath(int processId)
- {
- try
- {
- var path = proc_pidpath(processId);
- var candidateDir = Path.GetDirectoryName(path);
- var candidateName = Path.GetFileNameWithoutExtension(path);
- return Path.Combine(candidateDir, $"{candidateName}.eventpipeconfig");
- }
- catch (InvalidOperationException)
- {
- return null; // The pinvoke above may fail - return null in that case to handle error gracefully.
- }
- }
- }
-
- private static class Linux
- {
- public static string TryDetectConfigPath(int processId)
- {
- // Read procfs maps list
- var lines = File.ReadAllLines($"/proc/{processId}/maps");
-
- foreach (var line in lines)
- {
- try
- {
- var parser = new StringParser(line, separator: ' ', skipEmpty: true);
-
- // Skip the address range
- parser.MoveNext();
-
- var permissions = parser.MoveAndExtractNext();
-
- // The managed entry point is Read-Only, Non-Execute and Shared.
- if (!string.Equals(permissions, "r--s", StringComparison.Ordinal))
- {
- continue;
- }
-
- // Skip offset, dev, and inode
- parser.MoveNext();
- parser.MoveNext();
- parser.MoveNext();
-
- // Parse the path
- if (!parser.MoveNext())
- {
- continue;
- }
-
- var path = parser.ExtractCurrentToEnd();
- var candidateDir = Path.GetDirectoryName(path);
- var candidateName = Path.GetFileNameWithoutExtension(path);
- if (File.Exists(Path.Combine(candidateDir, $"{candidateName}.deps.json")))
- {
- return Path.Combine(candidateDir, $"{candidateName}.eventpipeconfig");
- }
- }
- catch (ArgumentNullException) { return null; }
- catch (InvalidDataException) { return null; }
- catch (InvalidOperationException) { return null; }
- }
- return null;
- }
- }
-
- private static class Windows
- {
- private static readonly HashSet<string> _knownNativeLibraries = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- // .NET Core Host
- "dotnet.exe",
- "hostfxr.dll",
- "hostpolicy.dll",
- "coreclr.dll",
-
- // Windows Native Libraries
- "ntdll.dll",
- "kernel32.dll",
- "kernelbase.dll",
- "apphelp.dll",
- "ucrtbase.dll",
- "advapi32.dll",
- "msvcrt.dll",
- "sechost.dll",
- "rpcrt4.dll",
- "ole32.dll",
- "combase.dll",
- "bcryptPrimitives.dll",
- "gdi32.dll",
- "gdi32full.dll",
- "msvcp_win.dll",
- "user32.dll",
- "win32u.dll",
- "oleaut32.dll",
- "shlwapi.dll",
- "version.dll",
- "bcrypt.dll",
- "imm32.dll",
- "kernel.appcore.dll",
- };
-
- public static string TryDetectConfigPath(int processId)
- {
- var process = Process.GetProcessById(processId);
-
- // Iterate over modules
- foreach (var module in process.Modules.Cast<ProcessModule>())
- {
- // Filter out things that aren't exes and dlls (useful on Unix/macOS to skip native libraries)
- var extension = Path.GetExtension(module.FileName);
- var name = Path.GetFileName(module.FileName);
- if (_managedExtensions.Contains(extension) && !_knownNativeLibraries.Contains(name) && !_platformAssemblies.Contains(name))
- {
- var candidateDir = Path.GetDirectoryName(module.FileName);
- var appName = Path.GetFileNameWithoutExtension(module.FileName);
-
- // Check for the deps.json file
- // TODO: Self-contained apps?
- if (File.Exists(Path.Combine(candidateDir, $"{appName}.deps.json")))
- {
- // This is an app!
- return Path.Combine(candidateDir, $"{appName}.eventpipeconfig");
- }
- }
- }
-
- return null;
- }
- }
- }
-}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.Tracing;
-using System.IO;
-using System.Threading.Tasks;
-using Microsoft.Diagnostics.Tracing;
-using Microsoft.Diagnostics.Tracing.Session;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public class EtwCollector : EventCollector
- {
- private readonly CollectionConfiguration _config;
- private TraceEventSession _session;
-
- public EtwCollector(CollectionConfiguration config)
- {
- _config = config;
- }
-
- public override Task StartCollectingAsync()
- {
- // TODO: Allow a file name to be provided
- var outputFile = _config.ProcessId == null ?
- Path.Combine(_config.OutputPath, "dotnet-collect.etl") :
- Path.Combine(_config.OutputPath, $"dotnet-collect.{_config.ProcessId.Value}.etl");
- if (File.Exists(outputFile))
- {
- throw new InvalidOperationException($"Target file already exists: {outputFile}");
- }
- _session = new TraceEventSession("dotnet-collect", outputFile);
-
- if (_config.CircularMB is int circularMb)
- {
- _session.CircularBufferMB = circularMb;
- }
-
- var options = new TraceEventProviderOptions();
- if (_config.ProcessId is int pid)
- {
- options.ProcessIDFilter = new List<int>() { pid };
- }
-
- // Enable the providers requested
- foreach (var provider in _config.Providers)
- {
- _session.EnableProvider(provider.Provider, ConvertLevel(provider.Level), provider.Keywords, options);
- }
-
- return Task.CompletedTask;
- }
-
- private TraceEventLevel ConvertLevel(EventLevel level)
- {
- switch (level)
- {
- case EventLevel.Critical: return TraceEventLevel.Critical;
- case EventLevel.Error: return TraceEventLevel.Error;
- case EventLevel.Informational: return TraceEventLevel.Informational;
- case EventLevel.LogAlways: return TraceEventLevel.Always;
- case EventLevel.Verbose: return TraceEventLevel.Verbose;
- case EventLevel.Warning: return TraceEventLevel.Warning;
- default:
- throw new InvalidOperationException($"Unknown EventLevel: {level}");
- }
- }
-
- public override Task StopCollectingAsync()
- {
- _session.Dispose();
- return Task.CompletedTask;
- }
- }
-}
+++ /dev/null
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public abstract class EventCollector
- {
- public abstract Task StartCollectingAsync();
- public abstract Task StopCollectingAsync();
- }
-}
+++ /dev/null
-using System.IO;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public class EventPipeCollector : EventCollector
- {
- private readonly CollectionConfiguration _config;
- private readonly string _configPath;
-
- public EventPipeCollector(CollectionConfiguration config, string configPath)
- {
- _config = config;
- _configPath = configPath;
- }
-
- public override Task StartCollectingAsync()
- {
- var configContent = _config.ToConfigString();
- return File.WriteAllTextAsync(_configPath, configContent);
- }
-
- public override Task StopCollectingAsync()
- {
- File.Delete(_configPath);
- return Task.CompletedTask;
- }
- }
-}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.Tracing;
-using System.Globalization;
-using System.Text;
-using Microsoft.Internal.Utilities;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public class EventSpec
- {
- public string Provider { get; }
- public ulong Keywords { get; }
- public EventLevel Level { get; }
- public IDictionary<string, string> Parameters { get; }
-
- public EventSpec(string provider, ulong keywords, EventLevel level)
- : this(provider, keywords, level, new Dictionary<string, string>())
- {
- }
-
- public EventSpec(string provider, ulong keywords, EventLevel level, IDictionary<string, string> parameters)
- {
- Provider = provider;
- Keywords = keywords;
- Level = level;
- Parameters = parameters;
- }
-
- public static bool TryParse(string input, out EventSpec spec)
- {
- spec = null;
- var splat = input.Split(':');
-
- if (splat.Length == 0)
- {
- return false;
- }
-
- var provider = splat[0];
- var keywords = ulong.MaxValue;
- var level = EventLevel.Verbose;
- var parameters = new Dictionary<string, string>();
-
- if (splat.Length > 1)
- {
- if (!TryParseKeywords(splat[1], provider, out keywords))
- {
- return false;
- }
- }
-
- if (splat.Length > 2)
- {
- if (!TryParseLevel(splat[2], out level))
- {
- return false;
- }
- }
-
- if (splat.Length > 3)
- {
- if (!TryParseParameters(splat[3], parameters))
- {
- return false;
- }
- }
-
- spec = new EventSpec(provider, keywords, level, parameters);
- return true;
- }
-
- public string ToConfigString()
- {
- var config = $"{Provider}:0x{Keywords:X}:{(int)Level}";
- if(Parameters.Count > 0)
- {
- config += $":{FormatParameters(Parameters)}";
- }
- return config;
- }
-
- private static string FormatParameters(IDictionary<string, string> parameters)
- {
- var builder = new StringBuilder();
- foreach(var (key, value) in parameters)
- {
- builder.Append($"{key}={value};");
- }
-
- // Remove the trailing ';'
- builder.Length -= 1;
-
- return builder.ToString();
- }
-
- private static bool TryParseParameters(string input, IDictionary<string, string> parameters)
- {
- var splat = input.Split(';');
- foreach(var item in splat)
- {
- var splot = item.Split('=');
- if(splot.Length != 2)
- {
- return false;
- }
-
- parameters[splot[0]] = splot[1];
- }
-
- return true;
- }
-
- private static bool TryParseLevel(string input, out EventLevel level)
- {
- level = EventLevel.Verbose;
- if (int.TryParse(input, out var intLevel))
- {
- if (intLevel >= (int)EventLevel.LogAlways && intLevel <= (int)EventLevel.Verbose)
- {
- level = (EventLevel)intLevel;
- return true;
- }
- }
- else if (Enum.TryParse(input, ignoreCase: true, out level))
- {
- return true;
- }
- return false;
- }
-
- private static bool TryParseKeywords(string input, string provider, out ulong keywords)
- {
- if (string.Equals("*", input, StringComparison.Ordinal))
- {
- keywords = ulong.MaxValue;
- return true;
- }
- else if (input.StartsWith("0x"))
- {
- // Keywords
- if (ulong.TryParse(input, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out keywords))
- {
- return true;
- }
- }
- else if(KnownData.TryGetProvider(provider, out var knownProvider))
- {
- var splat = input.Split(',');
- keywords = 0;
- foreach(var item in splat)
- {
- if(knownProvider.Keywords.TryGetValue(item, out var knownKeyword))
- {
- keywords |= knownKeyword.Value;
- }
- else
- {
- throw new CommandLineException($"Keyword '{item}' is not a well-known keyword for '{provider}'");
- }
- }
- return true;
- }
-
- keywords = ulong.MaxValue;
- return false;
- }
- }
-}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.Tracing;
-using System.Linq;
-using Microsoft.Diagnostics.Tracing.Parsers;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- internal static class KnownData
- {
- private static readonly IReadOnlyDictionary<string, KnownProvider> _knownProviders =
- CreateKnownProviders().ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
-
- private static readonly IReadOnlyDictionary<string, CollectionProfile> _knownProfiles =
- CreateProfiles().ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
-
- private static IEnumerable<KnownProvider> CreateKnownProviders()
- {
- yield return CreateClrProvider();
- }
-
- private static IEnumerable<CollectionProfile> CreateProfiles()
- {
- yield return new CollectionProfile(
- CollectionProfile.DefaultProfileName,
- "A default set of event providers useful for diagosing problems in any .NET application.",
- new[] {
- new EventSpec(ClrTraceEventParser.ProviderName, (ulong)ClrTraceEventParser.Keywords.Default, EventLevel.Informational)
- },
- Array.Empty<LoggerSpec>());
-
- yield return new CollectionProfile(
- "AspNetCore",
- "A set of event providers useful for diagnosing problems in ASP.NET Core applications.",
- new[]
- {
- new EventSpec("Microsoft-AspNetCore-Hosting", ulong.MaxValue, EventLevel.Informational),
- },
- Array.Empty<LoggerSpec>());
-
- yield return new CollectionProfile(
- "Kestrel",
- "Detailed events for ASP.NET Core Kestrel",
- new[]
- {
- new EventSpec("Microsoft-AspNetCore-Server-Kestrel", ulong.MaxValue, EventLevel.Verbose),
- },
- Array.Empty<LoggerSpec>());
- }
-
- private static KnownProvider CreateClrProvider()
- {
- return new KnownProvider(
- ClrTraceEventParser.ProviderName,
- ClrTraceEventParser.ProviderGuid,
- ScanKeywordType(typeof(ClrTraceEventParser.Keywords)));
- }
-
- public static IReadOnlyList<CollectionProfile> GetAllProfiles() => _knownProfiles.Values.ToList();
- public static IReadOnlyList<KnownProvider> GetAllProviders() => _knownProviders.Values.ToList();
-
- public static bool TryGetProvider(string providerName, out KnownProvider provider) => _knownProviders.TryGetValue(providerName, out provider);
- public static bool TryGetProfile(string profileName, out CollectionProfile profile) => _knownProfiles.TryGetValue(profileName, out profile);
-
- private static IEnumerable<KnownKeyword> ScanKeywordType(Type keywordType)
- {
- var values = Enum.GetValues(keywordType).Cast<long>().ToList();
- var keywords = values.Distinct().Select(v => new KnownKeyword(Enum.GetName(keywordType, v), (ulong)v)).ToList();
- return keywords;
- }
- }
-
- internal class KnownProvider
- {
- public string Name { get; }
- public Guid Guid { get; }
- public IReadOnlyDictionary<string, KnownKeyword> Keywords { get; }
-
- public KnownProvider(string name, Guid guid, IEnumerable<KnownKeyword> keywords)
- {
- Name = name;
- Guid = guid;
- Keywords = keywords.ToDictionary(k => k.Name, StringComparer.OrdinalIgnoreCase);
- }
- }
-
- internal class KnownKeyword
- {
- public string Name { get; }
- public ulong Value { get; }
-
- public KnownKeyword(string name, ulong value)
- {
- Name = name;
- Value = value;
- }
- }
-}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- public class LoggerSpec
- {
- // Handles case normalization because key lookup is case-insensitive.
- private static readonly Dictionary<string, string> _levelMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
- {
- { "Trace", "Trace" },
- { "Debug", "Debug" },
- { "Information", "Information" },
- { "Warning", "Warning" },
- { "Error", "Error" },
- { "Critical", "Critical" },
- };
-
- public string Prefix { get; }
- public string Level { get; }
-
- public LoggerSpec(string prefix, string level)
- {
- Prefix = prefix;
- Level = level;
- }
-
- public static bool TryParse(string input, out LoggerSpec spec)
- {
- var splat = input.Split(':');
-
- var prefix = splat[0];
- string level = null;
- if (splat.Length > 1)
- {
- if (!_levelMap.TryGetValue(splat[1], out level))
- {
- spec = null;
- return false;
- }
- }
-
- spec = new LoggerSpec(prefix, level);
- return true;
- }
- }
-}
+++ /dev/null
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-using McMaster.Extensions.CommandLineUtils;
-using Microsoft.Internal.Utilities;
-
-namespace Microsoft.Diagnostics.Tools.Collect
-{
- [Command(Name = "dotnet-collect", Description = "Collects Event Traces from .NET processes")]
- internal class Program
- {
-
- [Option("-c|--config-path <CONFIG_PATH>", Description = "The path of the EventPipe config file to write, must be named [AppName].eventpipeconfig and be in the base directory for a managed app.")]
- public string ConfigPath { get; set; }
-
- [Option("--etw", Description = "Specify this flag to use ETW to collect events rather than using EventPipe (Windows only).")]
- public bool Etw { get; set; }
-
- [Required]
- [Option("-p|--process-id <PROCESS_ID>", Description = "Filter to only the process with the specified process ID.")]
- public int ProcessId { get; set; }
-
- [Option("-o|--output <OUTPUT_DIRECTORY>", Description = "The directory to write the trace to. Defaults to the current working directory.")]
- public string OutputDir { get; set; }
-
- [Option("--buffer <BUFFER_SIZE_IN_MB>", Description = "The size of the in-memory circular buffer in megabytes.")]
- public int? CircularMB { get; set; }
-
- [Option("--provider <PROVIDER_SPEC>", Description = "An EventPipe provider to enable. A string in the form '<provider name>:<keywords>:<level>:<parameters>'. Can be specified multiple times to enable multiple providers.")]
- public IList<string> Providers { get; set; }
-
- [Option("--profile <PROFILE_NAME>", Description = "A collection profile to enable. Use '--list-profiles' to get a list of all available profiles. Can be mixed with '--provider' and specified multiple times.")]
- public IList<string> Profiles { get; set; }
-
- [Option("--logger <LOGGER_NAME>", Description = "A Microsoft.Extensions.Logging logger to enable. A string in the form '<logger prefix>:<level>'. Can be specified multiple times to enable multiple loggers.")]
- public IList<string> Loggers { get; set; }
-
- [Option("--keywords-for <PROVIDER_NAME>", Description = "Gets a list of known keywords (if any) for the specified provider.")]
- public string KeywordsForProvider { get; set; }
-
- [Option("--list-profiles", Description = "Gets a list of predefined collection profiles.")]
- public bool ListProfiles { get; set; }
-
- [Option("--no-default", Description = "Don't enable the default profile.")]
- public bool NoDefault { get; set; }
-
- public async Task<int> OnExecuteAsync(IConsole console, CommandLineApplication app)
- {
- if (ListProfiles)
- {
- WriteProfileList(console.Out);
- return 0;
- }
-
- if (!string.IsNullOrEmpty(KeywordsForProvider))
- {
- return ExecuteKeywordsForAsync(console);
- }
-
- if(string.IsNullOrEmpty(ConfigPath))
- {
- ConfigPath = ConfigPathDetector.TryDetectConfigPath(ProcessId);
- if(string.IsNullOrEmpty(ConfigPath))
- {
- console.Error.WriteLine("Couldn't determine the path for the eventpipeconfig file from the process ID. Specify the '--config-path' option to provide it manually.");
- return 1;
- }
- console.WriteLine($"Detected config file path: {ConfigPath}");
- }
-
- var config = new CollectionConfiguration()
- {
- ProcessId = ProcessId,
- CircularMB = CircularMB,
- OutputPath = string.IsNullOrEmpty(OutputDir) ? Directory.GetCurrentDirectory() : OutputDir
- };
-
- if (Profiles != null && Profiles.Count > 0)
- {
- foreach (var profile in Profiles)
- {
- if (!KnownData.TryGetProfile(profile, out var collectionProfile))
- {
- console.Error.WriteLine($"Unknown profile name: '{profile}'. See 'dotnet-collect --list-profiles' to get a list of profiles.");
- return 1;
- }
- config.AddProfile(collectionProfile);
- }
- }
-
- if (Providers != null && Providers.Count > 0)
- {
- foreach (var provider in Providers)
- {
- if (!EventSpec.TryParse(provider, out var providerSpec))
- {
- console.Error.WriteLine($"Invalid provider specification: '{provider}'. See 'dotnet-collect --help' for more information.");
- return 1;
- }
- config.Providers.Add(providerSpec);
- }
- }
-
- if (Loggers != null && Loggers.Count > 0)
- {
- foreach (var logger in Loggers)
- {
- if (!LoggerSpec.TryParse(logger, out var loggerSpec))
- {
- console.Error.WriteLine($"Invalid logger specification: '{logger}'. See 'dotnet-collect --help' for more information.");
- return 1;
- }
- config.Loggers.Add(loggerSpec);
- }
- }
-
- if (!NoDefault)
- {
- // Enable the default profile if nothing is specified
- if (!KnownData.TryGetProfile(CollectionProfile.DefaultProfileName, out var defaultProfile))
- {
- console.Error.WriteLine("No providers or profiles were specified and there is no default profile available.");
- return 1;
- }
- config.AddProfile(defaultProfile);
- }
-
- if (!TryCreateCollector(console, config, out var collector))
- {
- return 1;
- }
-
- // Write the config file contents
- await collector.StartCollectingAsync();
- console.WriteLine("Tracing has started. Press Ctrl-C to stop.");
-
- await console.WaitForCtrlCAsync();
-
- await collector.StopCollectingAsync();
- console.WriteLine($"Tracing stopped. Trace files written to {config.OutputPath}");
-
- return 0;
- }
-
- private static void WriteProfileList(TextWriter console)
- {
- var profiles = KnownData.GetAllProfiles();
- var maxNameLength = profiles.Max(p => p.Name.Length);
- console.WriteLine("Available profiles:");
- foreach (var profile in profiles)
- {
- console.WriteLine($"* {profile.Name.PadRight(maxNameLength)} {profile.Description}");
- }
- }
-
- private int ExecuteKeywordsForAsync(IConsole console)
- {
- if (KnownData.TryGetProvider(KeywordsForProvider, out var provider))
- {
- console.WriteLine($"Known keywords for {provider.Name} ({provider.Guid}):");
- foreach (var keyword in provider.Keywords.Values)
- {
- console.WriteLine($"* 0x{keyword.Value:x16} {keyword.Name}");
- }
- return 0;
- }
- else
- {
- console.WriteLine($"There are no known keywords for {KeywordsForProvider}.");
- return 1;
- }
- }
-
- private bool TryCreateCollector(IConsole console, CollectionConfiguration config, out EventCollector collector)
- {
- collector = null;
-
- if (Etw)
- {
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- console.Error.WriteLine("Error: ETW-based collection is only supported on Windows.");
- return false;
- }
-
- if (!string.IsNullOrEmpty(ConfigPath))
- {
- console.Error.WriteLine("WARNING: The '-c' option is ignored when using ETW-based collection.");
- }
- collector = new EtwCollector(config);
- return true;
- }
- else
- {
- if (File.Exists(ConfigPath))
- {
- console.Error.WriteLine("Config file already exists, tracing is already underway by a different consumer.");
- return false;
- }
-
- collector = new EventPipeCollector(config, ConfigPath);
- return true;
- }
- }
-
- private static int Main(string[] args)
- {
- DebugUtil.WaitForDebuggerIfRequested(ref args);
-
- try
- {
- var app = new CommandLineApplication<Program>();
- app.Conventions.UseDefaultConventions();
- app.ExtendedHelpText = GetExtendedHelp();
- return app.Execute(args);
- }
- catch (PlatformNotSupportedException ex)
- {
- Console.Error.WriteLine(ex.Message);
- return 1;
- }
- catch (CommandLineException clex)
- {
- Console.Error.WriteLine(clex.Message);
- return 1;
- }
- catch (OperationCanceledException)
- {
- return 0;
- }
- }
-
- private static string GetExtendedHelp()
- {
- using (var writer = new StringWriter())
- {
- writer.WriteLine();
- writer.WriteLine("Profiles");
- writer.WriteLine(" Profiles are sets of well-defined provider configurations that provide useful information.");
- writer.WriteLine();
- WriteProfileList(writer);
- writer.WriteLine();
- writer.WriteLine("Specifying Loggers:");
- writer.WriteLine(" Use one of the following formats to specify a logger in '--logger'");
- writer.WriteLine(" * - Enable all messages at all levels from all loggers.");
- writer.WriteLine(" *:<level> - Enable messages at the specified '<level>' or higher from all loggers.");
- writer.WriteLine(" <loggerPrefix> - Enable all messages at all levels from all loggers starting with '<loggerPrefix>'.");
- writer.WriteLine(" <loggerPrefix>:<level> - Enable messages at the specified '<level>' or higher from all loggers starting with '<loggerPrefix>'.");
- writer.WriteLine();
- writer.WriteLine(" '<loggerPrefix>' is the prefix for a logger to enable. For example 'Microsoft.AspNetCore' to enable all ASP.NET Core loggers.");
- writer.WriteLine(" '<level>' can be one of: Critical, Error, Warning, Informational, Debug, or Trace.");
- writer.WriteLine();
- writer.WriteLine("Specifying Providers:");
- writer.WriteLine(" Use one of the following formats to specify a provider in '--provider'");
- writer.WriteLine(" <providerName> - Enable all events at all levels for the provider.");
- writer.WriteLine(" <providerName>:<keywords> - Enable events matching the specified keywords for the specified provider.");
- writer.WriteLine(" <providerName>:<keywords>:<level> - Enable events matching the specified keywords, at the specified level for the specified provider.");
- writer.WriteLine(" <providerName>:<keywords>:<level>:<parameters> - Enable events matching the specified keywords, at the specified level for the specified provider and provide key-value parameters.");
- writer.WriteLine();
- writer.WriteLine(" '<provider>' must be the name of the EventSource.");
- writer.WriteLine(" '<level>' can be one of: Critical (1), Error (2), Warning (3), Informational (4), Verbose (5). Either the name or number can be specified.");
- writer.WriteLine(" '<keywords>' is one of the following:");
- writer.WriteLine(" A '*' character, indicating ALL keywords should be enabled (this can be very costly for some providers!)");
- writer.WriteLine(" A comma-separated list of known keywords for a provider (use 'dotnet collect --keywords-for [providerName]' to get a list of known keywords for a provider)");
- writer.WriteLine(" A 64-bit hexadecimal number, starting with '0x' indicating the keywords to enable");
- writer.WriteLine(" '<parameters>' is an optional list of key-value parameters to provide to the EventPipe provider. The expected values depend on the provider you are enabling.");
- writer.WriteLine(" This should be a list of key-value pairs, in the form: '<key1>=<value1>;<key2>=<value2>;...'. Note that some shells, such as PowerShell, require that you");
- writer.WriteLine(" quote or escape the ';' character.");
- return writer.GetStringBuilder().ToString();
- }
- }
- }
-}
+++ /dev/null
-// Borrowed from https://github.com/dotnet/corefx/blob/b2f960abe1d8690be9d68dd9b56ea7636fb4a38b/src/Common/src/System/IO/StringParser.cs
-
-// 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 System.Diagnostics;
-
-namespace System.IO
-{
- /// <summary>
- /// Provides a string parser that may be used instead of String.Split
- /// to avoid unnecessary string and array allocations.
- /// </summary>
- internal struct StringParser
- {
- /// <summary>The string being parsed.</summary>
- private readonly string _buffer;
-
- /// <summary>The separator character used to separate subcomponents of the larger string.</summary>
- private readonly char _separator;
-
- /// <summary>true if empty subcomponents should be skipped; false to treat them as valid entries.</summary>
- private readonly bool _skipEmpty;
-
- /// <summary>The starting index from which to parse the current entry.</summary>
- private int _startIndex;
-
- /// <summary>The ending index that represents the next index after the last character that's part of the current entry.</summary>
- private int _endIndex;
-
- /// <summary>Initialize the StringParser.</summary>
- /// <param name="buffer">The string to parse.</param>
- /// <param name="separator">The separator character used to separate subcomponents of <paramref name="buffer"/>.</param>
- /// <param name="skipEmpty">true if empty subcomponents should be skipped; false to treat them as valid entries. Defaults to false.</param>
- public StringParser(string buffer, char separator, bool skipEmpty = false)
- {
- if (buffer == null)
- {
- throw new ArgumentNullException(nameof(buffer));
- }
- _buffer = buffer;
- _separator = separator;
- _skipEmpty = skipEmpty;
- _startIndex = -1;
- _endIndex = -1;
- }
-
- /// <summary>Moves to the next component of the string.</summary>
- /// <returns>true if there is a next component to be parsed; otherwise, false.</returns>
- public bool MoveNext()
- {
- if (_buffer == null)
- {
- throw new InvalidOperationException();
- }
-
- while (true)
- {
- if (_endIndex >= _buffer.Length)
- {
- _startIndex = _endIndex;
- return false;
- }
-
- int nextSeparator = _buffer.IndexOf(_separator, _endIndex + 1);
- _startIndex = _endIndex + 1;
- _endIndex = nextSeparator >= 0 ? nextSeparator : _buffer.Length;
-
- if (!_skipEmpty || _endIndex >= _startIndex + 1)
- {
- return true;
- }
- }
- }
-
- /// <summary>
- /// Moves to the next component of the string. If there isn't one, it throws an exception.
- /// </summary>
- public void MoveNextOrFail()
- {
- if (!MoveNext())
- {
- ThrowForInvalidData();
- }
- }
-
- /// <summary>
- /// Moves to the next component of the string and returns it as a string.
- /// </summary>
- /// <returns></returns>
- public string MoveAndExtractNext()
- {
- MoveNextOrFail();
- return _buffer.Substring(_startIndex, _endIndex - _startIndex);
- }
-
- /// <summary>
- /// Moves to the next component of the string, which must be enclosed in the only set of top-level parentheses
- /// in the string. The extracted value will be everything between (not including) those parentheses.
- /// </summary>
- /// <returns></returns>
- public string MoveAndExtractNextInOuterParens()
- {
- // Move to the next position
- MoveNextOrFail();
-
- // After doing so, we should be sitting at a the opening paren.
- if (_buffer[_startIndex] != '(')
- {
- ThrowForInvalidData();
- }
-
- // Since we only allow for one top-level set of parentheses, find the last
- // parenthesis in the string; it's paired with the opening one we just found.
- int lastParen = _buffer.LastIndexOf(')');
- if (lastParen == -1 || lastParen < _startIndex)
- {
- ThrowForInvalidData();
- }
-
- // Extract the contents of the parens, then move our ending position to be after the paren
- string result = _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1);
- _endIndex = lastParen + 1;
-
- return result;
- }
-
- /// <summary>
- /// Gets the current subcomponent of the string as a string.
- /// </summary>
- public string ExtractCurrent()
- {
- if (_buffer == null || _startIndex == -1)
- {
- throw new InvalidOperationException();
- }
- return _buffer.Substring(_startIndex, _endIndex - _startIndex);
- }
-
- /// <summary>Moves to the next component and parses it as an Int32.</summary>
- public unsafe int ParseNextInt32()
- {
- MoveNextOrFail();
-
- bool negative = false;
- int result = 0;
-
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
-
- if (p == end)
- {
- ThrowForInvalidData();
- }
-
- if (*p == '-')
- {
- negative = true;
- p++;
- if (p == end)
- {
- ThrowForInvalidData();
- }
- }
-
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = negative ? checked((result * 10) - d) : checked((result * 10) + d);
-
- p++;
- }
- }
-
- Debug.Assert(result == int.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as an Int64.</summary>
- public unsafe long ParseNextInt64()
- {
- MoveNextOrFail();
-
- bool negative = false;
- long result = 0;
-
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
-
- if (p == end)
- {
- ThrowForInvalidData();
- }
-
- if (*p == '-')
- {
- negative = true;
- p++;
- if (p == end)
- {
- ThrowForInvalidData();
- }
- }
-
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = negative ? checked((result * 10) - d) : checked((result * 10) + d);
-
- p++;
- }
- }
-
- Debug.Assert(result == long.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as a UInt32.</summary>
- public unsafe uint ParseNextUInt32()
- {
- MoveNextOrFail();
- if (_startIndex == _endIndex)
- {
- ThrowForInvalidData();
- }
-
- uint result = 0;
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = (uint)checked((result * 10) + d);
-
- p++;
- }
- }
-
- Debug.Assert(result == uint.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as a UInt64.</summary>
- public unsafe ulong ParseNextUInt64()
- {
- MoveNextOrFail();
-
- ulong result = 0;
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = checked((result * 10ul) + (ulong)d);
-
- p++;
- }
- }
-
- Debug.Assert(result == ulong.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as a Char.</summary>
- public char ParseNextChar()
- {
- MoveNextOrFail();
-
- if (_endIndex - _startIndex != 1)
- {
- ThrowForInvalidData();
- }
- char result = _buffer[_startIndex];
-
- Debug.Assert(result == char.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- internal delegate T ParseRawFunc<T>(string buffer, ref int startIndex, ref int endIndex);
-
- /// <summary>
- /// Moves to the next component and hands the raw buffer and indexing data to a selector function
- /// that can validate and return the appropriate data from the component.
- /// </summary>
- internal T ParseRaw<T>(ParseRawFunc<T> selector)
- {
- MoveNextOrFail();
- return selector(_buffer, ref _startIndex, ref _endIndex);
- }
-
- /// <summary>
- /// Gets the current subcomponent and all remaining components of the string as a string.
- /// </summary>
- public string ExtractCurrentToEnd()
- {
- if (_buffer == null || _startIndex == -1)
- {
- throw new InvalidOperationException();
- }
- return _buffer.Substring(_startIndex);
- }
-
- /// <summary>Throws unconditionally for invalid data.</summary>
- private static void ThrowForInvalidData()
- {
- throw new InvalidDataException();
- }
- }
-}
+++ /dev/null
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <OutputType>Exe</OutputType>
-
- <!-- Target .NET Core 2.1 so it will run on LTS -->
- <TargetFramework>netcoreapp2.1</TargetFramework>
-
- <RootNamespace>Microsoft.Diagnostics.Tools.Collect</RootNamespace>
- <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-
- <!-- Don't pack until ship engineering is done. Currently causing the official job to fail.
- <IsPackable>true</IsPackable>
- <PackAsTool>true</PackAsTool>
- -->
- </PropertyGroup>
-
- <ItemGroup>
- <Compile Include="..\Common\CommandLineException.cs" Link="CommandLineException.cs" />
- <Compile Include="..\Common\ConsoleCancellation.cs" Link="ConsoleCancellation.cs" />
- <Compile Include="..\Common\DebugUtil.cs" Link="DebugUtil.cs" />
- </ItemGroup>
-
- <ItemGroup>
- <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.2.5" />
- <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.30" />
- </ItemGroup>
-
-</Project>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public class CollectionConfiguration
+ {
+ public int? ProcessId { get; set; }
+ public string OutputPath { get; set; }
+ public int? CircularMB { get; set; }
+ public IList<EventSpec> Providers { get; set; } = new List<EventSpec>();
+ public IList<LoggerSpec> Loggers { get; set; } = new List<LoggerSpec>();
+
+ internal string ToConfigString()
+ {
+ var builder = new StringBuilder();
+ if (ProcessId != null)
+ {
+ builder.AppendLine($"ProcessId={ProcessId.Value}");
+ }
+ if (!string.IsNullOrEmpty(OutputPath))
+ {
+ builder.AppendLine($"OutputPath={OutputPath}");
+ }
+ if (CircularMB != null)
+ {
+ builder.AppendLine($"CircularMB={CircularMB}");
+ }
+ if (Providers != null && Providers.Count > 0)
+ {
+ builder.AppendLine($"Providers={SerializeProviders(Enumerable.Concat(Providers, GenerateLoggerSpec(Loggers)))}");
+ }
+ return builder.ToString();
+ }
+
+ public void AddProfile(CollectionProfile profile)
+ {
+ foreach (var provider in profile.EventSpecs)
+ {
+ Providers.Add(provider);
+ }
+
+ foreach (var logger in profile.LoggerSpecs)
+ {
+ Loggers.Add(logger);
+ }
+ }
+
+ private string SerializeProviders(IEnumerable<EventSpec> providers) => string.Join(",", providers.Select(s => s.ToConfigString()));
+
+ private IEnumerable<EventSpec> GenerateLoggerSpec(IList<LoggerSpec> loggers)
+ {
+ if (loggers.Count > 0)
+ {
+ var filterSpec = new StringBuilder();
+ foreach (var logger in loggers)
+ {
+ if (string.IsNullOrEmpty(logger.Level))
+ {
+ filterSpec.Append($"{logger.Prefix}");
+ }
+ else
+ {
+ filterSpec.Append($"{logger.Prefix}:{logger.Level}");
+ }
+ filterSpec.Append(";");
+ }
+
+ // Remove trailing ';'
+ filterSpec.Length -= 1;
+
+ yield return new EventSpec(
+ provider: "Microsoft-Extensions-Logging",
+ keywords: 0x04 | 0x08, // FormattedMessage | JsonMessage (source: https://github.com/aspnet/Extensions/blob/aa7fa91cfc8f6ff078b020a428bcad71ae7a32ab/src/Logging/Logging.EventSource/src/LoggingEventSource.cs#L95)
+ level: EventLevel.LogAlways,
+ parameters: new Dictionary<string, string>() {
+ { "FilterSpecs", filterSpec.ToString() }
+ });
+ }
+ }
+ }
+}
--- /dev/null
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public class CollectionProfile
+ {
+ public static readonly string DefaultProfileName = "Default";
+
+ public string Name { get; }
+ public string Description { get; }
+ public IReadOnlyList<EventSpec> EventSpecs { get; }
+ public IReadOnlyList<LoggerSpec> LoggerSpecs { get; }
+
+ public CollectionProfile(string name, string description, IEnumerable<EventSpec> eventSpecs, IEnumerable<LoggerSpec> loggerSpecs)
+ {
+ Name = name;
+ Description = description;
+ EventSpecs = eventSpecs.ToList();
+ LoggerSpecs = loggerSpecs.ToList();
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ internal static class ConfigPathDetector
+ {
+ private static readonly HashSet<string> _managedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".exe", ".dll" };
+
+ // Known .NET Platform Assemblies
+ private static readonly HashSet<string> _platformAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "System.Private.CoreLib.dll",
+ "clrjit.dll",
+ };
+
+ internal static string TryDetectConfigPath(int processId)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return Windows.TryDetectConfigPath(processId);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return Linux.TryDetectConfigPath(processId);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return OSX.TryDetectConfigPath(processId);
+ }
+ return null;
+ }
+
+ private static class OSX
+ {
+ // This is defined in proc_info.h (https://opensource.apple.com/source/xnu/xnu-1228/bsd/sys/proc_info.h)
+ private const int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
+
+ /// <summary>
+ /// Gets the full path to the executable file identified by the specified PID
+ /// </summary>
+ /// <param name="pid">The PID of the running process</param>
+ /// <param name="buffer">A pointer to an allocated block of memory that will be filled with the process path</param>
+ /// <param name="bufferSize">The size of the buffer, should be PROC_PIDPATHINFO_MAXSIZE</param>
+ /// <returns>Returns the length of the path returned on success</returns>
+ [DllImport("libproc.dylib", SetLastError = true)]
+ private static extern unsafe int proc_pidpath(
+ int pid,
+ byte* buffer,
+ uint bufferSize);
+
+ /// <summary>
+ /// Gets the full path to the executable file identified by the specified PID
+ /// </summary>
+ /// <param name="pid">The PID of the running process</param>
+ /// <returns>Returns the full path to the process executable</returns>
+ internal static unsafe string proc_pidpath(int pid)
+ {
+ // The path is a fixed buffer size, so use that and trim it after
+ int result = 0;
+ byte* pBuffer = stackalloc byte[PROC_PIDPATHINFO_MAXSIZE];
+
+ // WARNING - Despite its name, don't try to pass in a smaller size than specified by PROC_PIDPATHINFO_MAXSIZE.
+ // For some reason libproc returns -1 if you specify something that's NOT EQUAL to PROC_PIDPATHINFO_MAXSIZE
+ // even if you declare your buffer to be smaller/larger than this size.
+ result = proc_pidpath(pid, pBuffer, (uint)(PROC_PIDPATHINFO_MAXSIZE * sizeof(byte)));
+ if (result <= 0)
+ {
+ throw new InvalidOperationException("Could not find procpath using libproc.");
+ }
+
+ // OS X uses UTF-8. The conversion may not strip off all trailing \0s so remove them here
+ return System.Text.Encoding.UTF8.GetString(pBuffer, result);
+ }
+
+ public static string TryDetectConfigPath(int processId)
+ {
+ try
+ {
+ var path = proc_pidpath(processId);
+ var candidateDir = Path.GetDirectoryName(path);
+ var candidateName = Path.GetFileNameWithoutExtension(path);
+ return Path.Combine(candidateDir, $"{candidateName}.eventpipeconfig");
+ }
+ catch (InvalidOperationException)
+ {
+ return null; // The pinvoke above may fail - return null in that case to handle error gracefully.
+ }
+ }
+ }
+
+ private static class Linux
+ {
+ public static string TryDetectConfigPath(int processId)
+ {
+ // Read procfs maps list
+ var lines = File.ReadAllLines($"/proc/{processId}/maps");
+
+ foreach (var line in lines)
+ {
+ try
+ {
+ var parser = new StringParser(line, separator: ' ', skipEmpty: true);
+
+ // Skip the address range
+ parser.MoveNext();
+
+ var permissions = parser.MoveAndExtractNext();
+
+ // The managed entry point is Read-Only, Non-Execute and Shared.
+ if (!string.Equals(permissions, "r--s", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ // Skip offset, dev, and inode
+ parser.MoveNext();
+ parser.MoveNext();
+ parser.MoveNext();
+
+ // Parse the path
+ if (!parser.MoveNext())
+ {
+ continue;
+ }
+
+ var path = parser.ExtractCurrentToEnd();
+ var candidateDir = Path.GetDirectoryName(path);
+ var candidateName = Path.GetFileNameWithoutExtension(path);
+ if (File.Exists(Path.Combine(candidateDir, $"{candidateName}.deps.json")))
+ {
+ return Path.Combine(candidateDir, $"{candidateName}.eventpipeconfig");
+ }
+ }
+ catch (ArgumentNullException) { return null; }
+ catch (InvalidDataException) { return null; }
+ catch (InvalidOperationException) { return null; }
+ }
+ return null;
+ }
+ }
+
+ private static class Windows
+ {
+ private static readonly HashSet<string> _knownNativeLibraries = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ // .NET Core Host
+ "dotnet.exe",
+ "hostfxr.dll",
+ "hostpolicy.dll",
+ "coreclr.dll",
+
+ // Windows Native Libraries
+ "ntdll.dll",
+ "kernel32.dll",
+ "kernelbase.dll",
+ "apphelp.dll",
+ "ucrtbase.dll",
+ "advapi32.dll",
+ "msvcrt.dll",
+ "sechost.dll",
+ "rpcrt4.dll",
+ "ole32.dll",
+ "combase.dll",
+ "bcryptPrimitives.dll",
+ "gdi32.dll",
+ "gdi32full.dll",
+ "msvcp_win.dll",
+ "user32.dll",
+ "win32u.dll",
+ "oleaut32.dll",
+ "shlwapi.dll",
+ "version.dll",
+ "bcrypt.dll",
+ "imm32.dll",
+ "kernel.appcore.dll",
+ };
+
+ public static string TryDetectConfigPath(int processId)
+ {
+ var process = Process.GetProcessById(processId);
+
+ // Iterate over modules
+ foreach (var module in process.Modules.Cast<ProcessModule>())
+ {
+ // Filter out things that aren't exes and dlls (useful on Unix/macOS to skip native libraries)
+ var extension = Path.GetExtension(module.FileName);
+ var name = Path.GetFileName(module.FileName);
+ if (_managedExtensions.Contains(extension) && !_knownNativeLibraries.Contains(name) && !_platformAssemblies.Contains(name))
+ {
+ var candidateDir = Path.GetDirectoryName(module.FileName);
+ var appName = Path.GetFileNameWithoutExtension(module.FileName);
+
+ // Check for the deps.json file
+ // TODO: Self-contained apps?
+ if (File.Exists(Path.Combine(candidateDir, $"{appName}.deps.json")))
+ {
+ // This is an app!
+ return Path.Combine(candidateDir, $"{appName}.eventpipeconfig");
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Session;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public class EtwCollector : EventCollector
+ {
+ private readonly CollectionConfiguration _config;
+ private TraceEventSession _session;
+
+ public EtwCollector(CollectionConfiguration config)
+ {
+ _config = config;
+ }
+
+ public override Task StartCollectingAsync()
+ {
+ // TODO: Allow a file name to be provided
+ var outputFile = _config.ProcessId == null ?
+ Path.Combine(_config.OutputPath, "dotnet-trace.etl") :
+ Path.Combine(_config.OutputPath, $"dotnet-trace.{_config.ProcessId.Value}.etl");
+ if (File.Exists(outputFile))
+ {
+ throw new InvalidOperationException($"Target file already exists: {outputFile}");
+ }
+ _session = new TraceEventSession("dotnet-trace", outputFile);
+
+ if (_config.CircularMB is int circularMb)
+ {
+ _session.CircularBufferMB = circularMb;
+ }
+
+ var options = new TraceEventProviderOptions();
+ if (_config.ProcessId is int pid)
+ {
+ options.ProcessIDFilter = new List<int>() { pid };
+ }
+
+ // Enable the providers requested
+ foreach (var provider in _config.Providers)
+ {
+ _session.EnableProvider(provider.Provider, ConvertLevel(provider.Level), provider.Keywords, options);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private TraceEventLevel ConvertLevel(EventLevel level)
+ {
+ switch (level)
+ {
+ case EventLevel.Critical: return TraceEventLevel.Critical;
+ case EventLevel.Error: return TraceEventLevel.Error;
+ case EventLevel.Informational: return TraceEventLevel.Informational;
+ case EventLevel.LogAlways: return TraceEventLevel.Always;
+ case EventLevel.Verbose: return TraceEventLevel.Verbose;
+ case EventLevel.Warning: return TraceEventLevel.Warning;
+ default:
+ throw new InvalidOperationException($"Unknown EventLevel: {level}");
+ }
+ }
+
+ public override Task StopCollectingAsync()
+ {
+ _session.Dispose();
+ return Task.CompletedTask;
+ }
+ }
+}
--- /dev/null
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public abstract class EventCollector
+ {
+ public abstract Task StartCollectingAsync();
+ public abstract Task StopCollectingAsync();
+ }
+}
--- /dev/null
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public class EventPipeCollector : EventCollector
+ {
+ private readonly CollectionConfiguration _config;
+ private readonly string _configPath;
+
+ public EventPipeCollector(CollectionConfiguration config, string configPath)
+ {
+ _config = config;
+ _configPath = configPath;
+ }
+
+ public override Task StartCollectingAsync()
+ {
+ var configContent = _config.ToConfigString();
+ return File.WriteAllTextAsync(_configPath, configContent);
+ }
+
+ public override Task StopCollectingAsync()
+ {
+ File.Delete(_configPath);
+ return Task.CompletedTask;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.Globalization;
+using System.Text;
+using Microsoft.Internal.Utilities;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public class EventSpec
+ {
+ public string Provider { get; }
+ public ulong Keywords { get; }
+ public EventLevel Level { get; }
+ public IDictionary<string, string> Parameters { get; }
+
+ public EventSpec(string provider, ulong keywords, EventLevel level)
+ : this(provider, keywords, level, new Dictionary<string, string>())
+ {
+ }
+
+ public EventSpec(string provider, ulong keywords, EventLevel level, IDictionary<string, string> parameters)
+ {
+ Provider = provider;
+ Keywords = keywords;
+ Level = level;
+ Parameters = parameters;
+ }
+
+ public static bool TryParse(string input, out EventSpec spec)
+ {
+ spec = null;
+ var splat = input.Split(':');
+
+ if (splat.Length == 0)
+ {
+ return false;
+ }
+
+ var provider = splat[0];
+ var keywords = ulong.MaxValue;
+ var level = EventLevel.Verbose;
+ var parameters = new Dictionary<string, string>();
+
+ if (splat.Length > 1)
+ {
+ if (!TryParseKeywords(splat[1], provider, out keywords))
+ {
+ return false;
+ }
+ }
+
+ if (splat.Length > 2)
+ {
+ if (!TryParseLevel(splat[2], out level))
+ {
+ return false;
+ }
+ }
+
+ if (splat.Length > 3)
+ {
+ if (!TryParseParameters(splat[3], parameters))
+ {
+ return false;
+ }
+ }
+
+ spec = new EventSpec(provider, keywords, level, parameters);
+ return true;
+ }
+
+ public string ToConfigString()
+ {
+ var config = $"{Provider}:0x{Keywords:X}:{(int)Level}";
+ if(Parameters.Count > 0)
+ {
+ config += $":{FormatParameters(Parameters)}";
+ }
+ return config;
+ }
+
+ private static string FormatParameters(IDictionary<string, string> parameters)
+ {
+ var builder = new StringBuilder();
+ foreach(var (key, value) in parameters)
+ {
+ builder.Append($"{key}={value};");
+ }
+
+ // Remove the trailing ';'
+ builder.Length -= 1;
+
+ return builder.ToString();
+ }
+
+ private static bool TryParseParameters(string input, IDictionary<string, string> parameters)
+ {
+ var splat = input.Split(';');
+ foreach(var item in splat)
+ {
+ var splot = item.Split('=');
+ if(splot.Length != 2)
+ {
+ return false;
+ }
+
+ parameters[splot[0]] = splot[1];
+ }
+
+ return true;
+ }
+
+ private static bool TryParseLevel(string input, out EventLevel level)
+ {
+ level = EventLevel.Verbose;
+ if (int.TryParse(input, out var intLevel))
+ {
+ if (intLevel >= (int)EventLevel.LogAlways && intLevel <= (int)EventLevel.Verbose)
+ {
+ level = (EventLevel)intLevel;
+ return true;
+ }
+ }
+ else if (Enum.TryParse(input, ignoreCase: true, out level))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ private static bool TryParseKeywords(string input, string provider, out ulong keywords)
+ {
+ if (string.Equals("*", input, StringComparison.Ordinal))
+ {
+ keywords = ulong.MaxValue;
+ return true;
+ }
+ else if (input.StartsWith("0x"))
+ {
+ // Keywords
+ if (ulong.TryParse(input, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out keywords))
+ {
+ return true;
+ }
+ }
+ else if(KnownData.TryGetProvider(provider, out var knownProvider))
+ {
+ var splat = input.Split(',');
+ keywords = 0;
+ foreach(var item in splat)
+ {
+ if(knownProvider.Keywords.TryGetValue(item, out var knownKeyword))
+ {
+ keywords |= knownKeyword.Value;
+ }
+ else
+ {
+ throw new CommandLineException($"Keyword '{item}' is not a well-known keyword for '{provider}'");
+ }
+ }
+ return true;
+ }
+
+ keywords = ulong.MaxValue;
+ return false;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.Linq;
+using Microsoft.Diagnostics.Tracing.Parsers;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ internal static class KnownData
+ {
+ private static readonly IReadOnlyDictionary<string, KnownProvider> _knownProviders =
+ CreateKnownProviders().ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
+
+ private static readonly IReadOnlyDictionary<string, CollectionProfile> _knownProfiles =
+ CreateProfiles().ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
+
+ private static IEnumerable<KnownProvider> CreateKnownProviders()
+ {
+ yield return CreateClrProvider();
+ }
+
+ private static IEnumerable<CollectionProfile> CreateProfiles()
+ {
+ yield return new CollectionProfile(
+ CollectionProfile.DefaultProfileName,
+ "A default set of event providers useful for diagosing problems in any .NET application.",
+ new[] {
+ new EventSpec(ClrTraceEventParser.ProviderName, (ulong)ClrTraceEventParser.Keywords.Default, EventLevel.Informational)
+ },
+ Array.Empty<LoggerSpec>());
+
+ yield return new CollectionProfile(
+ "AspNetCore",
+ "A set of event providers useful for diagnosing problems in ASP.NET Core applications.",
+ new[]
+ {
+ new EventSpec("Microsoft-AspNetCore-Hosting", ulong.MaxValue, EventLevel.Informational),
+ },
+ Array.Empty<LoggerSpec>());
+
+ yield return new CollectionProfile(
+ "Kestrel",
+ "Detailed events for ASP.NET Core Kestrel",
+ new[]
+ {
+ new EventSpec("Microsoft-AspNetCore-Server-Kestrel", ulong.MaxValue, EventLevel.Verbose),
+ },
+ Array.Empty<LoggerSpec>());
+ }
+
+ private static KnownProvider CreateClrProvider()
+ {
+ return new KnownProvider(
+ ClrTraceEventParser.ProviderName,
+ ClrTraceEventParser.ProviderGuid,
+ ScanKeywordType(typeof(ClrTraceEventParser.Keywords)));
+ }
+
+ public static IReadOnlyList<CollectionProfile> GetAllProfiles() => _knownProfiles.Values.ToList();
+ public static IReadOnlyList<KnownProvider> GetAllProviders() => _knownProviders.Values.ToList();
+
+ public static bool TryGetProvider(string providerName, out KnownProvider provider) => _knownProviders.TryGetValue(providerName, out provider);
+ public static bool TryGetProfile(string profileName, out CollectionProfile profile) => _knownProfiles.TryGetValue(profileName, out profile);
+
+ private static IEnumerable<KnownKeyword> ScanKeywordType(Type keywordType)
+ {
+ var values = Enum.GetValues(keywordType).Cast<long>().ToList();
+ var keywords = values.Distinct().Select(v => new KnownKeyword(Enum.GetName(keywordType, v), (ulong)v)).ToList();
+ return keywords;
+ }
+ }
+
+ internal class KnownProvider
+ {
+ public string Name { get; }
+ public Guid Guid { get; }
+ public IReadOnlyDictionary<string, KnownKeyword> Keywords { get; }
+
+ public KnownProvider(string name, Guid guid, IEnumerable<KnownKeyword> keywords)
+ {
+ Name = name;
+ Guid = guid;
+ Keywords = keywords.ToDictionary(k => k.Name, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ internal class KnownKeyword
+ {
+ public string Name { get; }
+ public ulong Value { get; }
+
+ public KnownKeyword(string name, ulong value)
+ {
+ Name = name;
+ Value = value;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ public class LoggerSpec
+ {
+ // Handles case normalization because key lookup is case-insensitive.
+ private static readonly Dictionary<string, string> _levelMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Trace", "Trace" },
+ { "Debug", "Debug" },
+ { "Information", "Information" },
+ { "Warning", "Warning" },
+ { "Error", "Error" },
+ { "Critical", "Critical" },
+ };
+
+ public string Prefix { get; }
+ public string Level { get; }
+
+ public LoggerSpec(string prefix, string level)
+ {
+ Prefix = prefix;
+ Level = level;
+ }
+
+ public static bool TryParse(string input, out LoggerSpec spec)
+ {
+ var splat = input.Split(':');
+
+ var prefix = splat[0];
+ string level = null;
+ if (splat.Length > 1)
+ {
+ if (!_levelMap.TryGetValue(splat[1], out level))
+ {
+ spec = null;
+ return false;
+ }
+ }
+
+ spec = new LoggerSpec(prefix, level);
+ return true;
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using McMaster.Extensions.CommandLineUtils;
+using Microsoft.Internal.Utilities;
+
+namespace Microsoft.Diagnostics.Tools.Collect
+{
+ [Command(Name = "dotnet-trace", Description = "Collects Event Traces from .NET processes")]
+ internal class Program
+ {
+
+ [Option("-c|--config-path <CONFIG_PATH>", Description = "The path of the EventPipe config file to write, must be named [AppName].eventpipeconfig and be in the base directory for a managed app.")]
+ public string ConfigPath { get; set; }
+
+ [Option("--etw", Description = "Specify this flag to use ETW to collect events rather than using EventPipe (Windows only).")]
+ public bool Etw { get; set; }
+
+ [Required]
+ [Option("-p|--process-id <PROCESS_ID>", Description = "Filter to only the process with the specified process ID.")]
+ public int ProcessId { get; set; }
+
+ [Option("-o|--output <OUTPUT_DIRECTORY>", Description = "The directory to write the trace to. Defaults to the current working directory.")]
+ public string OutputDir { get; set; }
+
+ [Option("--buffer <BUFFER_SIZE_IN_MB>", Description = "The size of the in-memory circular buffer in megabytes.")]
+ public int? CircularMB { get; set; }
+
+ [Option("--provider <PROVIDER_SPEC>", Description = "An EventPipe provider to enable. A string in the form '<provider name>:<keywords>:<level>:<parameters>'. Can be specified multiple times to enable multiple providers.")]
+ public IList<string> Providers { get; set; }
+
+ [Option("--profile <PROFILE_NAME>", Description = "A collection profile to enable. Use '--list-profiles' to get a list of all available profiles. Can be mixed with '--provider' and specified multiple times.")]
+ public IList<string> Profiles { get; set; }
+
+ [Option("--logger <LOGGER_NAME>", Description = "A Microsoft.Extensions.Logging logger to enable. A string in the form '<logger prefix>:<level>'. Can be specified multiple times to enable multiple loggers.")]
+ public IList<string> Loggers { get; set; }
+
+ [Option("--keywords-for <PROVIDER_NAME>", Description = "Gets a list of known keywords (if any) for the specified provider.")]
+ public string KeywordsForProvider { get; set; }
+
+ [Option("--list-profiles", Description = "Gets a list of predefined collection profiles.")]
+ public bool ListProfiles { get; set; }
+
+ [Option("--no-default", Description = "Don't enable the default profile.")]
+ public bool NoDefault { get; set; }
+
+ public async Task<int> OnExecuteAsync(IConsole console, CommandLineApplication app)
+ {
+ if (ListProfiles)
+ {
+ WriteProfileList(console.Out);
+ return 0;
+ }
+
+ if (!string.IsNullOrEmpty(KeywordsForProvider))
+ {
+ return ExecuteKeywordsForAsync(console);
+ }
+
+ if(string.IsNullOrEmpty(ConfigPath))
+ {
+ ConfigPath = ConfigPathDetector.TryDetectConfigPath(ProcessId);
+ if(string.IsNullOrEmpty(ConfigPath))
+ {
+ console.Error.WriteLine("Couldn't determine the path for the eventpipeconfig file from the process ID. Specify the '--config-path' option to provide it manually.");
+ return 1;
+ }
+ console.WriteLine($"Detected config file path: {ConfigPath}");
+ }
+
+ var config = new CollectionConfiguration()
+ {
+ ProcessId = ProcessId,
+ CircularMB = CircularMB,
+ OutputPath = string.IsNullOrEmpty(OutputDir) ? Directory.GetCurrentDirectory() : OutputDir
+ };
+
+ if (Profiles != null && Profiles.Count > 0)
+ {
+ foreach (var profile in Profiles)
+ {
+ if (!KnownData.TryGetProfile(profile, out var collectionProfile))
+ {
+ console.Error.WriteLine($"Unknown profile name: '{profile}'. See 'dotnet-trace --list-profiles' to get a list of profiles.");
+ return 1;
+ }
+ config.AddProfile(collectionProfile);
+ }
+ }
+
+ if (Providers != null && Providers.Count > 0)
+ {
+ foreach (var provider in Providers)
+ {
+ if (!EventSpec.TryParse(provider, out var providerSpec))
+ {
+ console.Error.WriteLine($"Invalid provider specification: '{provider}'. See 'dotnet-trace --help' for more information.");
+ return 1;
+ }
+ config.Providers.Add(providerSpec);
+ }
+ }
+
+ if (Loggers != null && Loggers.Count > 0)
+ {
+ foreach (var logger in Loggers)
+ {
+ if (!LoggerSpec.TryParse(logger, out var loggerSpec))
+ {
+ console.Error.WriteLine($"Invalid logger specification: '{logger}'. See 'dotnet-trace --help' for more information.");
+ return 1;
+ }
+ config.Loggers.Add(loggerSpec);
+ }
+ }
+
+ if (!NoDefault)
+ {
+ // Enable the default profile if nothing is specified
+ if (!KnownData.TryGetProfile(CollectionProfile.DefaultProfileName, out var defaultProfile))
+ {
+ console.Error.WriteLine("No providers or profiles were specified and there is no default profile available.");
+ return 1;
+ }
+ config.AddProfile(defaultProfile);
+ }
+
+ if (!TryCreateCollector(console, config, out var collector))
+ {
+ return 1;
+ }
+
+ // Write the config file contents
+ await collector.StartCollectingAsync();
+ console.WriteLine("Tracing has started. Press Ctrl-C to stop.");
+
+ await console.WaitForCtrlCAsync();
+
+ await collector.StopCollectingAsync();
+ console.WriteLine($"Tracing stopped. Trace files written to {config.OutputPath}");
+
+ return 0;
+ }
+
+ private static void WriteProfileList(TextWriter console)
+ {
+ var profiles = KnownData.GetAllProfiles();
+ var maxNameLength = profiles.Max(p => p.Name.Length);
+ console.WriteLine("Available profiles:");
+ foreach (var profile in profiles)
+ {
+ console.WriteLine($"* {profile.Name.PadRight(maxNameLength)} {profile.Description}");
+ }
+ }
+
+ private int ExecuteKeywordsForAsync(IConsole console)
+ {
+ if (KnownData.TryGetProvider(KeywordsForProvider, out var provider))
+ {
+ console.WriteLine($"Known keywords for {provider.Name} ({provider.Guid}):");
+ foreach (var keyword in provider.Keywords.Values)
+ {
+ console.WriteLine($"* 0x{keyword.Value:x16} {keyword.Name}");
+ }
+ return 0;
+ }
+ else
+ {
+ console.WriteLine($"There are no known keywords for {KeywordsForProvider}.");
+ return 1;
+ }
+ }
+
+ private bool TryCreateCollector(IConsole console, CollectionConfiguration config, out EventCollector collector)
+ {
+ collector = null;
+
+ if (Etw)
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ console.Error.WriteLine("Error: ETW-based collection is only supported on Windows.");
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(ConfigPath))
+ {
+ console.Error.WriteLine("WARNING: The '-c' option is ignored when using ETW-based collection.");
+ }
+ collector = new EtwCollector(config);
+ return true;
+ }
+ else
+ {
+ if (File.Exists(ConfigPath))
+ {
+ console.Error.WriteLine("Config file already exists, tracing is already underway by a different consumer.");
+ return false;
+ }
+
+ collector = new EventPipeCollector(config, ConfigPath);
+ return true;
+ }
+ }
+
+ private static int Main(string[] args)
+ {
+ DebugUtil.WaitForDebuggerIfRequested(ref args);
+
+ try
+ {
+ var app = new CommandLineApplication<Program>();
+ app.Conventions.UseDefaultConventions();
+ app.ExtendedHelpText = GetExtendedHelp();
+ return app.Execute(args);
+ }
+ catch (PlatformNotSupportedException ex)
+ {
+ Console.Error.WriteLine(ex.Message);
+ return 1;
+ }
+ catch (CommandLineException clex)
+ {
+ Console.Error.WriteLine(clex.Message);
+ return 1;
+ }
+ catch (OperationCanceledException)
+ {
+ return 0;
+ }
+ }
+
+ private static string GetExtendedHelp()
+ {
+ using (var writer = new StringWriter())
+ {
+ writer.WriteLine();
+ writer.WriteLine("Profiles");
+ writer.WriteLine(" Profiles are sets of well-defined provider configurations that provide useful information.");
+ writer.WriteLine();
+ WriteProfileList(writer);
+ writer.WriteLine();
+ writer.WriteLine("Specifying Loggers:");
+ writer.WriteLine(" Use one of the following formats to specify a logger in '--logger'");
+ writer.WriteLine(" * - Enable all messages at all levels from all loggers.");
+ writer.WriteLine(" *:<level> - Enable messages at the specified '<level>' or higher from all loggers.");
+ writer.WriteLine(" <loggerPrefix> - Enable all messages at all levels from all loggers starting with '<loggerPrefix>'.");
+ writer.WriteLine(" <loggerPrefix>:<level> - Enable messages at the specified '<level>' or higher from all loggers starting with '<loggerPrefix>'.");
+ writer.WriteLine();
+ writer.WriteLine(" '<loggerPrefix>' is the prefix for a logger to enable. For example 'Microsoft.AspNetCore' to enable all ASP.NET Core loggers.");
+ writer.WriteLine(" '<level>' can be one of: Critical, Error, Warning, Informational, Debug, or Trace.");
+ writer.WriteLine();
+ writer.WriteLine("Specifying Providers:");
+ writer.WriteLine(" Use one of the following formats to specify a provider in '--provider'");
+ writer.WriteLine(" <providerName> - Enable all events at all levels for the provider.");
+ writer.WriteLine(" <providerName>:<keywords> - Enable events matching the specified keywords for the specified provider.");
+ writer.WriteLine(" <providerName>:<keywords>:<level> - Enable events matching the specified keywords, at the specified level for the specified provider.");
+ writer.WriteLine(" <providerName>:<keywords>:<level>:<parameters> - Enable events matching the specified keywords, at the specified level for the specified provider and provide key-value parameters.");
+ writer.WriteLine();
+ writer.WriteLine(" '<provider>' must be the name of the EventSource.");
+ writer.WriteLine(" '<level>' can be one of: Critical (1), Error (2), Warning (3), Informational (4), Verbose (5). Either the name or number can be specified.");
+ writer.WriteLine(" '<keywords>' is one of the following:");
+ writer.WriteLine(" A '*' character, indicating ALL keywords should be enabled (this can be very costly for some providers!)");
+ writer.WriteLine(" A comma-separated list of known keywords for a provider (use 'dotnet trace collect --keywords-for [providerName]' to get a list of known keywords for a provider)");
+ writer.WriteLine(" A 64-bit hexadecimal number, starting with '0x' indicating the keywords to enable");
+ writer.WriteLine(" '<parameters>' is an optional list of key-value parameters to provide to the EventPipe provider. The expected values depend on the provider you are enabling.");
+ writer.WriteLine(" This should be a list of key-value pairs, in the form: '<key1>=<value1>;<key2>=<value2>;...'. Note that some shells, such as PowerShell, require that you");
+ writer.WriteLine(" quote or escape the ';' character.");
+ return writer.GetStringBuilder().ToString();
+ }
+ }
+ }
+}
--- /dev/null
+// Borrowed from https://github.com/dotnet/corefx/blob/b2f960abe1d8690be9d68dd9b56ea7636fb4a38b/src/Common/src/System/IO/StringParser.cs
+
+// 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 System.Diagnostics;
+
+namespace System.IO
+{
+ /// <summary>
+ /// Provides a string parser that may be used instead of String.Split
+ /// to avoid unnecessary string and array allocations.
+ /// </summary>
+ internal struct StringParser
+ {
+ /// <summary>The string being parsed.</summary>
+ private readonly string _buffer;
+
+ /// <summary>The separator character used to separate subcomponents of the larger string.</summary>
+ private readonly char _separator;
+
+ /// <summary>true if empty subcomponents should be skipped; false to treat them as valid entries.</summary>
+ private readonly bool _skipEmpty;
+
+ /// <summary>The starting index from which to parse the current entry.</summary>
+ private int _startIndex;
+
+ /// <summary>The ending index that represents the next index after the last character that's part of the current entry.</summary>
+ private int _endIndex;
+
+ /// <summary>Initialize the StringParser.</summary>
+ /// <param name="buffer">The string to parse.</param>
+ /// <param name="separator">The separator character used to separate subcomponents of <paramref name="buffer"/>.</param>
+ /// <param name="skipEmpty">true if empty subcomponents should be skipped; false to treat them as valid entries. Defaults to false.</param>
+ public StringParser(string buffer, char separator, bool skipEmpty = false)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ _buffer = buffer;
+ _separator = separator;
+ _skipEmpty = skipEmpty;
+ _startIndex = -1;
+ _endIndex = -1;
+ }
+
+ /// <summary>Moves to the next component of the string.</summary>
+ /// <returns>true if there is a next component to be parsed; otherwise, false.</returns>
+ public bool MoveNext()
+ {
+ if (_buffer == null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ while (true)
+ {
+ if (_endIndex >= _buffer.Length)
+ {
+ _startIndex = _endIndex;
+ return false;
+ }
+
+ int nextSeparator = _buffer.IndexOf(_separator, _endIndex + 1);
+ _startIndex = _endIndex + 1;
+ _endIndex = nextSeparator >= 0 ? nextSeparator : _buffer.Length;
+
+ if (!_skipEmpty || _endIndex >= _startIndex + 1)
+ {
+ return true;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Moves to the next component of the string. If there isn't one, it throws an exception.
+ /// </summary>
+ public void MoveNextOrFail()
+ {
+ if (!MoveNext())
+ {
+ ThrowForInvalidData();
+ }
+ }
+
+ /// <summary>
+ /// Moves to the next component of the string and returns it as a string.
+ /// </summary>
+ /// <returns></returns>
+ public string MoveAndExtractNext()
+ {
+ MoveNextOrFail();
+ return _buffer.Substring(_startIndex, _endIndex - _startIndex);
+ }
+
+ /// <summary>
+ /// Moves to the next component of the string, which must be enclosed in the only set of top-level parentheses
+ /// in the string. The extracted value will be everything between (not including) those parentheses.
+ /// </summary>
+ /// <returns></returns>
+ public string MoveAndExtractNextInOuterParens()
+ {
+ // Move to the next position
+ MoveNextOrFail();
+
+ // After doing so, we should be sitting at a the opening paren.
+ if (_buffer[_startIndex] != '(')
+ {
+ ThrowForInvalidData();
+ }
+
+ // Since we only allow for one top-level set of parentheses, find the last
+ // parenthesis in the string; it's paired with the opening one we just found.
+ int lastParen = _buffer.LastIndexOf(')');
+ if (lastParen == -1 || lastParen < _startIndex)
+ {
+ ThrowForInvalidData();
+ }
+
+ // Extract the contents of the parens, then move our ending position to be after the paren
+ string result = _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1);
+ _endIndex = lastParen + 1;
+
+ return result;
+ }
+
+ /// <summary>
+ /// Gets the current subcomponent of the string as a string.
+ /// </summary>
+ public string ExtractCurrent()
+ {
+ if (_buffer == null || _startIndex == -1)
+ {
+ throw new InvalidOperationException();
+ }
+ return _buffer.Substring(_startIndex, _endIndex - _startIndex);
+ }
+
+ /// <summary>Moves to the next component and parses it as an Int32.</summary>
+ public unsafe int ParseNextInt32()
+ {
+ MoveNextOrFail();
+
+ bool negative = false;
+ int result = 0;
+
+ fixed (char* bufferPtr = _buffer)
+ {
+ char* p = bufferPtr + _startIndex;
+ char* end = bufferPtr + _endIndex;
+
+ if (p == end)
+ {
+ ThrowForInvalidData();
+ }
+
+ if (*p == '-')
+ {
+ negative = true;
+ p++;
+ if (p == end)
+ {
+ ThrowForInvalidData();
+ }
+ }
+
+ while (p != end)
+ {
+ int d = *p - '0';
+ if (d < 0 || d > 9)
+ {
+ ThrowForInvalidData();
+ }
+ result = negative ? checked((result * 10) - d) : checked((result * 10) + d);
+
+ p++;
+ }
+ }
+
+ Debug.Assert(result == int.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
+ return result;
+ }
+
+ /// <summary>Moves to the next component and parses it as an Int64.</summary>
+ public unsafe long ParseNextInt64()
+ {
+ MoveNextOrFail();
+
+ bool negative = false;
+ long result = 0;
+
+ fixed (char* bufferPtr = _buffer)
+ {
+ char* p = bufferPtr + _startIndex;
+ char* end = bufferPtr + _endIndex;
+
+ if (p == end)
+ {
+ ThrowForInvalidData();
+ }
+
+ if (*p == '-')
+ {
+ negative = true;
+ p++;
+ if (p == end)
+ {
+ ThrowForInvalidData();
+ }
+ }
+
+ while (p != end)
+ {
+ int d = *p - '0';
+ if (d < 0 || d > 9)
+ {
+ ThrowForInvalidData();
+ }
+ result = negative ? checked((result * 10) - d) : checked((result * 10) + d);
+
+ p++;
+ }
+ }
+
+ Debug.Assert(result == long.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
+ return result;
+ }
+
+ /// <summary>Moves to the next component and parses it as a UInt32.</summary>
+ public unsafe uint ParseNextUInt32()
+ {
+ MoveNextOrFail();
+ if (_startIndex == _endIndex)
+ {
+ ThrowForInvalidData();
+ }
+
+ uint result = 0;
+ fixed (char* bufferPtr = _buffer)
+ {
+ char* p = bufferPtr + _startIndex;
+ char* end = bufferPtr + _endIndex;
+ while (p != end)
+ {
+ int d = *p - '0';
+ if (d < 0 || d > 9)
+ {
+ ThrowForInvalidData();
+ }
+ result = (uint)checked((result * 10) + d);
+
+ p++;
+ }
+ }
+
+ Debug.Assert(result == uint.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
+ return result;
+ }
+
+ /// <summary>Moves to the next component and parses it as a UInt64.</summary>
+ public unsafe ulong ParseNextUInt64()
+ {
+ MoveNextOrFail();
+
+ ulong result = 0;
+ fixed (char* bufferPtr = _buffer)
+ {
+ char* p = bufferPtr + _startIndex;
+ char* end = bufferPtr + _endIndex;
+ while (p != end)
+ {
+ int d = *p - '0';
+ if (d < 0 || d > 9)
+ {
+ ThrowForInvalidData();
+ }
+ result = checked((result * 10ul) + (ulong)d);
+
+ p++;
+ }
+ }
+
+ Debug.Assert(result == ulong.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
+ return result;
+ }
+
+ /// <summary>Moves to the next component and parses it as a Char.</summary>
+ public char ParseNextChar()
+ {
+ MoveNextOrFail();
+
+ if (_endIndex - _startIndex != 1)
+ {
+ ThrowForInvalidData();
+ }
+ char result = _buffer[_startIndex];
+
+ Debug.Assert(result == char.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
+ return result;
+ }
+
+ internal delegate T ParseRawFunc<T>(string buffer, ref int startIndex, ref int endIndex);
+
+ /// <summary>
+ /// Moves to the next component and hands the raw buffer and indexing data to a selector function
+ /// that can validate and return the appropriate data from the component.
+ /// </summary>
+ internal T ParseRaw<T>(ParseRawFunc<T> selector)
+ {
+ MoveNextOrFail();
+ return selector(_buffer, ref _startIndex, ref _endIndex);
+ }
+
+ /// <summary>
+ /// Gets the current subcomponent and all remaining components of the string as a string.
+ /// </summary>
+ public string ExtractCurrentToEnd()
+ {
+ if (_buffer == null || _startIndex == -1)
+ {
+ throw new InvalidOperationException();
+ }
+ return _buffer.Substring(_startIndex);
+ }
+
+ /// <summary>Throws unconditionally for invalid data.</summary>
+ private static void ThrowForInvalidData()
+ {
+ throw new InvalidDataException();
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+
+ <!-- Target .NET Core 2.1 so it will run on LTS -->
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+
+ <RootNamespace>Microsoft.Diagnostics.Tools.Collect</RootNamespace>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+
+ <!-- Don't pack until ship engineering is done. Currently causing the official job to fail.
+ <IsPackable>true</IsPackable>
+ <PackAsTool>true</PackAsTool>
+ -->
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\Common\CommandLineException.cs" Link="CommandLineException.cs" />
+ <Compile Include="..\Common\ConsoleCancellation.cs" Link="ConsoleCancellation.cs" />
+ <Compile Include="..\Common\DebugUtil.cs" Link="DebugUtil.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.2.5" />
+ <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.30" />
+ </ItemGroup>
+
+</Project>
\ No newline at end of file