Allow process selection using runtime instance cookie. (#1414)
authorJustin Anderson <jander-msft@users.noreply.github.com>
Fri, 7 Aug 2020 20:42:23 +0000 (13:42 -0700)
committerGitHub <noreply@github.com>
Fri, 7 Aug 2020 20:42:23 +0000 (13:42 -0700)
src/Microsoft.Diagnostics.Monitoring.RestServer/Controllers/DiagController.cs
src/Microsoft.Diagnostics.Monitoring.RestServer/MetricsService.cs
src/Microsoft.Diagnostics.Monitoring/Contracts/IDiagnosticServices.cs
src/Microsoft.Diagnostics.Monitoring/Contracts/ProcessFilter.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring/DiagnosticServices.cs
src/Tools/dotnet-monitor/Startup.cs

index f0238863060e6b2bbff65cbdf85b7cfde71a76e7..85fcd5b027fddb9357f48f116f076630aef0faa3 100644 (file)
@@ -57,13 +57,15 @@ namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
             });
         }
 
-        [HttpGet("dump/{pid?}")]
-        public Task<ActionResult> GetDump(int? pid, [FromQuery] DumpType type = DumpType.WithHeap)
+        [HttpGet("dump/{processFilter?}")]
+        public Task<ActionResult> GetDump(
+            ProcessFilter? processFilter,
+            [FromQuery] DumpType type = DumpType.WithHeap)
         {
             return this.InvokeService(async () =>
             {
-                int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
-                Stream result = await _diagnosticServices.GetDump(pidValue, type, HttpContext.RequestAborted);
+                IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+                Stream result = await _diagnosticServices.GetDump(processInfo, type, HttpContext.RequestAborted);
 
                 string dumpFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
                     FormattableString.Invariant($"dump_{GetFileNameTimeStampUtcNow()}.dmp") :
@@ -75,20 +77,21 @@ namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
             });
         }
 
-        [HttpGet("gcdump/{pid?}")]
-        public Task<ActionResult> GetGcDump(int? pid)
+        [HttpGet("gcdump/{processFilter?}")]
+        public Task<ActionResult> GetGcDump(
+            ProcessFilter? processFilter)
         {
             return this.InvokeService(async () =>
             {
-                int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
-                Stream result = await _diagnosticServices.GetGcDump(pidValue, this.HttpContext.RequestAborted);
-                return File(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{pidValue}.gcdump"));
+                IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+                Stream result = await _diagnosticServices.GetGcDump(processInfo, this.HttpContext.RequestAborted);
+                return File(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.Pid}.gcdump"));
             });
         }
 
-        [HttpGet("trace/{pid?}")]
+        [HttpGet("trace/{processFilter?}")]
         public Task<ActionResult> Trace(
-            int? pid,
+            ProcessFilter? processFilter,
             [FromQuery]TraceProfile profile = DefaultTraceProfiles,
             [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
             [FromQuery][Range(1, int.MaxValue)] int metricsIntervalSeconds = 1)
@@ -117,13 +120,13 @@ namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
 
                 var aggregateConfiguration = new AggregateSourceConfiguration(configurations.ToArray());
 
-                return await StartTrace(pid, aggregateConfiguration, duration);
+                return await StartTrace(processFilter, aggregateConfiguration, duration);
             });
         }
 
-        [HttpPost("trace/{pid?}")]
+        [HttpPost("trace/{processFilter?}")]
         public Task<ActionResult> TraceCustomConfiguration(
-            int? pid,
+            ProcessFilter? processFilter,
             [FromBody][Required] EventPipeConfigurationModel configuration,
             [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30)
         {
@@ -153,18 +156,21 @@ namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
                     requestRundown: configuration.RequestRundown,
                     bufferSizeInMB: configuration.BufferSizeInMB);
 
-                return await StartTrace(pid, traceConfiguration, duration);
+                return await StartTrace(processFilter, traceConfiguration, duration);
             });
         }
 
-        [HttpGet("logs/{pid?}")]
+        [HttpGet("logs/{processFilter?}")]
         [Produces(ContentTypeEventStream, ContentTypeNdJson, ContentTypeJson)]
