+++ /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.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<IEnumerable<IEndpointInfo>> GetEndpointInfoAsync(CancellationToken token)
- {
- var endpointInfoTasks = new List<Task<EndpointInfo>>();
- // 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);
- }
- }
-}
+++ /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.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();
- }
- }
-}
+++ /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.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}");
- }
-}
+++ /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.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<IEnumerable<IEndpointInfo>> GetEndpointInfoAsync(CancellationToken token);
- }
-}
+++ /dev/null
-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
+++ /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.NETCore.Client;
-
-namespace Microsoft.Diagnostics.Monitoring
-{
- /// <summary>
- /// Aggregates diagnostic endpoints that are established at a transport path via a reversed server.
- /// </summary>
- 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<EndpointInfo> _endpointInfos = new List<EndpointInfo>();
- private readonly SemaphoreSlim _endpointInfosSemaphore = new SemaphoreSlim(1);
- private readonly string _transportPath;
-
- private Task _listenTask;
- private bool _disposed = false;
- private ReversedDiagnosticsServer _server;
-
- /// <summary>
- /// Constructs a <see cref="ServerEndpointInfoSource"/> that aggreates diagnostic endpoints
- /// from a reversed diagnostics server at path specified by <paramref name="transportPath"/>.
- /// </summary>
- /// <param name="transportPath">
- /// 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.
- /// </param>
- 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;
- }
- }
-
- /// <summary>
- /// Starts listening to the reversed diagnostics server for new connections.
- /// </summary>
- public void Start()
- {
- Start(ReversedDiagnosticsServer.MaxAllowedConnections);
- }
-
- /// <summary>
- /// Starts listening to the reversed diagnostics server for new connections.
- /// </summary>
- /// <param name="maxConnections">The maximum number of connections the server will support.</param>
- 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);
- }
-
- /// <summary>
- /// Gets the list of <see cref="IpcEndpointInfo"/> served from the reversed diagnostics server.
- /// </summary>
- /// <param name="token">The token to monitor for cancellation requests.</param>
- /// <returns>A list of active <see cref="IEndpointInfo"/> instances.</returns>
- public async Task<IEnumerable<IEndpointInfo>> 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<EndpointInfo, Task<bool>> checkMap = new Dictionary<EndpointInfo, Task<bool>>();
- 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<EndpointInfo, Task<bool>> 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();
- }
- }
-
- /// <summary>
- /// Returns true if the connection is not longer viable.
- /// </summary>
- private static async Task<bool> 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;
- }
-
- /// <summary>
- /// Accepts endpoint infos from the reversed diagnostics server.
- /// </summary>
- /// <param name="token">The token to monitor for cancellation requests.</param>
- private async Task ListenAsync(int maxConnections, CancellationToken token)
- {
- _server.Start(maxConnections);
- // Continuously accept endpoint infos from the reversed diagnostics server so
- // that <see cref="ReversedDiagnosticsServer.AcceptAsync(CancellationToken)"/>
- // 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;
- }
-}
+++ /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.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));
- }
- }
-}
+++ /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.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;
- }
-
- /// <summary>
- /// Tests that other <see cref="ServerEndpointInfoSource"> methods throw if
- /// <see cref="ServerEndpointInfoSource.Start"/> is not called.
- /// </summary>
- [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<InvalidOperationException>(
- () => source.GetEndpointInfoAsync(cancellation.Token));
- }
-
- /// <summary>
- /// Tests that the server endpoint info source has not connections if no processes connect to it.
- /// </summary>
- [Fact]
- public async Task ServerSourceNoConnectionsTest()
- {
- await using var source = CreateServerSource(out _);
- source.Start();
-
- var endpointInfos = await GetEndpointInfoAsync(source);
- Assert.Empty(endpointInfos);
- }
-
- /// <summary>
- /// Tests that server endpoint info source should throw ObjectDisposedException
- /// from API surface after being disposed.
- /// </summary>
- [Fact]
- public async Task ServerSourceThrowsWhenDisposedTest()
- {
- var source = CreateServerSource(out _);
- source.Start();
-
- await source.DisposeAsync();
-
- // Validate source surface throws after disposal
- Assert.Throws<ObjectDisposedException>(
- () => source.Start());
-
- Assert.Throws<ObjectDisposedException>(
- () => source.Start(1));
-
- using var cancellation = new CancellationTokenSource(DefaultNegativeVerificationTimeout);
- await Assert.ThrowsAsync<ObjectDisposedException>(
- () => source.GetEndpointInfoAsync(cancellation.Token));
- }
-
- /// <summary>
- /// Tests that server endpoint info source should throw an exception from
- /// <see cref="ServerEndpointInfoSource.Start"/> and
- /// <see cref="ServerEndpointInfoSource.Start(int)"/> after listening was already started.
- /// </summary>
- [Fact]
- public async Task ServerSourceThrowsWhenMultipleStartTest()
- {
- await using var source = CreateServerSource(out _);
- source.Start();
-
- Assert.Throws<InvalidOperationException>(
- () => source.Start());
-
- Assert.Throws<InvalidOperationException>(
- () => source.Start(1));
- }
-
- /// <summary>
- /// Tests that the server endpoint info source can properly enumerate endpoint infos when a single
- /// target connects to it and "disconnects" from it.
- /// </summary>
- [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);
- }
-
- /// <summary>
- /// Tests that the server endpoint info source can properly enumerate endpoint infos when multiple
- /// targets connect to it and "disconnect" from it.
- /// </summary>
- [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<IEnumerable<IEndpointInfo>> GetEndpointInfoAsync(ServerEndpointInfoSource source)
- {
- _outputHelper.WriteLine("Getting endpoint infos.");
- using CancellationTokenSource cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
- return await source.GetEndpointInfoAsync(cancellationSource.Token);
- }
-
- /// <summary>
- /// Verifies basic information on the connection and that it matches the target process from the runner.
- /// </summary>
- 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<TaskCompletionSource<EndpointInfo>> _addedEndpointInfoSources = new List<TaskCompletionSource<EndpointInfo>>();
-
- public TestServerEndpointInfoSource(string transportPath, ITestOutputHelper outputHelper)
- : base(transportPath)
- {
- _outputHelper = outputHelper;
- }
-
- public async Task<EndpointInfo> WaitForNewEndpointInfoAsync(TimeSpan timeout)
- {
- TaskCompletionSource<EndpointInfo> addedEndpointInfoSource = new TaskCompletionSource<EndpointInfo>(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}");
- }
- }
- }
-}