* Filter dotnet-monitor from available processes.
* Change port description to be supplied by options configuration.
* Enable synchronous IO on response output streams.
* Use Brotli compression instead of GZip.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>netstandard2.0</TargetFramework>
+ <TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFrameworks>
<NoWarn>;1591;1701</NoWarn>
<Description>REST Api surface for dotnet-monitor</Description>
<!-- Tentatively create package so other teams can tenatively consume. -->
<OutputType>Library</OutputType>
</PropertyGroup>
- <ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.HttpsPolicy" Version="$(MicrosoftAspNetCoreHttpsPolicyVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcVersion)" />
using System.Diagnostics.Tracing;
using System.Runtime.Serialization;
using Microsoft.Diagnostics.Monitoring.RestServer.Validation;
-using Newtonsoft.Json;
namespace Microsoft.Diagnostics.Monitoring.RestServer.Models
{
-using System.IO;
+using System;
+using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.Diagnostics.Monitoring
{
public static class RuntimeInfo
{
+ public static bool IsDiagnosticsEnabled
+ {
+ get
+ {
+ string enableDiagnostics = Environment.GetEnvironmentVariable("COMPlus_EnableDiagnostics");
+ return string.IsNullOrEmpty(enableDiagnostics) || !"0".Equals(enableDiagnostics, StringComparison.Ordinal);
+ }
+ }
+
public static bool IsInDockerContainer
{
get
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using Microsoft.Diagnostics.NETCore.Client;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Microsoft.Diagnostics.Monitoring
-{
- public static class ServiceCollectionExtensions
- {
- public static IServiceCollection AddEndpointInfoSource(this IServiceCollection services, string reversedServerAddress, int? maxConnections = null)
- {
- if (string.IsNullOrWhiteSpace(reversedServerAddress))
- {
- return services.AddSingleton<IEndpointInfoSource, ClientEndpointInfoSource>();
- }
- else
- {
- // Construct the source now rather than delayed construction
- // in order to be able to accept diagnostics connections immediately.
- var serverSource = new ServerEndpointInfoSource(reversedServerAddress);
- serverSource.Start(maxConnections.GetValueOrDefault(ReversedDiagnosticsServer.MaxAllowedConnections));
-
- return services.AddSingleton<IEndpointInfoSource>(serverSource);
- }
- }
- }
-}
}
}
+ internal ProcessInfo GetProcessInfo()
+ {
+ IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo);
+ var response = IpcClient.SendMessage(_endpoint, message);
+ switch ((DiagnosticsServerResponseId)response.Header.CommandId)
+ {
+ case DiagnosticsServerResponseId.Error:
+ var hr = BitConverter.ToInt32(response.Payload, 0);
+ throw new ServerErrorException($"Get process info failed (HRESULT: 0x{hr:X8})");
+ case DiagnosticsServerResponseId.OK:
+ return ProcessInfo.Parse(response.Payload);
+ default:
+ throw new ServerErrorException($"Get process info failed - server responded with unknown command");
+ }
+ }
+
/// <summary>
/// Get all the active processes that can be attached to.
/// </summary>
--- /dev/null
+// 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;
+using System.Text;
+
+namespace Microsoft.Diagnostics.NETCore.Client
+{
+ /**
+ * ==ProcessInfo==
+ * The response payload to issuing the GetProcessInfo command.
+ *
+ * 8 bytes - PID (little-endian)
+ * 16 bytes - CLR Runtime Instance Cookie (little-endian)
+ * # bytes - Command line string length and data
+ * # bytes - Operating system string length and data
+ * # bytes - Process architecture string length and data
+ *
+ * The "string length and data" fields are variable length:
+ * 4 bytes - Length of string data in UTF-16 characters
+ * (2 * length) bytes - The data of the string encoded using Unicode
+ * (includes null terminating character)
+ */
+
+ internal class ProcessInfo
+ {
+ private static readonly int GuidSizeInBytes = 16;
+
+ public static ProcessInfo Parse(byte[] payload)
+ {
+ ProcessInfo processInfo = new ProcessInfo();
+
+ int index = 0;
+ processInfo.ProcessId = BitConverter.ToUInt64(payload, index);
+ index += sizeof(UInt64);
+
+ byte[] cookieBuffer = new byte[GuidSizeInBytes];
+ Array.Copy(payload, index, cookieBuffer, 0, GuidSizeInBytes);
+ processInfo.RuntimeInstanceCookie = new Guid(cookieBuffer);
+ index += GuidSizeInBytes;
+
+ processInfo.CommandLine = ReadString(payload, ref index);
+ processInfo.OperatingSystem = ReadString(payload, ref index);
+ processInfo.ProcessArchitecture = ReadString(payload, ref index);
+
+ return processInfo;
+ }
+
+ private static string ReadString(byte[] buffer, ref int index)
+ {
+ // Length of the string of UTF-16 characters
+ int length = (int)BitConverter.ToUInt32(buffer, index);
+ index += sizeof(UInt32);
+
+ int size = (int)length * sizeof(char);
+ // The string contains an ending null character; remove it before returning the value
+ string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1);
+ index += size;
+ return value;
+ }
+
+ public UInt64 ProcessId { get; private set; }
+ public Guid RuntimeInstanceCookie { get; private set; }
+ public string CommandLine { get; private set; }
+ public string OperatingSystem { get; private set; }
+ public string ProcessArchitecture { get; private set; }
+ }
+}
\ No newline at end of file
</ItemGroup>
<ItemGroup>
+ <InternalsVisibleTo Include="dotnet-monitor" />
<InternalsVisibleTo Include="DotnetMonitor.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Diagnostics.Monitoring" />
<InternalsVisibleTo Include="Microsoft.Diagnostics.NETCore.Client.UnitTests" />
--- /dev/null
+// 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.
+
+namespace Microsoft.Diagnostics.Tools.Monitor
+{
+ public class DiagnosticPortConfiguration
+ {
+ public DiagnosticPortConnectionMode ConnectionMode { get; set; }
+
+ public string EndpointName { get; set; }
+
+ public int? MaxConnections { get; set; }
+ }
+
+ public enum DiagnosticPortConnectionMode
+ {
+ Connect,
+ Listen
+ }
+}
IWebHostBuilder builder = WebHost.CreateDefaultBuilder()
.ConfigureAppConfiguration((IConfigurationBuilder builder) =>
{
+ ConfigureEndpointInfoSource(builder, reversedServerAddress);
if (metrics)
{
//Note these are in precedence order.
})
.ConfigureServices((WebHostBuilderContext context, IServiceCollection services) =>
{
- services.AddEndpointInfoSource(reversedServerAddress);
//TODO Many of these service additions should be done through extension methods
+ services.Configure<DiagnosticPortConfiguration>(context.Configuration.GetSection(nameof(DiagnosticPortConfiguration)));
+ services.AddSingleton<IEndpointInfoSource, FilteredEndpointInfoSource>();
+ services.AddHostedService<FilteredEndpointInfoSourceHostedService>();
services.AddSingleton<IDiagnosticServices, DiagnosticServices>();
if (metrics)
{
});
}
+ private static void ConfigureEndpointInfoSource(IConfigurationBuilder builder, string diagnosticPort)
+ {
+ DiagnosticPortConnectionMode connectionMode = string.IsNullOrEmpty(diagnosticPort) ? DiagnosticPortConnectionMode.Connect : DiagnosticPortConnectionMode.Listen;
+ builder.AddInMemoryCollection(new Dictionary<string, string>
+ {
+ {MakeKey(nameof(DiagnosticPortConfiguration), nameof(DiagnosticPortConfiguration.ConnectionMode)), connectionMode.ToString()},
+ {MakeKey(nameof(DiagnosticPortConfiguration), nameof(DiagnosticPortConfiguration.EndpointName)), diagnosticPort}
+ });
+ }
+
private static string MakeKey(string parent, string child)
{
return FormattableString.Invariant($"{parent}:{child}");
--- /dev/null
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.Monitoring;
+using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Diagnostics.Tools.Monitor
+{
+ /// <summary>
+ /// Wraps an <see cref="IEndpointInfoSource"/> based on the provided configuration
+ /// and filters the current process from consideration.
+ /// </summary>
+ internal class FilteredEndpointInfoSource : IEndpointInfoSourceInternal, IAsyncDisposable
+ {
+ private readonly DiagnosticPortConfiguration _configuration;
+ private readonly int? _processIdToFilterOut;
+ private readonly Guid? _runtimeInstanceCookieToFilterOut;
+ private readonly IEndpointInfoSourceInternal _source;
+
+ public FilteredEndpointInfoSource(IOptions<DiagnosticPortConfiguration> configuration)
+ {
+ _configuration = configuration.Value;
+ switch (_configuration.ConnectionMode)
+ {
+ case DiagnosticPortConnectionMode.Connect:
+ _source = new ClientEndpointInfoSource();
+ break;
+ case DiagnosticPortConnectionMode.Listen:
+ _source = new ServerEndpointInfoSource(_configuration.EndpointName);
+ break;
+ default:
+ throw new InvalidOperationException($"Unhandled connection mode: {_configuration.ConnectionMode}");
+ }
+
+ // Filter out the current process based on the connection mode.
+ if (RuntimeInfo.IsDiagnosticsEnabled)
+ {
+ int pid = Process.GetCurrentProcess().Id;
+
+ // Regardless of connection mode, can use the runtime instance cookie to filter self out.
+ try
+ {
+ var client = new DiagnosticsClient(pid);
+ Guid runtimeInstanceCookie = client.GetProcessInfo().RuntimeInstanceCookie;
+ if (Guid.Empty != runtimeInstanceCookie)
+ {
+ _runtimeInstanceCookieToFilterOut = runtimeInstanceCookie;
+ }
+ }
+ catch (Exception)
+ {
+ }
+
+ // If connecting to runtime instances, filter self out. In listening mode, it's likely
+ // that multiple processes have the same PID in multi-container scenarios.
+ if (DiagnosticPortConnectionMode.Connect == configuration.Value.ConnectionMode)
+ {
+ _processIdToFilterOut = pid;
+ }
+ }
+ }
+
+ public async Task<IEnumerable<IEndpointInfo>> GetEndpointInfoAsync(CancellationToken token)
+ {
+ var endpointInfos = await _source.GetEndpointInfoAsync(token);
+
+ // Apply process ID filter
+ if (_processIdToFilterOut.HasValue)
+ {
+ endpointInfos = endpointInfos.Where(info => info.ProcessId != _processIdToFilterOut.Value);
+ }
+
+ // Apply runtime instance cookie filter
+ if (_runtimeInstanceCookieToFilterOut.HasValue)
+ {
+ endpointInfos = endpointInfos.Where(info => info.RuntimeInstanceCookie != _runtimeInstanceCookieToFilterOut.Value);
+ }
+
+ return endpointInfos;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_source is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ else if (_source is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.ConfigureAwait(false).DisposeAsync();
+ }
+ }
+
+ public void Start()
+ {
+ if (_source is ServerEndpointInfoSource source)
+ {
+ source.Start(_configuration.MaxConnections.GetValueOrDefault(ReversedDiagnosticsServer.MaxAllowedConnections));
+ }
+ }
+ }
+}
--- /dev/null
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.Monitoring;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Diagnostics.Tools.Monitor
+{
+ /// <summary>
+ /// A hosted service that ensures the <see cref="FilteredEndpointInfoSource"/>
+ /// starts monitoring for connectable processes.
+ /// </summary>
+ internal class FilteredEndpointInfoSourceHostedService : IHostedService
+ {
+ private readonly FilteredEndpointInfoSource _source;
+
+ public FilteredEndpointInfoSourceHostedService(IEndpointInfoSource source)
+ {
+ _source = (FilteredEndpointInfoSource)source;
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _source.Start();
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
// 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;
using System.Collections.Generic;
using System.IO.Compression;
-using System.Linq;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.ResponseCompression;
-using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Diagnostics.Monitoring.RestServer;
+using Microsoft.Diagnostics.Monitoring.RestServer.Controllers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
namespace Microsoft.Diagnostics.Monitoring
{
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddMvc((MvcOptions options) =>
- {
- options.Filters.Add(new ProducesAttribute("application/json"));
+ services.AddMvc(options =>
+ {
+ options.Filters.Add(new ProducesAttribute("application/json"));
- // 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);
+ options.EnableEndpointRouting = false;
+ })
+ .SetCompatibilityVersion(CompatibilityVersion.Latest)
+ .AddApplicationPart(typeof(DiagController).Assembly);
services.Configure<ApiBehaviorOptions>(options =>
{
};
});
- services.Configure<GzipCompressionProviderOptions>(options =>
+ services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
services.AddResponseCompression(configureOptions =>
{
- configureOptions.Providers.Add<GzipCompressionProvider>();
+ configureOptions.Providers.Add<BrotliCompressionProvider>();
configureOptions.MimeTypes = new List<string> { "application/octet-stream" };
});
- var config = new PrometheusConfiguration();
- Configuration.Bind(nameof(PrometheusConfiguration), config);
- if (config.Enabled)
+ // This is needed to allow the StreamingLogger to synchronously write to the output stream.
+ // Eventually should switch StreamingLoggger to something that allows for async operations.
+ services.Configure<KestrelServerOptions>(options =>
+ {
+ options.AllowSynchronousIO = true;
+ });
+
+ var prometheusConfig = new PrometheusConfiguration();
+ Configuration.Bind(nameof(PrometheusConfiguration), prometheusConfig);
+ if (prometheusConfig.Enabled)
{
services.AddSingleton<MetricsStoreService>();
services.AddHostedService<MetricsService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netcoreapp2.1</TargetFramework>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
<RuntimeIdentifiers>linux-x64;linux-musl-x64;win-x64</RuntimeIdentifiers>
<PackAsToolShimRuntimeIdentifiers>linux-x64;linux-musl-x64;win-x64</PackAsToolShimRuntimeIdentifiers>
<RootNamespace>Microsoft.Diagnostics.Tools.Monitor</RootNamespace>
<PackageTags>Diagnostic</PackageTags>
<IsShipping>false</IsShipping>
<PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+ <RollForward>Major</RollForward>
<!-- This forces the creation of a checksum file and uploads it to blob storage
using this name as part of the blob relative path. -->
<BlobGroupPrefix>monitor</BlobGroupPrefix>
+++ /dev/null
-{
- "rollForwardOnNoCandidateFx": 2
-}
\ No newline at end of file