diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs index be435b66345..8ff19f864c2 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs @@ -34,7 +34,7 @@ partial class DiagController [ProducesWithProblemDetails(ContentTypes.ApplicationJsonSequence)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Metrics)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Metrics)] [EgressValidation] public Task CaptureMetrics( [FromQuery] @@ -72,13 +72,13 @@ public Task CaptureMetrics( await eventCounterPipeline.RunAsync(token); }; - return await Result(ArtifactType_Metrics, + return await Result(Utilities.ArtifactType_Metrics, egressProvider, action, fileName, ContentTypes.ApplicationJsonSequence, processInfo.EndpointInfo); - }, processKey, ArtifactType_Metrics); + }, processKey, Utilities.ArtifactType_Metrics); } /// @@ -95,7 +95,7 @@ public Task CaptureMetrics( [ProducesWithProblemDetails(ContentTypes.ApplicationJsonSequence)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Metrics)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Metrics)] [EgressValidation] public Task CaptureMetricsCustom( [FromBody][Required] @@ -135,16 +135,16 @@ public Task CaptureMetricsCustom( await eventCounterPipeline.RunAsync(token); }; - return await Result(ArtifactType_Metrics, + return await Result(Utilities.ArtifactType_Metrics, egressProvider, action, fileName, ContentTypes.ApplicationJsonSequence, processInfo.EndpointInfo); - }, processKey, ArtifactType_Metrics); + }, processKey, Utilities.ArtifactType_Metrics); } private static string GetMetricFilename(IProcessInfo processInfo) => - FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.metrics.json"); + FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.metrics.json"); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs index d91a48a5627..fa502735a9b 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs @@ -16,12 +16,9 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.Net; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -38,12 +35,6 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi.Controllers [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] public partial class DiagController : ControllerBase { - public const string ArtifactType_Dump = "dump"; - public const string ArtifactType_GCDump = "gcdump"; - public const string ArtifactType_Logs = "logs"; - public const string ArtifactType_Trace = "trace"; - public const string ArtifactType_Metrics = "collectmetrics"; - private const Models.TraceProfile DefaultTraceProfiles = Models.TraceProfile.Cpu | Models.TraceProfile.Http | Models.TraceProfile.Metrics; private static readonly MediaTypeHeaderValue NdJsonHeader = new MediaTypeHeaderValue(ContentTypes.ApplicationNdJson); private static readonly MediaTypeHeaderValue JsonSequenceHeader = new MediaTypeHeaderValue(ContentTypes.ApplicationJsonSequence); @@ -53,6 +44,7 @@ public partial class DiagController : ControllerBase private readonly IDiagnosticServices _diagnosticServices; private readonly IOptions _diagnosticPortOptions; private readonly EgressOperationStore _operationsStore; + private readonly IDumpService _dumpService; public DiagController(ILogger logger, IServiceProvider serviceProvider) @@ -61,6 +53,7 @@ public DiagController(ILogger logger, _diagnosticServices = serviceProvider.GetRequiredService(); _diagnosticPortOptions = serviceProvider.GetService>(); _operationsStore = serviceProvider.GetRequiredService(); + _dumpService = serviceProvider.GetRequiredService(); } /// @@ -194,7 +187,7 @@ public Task>> GetProcessEnvironment( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Dump)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Dump)] [EgressValidation] public Task CaptureDump( [FromQuery] @@ -212,13 +205,11 @@ public Task CaptureDump( return InvokeForProcess(async processInfo => { - string dumpFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? - FormattableString.Invariant($"dump_{GetFileNameTimeStampUtcNow()}.dmp") : - FormattableString.Invariant($"core_{GetFileNameTimeStampUtcNow()}"); + string dumpFileName = Utilities.GenerateDumpFileName(); if (string.IsNullOrEmpty(egressProvider)) { - Stream dumpStream = await _diagnosticServices.GetDump(processInfo, type, HttpContext.RequestAborted); + Stream dumpStream = await _dumpService.DumpAsync(processInfo.EndpointInfo, type, HttpContext.RequestAborted); _logger.WrittenToHttpStream(); //Compression is done automatically by the response @@ -227,19 +218,17 @@ public Task CaptureDump( } else { - KeyValueLogScope scope = new KeyValueLogScope(); - scope.AddArtifactType(ArtifactType_Dump); - scope.AddEndpointInfo(processInfo.EndpointInfo); + KeyValueLogScope scope = Utilities.GetScope(Utilities.ArtifactType_Dump, processInfo.EndpointInfo); return await SendToEgress(new EgressOperation( - token => _diagnosticServices.GetDump(processInfo, type, token), + token => _dumpService.DumpAsync(processInfo.EndpointInfo, type, token), egressProvider, dumpFileName, processInfo.EndpointInfo, ContentTypes.ApplicationOctetStream, - scope), limitKey: ArtifactType_Dump); + scope), limitKey: Utilities.ArtifactType_Dump); } - }, processKey, ArtifactType_Dump); + }, processKey, Utilities.ArtifactType_Dump); } /// @@ -257,7 +246,7 @@ public Task CaptureDump( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_GCDump)] + [RequestLimit(LimitKey = Utilities.ArtifactType_GCDump)] [EgressValidation] public Task CaptureGcDump( [FromQuery] @@ -273,7 +262,7 @@ public Task CaptureGcDump( return InvokeForProcess(processInfo => { - string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.gcdump"); + string fileName = FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.gcdump"); Func> action = async (token) => { var graph = new Graphs.MemoryGraph(50_000); @@ -295,13 +284,13 @@ public Task CaptureGcDump( }; return Result( - ArtifactType_GCDump, + Utilities.ArtifactType_GCDump, egressProvider, ConvertFastSerializeAction(action), fileName, ContentTypes.ApplicationOctetStream, processInfo.EndpointInfo); - }, processKey, ArtifactType_GCDump); + }, processKey, Utilities.ArtifactType_GCDump); } /// @@ -321,7 +310,7 @@ public Task CaptureGcDump( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Trace)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Trace)] [EgressValidation] public Task CaptureTrace( [FromQuery] @@ -370,7 +359,7 @@ public Task CaptureTrace( var aggregateConfiguration = new AggregateSourceConfiguration(configurations.ToArray()); return StartTrace(processInfo, aggregateConfiguration, duration, egressProvider); - }, processKey, ArtifactType_Trace); + }, processKey, Utilities.ArtifactType_Trace); } /// @@ -389,7 +378,7 @@ public Task CaptureTrace( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Trace)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Trace)] [EgressValidation] public Task CaptureTraceCustom( [FromBody][Required] @@ -434,7 +423,7 @@ public Task CaptureTraceCustom( bufferSizeInMB: configuration.BufferSizeInMB); return StartTrace(processInfo, traceConfiguration, duration, egressProvider); - }, processKey, ArtifactType_Trace); + }, processKey, Utilities.ArtifactType_Trace); } /// @@ -451,7 +440,7 @@ public Task CaptureTraceCustom( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Logs)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Logs)] [EgressValidation] public Task CaptureLogs( [FromQuery] @@ -490,7 +479,7 @@ public Task CaptureLogs( } return StartLogs(processInfo, settings, egressProvider); - }, processKey, ArtifactType_Logs); + }, processKey, Utilities.ArtifactType_Logs); } /// @@ -507,7 +496,7 @@ public Task CaptureLogs( [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] - [RequestLimit(LimitKey = ArtifactType_Logs)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Logs)] [EgressValidation] public Task CaptureLogsCustom( [FromBody] @@ -538,7 +527,7 @@ public Task CaptureLogsCustom( }; return StartLogs(processInfo, settings, egressProvider); - }, processKey, ArtifactType_Logs); + }, processKey, Utilities.ArtifactType_Logs); } /// @@ -601,7 +590,7 @@ private Task StartTrace( TimeSpan duration, string egressProvider) { - string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.nettrace"); + string fileName = FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.nettrace"); Func action = async (outputStream, token) => { @@ -624,7 +613,7 @@ private Task StartTrace( }; return Result( - ArtifactType_Trace, + Utilities.ArtifactType_Trace, egressProvider, action, fileName, @@ -643,7 +632,7 @@ private Task StartLogs( return Task.FromResult(this.NotAcceptable()); } - string fileName = FormattableString.Invariant($"{GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.txt"); + string fileName = FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.txt"); string contentType = ContentTypes.TextEventStream; if (format == LogFormat.EventStream) @@ -672,7 +661,7 @@ private Task StartLogs( }; return Result( - ArtifactType_Logs, + Utilities.ArtifactType_Logs, egressProvider, action, fileName, @@ -693,11 +682,6 @@ private static TimeSpan ConvertSecondsToTimeSpan(int durationSeconds) return (pid == null && uid == null && name == null) ? null : new ProcessKey(pid, uid, name); } - private static string GetFileNameTimeStampUtcNow() - { - return DateTime.UtcNow.ToString("yyyyMMdd_HHmmss"); - } - private static LogFormat ComputeLogFormat(IList acceptedHeaders) { if (acceptedHeaders == null) @@ -741,9 +725,7 @@ private Task Result( IEndpointInfo endpointInfo, bool asAttachment = true) { - KeyValueLogScope scope = new KeyValueLogScope(); - scope.AddArtifactType(artifactType); - scope.AddEndpointInfo(endpointInfo); + KeyValueLogScope scope = Utilities.GetScope(artifactType, endpointInfo); if (string.IsNullOrEmpty(providerName)) { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs index 9b6cb8c8f5a..59c29016914 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/DiagnosticServices.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -29,15 +27,12 @@ internal sealed class DiagnosticServices : IDiagnosticServices private readonly IEndpointInfoSourceInternal _endpointInfoSource; private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); - private readonly IOptionsMonitor _storageOptions; private readonly IOptionsMonitor _defaultProcessOptions; public DiagnosticServices(IEndpointInfoSource endpointInfoSource, - IOptionsMonitor storageOptions, IOptionsMonitor defaultProcessMonitor) { _endpointInfoSource = (IEndpointInfoSourceInternal)endpointInfoSource; - _storageOptions = storageOptions; _defaultProcessOptions = defaultProcessMonitor; } @@ -81,48 +76,6 @@ public async Task> GetProcessesAsync(DiagProcessFilter return processes.ToArray(); } - public async Task GetDump(IProcessInfo pi, Models.DumpType mode, CancellationToken token) - { - string dumpFilePath = Path.Combine(_storageOptions.CurrentValue.DumpTempFolder, FormattableString.Invariant($"{Guid.NewGuid()}_{pi.EndpointInfo.ProcessId}")); - DumpType dumpType = MapDumpType(mode); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Get the process - Process process = Process.GetProcessById(pi.EndpointInfo.ProcessId); - await Dumper.CollectDumpAsync(process, dumpFilePath, dumpType); - } - else - { - var client = new DiagnosticsClient(pi.EndpointInfo.Endpoint); - await client.WriteDumpAsync(dumpType, dumpFilePath, logDumpGeneration: false, token); - } - - return new AutoDeleteFileStream(dumpFilePath); - } - - private static DumpType MapDumpType(Models.DumpType dumpType) - { - switch (dumpType) - { - case Models.DumpType.Full: - return DumpType.Full; - case Models.DumpType.WithHeap: - return DumpType.WithHeap; - case Models.DumpType.Triage: - return DumpType.Triage; - case Models.DumpType.Mini: - return DumpType.Normal; - default: - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - Strings.ErrorMessage_UnexpectedType, - nameof(DumpType), - dumpType), - nameof(dumpType)); - } - } public Task GetProcessAsync(ProcessKey? processKey, CancellationToken token) { @@ -179,7 +132,6 @@ public AutoDeleteFileStream(string path) : base(path, FileMode.Open, FileAccess. 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 diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs new file mode 100644 index 00000000000..a9e921c6962 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs @@ -0,0 +1,89 @@ +// 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.Options; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal class DumpService : IDumpService + { + private readonly IOptionsMonitor _storageOptions; + + public DumpService(IOptionsMonitor storageOptions) + { + _storageOptions = storageOptions ?? throw new ArgumentNullException(nameof(StorageOptions)); + } + + public async Task DumpAsync(IEndpointInfo endpointInfo, Models.DumpType mode, CancellationToken token) + { + if (endpointInfo == null) + { + throw new ArgumentNullException(nameof(endpointInfo)); + } + + string dumpFilePath = Path.Combine(_storageOptions.CurrentValue.DumpTempFolder, FormattableString.Invariant($"{Guid.NewGuid()}_{endpointInfo.ProcessId}")); + DumpType dumpType = MapDumpType(mode); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Get the process + Process process = Process.GetProcessById(endpointInfo.ProcessId); + await Dumper.CollectDumpAsync(process, dumpFilePath, dumpType); + } + else + { + var client = new DiagnosticsClient(endpointInfo.Endpoint); + await client.WriteDumpAsync(dumpType, dumpFilePath, logDumpGeneration: false, token); + } + + return new AutoDeleteFileStream(dumpFilePath); + } + + /// + /// 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 static DumpType MapDumpType(Models.DumpType dumpType) + { + switch (dumpType) + { + case Models.DumpType.Full: + return DumpType.Full; + case Models.DumpType.WithHeap: + return DumpType.WithHeap; + case Models.DumpType.Triage: + return DumpType.Triage; + case Models.DumpType.Mini: + return DumpType.Normal; + default: + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_UnexpectedType, + nameof(DumpType), + dumpType), + nameof(dumpType)); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/IDiagnosticServices.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IDiagnosticServices.cs index ccde596bd00..9f3b87e51db 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/IDiagnosticServices.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IDiagnosticServices.cs @@ -30,11 +30,8 @@ internal interface IDiagnosticServices : IDisposable /// situations allow a different process object. /// Task GetProcessAsync(ProcessKey? processKey, CancellationToken token); - - Task GetDump(IProcessInfo pi, Models.DumpType mode, CancellationToken token); } - internal interface IProcessInfo { IEndpointInfo EndpointInfo { get; } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/IDumpService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IDumpService.cs new file mode 100644 index 00000000000..c914b1c1690 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IDumpService.cs @@ -0,0 +1,15 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal interface IDumpService + { + Task DumpAsync(IEndpointInfo endpointInfo, Models.DumpType mode, CancellationToken token); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs index 34eb54f7c60..8b715c70dbe 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs @@ -28,11 +28,11 @@ public RequestLimitTracker(ILogger logger) { //CONSIDER Should we have configuration for these? - _requestLimitTable.Add(Controllers.DiagController.ArtifactType_Dump, 1); - _requestLimitTable.Add(Controllers.DiagController.ArtifactType_GCDump, 1); - _requestLimitTable.Add(Controllers.DiagController.ArtifactType_Logs, 3); - _requestLimitTable.Add(Controllers.DiagController.ArtifactType_Trace, 3); - _requestLimitTable.Add(Controllers.DiagController.ArtifactType_Metrics, 3); + _requestLimitTable.Add(Utilities.ArtifactType_Dump, 1); + _requestLimitTable.Add(Utilities.ArtifactType_GCDump, 1); + _requestLimitTable.Add(Utilities.ArtifactType_Logs, 3); + _requestLimitTable.Add(Utilities.ArtifactType_Trace, 3); + _requestLimitTable.Add(Utilities.ArtifactType_Metrics, 3); _logger = logger; } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities.cs new file mode 100644 index 00000000000..c24124060e9 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities.cs @@ -0,0 +1,39 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class Utilities + { + public const string ArtifactType_Dump = "dump"; + public const string ArtifactType_GCDump = "gcdump"; + public const string ArtifactType_Logs = "logs"; + public const string ArtifactType_Trace = "trace"; + public const string ArtifactType_Metrics = "collectmetrics"; + + public static string GenerateDumpFileName() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + FormattableString.Invariant($"dump_{GetFileNameTimeStampUtcNow()}.dmp") : + FormattableString.Invariant($"core_{GetFileNameTimeStampUtcNow()}"); + } + + public static string GetFileNameTimeStampUtcNow() + { + return DateTime.UtcNow.ToString("yyyyMMdd_HHmmss"); + } + + public static KeyValueLogScope GetScope(string artifactType, IEndpointInfo endpointInfo) + { + KeyValueLogScope scope = new KeyValueLogScope(); + scope.AddArtifactType(artifactType); + scope.AddEndpointInfo(endpointInfo); + + return scope; + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs index fd269a050a7..d2203a0dc36 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs @@ -22,5 +22,13 @@ public static class CommonTestTimeouts /// Default timeout for waiting for an executable to exit. /// public static readonly TimeSpan WaitForExit = TimeSpan.FromSeconds(15); + + /// + /// Default timeout for dump collection. + /// + /// + /// Dumps (especially full dumps) can be quite large and take a significant amount of time to transfer. + /// + public static readonly TimeSpan DumpTimeout = TimeSpan.FromMinutes(3); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DumpTestUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DumpTestUtilities.cs new file mode 100644 index 00000000000..345a09b1683 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DumpTestUtilities.cs @@ -0,0 +1,108 @@ +// 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.FileFormats; +using Microsoft.FileFormats.ELF; +using Microsoft.FileFormats.MachO; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + public static class DumpTestUtilities + { + public const string EnableElfDumpOnMacOS = "COMPlus_DbgEnableElfDumpOnMacOS"; + + public static async Task ValidateDump(bool expectElfDump, Stream dumpStream) + { + Assert.NotNull(dumpStream); + + byte[] headerBuffer = new byte[64]; + + // Read enough to deserialize the header. + int read; + int total = 0; + using CancellationTokenSource cancellation = new(CommonTestTimeouts.DumpTimeout); + while (total < headerBuffer.Length && 0 != (read = await dumpStream.ReadAsync(headerBuffer, total, headerBuffer.Length - total, cancellation.Token))) + { + total += read; + } + Assert.Equal(headerBuffer.Length, total); + + // Read header and validate + using MemoryStream headerStream = new(headerBuffer); + + StreamAddressSpace dumpAddressSpace = new(headerStream); + Reader dumpReader = new(dumpAddressSpace); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + MinidumpHeader header = dumpReader.Read(0); + // Validate Signature + Assert.True(header.IsSignatureValid.Check()); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + ELFHeaderIdent ident = dumpReader.Read(0); + Assert.True(ident.IsIdentMagicValid.Check()); + Assert.True(ident.IsClassValid.Check()); + Assert.True(ident.IsDataValid.Check()); + + LayoutManager layoutManager = new(); + layoutManager.AddELFTypes( + isBigEndian: ident.Data == ELFData.BigEndian, + is64Bit: ident.Class == ELFClass.Class64); + Reader headerReader = new(dumpAddressSpace, layoutManager); + + ELFHeader header = headerReader.Read(0); + // Validate Signature + Assert.True(header.IsIdentMagicValid.Check()); + // Validate ELF file is a core dump + Assert.Equal(ELFHeaderType.Core, header.Type); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + if (expectElfDump) + { + ELFHeader header = dumpReader.Read(0); + // Validate Signature + Assert.True(header.IsIdentMagicValid.Check()); + // Validate ELF file is a core dump + Assert.Equal(ELFHeaderType.Core, header.Type); + } + else + { + MachHeader header = dumpReader.Read(0); + // Validate Signature + Assert.True(header.IsMagicValid.Check()); + // Validate MachO file is a core dump + Assert.True(header.IsFileTypeValid.Check()); + Assert.Equal(MachHeaderFileType.Core, header.FileType); + } + } + else + { + throw new NotImplementedException("Dump header check not implemented for this OS platform."); + } + } + + public class MinidumpHeader : TStruct + { + public uint Signature = 0; + public uint Version = 0; + public uint NumberOfStreams = 0; + public uint StreamDirectoryRva = 0; + public uint CheckSum = 0; + public uint TimeDateStamp = 0; + public ulong Flags = 0; + + // 50,4D,44,4D = PMDM + public ValidationRule IsSignatureValid => new ValidationRule("Invalid Signature", () => Signature == 0x504D444DU); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj index 23db369d110..651cba748f1 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj @@ -11,8 +11,9 @@ - + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DumpTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DumpTests.cs index ed88aab6670..55bdfacb088 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DumpTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DumpTests.cs @@ -10,14 +10,8 @@ using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.DependencyInjection; -using Microsoft.FileFormats; -using Microsoft.FileFormats.ELF; -using Microsoft.FileFormats.MachO; -using System; -using System.IO; using System.Net.Http; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -27,8 +21,6 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests [Collection(DefaultCollectionFixture.Name)] public class DumpTests { - private const string EnableElfDumpOnMacOS = "COMPlus_DbgEnableElfDumpOnMacOS"; - private readonly IHttpClientFactory _httpClientFactory; private readonly ITestOutputHelper _outputHelper; @@ -66,73 +58,7 @@ public Task DumpTest(DiagnosticPortConnectionMode mode, DumpType type) using ResponseStreamHolder holder = await client.CaptureDumpAsync(processId, type); Assert.NotNull(holder); - byte[] headerBuffer = new byte[64]; - - // Read enough to deserialize the header. - int read; - int total = 0; - using CancellationTokenSource cancellation = new(TestTimeouts.DumpTimeout); - while (total < headerBuffer.Length && 0 != (read = await holder.Stream.ReadAsync(headerBuffer, total, headerBuffer.Length - total, cancellation.Token))) - { - total += read; - } - Assert.Equal(headerBuffer.Length, total); - - // Read header and validate - using MemoryStream headerStream = new(headerBuffer); - - StreamAddressSpace dumpAddressSpace = new(headerStream); - Reader dumpReader = new(dumpAddressSpace); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - MinidumpHeader header = dumpReader.Read(0); - // Validate Signature - Assert.True(header.IsSignatureValid.Check()); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - ELFHeaderIdent ident = dumpReader.Read(0); - Assert.True(ident.IsIdentMagicValid.Check()); - Assert.True(ident.IsClassValid.Check()); - Assert.True(ident.IsDataValid.Check()); - - LayoutManager layoutManager = new(); - layoutManager.AddELFTypes( - isBigEndian: ident.Data == ELFData.BigEndian, - is64Bit: ident.Class == ELFClass.Class64); - Reader headerReader = new(dumpAddressSpace, layoutManager); - - ELFHeader header = headerReader.Read(0); - // Validate Signature - Assert.True(header.IsIdentMagicValid.Check()); - // Validate ELF file is a core dump - Assert.Equal(ELFHeaderType.Core, header.Type); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - if (runner.Environment.ContainsKey(EnableElfDumpOnMacOS)) - { - ELFHeader header = dumpReader.Read(0); - // Validate Signature - Assert.True(header.IsIdentMagicValid.Check()); - // Validate ELF file is a core dump - Assert.Equal(ELFHeaderType.Core, header.Type); - } - else - { - MachHeader header = dumpReader.Read(0); - // Validate Signature - Assert.True(header.IsMagicValid.Check()); - // Validate MachO file is a core dump - Assert.True(header.IsFileTypeValid.Check()); - Assert.Equal(MachHeaderFileType.Core, header.FileType); - } - } - else - { - throw new NotImplementedException("Dump header check not implemented for this OS platform."); - } + await DumpTestUtilities.ValidateDump(runner.Environment.ContainsKey(DumpTestUtilities.EnableElfDumpOnMacOS), holder.Stream); await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }, @@ -141,23 +67,9 @@ public Task DumpTest(DiagnosticPortConnectionMode mode, DumpType type) // MachO not supported on .NET 5, only ELF: https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/xplat-minidump-generation.md#os-x if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && DotNetHost.RuntimeVersion.Major == 5) { - runner.Environment.Add(EnableElfDumpOnMacOS, "1"); + runner.Environment.Add(DumpTestUtilities.EnableElfDumpOnMacOS, "1"); } }); } - - private class MinidumpHeader : TStruct - { - public uint Signature = 0; - public uint Version = 0; - public uint NumberOfStreams = 0; - public uint StreamDirectoryRva = 0; - public uint CheckSum = 0; - public uint TimeDateStamp = 0; - public ulong Flags = 0; - - // 50,4D,44,4D = PMDM - public ValidationRule IsSignatureValid => new ValidationRule("Invalid Signature", () => Signature == 0x504D444DU); - } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs index ae1c3e25a1f..7e110cc5223 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.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.TestCommon; using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.Logging; @@ -206,7 +207,7 @@ public static async Task> GetProcessEnvironmentAsync( /// public static Task CaptureDumpAsync(this ApiClient client, int pid, DumpType dumpType) { - return client.CaptureDumpAsync(pid, dumpType, TestTimeouts.DumpTimeout); + return client.CaptureDumpAsync(pid, dumpType, CommonTestTimeouts.DumpTimeout); } /// diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs index 5bc8d897a0c..8d38863a348 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestTimeouts.cs @@ -18,14 +18,6 @@ internal static class TestTimeouts /// public static readonly TimeSpan LogsDuration = TimeSpan.FromSeconds(10); - /// - /// Default timeout for dump collection. - /// - /// - /// Dumps (especially full dumps) can be quite large and take a significant amount of time to transfer. - /// - public static readonly TimeSpan DumpTimeout = TimeSpan.FromMinutes(3); - /// /// Timeout for polling a long running operation to completion. /// This may need to be adjusted for individual calls that are longer than 30 seconds. diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs index 0726634714e..4b783675940 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs @@ -19,7 +19,7 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests public sealed class ActionListExecutorTests { private const int TokenTimeoutMs = 10000; - + private readonly ITestOutputHelper _outputHelper; private const string DefaultRuleName = "Default"; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectDumpActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectDumpActionTests.cs new file mode 100644 index 00000000000..22679076298 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectDumpActionTests.cs @@ -0,0 +1,109 @@ +// 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.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + public sealed class CollectDumpActionTests + { + private const string TempEgressDirectory = "/tmp"; + private const string ExpectedEgressProvider = "TmpEgressProvider"; + private const string DefaultRuleName = "Default"; + + private ITestOutputHelper _outputHelper; + private readonly EndpointUtilities _endpointUtilities; + + public CollectDumpActionTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _endpointUtilities = new(_outputHelper); + } + + [Theory] + [InlineData(DumpType.Full)] + [InlineData(DumpType.Mini)] + [InlineData(DumpType.Triage)] + [InlineData(DumpType.WithHeap)] + [InlineData(null)] + public async Task CollectDumpAction_Success(DumpType? dumpType) + { + DirectoryInfo uniqueEgressDirectory = null; + + try + { + uniqueEgressDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), TempEgressDirectory, Guid.NewGuid().ToString())); + + await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => + { + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, uniqueEgressDirectory.FullName); + + rootOptions.CreateCollectionRule(DefaultRuleName) + .AddCollectDumpAction(ExpectedEgressProvider, dumpType) + .SetStartupTrigger(); + }, async host => + { + IOptionsMonitor ruleOptionsMonitor = host.Services.GetService>(); + CollectDumpOptions options = (CollectDumpOptions)ruleOptionsMonitor.Get(DefaultRuleName).Actions[0].Settings; + + ICollectionRuleActionProxy action; + Assert.True(host.Services.GetService().TryCreateAction(KnownCollectionRuleActions.CollectDump, out action)); + + EndpointInfoSourceCallback callback = new(_outputHelper); + await using var source = _endpointUtilities.CreateServerSource(out string transportName, callback); + source.Start(); + + AppRunner runner = _endpointUtilities.CreateAppRunner(transportName, TargetFrameworkMoniker.Net60); // Arbitrarily chose Net60; should we test against other frameworks? + + Task newEndpointInfoTask = callback.WaitForNewEndpointInfoAsync(runner, CommonTestTimeouts.StartProcess); + + await runner.ExecuteAsync(async () => + { + IEndpointInfo endpointInfo = await newEndpointInfoTask; + + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(CommonTestTimeouts.DumpTimeout); + CollectionRuleActionResult result = await action.ExecuteAsync(options, endpointInfo, cancellationTokenSource.Token); + + Assert.NotNull(result.OutputValues); + Assert.True(result.OutputValues.TryGetValue(CollectDumpAction.EgressPathOutputValueName, out string egressPath)); + Assert.True(File.Exists(egressPath)); + + using FileStream dumpStream = new(egressPath, FileMode.Open, FileAccess.Read); + Assert.NotNull(dumpStream); + + await DumpTestUtilities.ValidateDump(runner.Environment.ContainsKey(DumpTestUtilities.EnableElfDumpOnMacOS), dumpStream); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }); + }); + } + finally + { + try + { + uniqueEgressDirectory?.Delete(recursive: true); + } + catch + { + } + } + } + } +} \ No newline at end of file diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index c66ff038820..823e3f0e846 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -261,7 +261,7 @@ public Task CollectionRuleOptions_CollectDumpAction_RoundTrip() { rootOptions.CreateCollectionRule(DefaultRuleName) .SetStartupTrigger() - .AddCollectDumpAction(ExpectedDumpType, ExpectedEgressProvider); + .AddCollectDumpAction(ExpectedEgressProvider, ExpectedDumpType); rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); }, ruleOptions => @@ -278,7 +278,7 @@ public Task CollectionRuleOptions_CollectDumpAction_PropertyValidation() { rootOptions.CreateCollectionRule(DefaultRuleName) .SetStartupTrigger() - .AddCollectDumpAction((DumpType)20, UnknownEgressName); + .AddCollectDumpAction(UnknownEgressName, (DumpType)20); }, ex => { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs index d716f57ee1f..ca01bef905e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfoSourceTests.cs @@ -5,11 +5,9 @@ using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Diagnostics.Tools.Monitor; using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -21,13 +19,13 @@ public class EndpointInfoSourceTests { private static readonly TimeSpan DefaultNegativeVerificationTimeout = TimeSpan.FromSeconds(2); - private static readonly TimeSpan GetEndpointInfoTimeout = TimeSpan.FromSeconds(10); - private readonly ITestOutputHelper _outputHelper; + private readonly EndpointUtilities _endpointUtilities; public EndpointInfoSourceTests(ITestOutputHelper outputHelper) { _outputHelper = outputHelper; + _endpointUtilities = new EndpointUtilities(_outputHelper); } /// @@ -37,7 +35,7 @@ public EndpointInfoSourceTests(ITestOutputHelper outputHelper) [Fact] public async Task ServerSourceNoStartTest() { - await using var source = CreateServerSource(out string transportName); + await using var source = _endpointUtilities.CreateServerSource(out string transportName); // Intentionally do not call Start using CancellationTokenSource cancellation = new(DefaultNegativeVerificationTimeout); @@ -52,10 +50,10 @@ await Assert.ThrowsAsync( [Fact] public async Task ServerSourceNoConnectionsTest() { - await using var source = CreateServerSource(out _); + await using var source = _endpointUtilities.CreateServerSource(out _); source.Start(); - var endpointInfos = await GetEndpointInfoAsync(source); + var endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); } @@ -66,7 +64,7 @@ public async Task ServerSourceNoConnectionsTest() [Fact] public async Task ServerSourceThrowsWhenDisposedTest() { - var source = CreateServerSource(out _); + var source = _endpointUtilities.CreateServerSource(out _); source.Start(); await source.DisposeAsync(); @@ -91,7 +89,7 @@ await Assert.ThrowsAsync( [Fact] public async Task ServerSourceThrowsWhenMultipleStartTest() { - await using var source = CreateServerSource(out _); + await using var source = _endpointUtilities.CreateServerSource(out _); source.Start(); Assert.Throws( @@ -110,13 +108,13 @@ public async Task ServerSourceThrowsWhenMultipleStartTest() public async Task ServerSourceAddRemoveSingleConnectionTest(TargetFrameworkMoniker appTfm) { EndpointInfoSourceCallback callback = new(_outputHelper); - await using var source = CreateServerSource(out string transportName, callback); + await using var source = _endpointUtilities.CreateServerSource(out string transportName, callback); source.Start(); - var endpointInfos = await GetEndpointInfoAsync(source); + var endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); - AppRunner runner = CreateAppRunner(transportName, appTfm); + AppRunner runner = _endpointUtilities.CreateAppRunner(transportName, appTfm); Task newEndpointInfoTask = callback.WaitForNewEndpointInfoAsync(runner, CommonTestTimeouts.StartProcess); @@ -124,20 +122,20 @@ await runner.ExecuteAsync(async () => { await newEndpointInfoTask; - endpointInfos = await GetEndpointInfoAsync(source); + endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); var endpointInfo = Assert.Single(endpointInfos); Assert.NotNull(endpointInfo.CommandLine); Assert.NotNull(endpointInfo.OperatingSystem); Assert.NotNull(endpointInfo.ProcessArchitecture); - await VerifyConnectionAsync(runner, endpointInfo); + await EndpointUtilities.VerifyConnectionAsync(runner, endpointInfo); await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }); await Task.Delay(TimeSpan.FromSeconds(1)); - endpointInfos = await GetEndpointInfoAsync(source); + endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); } @@ -151,10 +149,10 @@ await runner.ExecuteAsync(async () => public async Task ServerSourceAddRemoveMultipleConnectionTest(TargetFrameworkMoniker appTfm) { EndpointInfoSourceCallback callback = new(_outputHelper); - await using var source = CreateServerSource(out string transportName, callback); + await using var source = _endpointUtilities.CreateServerSource(out string transportName, callback); source.Start(); - var endpointInfos = await GetEndpointInfoAsync(source); + var endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); const int appCount = 5; @@ -164,7 +162,7 @@ public async Task ServerSourceAddRemoveMultipleConnectionTest(TargetFrameworkMon // Start all app instances for (int i = 0; i < appCount; i++) { - runners[i] = CreateAppRunner(transportName, appTfm, appId: i + 1); + runners[i] = _endpointUtilities.CreateAppRunner(transportName, appTfm, appId: i + 1); newEndpointInfoTasks[i] = callback.WaitForNewEndpointInfoAsync(runners[i], CommonTestTimeouts.StartProcess); } @@ -174,7 +172,7 @@ await runners.ExecuteAsync(async () => await Task.WhenAll(newEndpointInfoTasks); _outputHelper.WriteLine("Received all new endpoint info notifications."); - endpointInfos = await GetEndpointInfoAsync(source); + endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); Assert.Equal(appCount, endpointInfos.Count()); @@ -188,7 +186,7 @@ await runners.ExecuteAsync(async () => Assert.NotNull(endpointInfo.OperatingSystem); Assert.NotNull(endpointInfo.ProcessArchitecture); - await VerifyConnectionAsync(runners[i], endpointInfo); + await EndpointUtilities.VerifyConnectionAsync(runners[i], endpointInfo); await runners[i].SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); } @@ -201,7 +199,7 @@ await runners.ExecuteAsync(async () => await Task.Delay(TimeSpan.FromSeconds(1)); - endpointInfos = await GetEndpointInfoAsync(source); + endpointInfos = await _endpointUtilities.GetEndpointInfoAsync(source); Assert.Empty(endpointInfos); } @@ -211,46 +209,5 @@ public static IEnumerable GetTfmsSupportingPortListener() yield return new object[] { TargetFrameworkMoniker.Net50 }; yield return new object[] { TargetFrameworkMoniker.Net60 }; } - - private ServerEndpointInfoSource CreateServerSource(out string transportName, IEndpointInfoSourceCallbacks callback = null) - { - DiagnosticPortHelper.Generate(DiagnosticPortConnectionMode.Listen, out _, out transportName); - _outputHelper.WriteLine("Starting server endpoint info source at '" + transportName + "'."); - - List callbacks = new(); - if (null != callback) - { - callbacks.Add(callback); - } - return new ServerEndpointInfoSource(transportName, callbacks); - } - - private AppRunner CreateAppRunner(string transportName, TargetFrameworkMoniker tfm, int appId = 1) - { - AppRunner appRunner = new(_outputHelper, Assembly.GetExecutingAssembly(), appId, tfm); - appRunner.ConnectionMode = DiagnosticPortConnectionMode.Connect; - appRunner.DiagnosticPortPath = transportName; - appRunner.ScenarioName = TestAppScenarios.AsyncWait.Name; - return appRunner; - } - - private async Task> GetEndpointInfoAsync(ServerEndpointInfoSource source) - { - _outputHelper.WriteLine("Getting endpoint infos."); - using CancellationTokenSource cancellationSource = new(GetEndpointInfoTimeout); - return await source.GetEndpointInfoAsync(cancellationSource.Token); - } - - /// - /// Verifies basic information on the connection and that it matches the target process from the runner. - /// - private static async Task VerifyConnectionAsync(AppRunner runner, IEndpointInfo endpointInfo) - { - Assert.NotNull(runner); - Assert.NotNull(endpointInfo); - Assert.Equal(await runner.ProcessIdTask, endpointInfo.ProcessId); - Assert.NotEqual(Guid.Empty, endpointInfo.RuntimeInstanceCookie); - Assert.NotNull(endpointInfo.Endpoint); - } } -} +} \ No newline at end of file diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointUtilities.cs new file mode 100644 index 00000000000..6ee2a8a184c --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointUtilities.cs @@ -0,0 +1,71 @@ +// 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.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + internal class EndpointUtilities + { + private readonly ITestOutputHelper _outputHelper; + + private static readonly TimeSpan GetEndpointInfoTimeout = TimeSpan.FromSeconds(10); + + public EndpointUtilities(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + public ServerEndpointInfoSource CreateServerSource(out string transportName, EndpointInfoSourceCallback callback = null) + { + DiagnosticPortHelper.Generate(DiagnosticPortConnectionMode.Listen, out _, out transportName); + _outputHelper.WriteLine("Starting server endpoint info source at '" + transportName + "'."); + + List callbacks = new(); + if (null != callback) + { + callbacks.Add(callback); + } + return new ServerEndpointInfoSource(transportName, callbacks); + } + + public AppRunner CreateAppRunner(string transportName, TargetFrameworkMoniker tfm, int appId = 1) + { + AppRunner appRunner = new(_outputHelper, Assembly.GetExecutingAssembly(), appId, tfm); + appRunner.ConnectionMode = DiagnosticPortConnectionMode.Connect; + appRunner.DiagnosticPortPath = transportName; + appRunner.ScenarioName = TestAppScenarios.AsyncWait.Name; + return appRunner; + } + + public async Task> GetEndpointInfoAsync(ServerEndpointInfoSource source) + { + _outputHelper.WriteLine("Getting endpoint infos."); + using CancellationTokenSource cancellationSource = new(GetEndpointInfoTimeout); + return await source.GetEndpointInfoAsync(cancellationSource.Token); + } + + /// + /// Verifies basic information on the connection and that it matches the target process from the runner. + /// + public static async Task VerifyConnectionAsync(AppRunner runner, IEndpointInfo endpointInfo) + { + Assert.NotNull(runner); + Assert.NotNull(endpointInfo); + Assert.Equal(await runner.ProcessIdTask, endpointInfo.ProcessId); + Assert.NotEqual(Guid.Empty, endpointInfo.RuntimeInstanceCookie); + Assert.NotNull(endpointInfo.Endpoint); + } + } +} 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 f708d394b03..f4f2eb32023 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -33,7 +33,7 @@ public static CollectionRuleOptions AddAction(this CollectionRuleOptions options return options; } - public static CollectionRuleOptions AddCollectDumpAction(this CollectionRuleOptions options, DumpType type, string egress) + public static CollectionRuleOptions AddCollectDumpAction(this CollectionRuleOptions options, string egress, DumpType? type = null) { options.AddAction(KnownCollectionRuleActions.CollectDump, out CollectionRuleActionOptions actionOptions); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs index 04eb714ff7a..5f0323a9770 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.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.Tools.Monitor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -71,11 +72,16 @@ public static IHost CreateHost( outputHelper.WriteLine("End Configuration"); builder.AddInMemoryCollection(configurationValues); + + builder.ConfigureStorageDefaults(); }) - .ConfigureServices(services => + .ConfigureServices((HostBuilderContext context, IServiceCollection services) => { services.ConfigureCollectionRules(); services.ConfigureEgress(); + + services.AddSingleton(); + services.ConfigureStorage(context.Configuration); servicesCallback?.Invoke(services); }) .Build(); diff --git a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectDumpAction.cs b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectDumpAction.cs index 5b9074e81e5..7ad29571431 100644 --- a/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectDumpAction.cs +++ b/src/Tools/dotnet-monitor/CollectionRules/Actions/CollectDumpAction.cs @@ -3,19 +3,82 @@ // 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.Exceptions; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; +using Utils = Microsoft.Diagnostics.Monitoring.WebApi.Utilities; namespace Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions { - internal sealed class CollectDumpAction : - ICollectionRuleAction + internal sealed class CollectDumpAction : ICollectionRuleAction { - public Task ExecuteAsync(CollectDumpOptions options, IEndpointInfo endpointInfo, CancellationToken token) + private readonly IDumpService _dumpService; + private readonly IServiceProvider _serviceProvider; + + internal const string EgressPathOutputValueName = "EgressPath"; + + public CollectDumpAction(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _dumpService = serviceProvider.GetRequiredService(); + } + + public async Task ExecuteAsync(CollectDumpOptions options, IEndpointInfo endpointInfo, CancellationToken token) { - throw new NotImplementedException("TODO: Implement action"); + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (endpointInfo == null) + { + throw new ArgumentNullException(nameof(endpointInfo)); + } + + DumpType dumpType = options.Type.GetValueOrDefault(CollectDumpOptionsDefaults.Type); + string egressProvider = options.Egress; + + string dumpFileName = Utils.GenerateDumpFileName(); + + string dumpFilePath = string.Empty; + + ValidationContext context = new(options, _serviceProvider, items: null); + Validator.ValidateObject(options, context, validateAllProperties: true); + + KeyValueLogScope scope = Utils.GetScope(Utils.ArtifactType_Dump, endpointInfo); + + try + { + EgressOperation egressOperation = new EgressOperation( + token => _dumpService.DumpAsync(endpointInfo, dumpType, token), + egressProvider, + dumpFileName, + endpointInfo, + ContentTypes.ApplicationOctetStream, + scope); + + ExecutionResult result = await egressOperation.ExecuteAsync(_serviceProvider, token); + + dumpFilePath = result.Result.Value; + } + catch (Exception ex) + { + throw new CollectionRuleActionException(ex); + } + + return new CollectionRuleActionResult() + { + OutputValues = new Dictionary(StringComparer.Ordinal) + { + { EgressPathOutputValueName, dumpFilePath } + } + }; } } } diff --git a/src/Tools/dotnet-monitor/ConfigurationBuilderExtensions.cs b/src/Tools/dotnet-monitor/ConfigurationBuilderExtensions.cs index 5aedc801952..0e390224ea5 100644 --- a/src/Tools/dotnet-monitor/ConfigurationBuilderExtensions.cs +++ b/src/Tools/dotnet-monitor/ConfigurationBuilderExtensions.cs @@ -2,11 +2,13 @@ // 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.Extensions.Configuration; using Microsoft.Extensions.Configuration.KeyPerFile; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; @@ -31,6 +33,14 @@ public static IConfigurationBuilder AddKeyPerFileWithChangeTokenSupport(this ICo }); } + public static IConfigurationBuilder ConfigureStorageDefaults(this IConfigurationBuilder builder) + { + return builder.AddInMemoryCollection(new Dictionary + { + {ConfigurationPath.Combine(ConfigurationKeys.Storage, nameof(StorageOptions.DumpTempFolder)), StorageOptionsDefaults.DumpTempFolder } + }); + } + private class KeyPerFileConfigurationSourceWithChangeTokenSupport : KeyPerFileConfigurationSource, IConfigurationSource diff --git a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs index 303deff9402..6871a764c49 100644 --- a/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs +++ b/src/Tools/dotnet-monitor/DiagnosticsMonitorCommandHandler.cs @@ -141,7 +141,7 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st //Note these are in precedence order. ConfigureEndpointInfoSource(builder, diagnosticPort); ConfigureMetricsEndpoint(builder, metrics, metricUrls); - ConfigureStorageDefaults(builder); + builder.ConfigureStorageDefaults(); builder.AddCommandLine(new[] { "--urls", ConfigurationHelper.JoinValue(urls) }); @@ -239,6 +239,7 @@ public static IHostBuilder CreateHostBuilder(IConsole console, string[] urls, st services.AddSingleton(); services.AddHostedService(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.ConfigureOperationStore(); services.ConfigureEgress(); @@ -306,14 +307,6 @@ private static void ConfigureTempApiHashKey(IConfigurationBuilder builder, AuthC } } - private static void ConfigureStorageDefaults(IConfigurationBuilder builder) - { - builder.AddInMemoryCollection(new Dictionary - { - {ConfigurationPath.Combine(ConfigurationKeys.Storage, nameof(StorageOptions.DumpTempFolder)), StorageOptionsDefaults.DumpTempFolder } - }); - } - private static void ConfigureMetricsEndpoint(IConfigurationBuilder builder, bool enableMetrics, string[] metricEndpoints) { builder.AddInMemoryCollection(new Dictionary