From b41215f5efd12cc78e4f42e0e370371d48875757 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Wed, 16 Jun 2021 11:27:15 -0700 Subject: [PATCH] Remove endpoint info, runtime info, and command line helper. --- .../ClientEndpointInfoSource.cs | 48 --- .../CommandLineHelper.cs | 69 ---- .../EndpointInfo.cs | 99 ------ .../IEndpointInfoSource.cs | 36 --- .../RuntimeInfo.cs | 42 --- .../ServerEndpointInfoSource.cs | 271 ---------------- .../CommandLineHelperTests.cs | 44 --- .../EndpointInfoSourceTests.cs | 294 ------------------ 8 files changed, 903 deletions(-) delete mode 100644 src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.cs delete mode 100644 src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs delete mode 100644 src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs delete mode 100644 src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs delete mode 100644 src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs delete mode 100644 src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs delete mode 100644 src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs delete mode 100644 src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs diff --git a/src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.cs deleted file mode 100644 index 7cac048cb3..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.cs +++ /dev/null @@ -1,48 +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 System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - internal sealed class ClientEndpointInfoSource : IEndpointInfoSourceInternal - { - public async Task> GetEndpointInfoAsync(CancellationToken token) - { - var endpointInfoTasks = new List>(); - // Run the EndpointInfo creation parallel. The call to FromProcessId sends - // a GetProcessInfo command to the runtime instance to get additional information. - foreach (int pid in DiagnosticsClient.GetPublishedProcesses()) - { - endpointInfoTasks.Add(Task.Run(() => - { - try - { - return EndpointInfo.FromProcessId(pid); - } - //Catch when the application is running a more privilaged socket than dotnet-monitor. For example, running a web app as administrator - //while running dotnet-monitor without elevation. - catch (UnauthorizedAccessException) - { - return null; - } - //Most errors from IpcTransport, such as a stale socket. - catch (ServerNotAvailableException) - { - return null; - } - }, token)); - } - - await Task.WhenAll(endpointInfoTasks); - - return endpointInfoTasks.Where(t => t.Result != null).Select(t => t.Result); - } - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs b/src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs deleted file mode 100644 index 653467e841..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs +++ /dev/null @@ -1,69 +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 System.Text; - -namespace Microsoft.Diagnostics.Monitoring -{ - internal class CommandLineHelper - { - public static string ExtractExecutablePath(string commandLine, bool isWindows) - { - if (string.IsNullOrEmpty(commandLine)) - { - return commandLine; - } - - int commandLineLength = commandLine.Length; - bool isQuoted = false; - bool isEscaped = false; - int i = 0; - char c = commandLine[0]; - - // Search for the first whitespace character that is not quoted. - // Store character literals as it iterates the command line. Escaped - // characters within double quotes are unescaped for non-Windows systems. - // Algorithm based on INIT_FormatCommandLine behavior from - // https://github.com/dotnet/runtime/blob/main/src/coreclr/pal/src/init/pal.cpp - StringBuilder builder = new StringBuilder(commandLineLength); - do - { - if (isEscaped) - { - builder.Append(c); - isEscaped = false; - } - else if (c == '"') - { - isQuoted = !isQuoted; - } - else if (c == '\\' && !isWindows) - { - if (isQuoted) - { - isEscaped = true; - } - else - { - builder.Append(c); - } - } - else - { - builder.Append(c); - } - - if (commandLineLength == ++i) - { - break; - } - - c = commandLine[i]; - } - while (isQuoted || !char.IsWhiteSpace(c)); - - return builder.ToString(); - } - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs b/src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs deleted file mode 100644 index dbac65f2ed..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs +++ /dev/null @@ -1,99 +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 System; -using System.Diagnostics; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class EndpointInfo : IEndpointInfo - { - public static EndpointInfo FromProcessId(int processId) - { - var client = new DiagnosticsClient(processId); - - ProcessInfo processInfo = null; - try - { - // Primary motivation is to get the runtime instance cookie in order to - // keep parity with the FromIpcEndpointInfo implementation; store the - // remainder of the information since it already has access to it. - processInfo = client.GetProcessInfo(); - - Debug.Assert(processId == unchecked((int)processInfo.ProcessId)); - } - catch (ServerErrorException) - { - // The runtime likely doesn't understand the GetProcessInfo command. - } - catch (TimeoutException) - { - // Runtime didn't respond within client timeout. - } - - // CONSIDER: Generate a runtime instance identifier based on the pipe name - // for .NET Core 3.1 e.g. pid + disambiguator in GUID form. - return new EndpointInfo() - { - Endpoint = new PidIpcEndpoint(processId), - ProcessId = processId, - RuntimeInstanceCookie = processInfo?.RuntimeInstanceCookie ?? Guid.Empty, - CommandLine = processInfo?.CommandLine, - OperatingSystem = processInfo?.OperatingSystem, - ProcessArchitecture = processInfo?.ProcessArchitecture - }; - } - - public static EndpointInfo FromIpcEndpointInfo(IpcEndpointInfo info) - { - var client = new DiagnosticsClient(info.Endpoint); - - ProcessInfo processInfo = null; - try - { - // Primary motivation is to keep parity with the FromProcessId implementation, - // which provides the additional process information because it already has - // access to it. - processInfo = client.GetProcessInfo(); - - Debug.Assert(info.ProcessId == unchecked((int)processInfo.ProcessId)); - Debug.Assert(info.RuntimeInstanceCookie == processInfo.RuntimeInstanceCookie); - } - catch (ServerErrorException) - { - // The runtime likely doesn't understand the GetProcessInfo command. - } - catch (TimeoutException) - { - // Runtime didn't respond within client timeout. - } - - return new EndpointInfo() - { - Endpoint = info.Endpoint, - ProcessId = info.ProcessId, - RuntimeInstanceCookie = info.RuntimeInstanceCookie, - CommandLine = processInfo?.CommandLine, - OperatingSystem = processInfo?.OperatingSystem, - ProcessArchitecture = processInfo?.ProcessArchitecture - }; - } - - public IpcEndpoint Endpoint { get; private set; } - - public int 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; } - - internal string DebuggerDisplay => FormattableString.Invariant($"PID={ProcessId}, Cookie={RuntimeInstanceCookie}"); - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs deleted file mode 100644 index f3b90b50e8..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs +++ /dev/null @@ -1,36 +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 System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - internal interface IEndpointInfo - { - IpcEndpoint Endpoint { get; } - - int ProcessId { get; } - - Guid RuntimeInstanceCookie { get; } - - string CommandLine { get; } - - string OperatingSystem { get; } - - string ProcessArchitecture { get; } - } - - public interface IEndpointInfoSource - { - } - - internal interface IEndpointInfoSourceInternal : IEndpointInfoSource - { - Task> GetEndpointInfoAsync(CancellationToken token); - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs b/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs deleted file mode 100644 index 270b1703ad..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - // Check if one of the control groups of this process is owned by docker - if (File.ReadAllText("/proc/self/cgroup").Contains("/docker/")) - { - return true; - } - - // Most of the control groups are owned by "kubepods" when running in kubernetes; - // Check for docker environment file - return File.Exists("/.dockerenv"); - } - - // TODO: Add detection for other platforms - return false; - } - } - - public static bool IsInKubernetes => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST")); - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs deleted file mode 100644 index 615f4c902c..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs +++ /dev/null @@ -1,271 +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 System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - /// - /// Aggregates diagnostic endpoints that are established at a transport path via a reversed server. - /// - internal class ServerEndpointInfoSource : IEndpointInfoSourceInternal, IAsyncDisposable - { - // The amount of time to wait when checking if the a endpoint info should be - // pruned from the list of endpoint infos. If the runtime doesn't have a viable connection within - // this time, it will be pruned from the list. - private static readonly TimeSpan PruneWaitForConnectionTimeout = TimeSpan.FromMilliseconds(250); - - private readonly CancellationTokenSource _cancellation = new CancellationTokenSource(); - private readonly IList _endpointInfos = new List(); - private readonly SemaphoreSlim _endpointInfosSemaphore = new SemaphoreSlim(1); - private readonly string _transportPath; - - private Task _listenTask; - private bool _disposed = false; - private ReversedDiagnosticsServer _server; - - /// - /// Constructs a that aggreates diagnostic endpoints - /// from a reversed diagnostics server at path 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. - /// - public ServerEndpointInfoSource(string transportPath) - { - _transportPath = transportPath; - } - - public async ValueTask DisposeAsync() - { - if (!_disposed) - { - _cancellation.Cancel(); - - if (null != _listenTask) - { - try - { - await _listenTask.ConfigureAwait(false); - } - catch (Exception ex) - { - Debug.Fail(ex.Message); - } - } - - if (null != _server) - { - await _server.DisposeAsync().ConfigureAwait(false); - } - - _endpointInfosSemaphore.Dispose(); - - _cancellation.Dispose(); - - _disposed = true; - } - } - - /// - /// Starts listening to the reversed diagnostics server for new connections. - /// - public void Start() - { - Start(ReversedDiagnosticsServer.MaxAllowedConnections); - } - - /// - /// Starts listening to the reversed diagnostics server for new connections. - /// - /// The maximum number of connections the server will support. - public void Start(int maxConnections) - { - VerifyNotDisposed(); - - if (IsListening) - { - throw new InvalidOperationException(nameof(ServerEndpointInfoSource.Start) + " method can only be called once."); - } - - _server = new ReversedDiagnosticsServer(_transportPath); - - _listenTask = ListenAsync(maxConnections, _cancellation.Token); - } - - /// - /// Gets the list of served from the reversed diagnostics server. - /// - /// The token to monitor for cancellation requests. - /// A list of active instances. - public async Task> GetEndpointInfoAsync(CancellationToken token) - { - 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. - IDictionary> checkMap = new Dictionary>(); - foreach (EndpointInfo info in _endpointInfos) - { - checkMap.Add(info, Task.Run(() => CheckNotViable(info, linkedToken), linkedToken)); - } - - // Wait for all checks to complete - await Task.WhenAll(checkMap.Values).ConfigureAwait(false); - - // Remove connections for failed checks - foreach (KeyValuePair> entry in checkMap) - { - if (entry.Value.Result) - { - _endpointInfos.Remove(entry.Key); - OnRemovedEndpointInfo(entry.Key); - _server?.RemoveConnection(entry.Key.RuntimeInstanceCookie); - } - } - - return _endpointInfos.ToList(); - } - finally - { - _endpointInfosSemaphore.Release(); - } - } - - /// - /// Returns true if the connection is not longer viable. - /// - private static async Task CheckNotViable(EndpointInfo info, CancellationToken token) - { - using var timeoutSource = new CancellationTokenSource(); - using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutSource.Token); - - try - { - timeoutSource.CancelAfter(PruneWaitForConnectionTimeout); - - await info.Endpoint.WaitForConnectionAsync(linkedSource.Token).ConfigureAwait(false); - } - catch - { - // Only report not viable if check was not cancelled. - if (!token.IsCancellationRequested) - { - return true; - } - } - - return false; - } - - /// - /// Accepts endpoint infos from the reversed diagnostics server. - /// - /// The token to monitor for cancellation requests. - 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 - // as well as existing runtime instance reconnections. - while (!token.IsCancellationRequested) - { - try - { - IpcEndpointInfo info = await _server.AcceptAsync(token).ConfigureAwait(false); - - _ = Task.Run(() => ResumeAndQueueEndpointInfo(info, token), token); - } - catch (OperationCanceledException) - { - } - } - } - - private async Task ResumeAndQueueEndpointInfo(IpcEndpointInfo info, CancellationToken token) - { - try - { - // Send ResumeRuntime message for runtime instances that connect to the server. This will allow - // those instances that are configured to pause on start to resume after the diagnostics - // connection has been made. Instances that are not configured to pause on startup will ignore - // the command and return success. - var client = new DiagnosticsClient(info.Endpoint); - try - { - client.ResumeRuntime(); - } - catch (ServerErrorException) - { - // The runtime likely doesn't understand the ResumeRuntime command. - } - - EndpointInfo endpointInfo = EndpointInfo.FromIpcEndpointInfo(info); - - await _endpointInfosSemaphore.WaitAsync(token).ConfigureAwait(false); - try - { - _endpointInfos.Add(endpointInfo); - - OnAddedEndpointInfo(endpointInfo); - } - finally - { - _endpointInfosSemaphore.Release(); - } - } - catch (Exception) - { - _server?.RemoveConnection(info.RuntimeInstanceCookie); - - throw; - } - } - - internal virtual void OnAddedEndpointInfo(EndpointInfo info) - { - } - - internal virtual void OnRemovedEndpointInfo(EndpointInfo info) - { - } - - private void VerifyNotDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(ServerEndpointInfoSource)); - } - } - - 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; - } -} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs deleted file mode 100644 index 2bd81cfa83..0000000000 --- a/src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs +++ /dev/null @@ -1,44 +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.Monitoring; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostics.Monitoring.UnitTests -{ - public class CommandLineHelperTests - { - private readonly ITestOutputHelper _output; - - public CommandLineHelperTests(ITestOutputHelper output) - { - _output = output; - } - - [Theory] - [InlineData(true, null, null)] - [InlineData(true, "", "")] - [InlineData(true, @"C:\NoArgs\test.exe", @"C:\NoArgs\test.exe")] - [InlineData(true, @"C:\WithArgs\test.exe arg1 arg2", @"C:\WithArgs\test.exe")] - [InlineData(true, @"""C:\With Space No Args\test.exe""", @"C:\With Space No Args\test.exe")] - [InlineData(true, @"""C:\With Space With Args\test.exe"" arg1 arg2", @"C:\With Space With Args\test.exe")] - [InlineData(true, @"C:\With'Quotes'No'Args\test.exe", @"C:\With'Quotes'No'Args\test.exe")] - [InlineData(true, @"C:\With'Quotes'With'Args\test.exe arg1 arg2", @"C:\With'Quotes'With'Args\test.exe")] - [InlineData(false, null, null)] - [InlineData(false, "", "")] - [InlineData(false, "/home/noargs/test", "/home/noargs/test")] - [InlineData(false, "/home/withargs/test arg1 arg2", "/home/withargs/test")] - [InlineData(false, @"""/home/with space no args/test""", "/home/with space no args/test")] - [InlineData(false, @"""/home/with space with args/test"" arg1 arg2", "/home/with space with args/test")] - [InlineData(false, @"""/home/escaped\\backslashes\\no\\args/test""", @"/home/escaped\backslashes\no\args/test")] - [InlineData(false, @"""/home/escaped\\backslashes\\with\\args/test"" arg1 arg2", @"/home/escaped\backslashes\with\args/test")] - [InlineData(false, @"""/home/escaped\""quotes\""no\""args/test""", @"/home/escaped""quotes""no""args/test")] - [InlineData(false, @"""/home/escaped\""quotes\""with\""args/test"" arg1 arg2", @"/home/escaped""quotes""with""args/test")] - public void CommandLineValidPathTest(bool isWindows, string commandLine, string expectedProcessPath) - { - Assert.Equal(expectedProcessPath, CommandLineHelper.ExtractExecutablePath(commandLine, isWindows)); - } - } -} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs deleted file mode 100644 index c1f8871b36..0000000000 --- a/src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs +++ /dev/null @@ -1,294 +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 System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; -using Microsoft.Diagnostics.NETCore.Client.UnitTests; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostics.Monitoring.UnitTests -{ - public class EndpointInfoSourceTests - { - // Generous timeout to allow APIs to respond on slower or more constrained machines - private static readonly TimeSpan DefaultPositiveVerificationTimeout = TimeSpan.FromSeconds(30); - private static readonly TimeSpan DefaultNegativeVerificationTimeout = TimeSpan.FromSeconds(2); - - private readonly ITestOutputHelper _outputHelper; - - public EndpointInfoSourceTests(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - } - - /// - /// Tests that other methods throw if - /// is not called. - /// - [Fact] - public async Task ServerSourceNoStartTest() - { - await using var source = CreateServerSource(out string transportName); - // Intentionally do not call Start - - using CancellationTokenSource cancellation = new CancellationTokenSource(DefaultNegativeVerificationTimeout); - - await Assert.ThrowsAsync( - () => source.GetEndpointInfoAsync(cancellation.Token)); - } - - /// - /// Tests that the server endpoint info source has not connections if no processes connect to it. - /// - [Fact] - public async Task ServerSourceNoConnectionsTest() - { - await using var source = CreateServerSource(out _); - source.Start(); - - var endpointInfos = await GetEndpointInfoAsync(source); - Assert.Empty(endpointInfos); - } - - /// - /// Tests that server endpoint info source should throw ObjectDisposedException - /// from API surface after being disposed. - /// - [Fact] - public async Task ServerSourceThrowsWhenDisposedTest() - { - var source = CreateServerSource(out _); - source.Start(); - - await source.DisposeAsync(); - - // Validate source surface throws after disposal - Assert.Throws( - () => source.Start()); - - Assert.Throws( - () => source.Start(1)); - - using var cancellation = new CancellationTokenSource(DefaultNegativeVerificationTimeout); - await Assert.ThrowsAsync( - () => source.GetEndpointInfoAsync(cancellation.Token)); - } - - /// - /// Tests that server endpoint info source should throw an exception from - /// and - /// after listening was already started. - /// - [Fact] - public async Task ServerSourceThrowsWhenMultipleStartTest() - { - await using var source = CreateServerSource(out _); - source.Start(); - - Assert.Throws( - () => source.Start()); - - Assert.Throws( - () => source.Start(1)); - } - - /// - /// Tests that the server endpoint info source can properly enumerate endpoint infos when a single - /// target connects to it and "disconnects" from it. - /// - [Fact] - public async Task ServerSourceAddRemoveSingleConnectionTest() - { - await using var source = CreateServerSource(out string transportName); - source.Start(); - - var endpointInfos = await GetEndpointInfoAsync(source); - Assert.Empty(endpointInfos); - - Task newEndpointInfoTask = source.WaitForNewEndpointInfoAsync(DefaultPositiveVerificationTimeout); - - await using (var execution1 = StartTraceeProcess("LoggerRemoteTest", transportName)) - { - await newEndpointInfoTask; - - execution1.SendSignal(); - - endpointInfos = await GetEndpointInfoAsync(source); - - var endpointInfo = Assert.Single(endpointInfos); - Assert.NotNull(endpointInfo.CommandLine); - Assert.NotNull(endpointInfo.OperatingSystem); - Assert.NotNull(endpointInfo.ProcessArchitecture); - VerifyConnection(execution1.TestRunner, endpointInfo); - - _outputHelper.WriteLine("Stopping tracee."); - } - - await Task.Delay(TimeSpan.FromSeconds(1)); - - endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Empty(endpointInfos); - } - - /// - /// Tests that the server endpoint info source can properly enumerate endpoint infos when multiple - /// targets connect to it and "disconnect" from it. - /// - [Fact] - public async Task ServerSourceAddRemoveMultipleConnectionTest() - { - await using var source = CreateServerSource(out string transportName); - source.Start(); - - var endpointInfos = await GetEndpointInfoAsync(source); - Assert.Empty(endpointInfos); - - const int appCount = 5; - RemoteTestExecution[] executions = new RemoteTestExecution[appCount]; - - try - { - // Start all app instances - for (int i = 0; i < appCount; i++) - { - Task newEndpointInfoTask = source.WaitForNewEndpointInfoAsync(DefaultPositiveVerificationTimeout); - - executions[i] = StartTraceeProcess("LoggerRemoteTest", transportName); - - await newEndpointInfoTask; - - executions[i].SendSignal(); - } - - await Task.Delay(TimeSpan.FromSeconds(1)); - - endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Equal(appCount, endpointInfos.Count()); - - for (int i = 0; i < appCount; i++) - { - IEndpointInfo endpointInfo = endpointInfos.FirstOrDefault(info => info.ProcessId == executions[i].TestRunner.Pid); - Assert.NotNull(endpointInfo); - Assert.NotNull(endpointInfo.CommandLine); - Assert.NotNull(endpointInfo.OperatingSystem); - Assert.NotNull(endpointInfo.ProcessArchitecture); - - VerifyConnection(executions[i].TestRunner, endpointInfo); - } - } - finally - { - _outputHelper.WriteLine("Stopping tracees."); - - int executionCount = 0; - for (int i = 0; i < appCount; i++) - { - if (null != executions[i]) - { - executionCount++; - await executions[i].DisposeAsync(); - } - } - Assert.Equal(appCount, executionCount); - } - - await Task.Delay(TimeSpan.FromSeconds(1)); - - endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Empty(endpointInfos); - } - - private TestServerEndpointInfoSource CreateServerSource(out string transportName) - { - transportName = ReversedServerHelper.CreateServerTransportName(); - _outputHelper.WriteLine("Starting server endpoint info source at '" + transportName + "'."); - return new TestServerEndpointInfoSource(transportName, _outputHelper); - } - - private RemoteTestExecution StartTraceeProcess(string loggerCategory, string transportName = null) - { - _outputHelper.WriteLine("Starting tracee."); - string exePath = CommonHelper.GetTraceePathWithArgs("EventPipeTracee", targetFramework: "net5.0"); - return RemoteTestExecution.StartProcess(exePath + " " + loggerCategory, _outputHelper, transportName); - } - - private async Task> GetEndpointInfoAsync(ServerEndpointInfoSource source) - { - _outputHelper.WriteLine("Getting endpoint infos."); - using CancellationTokenSource cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - return await source.GetEndpointInfoAsync(cancellationSource.Token); - } - - /// - /// Verifies basic information on the connection and that it matches the target process from the runner. - /// - private static void VerifyConnection(TestRunner runner, IEndpointInfo endpointInfo) - { - Assert.NotNull(runner); - Assert.NotNull(endpointInfo); - Assert.Equal(runner.Pid, endpointInfo.ProcessId); - Assert.NotEqual(Guid.Empty, endpointInfo.RuntimeInstanceCookie); - Assert.NotNull(endpointInfo.Endpoint); - } - - private sealed class TestServerEndpointInfoSource : ServerEndpointInfoSource - { - private readonly ITestOutputHelper _outputHelper; - private readonly List> _addedEndpointInfoSources = new List>(); - - public TestServerEndpointInfoSource(string transportPath, ITestOutputHelper outputHelper) - : base(transportPath) - { - _outputHelper = outputHelper; - } - - public async Task WaitForNewEndpointInfoAsync(TimeSpan timeout) - { - TaskCompletionSource addedEndpointInfoSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using var timeoutCancellation = new CancellationTokenSource(); - var token = timeoutCancellation.Token; - using var _ = token.Register(() => addedEndpointInfoSource.TrySetCanceled(token)); - - lock (_addedEndpointInfoSources) - { - _addedEndpointInfoSources.Add(addedEndpointInfoSource); - } - - _outputHelper.WriteLine("Waiting for new endpoint info."); - timeoutCancellation.CancelAfter(timeout); - EndpointInfo endpointInfo = await addedEndpointInfoSource.Task; - _outputHelper.WriteLine("Notified of new endpoint info."); - - return endpointInfo; - } - - internal override void OnAddedEndpointInfo(EndpointInfo info) - { - _outputHelper.WriteLine($"Added endpoint info to collection: {info.DebuggerDisplay}"); - - lock (_addedEndpointInfoSources) - { - foreach (var source in _addedEndpointInfoSources) - { - source.TrySetResult(info); - } - _addedEndpointInfoSources.Clear(); - } - } - - internal override void OnRemovedEndpointInfo(EndpointInfo info) - { - _outputHelper.WriteLine($"Removed endpoint info from collection: {info.DebuggerDisplay}"); - } - } - } -}