diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 4a9d818015..4ea7cdee98 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,9 +4,9 @@ https://github.com/dotnet/command-line-api 166610c56ff732093f0145a2911d4f6c40b786da - + https://github.com/dotnet/arcade - ecec08a0eebbd92bb9538e351d475582551d9092 + 83fda4b3f6d93e713749fd1b27c4a6d40b118b13 https://github.com/dotnet/symstore @@ -14,9 +14,9 @@ - + https://github.com/dotnet/arcade - ecec08a0eebbd92bb9538e351d475582551d9092 + 83fda4b3f6d93e713749fd1b27c4a6d40b118b13 https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 1aa6fc7b87..5a4aa503b6 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -54,7 +54,7 @@ 4.7.0 2.4.1 2.0.3 - 5.0.0-beta.20411.8 + 5.0.0-beta.20417.6 10.0.18362 diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake index 2566707997..137736c0a2 100644 --- a/eng/common/cross/toolchain.cmake +++ b/eng/common/cross/toolchain.cmake @@ -19,7 +19,9 @@ if(TARGET_ARCH_NAME STREQUAL "armel") endif() elseif(TARGET_ARCH_NAME STREQUAL "arm") set(CMAKE_SYSTEM_PROCESSOR armv7l) - if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv7-alpine-linux-musleabihf) + set(TOOLCHAIN "armv7-alpine-linux-musleabihf") + elseif(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) set(TOOLCHAIN "armv6-alpine-linux-musleabihf") else() set(TOOLCHAIN "arm-linux-gnueabihf") diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml index 0e79a546b5..e814d5e033 100644 --- a/eng/common/templates/post-build/post-build.yml +++ b/eng/common/templates/post-build/post-build.yml @@ -39,6 +39,7 @@ parameters: NetEngLatestChannelId: 2 NetEngValidationChannelId: 9 NetDev5ChannelId: 131 + NetDev6ChannelId: 1296 GeneralTestingChannelId: 529 NETCoreToolingDevChannelId: 548 NETCoreToolingReleaseChannelId: 549 @@ -46,7 +47,6 @@ parameters: NETCoreExperimentalChannelId: 562 NetEngServicesIntChannelId: 678 NetEngServicesProdChannelId: 679 - Net5Preview7ChannelId: 1065 Net5Preview8ChannelId: 1155 Net5RC1ChannelId: 1157 NetCoreSDK313xxChannelId: 759 @@ -115,7 +115,7 @@ stages: inputs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/check-channel-consistency.ps1 arguments: -PromoteToChannels "$(TargetChannels)" - -AvailableChannelIds ${{parameters.NetEngLatestChannelId}},${{parameters.NetEngValidationChannelId}},${{parameters.NetDev5ChannelId}},${{parameters.GeneralTestingChannelId}},${{parameters.NETCoreToolingDevChannelId}},${{parameters.NETCoreToolingReleaseChannelId}},${{parameters.NETInternalToolingChannelId}},${{parameters.NETCoreExperimentalChannelId}},${{parameters.NetEngServicesIntChannelId}},${{parameters.NetEngServicesProdChannelId}},${{parameters.Net5Preview7ChannelId}},${{parameters.Net5Preview8ChannelId}},${{parameters.Net5RC1ChannelId}},${{parameters.NetCoreSDK313xxChannelId}},${{parameters.NetCoreSDK313xxInternalChannelId}},${{parameters.NetCoreSDK314xxChannelId}},${{parameters.NetCoreSDK314xxInternalChannelId}},${{parameters.VS166ChannelId}},${{parameters.VS167ChannelId}},${{parameters.VS168ChannelId}},${{parameters.VSMasterChannelId}} + -AvailableChannelIds ${{parameters.NetEngLatestChannelId}},${{parameters.NetEngValidationChannelId}},${{parameters.NetDev5ChannelId}},${{parameters.NetDev6ChannelId}},${{parameters.GeneralTestingChannelId}},${{parameters.NETCoreToolingDevChannelId}},${{parameters.NETCoreToolingReleaseChannelId}},${{parameters.NETInternalToolingChannelId}},${{parameters.NETCoreExperimentalChannelId}},${{parameters.NetEngServicesIntChannelId}},${{parameters.NetEngServicesProdChannelId}},${{parameters.Net5Preview8ChannelId}},${{parameters.Net5RC1ChannelId}},${{parameters.NetCoreSDK313xxChannelId}},${{parameters.NetCoreSDK313xxInternalChannelId}},${{parameters.NetCoreSDK314xxChannelId}},${{parameters.NetCoreSDK314xxInternalChannelId}},${{parameters.VS166ChannelId}},${{parameters.VS167ChannelId}},${{parameters.VS168ChannelId}},${{parameters.VSMasterChannelId}} - job: displayName: NuGet Validation @@ -276,13 +276,13 @@ stages: dependsOn: ${{ parameters.publishDependsOn }} publishInstallersAndChecksums: ${{ parameters.publishInstallersAndChecksums }} symbolPublishingAdditionalParameters: ${{ parameters.symbolPublishingAdditionalParameters }} - stageName: 'Net5_Preview7_Publish' - channelName: '.NET 5 Preview 7' - akaMSChannelName: 'net5/preview7' - channelId: ${{ parameters.Net5Preview7ChannelId }} - transportFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5-transport/nuget/v3/index.json' - shippingFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json' - symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5-symbols/nuget/v3/index.json' + stageName: 'NetCore_Dev6_Publish' + channelName: '.NET 6 Dev' + akaMSChannelName: 'net6/dev' + channelId: ${{ parameters.NetDev6ChannelId }} + transportFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6-transport/nuget/v3/index.json' + shippingFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json' + symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6-symbols/nuget/v3/index.json' - template: \eng\common\templates\post-build\channels\generic-internal-channel.yml parameters: @@ -298,7 +298,7 @@ stages: shippingFeed: 'https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet5-internal/nuget/v3/index.json' symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet5-internal-symbols/nuget/v3/index.json' - - template: \eng\common\templates\post-build\channels\generic-internal-channel.yml + - template: \eng\common\templates\post-build\channels\generic-public-channel.yml parameters: artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} dependsOn: ${{ parameters.publishDependsOn }} @@ -308,9 +308,9 @@ stages: channelName: '.NET 5 RC 1' akaMSChannelName: 'net5/rc1' channelId: ${{ parameters.Net5RC1ChannelId }} - transportFeed: 'https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet5-internal-transport/nuget/v3/index.json' - shippingFeed: 'https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet5-internal/nuget/v3/index.json' - symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet5-internal-symbols/nuget/v3/index.json' + transportFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5-transport/nuget/v3/index.json' + shippingFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json' + symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5-symbols/nuget/v3/index.json' - template: \eng\common\templates\post-build\channels\generic-public-channel.yml parameters: diff --git a/global.json b/global.json index dab466ef7e..9596057101 100644 --- a/global.json +++ b/global.json @@ -13,6 +13,6 @@ } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "5.0.0-beta.20411.8" + "Microsoft.DotNet.Arcade.Sdk": "5.0.0-beta.20417.6" } } diff --git a/src/Microsoft.Diagnostics.Monitoring.RestServer/Microsoft.Diagnostics.Monitoring.RestServer.csproj b/src/Microsoft.Diagnostics.Monitoring.RestServer/Microsoft.Diagnostics.Monitoring.RestServer.csproj index 2d5b47fdf4..757e7331c7 100644 --- a/src/Microsoft.Diagnostics.Monitoring.RestServer/Microsoft.Diagnostics.Monitoring.RestServer.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.RestServer/Microsoft.Diagnostics.Monitoring.RestServer.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;netcoreapp3.1 ;1591;1701 REST Api surface for dotnet-monitor @@ -15,7 +15,7 @@ Library - + diff --git a/src/Microsoft.Diagnostics.Monitoring.RestServer/Models/EventPipeProviderModel.cs b/src/Microsoft.Diagnostics.Monitoring.RestServer/Models/EventPipeProviderModel.cs index ce44f1dd6e..71a0e959ed 100644 --- a/src/Microsoft.Diagnostics.Monitoring.RestServer/Models/EventPipeProviderModel.cs +++ b/src/Microsoft.Diagnostics.Monitoring.RestServer/Models/EventPipeProviderModel.cs @@ -6,7 +6,6 @@ using System.Diagnostics.Tracing; using System.Runtime.Serialization; using Microsoft.Diagnostics.Monitoring.RestServer.Validation; -using Newtonsoft.Json; namespace Microsoft.Diagnostics.Monitoring.RestServer.Models { diff --git a/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs b/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs index 5c7af95c7a..e89a7aeeb9 100644 --- a/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs +++ b/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs @@ -1,10 +1,20 @@ -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 diff --git a/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs index 6a9d22e71a..5f79b10010 100644 --- a/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs +++ b/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -52,10 +53,20 @@ public async ValueTask DisposeAsync() if (null != _listenTask) { - await _listenTask.ConfigureAwait(false); + try + { + await _listenTask.ConfigureAwait(false); + } + catch (Exception ex) + { + Debug.Fail(ex.Message); + } } - _server?.Dispose(); + if (null != _server) + { + await _server.DisposeAsync().ConfigureAwait(false); + } _endpointInfosSemaphore.Dispose(); @@ -68,27 +79,27 @@ public async ValueTask DisposeAsync() /// /// Starts listening to the reversed diagnostics server for new connections. /// - public void Listen() + public void Start() { - Listen(ReversedDiagnosticsServer.MaxAllowedConnections); + Start(ReversedDiagnosticsServer.MaxAllowedConnections); } /// /// Starts listening to the reversed diagnostics server for new connections. /// /// The maximum number of connections the server will support. - public void Listen(int maxConnections) + public void Start(int maxConnections) { VerifyNotDisposed(); - if (null != _server || null != _listenTask) + if (IsListening) { - throw new InvalidOperationException(nameof(ServerEndpointInfoSource.Listen) + " method can only be called once."); + throw new InvalidOperationException(nameof(ServerEndpointInfoSource.Start) + " method can only be called once."); } - _server = new ReversedDiagnosticsServer(_transportPath, maxConnections); + _server = new ReversedDiagnosticsServer(_transportPath); - _listenTask = ListenAsync(_cancellation.Token); + _listenTask = ListenAsync(maxConnections, _cancellation.Token); } /// @@ -100,12 +111,15 @@ public async Task> GetEndpointInfoAsync(CancellationT { VerifyNotDisposed(); + VerifyIsListening(); + using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); CancellationToken linkedToken = linkedSource.Token; // Prune connections that no longer have an active runtime instance before // returning the list of connections. await _endpointInfosSemaphore.WaitAsync(linkedToken).ConfigureAwait(false); + try { // Check the transport for each endpoint info and remove it if the check fails. @@ -146,7 +160,7 @@ private async Task PruneIfNotViable(IpcEndpointInfo info, CancellationToken toke { _endpointInfos.Remove(info); OnRemovedEndpointInfo(info); - _server.RemoveConnection(info.RuntimeInstanceCookie); + _server?.RemoveConnection(info.RuntimeInstanceCookie); } } } @@ -155,8 +169,9 @@ private async Task PruneIfNotViable(IpcEndpointInfo info, CancellationToken toke /// Accepts endpoint infos from the reversed diagnostics server. /// /// The token to monitor for cancellation requests. - private async Task ListenAsync(CancellationToken token) + private async Task ListenAsync(int maxConnections, CancellationToken token) { + _server.Start(maxConnections); // Continuously accept endpoint infos from the reversed diagnostics server so // that // is always awaited in order to to handle new runtime instance connections @@ -207,7 +222,7 @@ private async Task ResumeAndQueueEndpointInfo(IpcEndpointInfo info, Cancellation } catch (Exception) { - _server.RemoveConnection(info.RuntimeInstanceCookie); + _server?.RemoveConnection(info.RuntimeInstanceCookie); throw; } @@ -229,6 +244,16 @@ private void VerifyNotDisposed() } } + private void VerifyIsListening() + { + if (!IsListening) + { + throw new InvalidOperationException(nameof(ServerEndpointInfoSource.Start) + " method must be called before invoking this operation."); + } + } + + private bool IsListening => null != _server && null != _listenTask; + private class EndpointInfo : IEndpointInfo { private readonly IpcEndpointInfo _info; diff --git a/src/Microsoft.Diagnostics.Monitoring/ServiceCollectionExtensions.cs b/src/Microsoft.Diagnostics.Monitoring/ServiceCollectionExtensions.cs deleted file mode 100644 index 06e16cbe0c..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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(); - } - 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.Listen(maxConnections.GetValueOrDefault(ReversedDiagnosticsServer.MaxAllowedConnections)); - - return services.AddSingleton(serverSource); - } - } - } -} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs index 89a4d4240b..06f9a0fac9 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -174,6 +174,22 @@ internal void ResumeRuntimeFallback() } } + 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"); + } + } + /// /// Get all the active processes that can be attached to. /// diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs new file mode 100644 index 0000000000..91bec0c7c9 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs @@ -0,0 +1,69 @@ +// 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 diff --git a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj index b80780a73e..b15b3b957f 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -14,6 +14,11 @@ + + + + + diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index 8429869797..24991a8cec 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -18,10 +18,13 @@ namespace Microsoft.Diagnostics.NETCore.Client /// Establishes server endpoint for runtime instances to connect when /// configured to provide diagnostic endpoints in reverse mode. /// - internal sealed class ReversedDiagnosticsServer : IDisposable + internal sealed class ReversedDiagnosticsServer : IAsyncDisposable { // Returns true if the handler is complete and should be removed from the list - delegate bool StreamHandler(Guid runtimeId, ref Stream stream); + delegate bool StreamHandler(Guid runtimeId, Stream stream, out bool consumed); + + // Returns true if the handler is complete and should be removed from the list + delegate bool EndpointInfoHandler(IpcEndpointInfo endpointInfo, out bool consumed); // The amount of time to allow parsing of the advertise data before cancelling. This allows the server to // remain responsive in case the advertise data is incomplete and the stream is not closed. @@ -30,11 +33,15 @@ internal sealed class ReversedDiagnosticsServer : IDisposable private readonly Dictionary _cachedEndpoints = new Dictionary(); private readonly Dictionary _cachedStreams = new Dictionary(); private readonly CancellationTokenSource _disposalSource = new CancellationTokenSource(); - private readonly List _handlers = new List(); - private readonly object _lock = new object(); - private readonly IpcServerTransport _transport; + private readonly List _newEndpointInfoHandlers = new List(); + private readonly List _newEndpointInfos = new List(); + private readonly object _newEndpointInfoLock = new object(); + private readonly List _streamHandlers = new List(); + private readonly object _streamLock = new object(); + private readonly string _transportPath; private bool _disposed = false; + private Task _listenTask; /// /// Constructs the instance with an endpoint bound @@ -46,33 +53,32 @@ internal sealed class ReversedDiagnosticsServer : IDisposable /// On all other systems, this must be the full file path of the socket. /// public ReversedDiagnosticsServer(string transportPath) - : this(transportPath, MaxAllowedConnections) { + _transportPath = transportPath; } - /// - /// Constructs the instance with an endpoint bound - /// to the location specified by . - /// - /// - /// The path of the server endpoint. - /// On Windows, this can be a full pipe path or the name without the "\\.\pipe\" prefix. - /// On all other systems, this must be the full file path of the socket. - /// - /// The maximum number of connections the server will support. - public ReversedDiagnosticsServer(string transportPath, int maxConnections) - { - _transport = IpcServerTransport.Create(transportPath, maxConnections); - } - - public void Dispose() + public async ValueTask DisposeAsync() { if (!_disposed) { _disposalSource.Cancel(); - lock (_lock) + if (null != _listenTask) + { + try + { + await _listenTask.ConfigureAwait(false); + } + catch (Exception ex) + { + Debug.Fail(ex.Message); + } + } + + lock (_streamLock) { + _newEndpointInfos.Clear(); + _cachedEndpoints.Clear(); foreach (Stream stream in _cachedStreams.Values) @@ -82,8 +88,6 @@ public void Dispose() _cachedStreams.Clear(); } - _transport.Dispose(); - _disposalSource.Dispose(); _disposed = true; @@ -91,82 +95,56 @@ public void Dispose() } /// - /// Provides endpoint information when a new runtime instance connects to the server. + /// Starts listening at the transport path for new connections. /// - /// The token to monitor for cancellation requests. - /// A that contains information about the new runtime instance connection. - /// - /// This will only provide endpoint information on the first time a runtime connects to the server. - /// If a connection is removed using and the same runtime instance, - /// reconnects after this call, then a new will be produced. - /// - public async Task AcceptAsync(CancellationToken token) + public void Start() + { + Start(MaxAllowedConnections); + } + + /// + /// Starts listening at the transport path for new connections. + /// + /// The maximum number of connections the server will support. + public void Start(int maxConnections) { VerifyNotDisposed(); - while (true) + if (IsStarted) { - Stream stream = null; - IpcAdvertise advertise = null; - try - { - stream = await _transport.AcceptAsync(token).ConfigureAwait(false); - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - // The advertise data could be incomplete if the runtime shuts down before completely writing - // the information. Catch the exception and continue waiting for a new connection. - } + throw new InvalidOperationException(nameof(ReversedDiagnosticsServer.Start) + " method can only be called once."); + } - token.ThrowIfCancellationRequested(); + _listenTask = ListenAsync(maxConnections, _disposalSource.Token); + } - if (null != stream) - { - // Cancel parsing of advertise data after timeout period to - // mitigate runtimes that write partial data and do not close the stream (avoid waiting forever). - using var parseCancellationSource = new CancellationTokenSource(); - using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, parseCancellationSource.Token); - try - { - parseCancellationSource.CancelAfter(ParseAdvertiseTimeout); + /// + /// Gets endpoint information when a new runtime instance connects to the server. + /// + /// The token to monitor for cancellation requests. + /// A task that completes with a value that contains information about the new runtime instance connection. + public async Task AcceptAsync(CancellationToken token) + { + VerifyNotDisposed(); - advertise = await IpcAdvertise.ParseAsync(stream, linkedSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) when (parseCancellationSource.IsCancellationRequested) - { - // Only handle cancellation if it was due to the parse timeout. - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - // Catch all other exceptions and continue waiting for a new connection. - } - } + VerifyIsStarted(); - token.ThrowIfCancellationRequested(); + var endpointInfoSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var methodRegistration = token.Register(() => endpointInfoSource.TrySetCanceled(token)); + using var disposalRegistration = _disposalSource.Token.Register( + () => endpointInfoSource.TrySetException(new ObjectDisposedException(nameof(ReversedDiagnosticsServer)))); - if (null != advertise) - { - Guid runtimeCookie = advertise.RuntimeInstanceCookie; - int pid = unchecked((int)advertise.ProcessId); + RegisterEndpointInfoHandler((IpcEndpointInfo endpointInfo, out bool consumed) => + { + consumed = endpointInfoSource.TrySetResult(endpointInfo); - lock (_lock) - { - ProvideStream(runtimeCookie, stream); - // Consumers should hold onto the endpoint info and use it for diagnostic communication, - // regardless of the number of times the same runtime instance connects. This requires consumers - // to continuously invoke the AcceptAsync method in order to handle runtime instance reconnects, - // even if the consumer only wants to handle a single endpoint. - if (!_cachedEndpoints.ContainsKey(runtimeCookie)) - { - ServerIpcEndpoint endpoint = new ServerIpcEndpoint(this, runtimeCookie); - _cachedEndpoints.Add(runtimeCookie, endpoint); - return new IpcEndpointInfo(endpoint, pid, runtimeCookie); - } - } - } + // Regardless of the registrant previously waiting or cancelled, + // the handler should be removed from consideration. + return true; + }); - token.ThrowIfCancellationRequested(); - } + // Wait for the handler to verify we have a connected stream + return await endpointInfoSource.Task.ConfigureAwait(false); } /// @@ -178,10 +156,12 @@ public bool RemoveConnection(Guid runtimeCookie) { VerifyNotDisposed(); + VerifyIsStarted(); + bool endpointExisted = false; Stream previousStream = null; - lock (_lock) + lock (_streamLock) { endpointExisted = _cachedEndpoints.Remove(runtimeCookie); if (endpointExisted) @@ -206,6 +186,76 @@ private void VerifyNotDisposed() } } + private void VerifyIsStarted() + { + if (!IsStarted) + { + throw new InvalidOperationException(nameof(ReversedDiagnosticsServer.Start) + " method must be called before invoking this operation."); + } + } + + /// + /// Listens at the transport path for new connections. + /// + /// The maximum number of connections the server will support. + /// The token to monitor for cancellation requests. + /// A task that completes when the server is no longer listening at the transport path. + private async Task ListenAsync(int maxConnections, CancellationToken token) + { + using var transport = IpcServerTransport.Create(_transportPath, maxConnections); + while (!token.IsCancellationRequested) + { + Stream stream = null; + IpcAdvertise advertise = null; + try + { + stream = await transport.AcceptAsync(token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception) + { + // The advertise data could be incomplete if the runtime shuts down before completely writing + // the information. Catch the exception and continue waiting for a new connection. + } + + if (null != stream) + { + // Cancel parsing of advertise data after timeout period to + // mitigate runtimes that write partial data and do not close the stream (avoid waiting forever). + using var parseCancellationSource = new CancellationTokenSource(); + using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, parseCancellationSource.Token); + try + { + parseCancellationSource.CancelAfter(ParseAdvertiseTimeout); + + advertise = await IpcAdvertise.ParseAsync(stream, linkedSource.Token).ConfigureAwait(false); + } + catch (Exception) + { + } + } + + if (null != advertise) + { + Guid runtimeCookie = advertise.RuntimeInstanceCookie; + int pid = unchecked((int)advertise.ProcessId); + + lock (_streamLock) + { + ProvideStream(runtimeCookie, stream); + if (!_cachedEndpoints.ContainsKey(runtimeCookie)) + { + ServerIpcEndpoint endpoint = new ServerIpcEndpoint(this, runtimeCookie); + _cachedEndpoints.Add(runtimeCookie, endpoint); + ProvideEndpointInfo(new IpcEndpointInfo(endpoint, pid, runtimeCookie)); + } + } + } + } + } + /// /// This will block until the diagnostic stream is provided. This block can happen if /// the stream is acquired previously and the runtime instance has not yet reconnected @@ -215,6 +265,8 @@ internal Stream Connect(Guid runtimeId, TimeSpan timeout) { VerifyNotDisposed(); + VerifyIsStarted(); + const int StreamStatePending = 0; const int StreamStateComplete = 1; const int StreamStateCancelled = 2; @@ -243,17 +295,16 @@ bool TrySetStream(int state, Stream value) using var methodRegistration = cancellationSource.Token.Register(() => TrySetStream(StreamStateCancelled, value: null)); using var disposalRegistration = _disposalSource.Token.Register(() => TrySetStream(StreamStateDisposed, value: null)); - RegisterHandler(runtimeId, (Guid id, ref Stream cachedStream) => + RegisterStreamHandler(runtimeId, (Guid id, Stream cachedStream, out bool consumed) => { + consumed = false; + if (id != runtimeId) { return false; } - if (TrySetStream(StreamStateComplete, cachedStream)) - { - cachedStream = null; - } + consumed = TrySetStream(StreamStateComplete, cachedStream); // Regardless of the registrant previously waiting or cancelled, // the handler should be removed from consideration. @@ -280,13 +331,17 @@ internal async Task WaitForConnectionAsync(Guid runtimeId, CancellationToken tok { VerifyNotDisposed(); + VerifyIsStarted(); + var hasConnectedStreamSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var methodRegistration = token.Register(() => hasConnectedStreamSource.TrySetCanceled(token)); using var disposalRegistration = _disposalSource.Token.Register( () => hasConnectedStreamSource.TrySetException(new ObjectDisposedException(nameof(ReversedDiagnosticsServer)))); - RegisterHandler(runtimeId, (Guid id, ref Stream cachedStream) => + RegisterStreamHandler(runtimeId, (Guid id, Stream cachedStream, out bool consumed) => { + consumed = false; + if (runtimeId != id) { return false; @@ -301,7 +356,7 @@ internal async Task WaitForConnectionAsync(Guid runtimeId, CancellationToken tok if (!TestStream(cachedStream)) { cachedStream.Dispose(); - cachedStream = null; + consumed = true; return false; } @@ -312,14 +367,14 @@ internal async Task WaitForConnectionAsync(Guid runtimeId, CancellationToken tok // the handler should be removed from consideration. return true; }); - + // Wait for the handler to verify we have a connected stream await hasConnectedStreamSource.Task.ConfigureAwait(false); } private void ProvideStream(Guid runtimeId, Stream stream) { - Debug.Assert(Monitor.IsEntered(_lock)); + Debug.Assert(Monitor.IsEntered(_streamLock)); // Get the previous stream in order to dispose it later _cachedStreams.TryGetValue(runtimeId, out Stream previousStream); @@ -332,24 +387,38 @@ private void ProvideStream(Guid runtimeId, Stream stream) private void RunStreamHandlers(Guid runtimeId, Stream stream) { - Debug.Assert(Monitor.IsEntered(_lock)); + Debug.Assert(Monitor.IsEntered(_streamLock)); // If there are any handlers waiting for a stream, provide // it to the first handler in the queue. - for (int i = 0; (i < _handlers.Count) && (null != stream); i++) + bool consumedStream = false; + for (int i = 0; !consumedStream && i < _streamHandlers.Count; i++) { - StreamHandler handler = _handlers[i]; - if (handler(runtimeId, ref stream)) + StreamHandler handler = _streamHandlers[i]; + if (handler(runtimeId, stream, out consumedStream)) { - _handlers.RemoveAt(i); + _streamHandlers.RemoveAt(i); i--; } } - // Store the stream for when a handler registers later. If - // a handler already captured the stream, this will be null, thus - // representing that no existing stream is waiting to be consumed. - _cachedStreams[runtimeId] = stream; + // Store the stream for when a handler registers later. + _cachedStreams[runtimeId] = consumedStream ? null : stream; + } + + private void RegisterStreamHandler(Guid runtimeId, StreamHandler handler) + { + lock (_streamLock) + { + _cachedStreams.TryGetValue(runtimeId, out Stream stream); + + _streamHandlers.Add(handler); + + if (stream != null) + { + RunStreamHandlers(runtimeId, stream); + } + } } private bool TestStream(Stream stream) @@ -395,24 +464,63 @@ private bool TestStream(Stream stream) return false; } - private void RegisterHandler(Guid runtimeId, StreamHandler handler) + private void ProvideEndpointInfo(in IpcEndpointInfo endpointInfo) { - lock (_lock) + lock (_newEndpointInfoLock) { - if (!_cachedStreams.TryGetValue(runtimeId, out Stream stream)) + bool consumedEndpointInfo = false; + // Provide the endpoint info to the first handler that will accept it. + for (int i = 0; !consumedEndpointInfo && i < _newEndpointInfoHandlers.Count; i++) { - throw new InvalidOperationException($"Runtime instance with identifier '{runtimeId}' is not registered."); + EndpointInfoHandler handler = _newEndpointInfoHandlers[i]; + // Handler will return true if it has completed + if (handler(endpointInfo, out consumedEndpointInfo)) + { + _newEndpointInfoHandlers.RemoveAt(i); + i--; + } } - _handlers.Add(handler); + // If the endpoint info was not consumed, add it to the list + if (!consumedEndpointInfo) + { + _newEndpointInfos.Add(endpointInfo); + } + } + } - if (stream != null) + private void RegisterEndpointInfoHandler(EndpointInfoHandler handler) + { + lock (_newEndpointInfoLock) + { + // Attempt to accept an endpoint info + for (int i = 0; i < _newEndpointInfos.Count && null != handler; i++) { - RunStreamHandlers(runtimeId, stream); + bool consumedEnpointInfo = false; + // Handler will return true if it has completed + if (handler(_newEndpointInfos[i], out consumedEnpointInfo)) + { + handler = null; + } + + // If the endpoint info was consumed, remove it from the list + if (consumedEnpointInfo) + { + _newEndpointInfos.RemoveAt(i); + i--; + } + } + + // If the handler did not signal completion, then add it to the handlers list. + if (null != handler) + { + _newEndpointInfoHandlers.Add(handler); } } } + private bool IsStarted => null != _listenTask; + public static int MaxAllowedConnections = IpcServerTransport.MaxAllowedConnections; } } diff --git a/src/Tools/dotnet-monitor/DiagnosticPortConfiguration.cs b/src/Tools/dotnet-monitor/DiagnosticPortConfiguration.cs new file mode 100644 index 0000000000..4ed543411c --- /dev/null +++ b/src/Tools/dotnet-monitor/DiagnosticPortConfiguration.cs @@ -0,0 +1,21 @@ +// 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 + } +} diff --git a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs index d2a90e896b..6aa568d90a 100644 --- a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs +++ b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs @@ -40,6 +40,7 @@ public IWebHostBuilder CreateWebHostBuilder(IConsole console, string[] urls, str IWebHostBuilder builder = WebHost.CreateDefaultBuilder() .ConfigureAppConfiguration((IConfigurationBuilder builder) => { + ConfigureEndpointInfoSource(builder, reversedServerAddress); if (metrics) { //Note these are in precedence order. @@ -50,8 +51,10 @@ public IWebHostBuilder CreateWebHostBuilder(IConsole console, string[] urls, str }) .ConfigureServices((WebHostBuilderContext context, IServiceCollection services) => { - services.AddEndpointInfoSource(reversedServerAddress); //TODO Many of these service additions should be done through extension methods + services.Configure(context.Configuration.GetSection(nameof(DiagnosticPortConfiguration))); + services.AddSingleton(); + services.AddHostedService(); services.AddSingleton(); if (metrics) { @@ -75,6 +78,16 @@ private static void ConfigureMetricsEndpoint(IConfigurationBuilder builder, stri }); } + private static void ConfigureEndpointInfoSource(IConfigurationBuilder builder, string diagnosticPort) + { + DiagnosticPortConnectionMode connectionMode = string.IsNullOrEmpty(diagnosticPort) ? DiagnosticPortConnectionMode.Connect : DiagnosticPortConnectionMode.Listen; + builder.AddInMemoryCollection(new Dictionary + { + {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}"); diff --git a/src/Tools/dotnet-monitor/FilteredEndpointInfoSource.cs b/src/Tools/dotnet-monitor/FilteredEndpointInfoSource.cs new file mode 100644 index 0000000000..a22754af56 --- /dev/null +++ b/src/Tools/dotnet-monitor/FilteredEndpointInfoSource.cs @@ -0,0 +1,110 @@ +// 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 +{ + /// + /// Wraps an based on the provided configuration + /// and filters the current process from consideration. + /// + internal class FilteredEndpointInfoSource : IEndpointInfoSourceInternal, IAsyncDisposable + { + private readonly DiagnosticPortConfiguration _configuration; + private readonly int? _processIdToFilterOut; + private readonly Guid? _runtimeInstanceCookieToFilterOut; + private readonly IEndpointInfoSourceInternal _source; + + public FilteredEndpointInfoSource(IOptions 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> 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)); + } + } + } +} diff --git a/src/Tools/dotnet-monitor/FilteredEndpointInfoSourceHostedService.cs b/src/Tools/dotnet-monitor/FilteredEndpointInfoSourceHostedService.cs new file mode 100644 index 0000000000..d72de2b6fa --- /dev/null +++ b/src/Tools/dotnet-monitor/FilteredEndpointInfoSourceHostedService.cs @@ -0,0 +1,37 @@ +// 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 +{ + /// + /// A hosted service that ensures the + /// starts monitoring for connectable processes. + /// + 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; + } + } +} diff --git a/src/Tools/dotnet-monitor/Startup.cs b/src/Tools/dotnet-monitor/Startup.cs index 7d91045508..50222bf9db 100644 --- a/src/Tools/dotnet-monitor/Startup.cs +++ b/src/Tools/dotnet-monitor/Startup.cs @@ -2,19 +2,18 @@ // 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 { @@ -30,14 +29,14 @@ public Startup(IConfiguration configuration) // 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(options => { @@ -50,20 +49,27 @@ public void ConfigureServices(IServiceCollection services) }; }); - services.Configure(options => + services.Configure(options => { options.Level = CompressionLevel.Optimal; }); services.AddResponseCompression(configureOptions => { - configureOptions.Providers.Add(); + configureOptions.Providers.Add(); configureOptions.MimeTypes = new List { "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(options => + { + options.AllowSynchronousIO = true; + }); + + var prometheusConfig = new PrometheusConfiguration(); + Configuration.Bind(nameof(PrometheusConfiguration), prometheusConfig); + if (prometheusConfig.Enabled) { services.AddSingleton(); services.AddHostedService(); @@ -71,7 +77,7 @@ public void ConfigureServices(IServiceCollection services) } // 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()) { diff --git a/src/Tools/dotnet-monitor/dotnet-monitor.csproj b/src/Tools/dotnet-monitor/dotnet-monitor.csproj index ddeee4220f..e76cc6674e 100644 --- a/src/Tools/dotnet-monitor/dotnet-monitor.csproj +++ b/src/Tools/dotnet-monitor/dotnet-monitor.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 linux-x64;linux-musl-x64;win-x64 linux-x64;linux-musl-x64;win-x64 Microsoft.Diagnostics.Tools.Monitor @@ -10,6 +10,7 @@ Diagnostic false $(Description) + Major monitor diff --git a/src/Tools/dotnet-monitor/runtimeconfig.template.json b/src/Tools/dotnet-monitor/runtimeconfig.template.json deleted file mode 100644 index f022b7ffce..0000000000 --- a/src/Tools/dotnet-monitor/runtimeconfig.template.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rollForwardOnNoCandidateFx": 2 -} \ No newline at end of file diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs index 27ab0f6b1a..27d0e9b010 100644 --- a/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs @@ -22,23 +22,50 @@ public ReversedServerTests(ITestOutputHelper outputHelper) _outputHelper = outputHelper; } + /// + /// Tests that server throws appropriate exceptions when not started. + /// + [Fact] + public async Task ReversedServerNoStartTest() + { + await using var server = CreateReversedServer(out string transportName); + // Intentionally did not start server + + TimeSpan CancellationTimeout = TimeSpan.FromSeconds(1); + using CancellationTokenSource cancellation = new CancellationTokenSource(CancellationTimeout); + + // All API surface (except for Start) should throw InvalidOperationException + await Assert.ThrowsAsync( + () => server.AcceptAsync(cancellation.Token)); + + Assert.Throws( + () => server.Connect(Guid.Empty, CancellationTimeout)); + + Assert.Throws( + () => server.RemoveConnection(Guid.Empty)); + + await Assert.ThrowsAsync( + () => server.WaitForConnectionAsync(Guid.Empty, cancellation.Token)); + } + /// /// Tests that server throws appropriate exceptions when disposed. /// [Fact] public async Task ReversedServerDisposeTest() { - var server = StartReversedServer(out string transportName); + var server = CreateReversedServer(out string transportName); + server.Start(); using CancellationTokenSource cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(1)); Task acceptTask = server.AcceptAsync(cancellation.Token); // Validate server surface throws after disposal - server.Dispose(); + await server.DisposeAsync(); - // Pending tasks should be cancelled and throw TaskCanceledException - await Assert.ThrowsAnyAsync(() => acceptTask); - Assert.True(acceptTask.IsCanceled); + // Pending tasks should throw ObjectDisposedException + await Assert.ThrowsAnyAsync(() => acceptTask); + Assert.True(acceptTask.IsFaulted); // Calls after dispose should throw ObjectDisposedException await Assert.ThrowsAsync( @@ -55,7 +82,8 @@ await Assert.ThrowsAsync( [Fact] public async Task ReversedServerAcceptAsyncYieldsTest() { - using var server = StartReversedServer(out string transportName); + await using var server = CreateReversedServer(out string transportName); + server.Start(); using var cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(1)); @@ -72,18 +100,19 @@ public async Task ReversedServerAcceptAsyncYieldsTest() [Fact] public async Task ReversedServerNonExistingRuntimeIdentifierTest() { - using var server = StartReversedServer(out string transportName); + await using var server = CreateReversedServer(out string transportName); + server.Start(); Guid nonExistingRuntimeId = Guid.NewGuid(); using CancellationTokenSource cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(1)); _outputHelper.WriteLine($"Testing {nameof(ReversedDiagnosticsServer.WaitForConnectionAsync)}"); - await Assert.ThrowsAsync( + await Assert.ThrowsAsync( () => server.WaitForConnectionAsync(nonExistingRuntimeId, cancellation.Token)); _outputHelper.WriteLine($"Testing {nameof(ReversedDiagnosticsServer.Connect)}"); - Assert.Throws( + Assert.Throws( () => server.Connect(nonExistingRuntimeId, TimeSpan.FromSeconds(1))); _outputHelper.WriteLine($"Testing {nameof(ReversedDiagnosticsServer.RemoveConnection)}"); @@ -102,8 +131,8 @@ await Assert.ThrowsAsync( [Fact] public async Task ReversedServerSingleTargetMultipleUseClientTest() { - using var server = StartReversedServer(out string transportName); - await using var accepter = new EndpointInfoAccepter(server, _outputHelper); + await using var server = CreateReversedServer(out string transportName); + server.Start(); TestRunner runner = null; IpcEndpointInfo info; @@ -112,12 +141,12 @@ public async Task ReversedServerSingleTargetMultipleUseClientTest() // Start client pointing to diagnostics server runner = StartTracee(transportName); - info = await AcceptAsync(accepter); + info = await AcceptAsync(server); await VerifyEndpointInfo(runner, info); // There should not be any new endpoint infos - await VerifyNoNewEndpointInfos(accepter); + await VerifyNoNewEndpointInfos(server); ResumeRuntime(info); @@ -138,7 +167,7 @@ public async Task ReversedServerSingleTargetMultipleUseClientTest() Assert.True(server.RemoveConnection(info.RuntimeInstanceCookie), "Expected to be able to remove connection from server."); // There should not be any more endpoint infos - await VerifyNoNewEndpointInfos(accepter); + await VerifyNoNewEndpointInfos(server); } /// @@ -147,8 +176,8 @@ public async Task ReversedServerSingleTargetMultipleUseClientTest() [Fact] public async Task ReversedServerSingleTargetExitsClientInviableTest() { - using var server = StartReversedServer(out string transportName); - await using var accepter = new EndpointInfoAccepter(server, _outputHelper); + await using var server = CreateReversedServer(out string transportName); + server.Start(); TestRunner runner = null; IpcEndpointInfo info; @@ -158,12 +187,12 @@ public async Task ReversedServerSingleTargetExitsClientInviableTest() runner = StartTracee(transportName); // Get client connection - info = await AcceptAsync(accepter); + info = await AcceptAsync(server); await VerifyEndpointInfo(runner, info); // There should not be any new endpoint infos - await VerifyNoNewEndpointInfos(accepter); + await VerifyNoNewEndpointInfos(server); ResumeRuntime(info); @@ -184,21 +213,21 @@ public async Task ReversedServerSingleTargetExitsClientInviableTest() Assert.True(server.RemoveConnection(info.RuntimeInstanceCookie), "Expected to be able to remove connection from server."); // There should not be any more endpoint infos - await VerifyNoNewEndpointInfos(accepter); + await VerifyNoNewEndpointInfos(server); } - private ReversedDiagnosticsServer StartReversedServer(out string transportName) + private ReversedDiagnosticsServer CreateReversedServer(out string transportName) { transportName = ReversedServerHelper.CreateServerTransportName(); _outputHelper.WriteLine("Starting reversed server at '" + transportName + "'."); return new ReversedDiagnosticsServer(transportName); } - private async Task AcceptAsync(EndpointInfoAccepter accepter) + private async Task AcceptAsync(ReversedDiagnosticsServer server) { using (var cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(3))) { - return await accepter.AcceptAsync(cancellationSource.Token); + return await server.AcceptAsync(cancellationSource.Token); } } @@ -230,14 +259,14 @@ await Assert.ThrowsAsync( /// /// Checks that the accepter does not provide a new endpoint info. /// - private async Task VerifyNoNewEndpointInfos(EndpointInfoAccepter accepter) + private async Task VerifyNoNewEndpointInfos(ReversedDiagnosticsServer server) { _outputHelper.WriteLine("Verifying there are no more connections."); using var cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(1)); - Task acceptTask = accepter.AcceptAsync(cancellationSource.Token); - await Assert.ThrowsAsync(() => acceptTask); + Task acceptTask = server.AcceptAsync(cancellationSource.Token); + await Assert.ThrowsAsync(() => acceptTask); Assert.True(acceptTask.IsCanceled); _outputHelper.WriteLine("Verified there are no more connections."); @@ -361,88 +390,5 @@ private Task VerifyEventStreamProvidesEventsAsync(IpcEndpointInfo info, EventPip await stoppedProcessingTask; }); } - - /// - /// Helper class for consuming endpoint infos from the reverse diagnostics server. - /// - /// - /// The diagnostics server requires that something is continuously attempting to accept endpoint infos - /// in order to process incoming connections. This helps facilitate that continuous accepting of - /// endpoint infos so the individual tests don't have to know about the behavior. - /// - private class EndpointInfoAccepter : IAsyncDisposable - { - private readonly CancellationTokenSource _cancellation = new CancellationTokenSource(); - private readonly Queue _connections = new Queue(); - private readonly SemaphoreSlim _connectionsSemaphore = new SemaphoreSlim(0); - private readonly Task _listenTask; - private readonly ITestOutputHelper _outputHelper; - private readonly ReversedDiagnosticsServer _server; - - private int _acceptedCount; - private bool _disposed; - - public EndpointInfoAccepter(ReversedDiagnosticsServer server, ITestOutputHelper outputHelper) - { - _server = server; - _outputHelper = outputHelper; - - _listenTask = ListenAsync(_cancellation.Token); - } - - public async ValueTask DisposeAsync() - { - if (!_disposed) - { - _cancellation.Cancel(); - - await _listenTask; - - _cancellation.Dispose(); - - _disposed = true; - } - } - - public async Task AcceptAsync(CancellationToken token) - { - using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); - - _outputHelper.WriteLine("Waiting for connection from accepter."); - await _connectionsSemaphore.WaitAsync(linkedSource.Token).ConfigureAwait(false); - _outputHelper.WriteLine("Received connection from accepter."); - - return _connections.Dequeue(); - } - - /// - /// Continuously accept endpoint infos from the reversed diagnostics server so - /// that - /// is always awaited in order to to handle new runtime instance connections - /// as well as existing runtime instance reconnections. - /// - private async Task ListenAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - IpcEndpointInfo info; - try - { - _outputHelper.WriteLine("Waiting for connection from server."); - info = await _server.AcceptAsync(token).ConfigureAwait(false); - - _acceptedCount++; - _outputHelper.WriteLine($"Accepted connection #{_acceptedCount} from server: {info.ToTestString()}"); - } - catch (OperationCanceledException) - { - break; - } - - _connections.Enqueue(info); - _connectionsSemaphore.Release(); - } - } - } } } diff --git a/src/tests/dotnet-monitor/EndpointInfoSourceTests.cs b/src/tests/dotnet-monitor/EndpointInfoSourceTests.cs index 9d973aab3b..9b31e02112 100644 --- a/src/tests/dotnet-monitor/EndpointInfoSourceTests.cs +++ b/src/tests/dotnet-monitor/EndpointInfoSourceTests.cs @@ -23,27 +23,20 @@ public EndpointInfoSourceTests(ITestOutputHelper outputHelper) } /// - /// Tests that the server endpoint info source has no connections - /// if is not called. + /// Tests that other methods throw if + /// is not called. /// [Fact] - public async Task ServerSourceNoListenTest() + public async Task ServerSourceNoStartTest() { await using var source = CreateServerSource(out string transportName); - // Intentionally do not call Listen + // Intentionally do not call Start - await using (var execution1 = StartTraceeProcess("LoggerRemoteTest", transportName)) - { - execution1.Start(); - - await Task.Delay(TimeSpan.FromSeconds(1)); + TimeSpan CancellationTimeout = TimeSpan.FromSeconds(1); + using CancellationTokenSource cancellation = new CancellationTokenSource(CancellationTimeout); - var endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Empty(endpointInfos); - - _outputHelper.WriteLine("Stopping tracee."); - } + await Assert.ThrowsAsync( + () => source.GetEndpointInfoAsync(cancellation.Token)); } /// @@ -53,7 +46,7 @@ public async Task ServerSourceNoListenTest() public async Task ServerSourceNoConnectionsTest() { await using var source = CreateServerSource(out _); - source.Listen(); + source.Start(); var endpointInfos = await GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); @@ -67,16 +60,16 @@ public async Task ServerSourceNoConnectionsTest() public async Task ServerSourceThrowsWhenDisposedTest() { var source = CreateServerSource(out _); - source.Listen(); + source.Start(); await source.DisposeAsync(); // Validate source surface throws after disposal Assert.Throws( - () => source.Listen()); + () => source.Start()); Assert.Throws( - () => source.Listen(1)); + () => source.Start(1)); await Assert.ThrowsAsync( () => source.GetEndpointInfoAsync(CancellationToken.None)); @@ -84,20 +77,20 @@ await Assert.ThrowsAsync( /// /// Tests that server endpoint info source should throw an exception from - /// and - /// after listening was already started. + /// and + /// after listening was already started. /// [Fact] - public async Task ServerSourceThrowsWhenMultipleListenTest() + public async Task ServerSourceThrowsWhenMultipleStartTest() { await using var source = CreateServerSource(out _); - source.Listen(); + source.Start(); Assert.Throws( - () => source.Listen()); + () => source.Start()); Assert.Throws( - () => source.Listen(1)); + () => source.Start(1)); } /// @@ -108,7 +101,7 @@ public async Task ServerSourceThrowsWhenMultipleListenTest() public async Task ServerSourceAddRemoveSingleConnectionTest() { await using var source = CreateServerSource(out string transportName); - source.Listen(); + source.Start(); var endpointInfos = await GetEndpointInfoAsync(source); Assert.Empty(endpointInfos);