From 7fd17002c1ea77e060a0f902f5c0494dd7676894 Mon Sep 17 00:00:00 2001 From: George <1641829+finsharp@users.noreply.github.com> Date: Tue, 30 Mar 2021 18:10:14 +0300 Subject: [PATCH 1/4] Fix exception thrown when Debug is called for Service modules. Show full request body and response when Debug is called. --- tools/Custom/HttpMessageFormatter.cs | 5 +---- tools/Custom/Module.cs | 18 +++++++----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/tools/Custom/HttpMessageFormatter.cs b/tools/Custom/HttpMessageFormatter.cs index e749d1a41aa..15b3d5f1182 100644 --- a/tools/Custom/HttpMessageFormatter.cs +++ b/tools/Custom/HttpMessageFormatter.cs @@ -202,10 +202,7 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon { Stream readStream = await _streamTask.Value; ValidateStreamForReading(readStream); - if (!_contentConsumed) - { - await Content.CopyToAsync(stream); - } + await Content.CopyToAsync(stream); } } diff --git a/tools/Custom/Module.cs b/tools/Custom/Module.cs index 1cf6dcb3091..7d7cc2058fb 100644 --- a/tools/Custom/Module.cs +++ b/tools/Custom/Module.cs @@ -40,7 +40,7 @@ partial void CustomInit() { this.EventListener = EventHandler; } - + /// /// Common Module Event Listener, allows to handle emitted by CmdLets /// @@ -88,11 +88,9 @@ private async Task Finally(string id, CancellationToken cancellationToken, Func< using (Extensions.NoSynchronizationContext) { var eventData = EventDataConverter.ConvertFrom(getEventData()); - using (var responseFormatter = new HttpMessageFormatter(eventData.ResponseMessage as HttpResponseMessage)) - { - var responseString = await responseFormatter.ReadAsStringAsync(); - await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(responseString)); - } + var responseFormatter = new HttpMessageFormatter(eventData.ResponseMessage as HttpResponseMessage); + var responseString = await responseFormatter.ReadAsStringAsync(); + await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(responseString)); } } @@ -111,11 +109,9 @@ private async Task BeforeCall(string id, CancellationToken cancellationToken, Fu using (Extensions.NoSynchronizationContext) { var eventData = EventDataConverter.ConvertFrom(getEventData()); - using (var requestFormatter = new HttpMessageFormatter(eventData.RequestMessage as HttpRequestMessage)) - { - var requestString = await requestFormatter.ReadAsStringAsync(); - await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(requestString)); - } + var requestFormatter = new HttpMessageFormatter(eventData.RequestMessage as HttpRequestMessage); + var requestString = await requestFormatter.ReadAsStringAsync(); + await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(requestString)); } } } From 68052e9e73aca7def9ecd208ca9dc70eceb2ab6f Mon Sep 17 00:00:00 2001 From: George Date: Tue, 6 Apr 2021 05:50:49 +0300 Subject: [PATCH 2/4] Handle empty content for HTTP requests that don't return body data. --- tools/Custom/HttpMessageFormatter.cs | 64 ++++++++++++++++++++++++++-- tools/Custom/Module.cs | 13 +++--- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/tools/Custom/HttpMessageFormatter.cs b/tools/Custom/HttpMessageFormatter.cs index 15b3d5f1182..f0db0a7ffd4 100644 --- a/tools/Custom/HttpMessageFormatter.cs +++ b/tools/Custom/HttpMessageFormatter.cs @@ -33,6 +33,9 @@ internal class HttpMessageFormatter : HttpContent private const string DefaultRequestMsgType = "request"; private const string DefaultResponseMsgType = "response"; + private const string DefaultRequestMediaType = DefaultMediaType + "; " + MsgTypeParameter + "=" + DefaultRequestMsgType; + private const string DefaultResponseMediaType = DefaultMediaType + "; " + MsgTypeParameter + "=" + DefaultResponseMsgType; + // Set of header fields that only support single values such as Set-Cookie. private static readonly HashSet _singleValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -101,7 +104,7 @@ private HttpContent Content private void InitializeStreamTask() { - _streamTask = new Lazy>(() => Content?.ReadAsStreamAsync()); + _streamTask = new Lazy>(() => Content == null ? null : Content.ReadAsStreamAsync()); } /// @@ -198,7 +201,7 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon byte[] header = SerializeHeader(); await stream.WriteAsync(header, 0, header.Length); - if (Content != null) + if (Content != null && Content.Headers.ContentLength > 0) { Stream readStream = await _streamTask.Value; ValidateStreamForReading(readStream); @@ -227,6 +230,19 @@ protected override bool TryComputeLength(out long length) length = 0; // Cases #1, #2, #3 + if (hasContent) + { + Stream readStream; + if (!_streamTask.Value.TryGetResult(out readStream) // Case #1 + || readStream == null || !readStream.CanSeek) // Case #2 + { + length = -1; + return false; + } + + length = readStream.Length; // Case #3 + } + // We serialize header to a StringBuilder so that we can determine the length // following the pattern for HttpContent to try and determine the message length. // The perf overhead is no larger than for the other HttpContent implementations. @@ -235,6 +251,30 @@ protected override bool TryComputeLength(out long length) return true; } + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (HttpRequestMessage != null) + { + HttpRequestMessage.Dispose(); + HttpRequestMessage = null; + } + + if (HttpResponseMessage != null) + { + HttpResponseMessage.Dispose(); + HttpResponseMessage = null; + } + } + + base.Dispose(disposing); + } + /// /// Serializes the HTTP request line. /// @@ -307,8 +347,8 @@ private static void SerializeHeaderFields(StringBuilder message, HttpHeaders hea private byte[] SerializeHeader() { StringBuilder message = new StringBuilder(DefaultHeaderAllocation); - HttpHeaders headers; - HttpContent content; + HttpHeaders headers = null; + HttpContent content = null; if (HttpRequestMessage != null) { SerializeRequestLine(message, HttpRequestMessage); @@ -351,5 +391,21 @@ private void ValidateStreamForReading(Stream stream) _contentConsumed = true; } + + } + + public static class TaskExtensions + { + public static bool TryGetResult(this Task task, out TResult result) + { + if (task.Status == TaskStatus.RanToCompletion) + { + result = task.Result; + return true; + } + + result = default(TResult); + return false; + } } } \ No newline at end of file diff --git a/tools/Custom/Module.cs b/tools/Custom/Module.cs index 7d7cc2058fb..40adec9c385 100644 --- a/tools/Custom/Module.cs +++ b/tools/Custom/Module.cs @@ -52,7 +52,7 @@ partial void CustomInit() /// The cmdlet's parameterset name /// the exception that is being thrown (if available) /// - /// A that will be complete when handling of the event is completed. + /// A that will be complete when handling of the event is completed. /// public async Task EventHandler(string id, CancellationToken cancellationToken, Func getEventData, Func, Task> signal, InvocationInfo invocationInfo, string parameterSetName, System.Exception exception) { @@ -81,13 +81,13 @@ public async Task EventHandler(string id, CancellationToken cancellationToken, F /// A delegate to get the detailed event data /// The callback for the event dispatcher /// - /// A that will be complete when handling of the event is completed. + /// A that will be complete when handling of the event is completed. /// private async Task Finally(string id, CancellationToken cancellationToken, Func getEventData, Func, Task> signal) { using (Extensions.NoSynchronizationContext) { - var eventData = EventDataConverter.ConvertFrom(getEventData()); + var eventData = EventDataConverter.ConvertFrom(getEventData()); var responseFormatter = new HttpMessageFormatter(eventData.ResponseMessage as HttpResponseMessage); var responseString = await responseFormatter.ReadAsStringAsync(); await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(responseString)); @@ -102,17 +102,18 @@ private async Task Finally(string id, CancellationToken cancellationToken, Func< /// A delegate to get the detailed event data /// The callback for the event dispatcher /// - /// A that will be complete when handling of the event is completed. + /// A that will be complete when handling of the event is completed. /// private async Task BeforeCall(string id, CancellationToken cancellationToken, Func getEventData, Func, Task> signal) { using (Extensions.NoSynchronizationContext) { - var eventData = EventDataConverter.ConvertFrom(getEventData()); + var eventData = EventDataConverter.ConvertFrom(getEventData()); var requestFormatter = new HttpMessageFormatter(eventData.RequestMessage as HttpRequestMessage); var requestString = await requestFormatter.ReadAsStringAsync(); await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(requestString)); } + } } -} +} \ No newline at end of file From e6145f90ccb199538376f2cfa7b37d38eb2a36d9 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 6 Apr 2021 13:39:51 +0300 Subject: [PATCH 3/4] Add stream tests. --- tools/Tests/DebugTests.ps1 | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tools/Tests/DebugTests.ps1 diff --git a/tools/Tests/DebugTests.ps1 b/tools/Tests/DebugTests.ps1 new file mode 100644 index 00000000000..511c4da1c1c --- /dev/null +++ b/tools/Tests/DebugTests.ps1 @@ -0,0 +1,23 @@ +BeforeAll { + $ModulePrefix = "Microsoft.Graph" + $AuthModuleName = "Authentication" + $AuthModulePath = Join-Path $PSScriptRoot "..\..\src\$AuthModuleName\$AuthModuleName\artifacts\$ModulePrefix.$AuthModuleName.psd1" + $TestModuleName = "DirectoryObjects" + $TestModulePath = Join-Path $PSScriptRoot "..\..\src\$TestModuleName\$TestModuleName\$ModulePrefix.$TestModuleName.psd1" + Import-Module $AuthModulePath -Force + Import-Module $TestModulePath -Force + + Connect-MgGraph + Select-MgProfile beta +} +Describe 'Cmdlets Streams' { + It 'Should Not Throw Exception when Debug Preference is Set'{ + $ps = [powershell]::Create() +$ps.AddScript(@' + $DebugPreference = 'Continue' + Test-MgDirectoryObjectProperty -DisplayName "New Name" -EntityType "Group" +'@).Invoke() + $ps.Streams.Debug | Should -notLike -BeLike "*Exception*" + $ps.Streams.Debug -like "*HTTP/1.1 200 OK*" + } + } \ No newline at end of file From b4c95708b31c4eb41499824ea4068d659ca19bd0 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 13 Apr 2021 15:41:15 +0300 Subject: [PATCH 4/4] Avoid checking for -Debug to respect DebugPreferences. --- tools/Custom/Module.cs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tools/Custom/Module.cs b/tools/Custom/Module.cs index 40adec9c385..ff67227ba0c 100644 --- a/tools/Custom/Module.cs +++ b/tools/Custom/Module.cs @@ -56,20 +56,17 @@ partial void CustomInit() /// public async Task EventHandler(string id, CancellationToken cancellationToken, Func getEventData, Func, Task> signal, InvocationInfo invocationInfo, string parameterSetName, System.Exception exception) { - if (invocationInfo.BoundParameters.ContainsKey("Debug")) + switch (id) { - switch (id) - { - case Events.BeforeCall: - await BeforeCall(id, cancellationToken, getEventData, signal); - break; - case Events.Finally: - await Finally(id, cancellationToken, getEventData, signal); - break; - default: - getEventData.Print(signal, cancellationToken, Events.Information, id); - break; - } + case Events.BeforeCall: + await BeforeCall(id, cancellationToken, getEventData, signal); + break; + case Events.Finally: + await Finally(id, cancellationToken, getEventData, signal); + break; + default: + getEventData.Print(signal, cancellationToken, Events.Information, id); + break; } } @@ -87,7 +84,7 @@ private async Task Finally(string id, CancellationToken cancellationToken, Func< { using (Extensions.NoSynchronizationContext) { - var eventData = EventDataConverter.ConvertFrom(getEventData()); + var eventData = EventDataConverter.ConvertFrom(getEventData()); var responseFormatter = new HttpMessageFormatter(eventData.ResponseMessage as HttpResponseMessage); var responseString = await responseFormatter.ReadAsStringAsync(); await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(responseString)); @@ -108,7 +105,7 @@ private async Task BeforeCall(string id, CancellationToken cancellationToken, Fu { using (Extensions.NoSynchronizationContext) { - var eventData = EventDataConverter.ConvertFrom(getEventData()); + var eventData = EventDataConverter.ConvertFrom(getEventData()); var requestFormatter = new HttpMessageFormatter(eventData.RequestMessage as HttpRequestMessage); var requestString = await requestFormatter.ReadAsStringAsync(); await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(requestString));