diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs index 65eb15aef9b..6d167ce27e3 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs @@ -2,29 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Diagnostics.Monitoring.EventPipe; -using Microsoft.Diagnostics.NETCore.Client; -using Microsoft.Extensions.Options; namespace Microsoft.Diagnostics.Monitoring.WebApi { internal sealed class DiagnosticServices : IDiagnosticServices { - // The value of the operating system field of the ProcessInfo result when the target process is running - // on a Windows operating system. - private const string ProcessOperatingSystemWindowsValue = "windows"; - - // The amount of time to wait before cancelling get additional process information (e.g. getting - // the process command line if the IEndpointInfo doesn't provide it). - private static readonly TimeSpan ExtendedProcessInfoTimeout = TimeSpan.FromMilliseconds(1000); - private readonly IEndpointInfoSourceInternal _endpointInfoSource; private readonly IOptionsMonitor _defaultProcessOptions; @@ -42,7 +30,7 @@ public async Task> GetProcessesAsync(DiagProcessFilter try { using CancellationTokenSource extendedInfoCancellation = CancellationTokenSource.CreateLinkedTokenSource(token); - IList> processInfoTasks = new List>(); + IList> processInfoTasks = new List>(); foreach (IEndpointInfo endpointInfo in await _endpointInfoSource.GetEndpointInfoAsync(token)) { // CONSIDER: Can this processing be pushed into the IEndpointInfoSource implementation and cached @@ -51,13 +39,13 @@ public async Task> GetProcessesAsync(DiagProcessFilter // - .NET Core 3.1 processes, which require issuing a brief event pipe session to get the process commmand // line information and parse out the process name // - Caching entrypoint information (when that becomes available). - processInfoTasks.Add(ProcessInfo.FromEndpointInfoAsync(endpointInfo, extendedInfoCancellation.Token)); + processInfoTasks.Add(ProcessInfoImpl.FromEndpointInfoAsync(endpointInfo, extendedInfoCancellation.Token)); } // FromEndpointInfoAsync can fill in the command line for .NET Core 3.1 processes by invoking the // event pipe and capturing the ProcessInfo event. Timebox this operation with the cancellation token // so that getting the process list does not take a long time or wait indefinitely. - extendedInfoCancellation.CancelAfter(ExtendedProcessInfoTimeout); + extendedInfoCancellation.CancelAfter(ProcessInfoImpl.ExtendedProcessInfoTimeout); await Task.WhenAll(processInfoTasks); @@ -110,127 +98,5 @@ private async Task GetProcessAsync(DiagProcessFilter processFilter throw new ArgumentException(Strings.ErrorMessage_MultipleTargetProcesses); } } - - /// - /// We want to make sure we destroy files we finish streaming. - /// We want to make sure that we stream out files since we compress on the fly; the size cannot be known upfront. - /// CONSIDER The above implies knowledge of how the file is used by the rest api. - /// - private sealed class AutoDeleteFileStream : FileStream - { - public AutoDeleteFileStream(string path) : base(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, - bufferSize: 4096, FileOptions.DeleteOnClose) - { - } - - public override bool CanSeek => false; - } - - private sealed class ProcessInfo : IProcessInfo - { - // String returned for a process field when its value could not be retrieved. This is the same - // value that is returned by the runtime when it could not determine the value for each of those fields. - private static readonly string ProcessFieldUnknownValue = "unknown"; - - public ProcessInfo( - IEndpointInfo endpointInfo, - string commandLine, - string processName) - { - EndpointInfo = endpointInfo; - - // The GetProcessInfo command will return "unknown" for values for which it does - // not know the value, such as operating system and process architecture if the - // process is running on one that is not predefined. Mimic the same behavior here - // when the extra process information was not provided. - CommandLine = commandLine ?? ProcessFieldUnknownValue; - ProcessName = processName ?? ProcessFieldUnknownValue; - } - - public static async Task FromEndpointInfoAsync(IEndpointInfo endpointInfo) - { - using CancellationTokenSource extendedInfoCancellation = new CancellationTokenSource(ExtendedProcessInfoTimeout); - return await FromEndpointInfoAsync(endpointInfo, extendedInfoCancellation.Token); - } - - // Creates a ProcessInfo object from the IEndpointInfo. Attempts to get the command line using event pipe - // if the endpoint information doesn't provide it. The cancelation token can be used to timebox this fallback - // mechansim. - public static async Task FromEndpointInfoAsync(IEndpointInfo endpointInfo, CancellationToken extendedInfoCancellationToken) - { - if (null == endpointInfo) - { - throw new ArgumentNullException(nameof(endpointInfo)); - } - - var client = new DiagnosticsClient(endpointInfo.Endpoint); - - string commandLine = endpointInfo.CommandLine; - if (string.IsNullOrEmpty(commandLine)) - { - try - { - var infoSettings = new EventProcessInfoPipelineSettings - { - Duration = Timeout.InfiniteTimeSpan, - }; - - await using var pipeline = new EventProcessInfoPipeline(client, infoSettings, - (cmdLine, token) => { commandLine = cmdLine; return Task.CompletedTask; }); - - await pipeline.RunAsync(extendedInfoCancellationToken); - } - catch - { - } - } - - string processName = null; - if (!string.IsNullOrEmpty(commandLine)) - { - // Get the process name from the command line - bool isWindowsProcess = false; - if (string.IsNullOrEmpty(endpointInfo.OperatingSystem)) - { - // If operating system is null, the process is likely .NET Core 3.1 (which doesn't have the GetProcessInfo command). - // Since the underlying diagnostic communication channel used by the .NET runtime requires that the diagnostic process - // must be running on the same type of operating system as the target process (e.g. dotnet-monitor must be running on Windows - // if the target process is running on Windows), then checking the local operating system should be a sufficient heuristic - // to determine the operating system of the target process. - isWindowsProcess = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - } - else - { - isWindowsProcess = ProcessOperatingSystemWindowsValue.Equals(endpointInfo.OperatingSystem, StringComparison.OrdinalIgnoreCase); - } - - string processPath = CommandLineHelper.ExtractExecutablePath(commandLine, isWindowsProcess); - if (!string.IsNullOrEmpty(processPath)) - { - processName = Path.GetFileName(processPath); - if (isWindowsProcess) - { - // Remove the extension on Windows to match the behavior of Process.ProcessName - processName = Path.GetFileNameWithoutExtension(processName); - } - } - } - - return new ProcessInfo( - endpointInfo, - commandLine, - processName); - } - - public IEndpointInfo EndpointInfo { get; } - - public string CommandLine { get; } - - public string OperatingSystem => EndpointInfo.OperatingSystem ?? ProcessFieldUnknownValue; - - public string ProcessArchitecture => EndpointInfo.ProcessArchitecture ?? ProcessFieldUnknownValue; - - public string ProcessName { get; } - } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs new file mode 100644 index 00000000000..42d846e0233 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs @@ -0,0 +1,131 @@ +// 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.EventPipe; +using Microsoft.Diagnostics.NETCore.Client; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// Named "ProcessInfoImpl" to disambiguate from Microsoft.Diagnostics.NETCore.client ProcessInfo + /// class returned from issuing GetProcessInfo command on diagnostic pipe. + internal sealed class ProcessInfoImpl : IProcessInfo + { + // The amount of time to wait before cancelling get additional process information (e.g. getting + // the process command line if the IEndpointInfo doesn't provide it). + public static readonly TimeSpan ExtendedProcessInfoTimeout = TimeSpan.FromMilliseconds(1000); + + // String returned for a process field when its value could not be retrieved. This is the same + // value that is returned by the runtime when it could not determine the value for each of those fields. + private static readonly string ProcessFieldUnknownValue = "unknown"; + + // The value of the operating system field of the ProcessInfo result when the target process is running + // on a Windows operating system. + private const string ProcessOperatingSystemWindowsValue = "windows"; + + public ProcessInfoImpl( + IEndpointInfo endpointInfo, + string commandLine, + string processName) + { + EndpointInfo = endpointInfo; + + // The GetProcessInfo command will return "unknown" for values for which it does + // not know the value, such as operating system and process architecture if the + // process is running on one that is not predefined. Mimic the same behavior here + // when the extra process information was not provided. + CommandLine = commandLine ?? ProcessFieldUnknownValue; + ProcessName = processName ?? ProcessFieldUnknownValue; + } + + public static async Task FromEndpointInfoAsync(IEndpointInfo endpointInfo) + { + using CancellationTokenSource extendedInfoCancellation = new(ExtendedProcessInfoTimeout); + return await FromEndpointInfoAsync(endpointInfo, extendedInfoCancellation.Token); + } + + // Creates an IProcessInfo object from the IEndpointInfo. Attempts to get the command line using event pipe + // if the endpoint information doesn't provide it. The cancelation token can be used to timebox this fallback + // mechanism. + public static async Task FromEndpointInfoAsync(IEndpointInfo endpointInfo, CancellationToken extendedInfoCancellationToken) + { + if (null == endpointInfo) + { + throw new ArgumentNullException(nameof(endpointInfo)); + } + + DiagnosticsClient client = new(endpointInfo.Endpoint); + + string commandLine = endpointInfo.CommandLine; + if (string.IsNullOrEmpty(commandLine)) + { + try + { + EventProcessInfoPipelineSettings infoSettings = new() + { + Duration = Timeout.InfiniteTimeSpan, + }; + + await using EventProcessInfoPipeline pipeline = new(client, infoSettings, + (cmdLine, token) => { commandLine = cmdLine; return Task.CompletedTask; }); + + await pipeline.RunAsync(extendedInfoCancellationToken); + } + catch + { + } + } + + string processName = null; + if (!string.IsNullOrEmpty(commandLine)) + { + // Get the process name from the command line + bool isWindowsProcess = false; + if (string.IsNullOrEmpty(endpointInfo.OperatingSystem)) + { + // If operating system is null, the process is likely .NET Core 3.1 (which doesn't have the GetProcessInfo command). + // Since the underlying diagnostic communication channel used by the .NET runtime requires that the diagnostic process + // must be running on the same type of operating system as the target process (e.g. dotnet-monitor must be running on Windows + // if the target process is running on Windows), then checking the local operating system should be a sufficient heuristic + // to determine the operating system of the target process. + isWindowsProcess = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + else + { + isWindowsProcess = ProcessOperatingSystemWindowsValue.Equals(endpointInfo.OperatingSystem, StringComparison.OrdinalIgnoreCase); + } + + string processPath = CommandLineHelper.ExtractExecutablePath(commandLine, isWindowsProcess); + if (!string.IsNullOrEmpty(processPath)) + { + processName = Path.GetFileName(processPath); + if (isWindowsProcess) + { + // Remove the extension on Windows to match the behavior of Process.ProcessName + processName = Path.GetFileNameWithoutExtension(processName); + } + } + } + + return new ProcessInfoImpl( + endpointInfo, + commandLine, + processName); + } + + public IEndpointInfo EndpointInfo { get; } + + public string CommandLine { get; } + + public string OperatingSystem => EndpointInfo.OperatingSystem ?? ProcessFieldUnknownValue; + + public string ProcessArchitecture => EndpointInfo.ProcessArchitecture ?? ProcessFieldUnknownValue; + + public string ProcessName { get; } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs index 1940bb9c9d2..139507a8096 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.Monitoring.TestCommon @@ -17,6 +18,10 @@ public partial class DotNetHost public static Version RuntimeVersion => s_runtimeVersionLazy.Value; + public static string HostExeNameWithoutExtension => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + Path.GetFileNameWithoutExtension(HostExePath) : + Path.GetFileName(HostExePath); + public static string HostExePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"..\..\..\..\..\.dotnet\dotnet.exe" : "../../../../../.dotnet/dotnet"; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs index 04c2bb87c76..9d4cb5f6880 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs @@ -13,6 +13,7 @@ using System; using System.IO; using System.Net.Http; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -119,6 +120,139 @@ await ScenarioRunner.SingleTarget( ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(DefaultRuleName); }); } + + /// + /// Validates that a collection rule with a command line filter can be matched to the + /// target process. + /// + [ConditionalTheory(nameof(IsNotNet5OrGreaterOnUnix))] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRule_CommandLineFilterMatchTest(DiagnosticPortConnectionMode mode) + { + Task startedTask = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.AsyncWait.Name, + appValidate: async (runner, client) => + { + await startedTask; + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCommandLineFilter(TestAppScenarios.AsyncWait.Name); + + startedTask = runner.WaitForCollectionRuleStartedAsync(DefaultRuleName); + }); + } + + /// + /// Validates that a collection rule with a command line filter can fail to match the + /// target process. + /// + [Theory] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRule_CommandLineFilterNoMatchTest(DiagnosticPortConnectionMode mode) + { + Task filteredTask = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.AsyncWait.Name, + appValidate: async (runner, client) => + { + await filteredTask; + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: runner => + { + // Note that the process name filter is specified as "SpinWait" whereas the + // actual command line of the target process will contain "AsyncWait". + runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddProcessNameFilter(TestAppScenarios.SpinWait.Name); + + filteredTask = runner.WaitForCollectionRuleUnmatchedFiltersAsync(DefaultRuleName); + }); + } + + /// + /// Validates that a collection rule with a process name filter can be matched to the + /// target process. + /// + [Theory] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRule_ProcessNameFilterMatchTest(DiagnosticPortConnectionMode mode) + { + Task startedTask = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.AsyncWait.Name, + appValidate: async (runner, client) => + { + await startedTask; + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddProcessNameFilter(DotNetHost.HostExeNameWithoutExtension); + + startedTask = runner.WaitForCollectionRuleStartedAsync(DefaultRuleName); + }); + } + + /// + /// Validates that a collection rule with a process name filter can fail to match the + /// target process. + /// + [Theory] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRule_ProcessNameFilterNoMatchTest(DiagnosticPortConnectionMode mode) + { + Task filteredTask = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.AsyncWait.Name, + appValidate: async (runner, client) => + { + await filteredTask; + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddProcessNameFilter("UmatchedName"); + + filteredTask = runner.WaitForCollectionRuleUnmatchedFiltersAsync(DefaultRuleName); + }); + } + + // The GetProcessInfo command is not providing command line arguments (only the process name) + // for .NET 5+ process on non-Windows when suspended. See https://github.com/dotnet/dotnet-monitor/issues/885 + private static bool IsNotNet5OrGreaterOnUnix => + DotNetHost.RuntimeVersion.Major < 5 || + RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + #endif } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.CollectionRule.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.CollectionRule.cs index 35d1b4bcfd4..ebe16aab58a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.CollectionRule.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.CollectionRule.cs @@ -17,14 +17,29 @@ partial class MonitorCollectRunner { private readonly ConcurrentDictionary>> _collectionRuleCallbacks = new(); - public async Task WaitForCollectionRuleCompleteAsync(string ruleName, CancellationToken token) + public Task WaitForCollectionRuleCompleteAsync(string ruleName, CancellationToken token) + { + return WaitForCollectionRuleEventAsync(LoggingEventIds.CollectionRuleCompleted, ruleName, token); + } + + public Task WaitForCollectionRuleUnmatchedFiltersAsync(string ruleName, CancellationToken token) + { + return WaitForCollectionRuleEventAsync(LoggingEventIds.CollectionRuleUnmatchedFilters, ruleName, token); + } + + public Task WaitForCollectionRuleStartedAsync(string ruleName, CancellationToken token) + { + return WaitForCollectionRuleEventAsync(LoggingEventIds.CollectionRuleStarted, ruleName, token); + } + + private async Task WaitForCollectionRuleEventAsync(int eventId, string ruleName, CancellationToken token) { TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); - CollectionRuleKey completedKey = new(LoggingEventIds.CollectionRuleCompleted, ruleName); + CollectionRuleKey eventKey = new(eventId, ruleName); CollectionRuleKey failedKey = new(LoggingEventIds.CollectionRuleFailed, ruleName); - AddCollectionRuleCallback(completedKey, tcs); + AddCollectionRuleCallback(eventKey, tcs); AddCollectionRuleCallback(failedKey, tcs); try @@ -33,7 +48,7 @@ public async Task WaitForCollectionRuleCompleteAsync(string ruleName, Cancellati } finally { - RemoveCollectionRuleCallback(completedKey, tcs); + RemoveCollectionRuleCallback(eventKey, tcs); RemoveCollectionRuleCallback(failedKey, tcs); } } @@ -45,12 +60,14 @@ private void HandleCollectionRuleEvent(ConsoleLogEvent logEvent) CollectionRuleKey key = new(logEvent.EventId, ruleName); switch (logEvent.EventId) { - case LoggingEventIds.CollectionRuleFailed: - FailCollectionRuleCallbacks(key, logEvent.Exception); - break; case LoggingEventIds.CollectionRuleCompleted: + case LoggingEventIds.CollectionRuleUnmatchedFilters: + case LoggingEventIds.CollectionRuleStarted: CompleteCollectionRuleCallbacks(key); break; + case LoggingEventIds.CollectionRuleFailed: + FailCollectionRuleCallbacks(key, logEvent.Exception); + break; } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs index 1859cc5148e..8744dcf5447 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs @@ -99,5 +99,27 @@ public static async Task WaitForCollectionRuleCompleteAsync(this MonitorCollectR using CancellationTokenSource cancellation = new(timeout); await runner.WaitForCollectionRuleCompleteAsync(ruleName, cancellation.Token); } + + public static Task WaitForCollectionRuleUnmatchedFiltersAsync(this MonitorCollectRunner runner, string ruleName) + { + return runner.WaitForCollectionRuleUnmatchedFiltersAsync(ruleName, TestTimeouts.CollectionRuleFilteredTimeout); + } + + public static async Task WaitForCollectionRuleUnmatchedFiltersAsync(this MonitorCollectRunner runner, string ruleName, TimeSpan timeout) + { + using CancellationTokenSource cancellation = new(timeout); + await runner.WaitForCollectionRuleUnmatchedFiltersAsync(ruleName, cancellation.Token); + } + + public static Task WaitForCollectionRuleStartedAsync(this MonitorCollectRunner runner, string ruleName) + { + return runner.WaitForCollectionRuleStartedAsync(ruleName, TestTimeouts.CollectionRuleCompletionTimeout); + } + + public static async Task WaitForCollectionRuleStartedAsync(this MonitorCollectRunner runner, string ruleName, TimeSpan timeout) + { + using CancellationTokenSource cancellation = new(timeout); + await runner.WaitForCollectionRuleStartedAsync(ruleName, cancellation.Token); + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs index 6524d20323b..ecd40cb7f3e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs @@ -33,5 +33,10 @@ internal static class TestTimeouts /// Timeout for waiting for a collection rule to complete. /// public static readonly TimeSpan CollectionRuleCompletionTimeout = TimeSpan.FromSeconds(30); + + /// + /// Timeout for waiting for a collection rule to be filtered. + /// + public static readonly TimeSpan CollectionRuleFilteredTimeout = TimeSpan.FromSeconds(10); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs index 150342de439..6d3ad711ff8 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -2,6 +2,7 @@ // 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.WebApi; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; @@ -17,6 +18,30 @@ namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options { internal static partial class CollectionRuleOptionsExtensions { + public static CollectionRuleOptions AddCommandLineFilter(this CollectionRuleOptions options, string value, ProcessFilterType matchType = ProcessFilterType.Contains) + { + options.Filters.Add(new ProcessFilterDescriptor() + { + Key = ProcessFilterKey.CommandLine, + Value = value, + MatchType = matchType + }); + + return options; + } + + public static CollectionRuleOptions AddProcessNameFilter(this CollectionRuleOptions options, string name) + { + options.Filters.Add(new ProcessFilterDescriptor() + { + Key = ProcessFilterKey.ProcessName, + Value = name, + MatchType = ProcessFilterType.Exact + }); + + return options; + } + public static CollectionRuleOptions AddAction(this CollectionRuleOptions options, string type) { return options.AddAction(type, out _); diff --git a/src/Tools/dotnet-monitor/CollectionRules/CollectionRuleService.cs b/src/Tools/dotnet-monitor/CollectionRules/CollectionRuleService.cs index e8c445f8c68..6e0bcc6e23c 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/CollectionRuleService.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/CollectionRuleService.cs @@ -89,11 +89,13 @@ public async Task ApplyRules( List> startedSources = new(ruleNames.Count); // Wrap the passed CancellationToken into a linked CancellationTokenSource so that the - // RunRuleAsync method is only cancellable for the execution of the StartAsync method. Don't - // want the caller to be able to cancel the run of the rules after having finished + // RunRuleAsync method is only cancellable for the execution of the ApplyRules method. + // Don't want the caller to be able to cancel the run of the rules after having finished // executing the ApplyRules method. using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token); + IProcessInfo processInfo = await ProcessInfoImpl.FromEndpointInfoAsync(endpointInfo); + foreach (string ruleName in ruleNames) { TaskCompletionSource startedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); @@ -107,7 +109,7 @@ public async Task ApplyRules( _actionListExecutor, _triggerOperations, _optionsMonitor, - endpointInfo, + processInfo, ruleName, startedSource, linkedSource.Token).SafeAwait()); @@ -126,13 +128,13 @@ private async Task RunRuleAsync( ActionListExecutor actionListExecutor, ICollectionRuleTriggerOperations triggerOperations, IOptionsMonitor optionsMonitor, - IEndpointInfo endpointInfo, + IProcessInfo processInfo, string ruleName, TaskCompletionSource startedSource, CancellationToken token) { KeyValueLogScope scope = new(); - scope.AddCollectionRuleEndpointInfo(endpointInfo); + scope.AddCollectionRuleEndpointInfo(processInfo.EndpointInfo); scope.AddCollectionRuleName(ruleName); using IDisposable loggerScope = _logger.BeginScope(scope); @@ -147,16 +149,23 @@ private async Task RunRuleAsync( if (null != options.Filters) { DiagProcessFilter filter = DiagProcessFilter.FromConfiguration(options.Filters); - // TODO: Filter collection rules by process information; this requires pushing - // more of the process information into IEndpointInfo. IProcessInfo is only - // available through the IDiagnosticServices implementation, which is created from - // the entries in the IEndpointInfoSource implementation. The collection rules - // are started before the process is registered with the IEndpointInfoSource. + + if (!filter.Filters.All(f => f.MatchFilter(processInfo))) + { + // Collection rule filter does not match target process + _logger.CollectionRuleUnmatchedFilters(ruleName); + + // Signal rule has "started" in order to not block + // resumption of the runtime instance. + startedSource.TrySetResult(null); + + return; + } } _logger.CollectionRuleStarted(ruleName); - CollectionRuleContext context = new(ruleName, options, endpointInfo, _logger); + CollectionRuleContext context = new(ruleName, options, processInfo.EndpointInfo, _logger); await using CollectionRulePipeline pipeline = new( actionListExecutor, diff --git a/src/Tools/dotnet-monitor/LoggingEventIds.cs b/src/Tools/dotnet-monitor/LoggingEventIds.cs index 36fabbe47dd..4c1e66fb873 100644 --- a/src/Tools/dotnet-monitor/LoggingEventIds.cs +++ b/src/Tools/dotnet-monitor/LoggingEventIds.cs @@ -49,5 +49,6 @@ internal static class LoggingEventIds public const int CollectionRuleActionsCompleted = 39; public const int ApplyingCollectionRules = 40; public const int DiagnosticRequestCancelled = 41; + public const int CollectionRuleUnmatchedFilters = 42; } } diff --git a/src/Tools/dotnet-monitor/LoggingExtensions.cs b/src/Tools/dotnet-monitor/LoggingExtensions.cs index b5d11cd4671..0ff00a6f352 100644 --- a/src/Tools/dotnet-monitor/LoggingExtensions.cs +++ b/src/Tools/dotnet-monitor/LoggingExtensions.cs @@ -217,6 +217,12 @@ internal static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_DiagnosticRequestCancelled); + private static readonly Action _collectionRuleUnmatchedFilters = + LoggerMessage.Define( + eventId: new EventId(LoggingEventIds.CollectionRuleUnmatchedFilters, "CollectionRuleUnmatchedFilters"), + logLevel: LogLevel.Information, + formatString: Strings.LogFormatString_CollectionRuleUnmatchedFilters); + public static void EgressProviderInvalidOptions(this ILogger logger, string providerName) { _egressProviderInvalidOptions(logger, providerName, null); @@ -390,5 +396,10 @@ public static void DiagnosticRequestCancelled(this ILogger logger, int processId { _diagnosticRequestCancelled(logger, processId, null); } + + public static void CollectionRuleUnmatchedFilters(this ILogger logger, string ruleName) + { + _collectionRuleUnmatchedFilters(logger, ruleName, null); + } } } diff --git a/src/Tools/dotnet-monitor/Strings.Designer.cs b/src/Tools/dotnet-monitor/Strings.Designer.cs index 1d895bf25b5..28b4ca6f164 100644 --- a/src/Tools/dotnet-monitor/Strings.Designer.cs +++ b/src/Tools/dotnet-monitor/Strings.Designer.cs @@ -645,6 +645,15 @@ internal static string LogFormatString_CollectionRuleTriggerStarted { } } + /// + /// Looks up a localized string similar to Collection rule '{ruleName}' filters do not match the process.. + /// + internal static string LogFormatString_CollectionRuleUnmatchedFilters { + get { + return ResourceManager.GetString("LogFormatString_CollectionRuleUnmatchedFilters", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cancelled waiting for diagnostic response from runtime in process {processId}.. /// diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx index 329bb3dc2f1..7c8d8a859d1 100644 --- a/src/Tools/dotnet-monitor/Strings.resx +++ b/src/Tools/dotnet-monitor/Strings.resx @@ -421,6 +421,9 @@ Collection rule '{ruleName}' trigger '{triggerType}' started. + + Collection rule '{ruleName}' filters do not match the process. + Cancelled waiting for diagnostic response from runtime in process {processId}.