-        public Task<ActionResult> Logs(int? pid, [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30, [FromQuery] LogLevel level = LogLevel.Debug)
+        public Task<ActionResult> Logs(
+            ProcessFilter? processFilter,
+            [FromQuery][Range(-1, int.MaxValue)] int durationSeconds = 30,
+            [FromQuery] LogLevel level = LogLevel.Debug)
         {
             TimeSpan duration = ConvertSecondsToTimeSpan(durationSeconds);
             return this.InvokeService(async () =>
             {
-                int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
+                IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
 
                 LogFormat format = ComputeLogFormat(Request.GetTypedHeaders().Accept);
                 if (format == LogFormat.None)
@@ -173,20 +179,23 @@ namespace Microsoft.Diagnostics.Monitoring.RestServer.Controllers
                 }
 
                 string contentType = (format == LogFormat.EventStream) ? ContentTypeEventStream : ContentTypeNdJson;
-                string downloadName = (format == LogFormat.EventStream) ? null : FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{pidValue}.txt");
+                string downloadName = (format == LogFormat.EventStream) ? null : FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.Pid}.txt");
 
                 return new OutputStreamResult(async (outputStream, token) =>
                 {
-                    await _diagnosticServices.StartLogs(outputStream, pidValue, duration, format, level, token);
+                    await _diagnosticServices.StartLogs(outputStream, processInfo, duration, format, level, token);
                 }, contentType, downloadName);
             });
         }
 
-        private async Task<StreamWithCleanupResult> StartTrace(int? pid, MonitoringSourceConfiguration configuration, TimeSpan duration)
+        private async Task<StreamWithCleanupResult> StartTrace(
+            ProcessFilter? processFilter,
+            MonitoringSourceConfiguration configuration,
+            TimeSpan duration)
         {
-            int pidValue = await _diagnosticServices.ResolveProcessAsync(pid, HttpContext.RequestAborted);
-            IStreamWithCleanup result = await _diagnosticServices.StartTrace(pidValue, configuration, duration, this.HttpContext.RequestAborted);
-            return new StreamWithCleanupResult(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{pidValue}.nettrace"));
+            IProcessInfo processInfo = await _diagnosticServices.GetProcessAsync(processFilter, HttpContext.RequestAborted);
+            IStreamWithCleanup result = await _diagnosticServices.StartTrace(processInfo, configuration, duration, this.HttpContext.RequestAborted);
+            return new StreamWithCleanupResult(result, "application/octet-stream", FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.Pid}.nettrace"));
         }
 
         private static TimeSpan ConvertSecondsToTimeSpan(int durationSeconds)
index 7d3b7333af5b204c546dcb917bd9992889623363..d3f786dad17d7197243aca7a6ca5cbc3d6f49a16 100644 (file)
@@ -43,9 +43,8 @@ namespace Microsoft.Diagnostics.Monitoring.RestServer
                     {
                         //TODO In multi-process scenarios, how do we decide which process to choose?
                         //One possibility is to enable metrics after a request to begin polling for metrics
-                        int pid = await _services.ResolveProcessAsync(pid: null, stoppingToken);
-                        var client = new DiagnosticsClient(pid);
-                        await _pipeProcessor.Process(client, pid, Timeout.InfiniteTimeSpan, stoppingToken);
+                        IProcessInfo pi = await _services.GetProcessAsync(filter: null, stoppingToken);
+                        await _pipeProcessor.Process(pi.Client, pi.Pid, Timeout.InfiniteTimeSpan, stoppingToken);
                     }
                     catch(Exception e) when (!(e is OperationCanceledException))
                     {
index d118a65e521b628105e0f54f60e330c4a5eb67e2..c23eff971412b196a31e253635768a8588538c5c 100644 (file)
@@ -2,12 +2,13 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
-using Microsoft.Extensions.Logging;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Extensions.Logging;
 
 namespace Microsoft.Diagnostics.Monitoring
 {
@@ -19,15 +20,15 @@ namespace Microsoft.Diagnostics.Monitoring
     {
         Task<IEnumerable<IProcessInfo>> GetProcessesAsync(CancellationToken token);
 
-        Task<int> ResolveProcessAsync(int? pid, CancellationToken token);
+        Task<IProcessInfo> GetProcessAsync(ProcessFilter? filter, CancellationToken token);
 
-        Task<Stream> GetDump(int pid, DumpType mode, CancellationToken token);
+        Task<Stream> GetDump(IProcessInfo pi, DumpType mode, CancellationToken token);
 
-        Task<Stream> GetGcDump(int pid, CancellationToken token);
+        Task<Stream> GetGcDump(IProcessInfo pi, CancellationToken token);
 
-        Task<IStreamWithCleanup> StartTrace(int pid, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token);
+        Task<IStreamWithCleanup> StartTrace(IProcessInfo pi, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token);
 
-        Task StartLogs(Stream outputStream, int pid, TimeSpan duration, LogFormat logFormat, LogLevel logLevel, CancellationToken token);
+        Task StartLogs(Stream outputStream, IProcessInfo pi, TimeSpan duration, LogFormat logFormat, LogLevel logLevel, CancellationToken token);
     }
 
     public interface IStreamWithCleanup : IAsyncDisposable
@@ -37,6 +38,8 @@ namespace Microsoft.Diagnostics.Monitoring
 
     public interface IProcessInfo
     {
+        DiagnosticsClient Client { get; }
+
         int Pid { get; }
 
         Guid Uid { get; }
diff --git a/src/Microsoft.Diagnostics.Monitoring/Contracts/ProcessFilter.cs b/src/Microsoft.Diagnostics.Monitoring/Contracts/ProcessFilter.cs
new file mode 100644 (file)
index 0000000..0b32019
--- /dev/null
@@ -0,0 +1,62 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Microsoft.Diagnostics.Monitoring
+{
+    [TypeConverter(typeof(ProcessFilterTypeConverter))]
+    public struct ProcessFilter
+    {
+        public ProcessFilter(int processId)
+        {
+            ProcessId = processId;
+            RuntimeInstanceCookie = null;
+        }
+
+        public ProcessFilter(Guid runtimeInstanceCookie)
+        {
+            ProcessId = null;
+            RuntimeInstanceCookie = runtimeInstanceCookie;
+        }
+
+        public int? ProcessId { get; }
+
+        public Guid? RuntimeInstanceCookie { get; }
+    }
+
+    internal class ProcessFilterTypeConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            if (null == sourceType)
+            {
+                throw new ArgumentNullException(nameof(sourceType));
+            }
+            return sourceType == typeof(string) || sourceType == typeof(ProcessFilter);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            if (value is string valueString)
+            {
+                if (string.IsNullOrEmpty(valueString))
+                {
+                    return null;
+                }
+                else if (Guid.TryParse(valueString, out Guid cookie))
+                {
+                    return new ProcessFilter(cookie);
+                }
+                else if (int.TryParse(valueString, out int processId))
+                {
+                    return new ProcessFilter(processId);
+                }
+            }
+            else if (value is ProcessFilter identifier)
+            {
+                return identifier;
+            }
+            throw new FormatException();
+        }
+    }
+}
index 45099f793965c7f5a2f8965956b1fd3495ec61cc..3bf4dacff576cdabbe625f141b0cacdc7be57e96 100644 (file)
@@ -19,7 +19,8 @@ namespace Microsoft.Diagnostics.Monitoring
 {
     public sealed class DiagnosticServices : IDiagnosticServices
     {
-        private const int DockerEntrypointProcessId = 1;
+        // A Docker container's entrypoint process ID is 1
+        private static readonly ProcessFilter DockerEntrypointProcessFilter = new ProcessFilter(1);
 
         // The amount of time to wait when checking if the docker entrypoint process is a .NET process
         // with a diagnostics transport connection.
@@ -39,7 +40,7 @@ namespace Microsoft.Diagnostics.Monitoring
             {
                 var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
 
-                return endpointInfos.Select(c => new ProcessInfo(c.RuntimeInstanceCookie, c.ProcessId));
+                return endpointInfos.Select(ProcessInfo.FromEndpointInfo);
             }
             catch (UnauthorizedAccessException)
             {
@@ -47,38 +48,36 @@ namespace Microsoft.Diagnostics.Monitoring
             }
         }
 
-        public async Task<Stream> GetDump(int pid, DumpType mode, CancellationToken token)
+        public async Task<Stream> GetDump(IProcessInfo pi, DumpType mode, CancellationToken token)
         {
-            string dumpFilePath = Path.Combine(Path.GetTempPath(), FormattableString.Invariant($"{Guid.NewGuid()}_{pid}"));
+            string dumpFilePath = Path.Combine(Path.GetTempPath(), FormattableString.Invariant($"{Guid.NewGuid()}_{pi.Pid}"));
             NETCore.Client.DumpType dumpType = MapDumpType(mode);
 
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
                 // Get the process
-                Process process = Process.GetProcessById(pid);
+                Process process = Process.GetProcessById(pi.Pid);
                 await Dumper.CollectDumpAsync(process, dumpFilePath, dumpType);
             }
             else
             {
-                var client = await GetClientAsync(pid, CancellationToken.None);
                 await Task.Run(() =>
                 {
-                    client.WriteDump(dumpType, dumpFilePath);
+                    pi.Client.WriteDump(dumpType, dumpFilePath);
                 });
             }
 
             return new AutoDeleteFileStream(dumpFilePath);
         }
 
-        public async Task<Stream> GetGcDump(int pid, CancellationToken token)
+        public async Task<Stream> GetGcDump(IProcessInfo pi, CancellationToken token)
         {
             var graph = new MemoryGraph(50_000);
             await using var processor = new DiagnosticsEventPipeProcessor(
                 PipeMode.GCDump,
                 gcGraph: graph);
 
-            var client = await GetClientAsync(pid, token);
-            await processor.Process(client, pid, Timeout.InfiniteTimeSpan, token);
+            await processor.Process(pi.Client, pi.Pid, Timeout.InfiniteTimeSpan, token);
 
             var dumper = new GCHeapDump(graph);
             dumper.CreationTool = "dotnet-monitor";
@@ -91,15 +90,14 @@ namespace Microsoft.Diagnostics.Monitoring
             return stream;
         }
 
-        public async Task<IStreamWithCleanup> StartTrace(int pid, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token)
+        public async Task<IStreamWithCleanup> StartTrace(IProcessInfo pi, MonitoringSourceConfiguration configuration, TimeSpan duration, CancellationToken token)
         {
             DiagnosticsMonitor monitor = new DiagnosticsMonitor(configuration);
-            var client = await GetClientAsync(pid, token);
-            Stream stream = await monitor.ProcessEvents(client, duration, token);
+            Stream stream = await monitor.ProcessEvents(pi.Client, duration, token);
             return new StreamWithCleanup(monitor, stream);
         }
 
-        public async Task StartLogs(Stream outputStream, int pid, TimeSpan duration, LogFormat format, LogLevel level, CancellationToken token)
+        public async Task StartLogs(Stream outputStream, IProcessInfo pi, TimeSpan duration, LogFormat format, LogLevel level, CancellationToken token)
         {
             using var loggerFactory = new LoggerFactory();
 
@@ -110,13 +108,12 @@ namespace Microsoft.Diagnostics.Monitoring
                 loggerFactory: loggerFactory,
                 logsLevel: level);
 
-            var client = await GetClientAsync(pid, token);
-            await processor.Process(client, pid, duration, token);
+            await processor.Process(pi.Client, pi.Pid, duration, token);
         }
 
         private static NETCore.Client.DumpType MapDumpType(DumpType dumpType)
         {
-            switch(dumpType)
+            switch (dumpType)
             {
                 case DumpType.Full:
                     return NETCore.Client.DumpType.Full;
@@ -131,11 +128,15 @@ namespace Microsoft.Diagnostics.Monitoring
             }
         }
 
-        public async Task<int> ResolveProcessAsync(int? pid, CancellationToken token)
+        public async Task<IProcessInfo> GetProcessAsync(ProcessFilter? filter, CancellationToken token)
         {
-            if (pid.HasValue)
+            var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
+
+            if (filter.HasValue)
             {
-                return pid.Value;
+                return GetSingleProcessInfo(
+                    endpointInfos,
+                    filter);
             }
 
             // Short-circuit for when running in a Docker container.
@@ -143,52 +144,63 @@ namespace Microsoft.Diagnostics.Monitoring
             {
                 try
                 {
-                    var client = await GetClientAsync(DockerEntrypointProcessId, token);
+                    IProcessInfo processInfo = GetSingleProcessInfo(
+                        endpointInfos,
+                        DockerEntrypointProcessFilter);
+
                     using var timeoutSource = new CancellationTokenSource(DockerEntrypointWaitTimeout);
-                    
-                    await client.WaitForConnectionAsync(timeoutSource.Token);
 
-                    return DockerEntrypointProcessId;
+                    await processInfo.Client.WaitForConnectionAsync(timeoutSource.Token);
+
+                    return processInfo;
                 }
                 catch
                 {
-                    // Process ID 1 doesn't exist or didn't advertise in the reverse pipe configuration.
+                    // Process ID 1 doesn't exist, didn't advertise in connect mode, or is not a .NET process.
+                }
+            }
+
+            return GetSingleProcessInfo(
+                endpointInfos,
+                filter: null);
+        }
+
+        private IProcessInfo GetSingleProcessInfo(IEnumerable<IEndpointInfo> endpointInfos, ProcessFilter? filter)
+        {
+            if (filter.HasValue)
+            {
+                if (filter.Value.RuntimeInstanceCookie.HasValue)
+                {
+                    Guid cookie = filter.Value.RuntimeInstanceCookie.Value;
+                    endpointInfos = endpointInfos.Where(info => info.RuntimeInstanceCookie == cookie);
+                }
+
+                if (filter.Value.ProcessId.HasValue)
+                {
+                    int pid = filter.Value.ProcessId.Value;
+                    endpointInfos = endpointInfos.Where(info => info.ProcessId == pid);
                 }
             }
 
-            // Only return a process ID if there is exactly one discoverable process.
-            IProcessInfo[] processes = (await GetProcessesAsync(token)).ToArray();
-            switch (processes.Length)
+            IEndpointInfo[] endpointInfoArray = endpointInfos.ToArray();
+            switch (endpointInfoArray.Length)
             {
                 case 0:
                     throw new ArgumentException("Unable to discover a target process.");
                 case 1:
-                    return processes[0].Pid;
+                    return ProcessInfo.FromEndpointInfo(endpointInfoArray[0]);
                 default:
 #if DEBUG
-                    Process process = processes.Select(p => Process.GetProcessById(p.Pid)).FirstOrDefault(p => string.Equals(p.ProcessName, "iisexpress", StringComparison.OrdinalIgnoreCase));
-                    if (process != null)
+                    IEndpointInfo endpointInfo = endpointInfoArray.FirstOrDefault(info => string.Equals(Process.GetProcessById(info.ProcessId).ProcessName, "iisexpress", StringComparison.OrdinalIgnoreCase));
+                    if (endpointInfo != null)
                     {
-                        return process.Id;
+                        return ProcessInfo.FromEndpointInfo(endpointInfo);
                     }
 #endif
                     throw new ArgumentException("Unable to select a single target process because multiple target processes have been discovered.");
             }
         }
 
-        private async Task<DiagnosticsClient> GetClientAsync(int processId, CancellationToken token)
-        {
-            var endpointInfos = await _endpointInfoSource.GetEndpointInfoAsync(token);
-            var endpointInfo = endpointInfos.FirstOrDefault(c => c.ProcessId == processId);
-
-            if (null == endpointInfo)
-            {
-                throw new InvalidOperationException($"Diagnostics client for process ID {processId} does not exist.");
-            }
-
-            return new DiagnosticsClient(endpointInfo.Endpoint);
-        }
-
         public void Dispose()
         {
             _tokenSource.Cancel();
@@ -241,12 +253,28 @@ namespace Microsoft.Diagnostics.Monitoring
 
         private sealed class ProcessInfo : IProcessInfo
         {
-            public ProcessInfo(Guid uid, int pid)
+            public ProcessInfo(DiagnosticsClient client, Guid uid, int pid)
             {
+                Client = client;
                 Pid = pid;
                 Uid = uid;
             }
 
+            public static ProcessInfo FromEndpointInfo(IEndpointInfo endpointInfo)
+            {
+                if (null == endpointInfo)
+                {
+                    throw new ArgumentNullException(nameof(endpointInfo));
+                }
+
+                return new ProcessInfo(
+                    new DiagnosticsClient(endpointInfo.Endpoint),
+                    endpointInfo.RuntimeInstanceCookie,
+                    endpointInfo.ProcessId);
+            }
+
+            public DiagnosticsClient Client { get; }
+
             public int Pid { get; }
 
             public Guid Uid { get; }
index 65cb015c9dea4d9274c37a4f2642b9dfdbb0a45b..7d910455086cb0345e20463200ceadc70b854357 100644 (file)
@@ -37,9 +37,19 @@ namespace Microsoft.Diagnostics.Monitoring
                 // HACK We need to disable EndpointRouting in order to run properly in 3.1
                 System.Reflection.PropertyInfo prop = options.GetType().GetProperty("EnableEndpointRouting");
                 prop?.SetValue(options, false);
-
             }).SetCompatibilityVersion(CompatibilityVersion.Latest);
 
+            services.Configure<ApiBehaviorOptions>(options =>
+            {
+                options.InvalidModelStateResponseFactory = context =>
+                {
+                    var details = new ValidationProblemDetails(context.ModelState);
+                    var result = new BadRequestObjectResult(details);
+                    result.ContentTypes.Add("application/problem+json");
+                    return result;
+                };
+            });
+
             services.Configure<GzipCompressionProviderOptions>(options =>
             {
                 options.Level = CompressionLevel.Optimal;