From 2df4bb96b971bbafde8e1fc21e108c16e2d3eee8 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 30 May 2022 15:25:44 -0700 Subject: [PATCH 01/77] Read exception being received but not thrown --- .../MultiplexingStream.Channel.cs | 45 ++++++++- .../MultiplexingStream.ControlCode.cs | 6 ++ src/Nerdbank.Streams/MultiplexingStream.cs | 94 ++++++++++++++++++- .../MultiplexingStreamTests.cs | 22 +++++ 4 files changed, 162 insertions(+), 5 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 099e92e3..1654922b 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -96,6 +96,11 @@ public class Channel : IDisposableObservable, IDuplexPipe /// private bool isDisposed; + /// + /// Indicates whether we closed the writing channel due to an exception. + /// + private bool receivedContentWriteError; + /// /// The to use to get data to be transmitted over the . /// @@ -449,8 +454,29 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence /// Called by the when when it will not be writing any more data to the channel. /// - internal void OnContentWritingCompleted() + /// If we are closing the writing channel due to us receiving an error, defaults to null. + internal void OnContentWritingCompleted(Exception? error = null) { + if (this.receivedContentWriteError) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Exiting from writing completed since it was already called"); + } + + // We received a content write error so we have already closed the channel + return; + } + + if (error != null) + { + this.receivedContentWriteError = true; + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Exception {0} passed into writing complete", error); + } + } + this.DisposeSelfOnFailure(Task.Run(async delegate { if (!this.IsDisposed) @@ -458,13 +484,13 @@ internal void OnContentWritingCompleted() try { PipeWriter? writer = this.GetReceivedMessagePipeWriter(); - await writer.CompleteAsync().ConfigureAwait(false); + await writer.CompleteAsync(error).ConfigureAwait(false); } catch (ObjectDisposedException) { if (this.mxStreamIOWriter != null) { - await this.mxStreamIOWriter.CompleteAsync().ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); } } } @@ -472,7 +498,7 @@ internal void OnContentWritingCompleted() { if (this.mxStreamIOWriter != null) { - await this.mxStreamIOWriter.CompleteAsync().ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); } } @@ -711,6 +737,11 @@ private async Task ProcessOutboundTransmissionsAsync() break; } + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.FrameReceived, "Received buffer of length {0} inside process outbound", result.Buffer.Length); + } + if (result.IsCanceled) { // We've been asked to cancel. Presumably the channel has been disposed. @@ -785,6 +816,12 @@ private async Task ProcessOutboundTransmissionsAsync() catch (Exception ex) { await this.mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Caught exception when processing outbound data"); + } + + this.MultiplexingStream.OnChannelWritingError(this, ex); throw; } finally diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index 91a9b364..fb4bbdfa 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -44,6 +44,12 @@ internal enum ControlCode : byte /// allowing them to send more data. /// ContentProcessed, + + /// + /// Sent when we encounter error writing data on a given channel and is sent before a + /// to indicate the reason for the content writing closure. + /// + ContentWritingError, } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 00e4769f..e0a8bb0f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -187,6 +187,7 @@ private enum TraceEventId FrameReceived, FrameSentPayload, FrameReceivedPayload, + WriteError, /// /// Raised when content arrives for a channel that has been disposed locally, resulting in discarding the content. @@ -827,6 +828,9 @@ private async Task ReadStreamAsync() case ControlCode.ContentWritingCompleted: this.OnContentWritingCompleted(header.RequiredChannelId); break; + case ControlCode.ContentWritingError: + this.OnContentWritingError(header.RequiredChannelId, frame.Value.Payload); + break; case ControlCode.ChannelTerminated: await this.OnChannelTerminatedAsync(header.RequiredChannelId).ConfigureAwait(false); break; @@ -900,6 +904,58 @@ private void OnContentWritingCompleted(QualifiedChannelId channelId) channel.OnContentWritingCompleted(); } + private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence message) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Received Write Error from channel {0}", channelId); + } + + Channel? channel; + lock (this.syncObject) + { + if (this.openChannels.ContainsKey(channelId)) + { + channel = this.openChannels[channelId]; + } + else + { + channel = null; + } + } + + if (channel == null) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Found no open channels {0}", channelId); + } + + // This is not an open channel so ignore the error message + return; + } + + if (channelId.Source == ChannelSource.Local && !channel.IsAccepted) + { + throw new MultiplexingProtocolException($"Remote party indicated error writing to channel {channelId} before accepting it."); + } + + // First close the channel and then throw the exception + string errorMessage = Encoding.Unicode.GetString(message.ToArray()); + Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); + + if (!this.channelsPendingTermination.Contains(channelId)) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Calling write complete for {0}", channel); + } + + // We haven't already sent a termination frame so close the channel + channel.OnContentWritingCompleted(remoteException); + } + } + private async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence payload, CancellationToken cancellationToken) { Channel channel; @@ -1088,7 +1144,6 @@ private void OnChannelDisposed(Channel channel) /// Indicates that the local end will not be writing any more data to this channel, /// leading to the transmission of a frame being sent for this channel. /// - /// The channel whose writing has finished. private void OnChannelWritingCompleted(Channel channel) { Requires.NotNull(channel, nameof(channel)); @@ -1102,6 +1157,43 @@ private void OnChannelWritingCompleted(Channel channel) } } + /// + /// Indicate that the local end encountered an error writing data to this channel, + /// leading to the transmission of a frame being sent to this channel. + /// + /// The channel we encountered writing the message to. + /// The error we encountered when trying to write to the channel. + private void OnChannelWritingError(Channel channel, Exception error) + { + Requires.NotNull(channel, nameof(channel)); + lock (this.syncObject) + { + // Only inform the remote side if this channel has not already been terminated. + if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) + { + string errorMessage = error.Message; + byte[] messageBytes = Encoding.Unicode.GetBytes(errorMessage); + ReadOnlySequence messageToSend = new ReadOnlySequence(messageBytes); + FrameHeader header = new FrameHeader + { + Code = ControlCode.ContentWritingError, + ChannelId = channel.QualifiedId, + }; + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Sending write error header {0} for channel {1}", + header, + channel); + } + + this.SendFrame(header, messageToSend, CancellationToken.None); + } + } + } + private void SendFrame(ControlCode code, QualifiedChannelId channelId) { var header = new FrameHeader diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 4a8c9f54..1a6c3c12 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -121,6 +121,28 @@ public async Task OfferReadOnlyDuplexPipe() await Task.WhenAll(ch1.Completion, ch2.Completion).WithCancellation(this.TimeoutToken); } + [Fact] + public async Task OfferPipeWithError() + { + try + { + // Prepare a readonly pipe that is already fully populated with data for the other end to read. + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + var writeException = new NullReferenceException("Write Error exception"); + pipe.Writer.Complete(writeException); + + MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); + ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); + } + catch (Exception error) + { + this.Logger.WriteLine("Encountered error inside offer pipe with error: " + error.Message); + } + } + [Fact] public async Task OfferReadOnlyPipe() { From 439906b983bb829c1046c6c3072726e9d5e24d30 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 30 May 2022 17:13:17 -0700 Subject: [PATCH 02/77] Basic Error Test --- .../MultiplexingStream.Channel.cs | 21 ++++--------------- .../MultiplexingStreamTests.cs | 6 +++++- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 1654922b..47ea6d71 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -96,11 +96,6 @@ public class Channel : IDisposableObservable, IDuplexPipe /// private bool isDisposed; - /// - /// Indicates whether we closed the writing channel due to an exception. - /// - private bool receivedContentWriteError; - /// /// The to use to get data to be transmitted over the . /// @@ -457,20 +452,9 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequenceIf we are closing the writing channel due to us receiving an error, defaults to null. internal void OnContentWritingCompleted(Exception? error = null) { - if (this.receivedContentWriteError) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Exiting from writing completed since it was already called"); - } - - // We received a content write error so we have already closed the channel - return; - } if (error != null) { - this.receivedContentWriteError = true; if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Exception {0} passed into writing complete", error); @@ -896,7 +880,10 @@ private void Fault(Exception exception) } this.mxStreamIOReader?.Complete(exception); - this.Dispose(); + if (!this.IsDisposed) + { + this.Dispose(); + } } private void DisposeSelfOnFailure(Task task) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 1a6c3c12..2951ace9 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -124,6 +124,7 @@ public async Task OfferReadOnlyDuplexPipe() [Fact] public async Task OfferPipeWithError() { + bool errorThrown = false; try { // Prepare a readonly pipe that is already fully populated with data for the other end to read. @@ -139,8 +140,11 @@ public async Task OfferPipeWithError() } catch (Exception error) { - this.Logger.WriteLine("Encountered error inside offer pipe with error: " + error.Message); + this.Logger.WriteLine("Encountered error inside Offer Pipe with error: " + error.Message); + errorThrown = true; } + + Assert.True(errorThrown); } [Fact] From 50aeb0f81d196fc833178bd6f3a49c66458ac756 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 30 May 2022 17:23:36 -0700 Subject: [PATCH 03/77] Update test to make sure error message is received --- .../MultiplexingStreamTests.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 2951ace9..072e08f9 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -125,23 +125,25 @@ public async Task OfferReadOnlyDuplexPipe() public async Task OfferPipeWithError() { bool errorThrown = false; + string errorMessage = "Hello World"; + + // Prepare a readonly pipe that is already fully populated with data for the other end to read. + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + var writeException = new NullReferenceException(errorMessage); + pipe.Writer.Complete(writeException); + + MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); + try { - // Prepare a readonly pipe that is already fully populated with data for the other end to read. - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - var writeException = new NullReferenceException("Write Error exception"); - pipe.Writer.Complete(writeException); - - MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); - await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); } - catch (Exception error) + catch (MultiplexingProtocolException error) { - this.Logger.WriteLine("Encountered error inside Offer Pipe with error: " + error.Message); - errorThrown = true; + errorThrown = error.Message.Contains(errorMessage); } Assert.True(errorThrown); From 5410649d50a7ae647e4df4b3a4d54f3ee1ed2cd1 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Wed, 1 Jun 2022 16:10:57 -0700 Subject: [PATCH 04/77] Added while loop in error test --- .../MultiplexingStreamTests.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 072e08f9..336c374d 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -127,7 +127,7 @@ public async Task OfferPipeWithError() bool errorThrown = false; string errorMessage = "Hello World"; - // Prepare a readonly pipe that is already fully populated with data for the other end to read. + // Prepare a readonly pipe that is already fully populated with data but is completed with an exception var pipe = new Pipe(); await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); var writeException = new NullReferenceException(errorMessage); @@ -137,13 +137,27 @@ public async Task OfferPipeWithError() await this.WaitForEphemeralChannelOfferToPropagateAsync(); MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); - try - { - ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); - } - catch (MultiplexingProtocolException error) + bool continueReading = true; + while (continueReading) { - errorThrown = error.Message.Contains(errorMessage); + try + { + ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); + if (readResult.IsCanceled || readResult.IsCompleted) + { + continueReading = false; + } + + ch2.Input.AdvanceTo(readResult.Buffer.End); + } + catch (MultiplexingProtocolException error) + { + errorThrown = error.Message.Contains(errorMessage); + if (errorThrown) + { + continueReading = false; + } + } } Assert.True(errorThrown); From 80b899e47491dec8dc6e9c4fa4580b4821908a57 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sun, 5 Jun 2022 00:01:18 -0700 Subject: [PATCH 05/77] Implemented basic interface for error serialization --- .../MultiplexingStream.Channel.cs | 1 - .../MultiplexingStream.ControlCode.cs | 3 +- .../MultiplexingStream.WriteError.cs | 36 +++++++++++++++++++ src/Nerdbank.Streams/MultiplexingStream.cs | 6 ++-- src/Nerdbank.Streams/Nerdbank.Streams.csproj | 2 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 3 ++ test/IsolatedTestHost/IsolatedTestHost.csproj | 2 ++ 7 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/Nerdbank.Streams/MultiplexingStream.WriteError.cs diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 47ea6d71..32368536 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -452,7 +452,6 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequenceIf we are closing the writing channel due to us receiving an error, defaults to null. internal void OnContentWritingCompleted(Exception? error = null) { - if (error != null) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index fb4bbdfa..94df33d3 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -47,7 +47,8 @@ internal enum ControlCode : byte /// /// Sent when we encounter error writing data on a given channel and is sent before a - /// to indicate the reason for the content writing closure. + /// to indicate the reason + /// for the content writing closure. /// ContentWritingError, } diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs new file mode 100644 index 00000000..08de0066 --- /dev/null +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -0,0 +1,36 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Nerdbank.Streams +{ + using MessagePack; + + /// + /// Contains the nested type. + /// + public partial class MultiplexingStream + { + /// + /// A class containing information about a write error and which is sent to the + /// remote alongside . + /// + [MessagePackObject] + public class WriteError + { + /// + /// Initializes a new instance of the class. + /// + /// The error message we want to send to the receiver. + public WriteError(string message) + { + this.ErrorMessage = message; + } + + /// + /// Gets the error message that we want to send to receiver. + /// + [Key(0)] + public string ErrorMessage { get; } + } + } +} diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index e0a8bb0f..750f744e 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -941,7 +941,8 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc } // First close the channel and then throw the exception - string errorMessage = Encoding.Unicode.GetString(message.ToArray()); + WriteError errorClass = MessagePackSerializer.Deserialize(message); + string errorMessage = errorClass.ErrorMessage; Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); if (!this.channelsPendingTermination.Contains(channelId)) @@ -1172,7 +1173,8 @@ private void OnChannelWritingError(Channel channel, Exception error) if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) { string errorMessage = error.Message; - byte[] messageBytes = Encoding.Unicode.GetBytes(errorMessage); + WriteError errorClass = new WriteError(errorMessage); + byte[] messageBytes = MessagePackSerializer.Serialize(errorClass); ReadOnlySequence messageToSend = new ReadOnlySequence(messageBytes); FrameHeader header = new FrameHeader { diff --git a/src/Nerdbank.Streams/Nerdbank.Streams.csproj b/src/Nerdbank.Streams/Nerdbank.Streams.csproj index c223bca1..d895bab5 100644 --- a/src/Nerdbank.Streams/Nerdbank.Streams.csproj +++ b/src/Nerdbank.Streams/Nerdbank.Streams.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt index b00dc10d..207e2a19 100644 --- a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ Nerdbank.Streams.BufferWriterExtensions +Nerdbank.Streams.MultiplexingStream.WriteError +Nerdbank.Streams.MultiplexingStream.WriteError.ErrorMessage.get -> string! +Nerdbank.Streams.MultiplexingStream.WriteError.WriteError(string! message) -> void Nerdbank.Streams.ReadOnlySequenceExtensions Nerdbank.Streams.StreamPipeReader Nerdbank.Streams.StreamPipeReader.Read() -> System.IO.Pipelines.ReadResult diff --git a/test/IsolatedTestHost/IsolatedTestHost.csproj b/test/IsolatedTestHost/IsolatedTestHost.csproj index e6b5dd16..ac1cd87f 100644 --- a/test/IsolatedTestHost/IsolatedTestHost.csproj +++ b/test/IsolatedTestHost/IsolatedTestHost.csproj @@ -6,6 +6,8 @@ + + From e9b529f024260e5c3876f942b571603116c1f772 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 9 Jun 2022 14:46:25 -0700 Subject: [PATCH 06/77] Added in locking for Dispose in MultiplexingStream.Channel and reverted back to not using message pack --- .../MultiplexingStream.Channel.cs | 143 +++++++++++------- .../MultiplexingStream.WriteError.cs | 2 - src/Nerdbank.Streams/MultiplexingStream.cs | 6 +- src/Nerdbank.Streams/Nerdbank.Streams.csproj | 2 - .../MultiplexingStreamTests.cs | 5 +- 5 files changed, 88 insertions(+), 70 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 32368536..687679fc 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -317,65 +317,75 @@ private long RemoteWindowRemaining /// public void Dispose() { - if (!this.IsDisposed) + + bool hasBeenDisposed; + lock (this.SyncObject) { - // The code in this delegate needs to happen in several branches including possibly asynchronously. - // We carefully define it here with no closure so that the C# compiler generates a static field for the delegate - // thus avoiding any extra allocations from reusing code in this way. - Action finalDisposalAction = (exOrAntecedent, state) => - { - var self = (Channel)state; - self.disposalTokenSource.Cancel(); - self.completionSource.TrySetResult(null); - self.MultiplexingStream.OnChannelDisposed(self); - }; + hasBeenDisposed = this.IsDisposed; + this.isDisposed = true; + } - this.acceptanceSource.TrySetCanceled(); - this.optionsAppliedTaskSource?.TrySetCanceled(); + if (hasBeenDisposed) + { + return; + } - PipeWriter? mxStreamIOWriter; - lock (this.SyncObject) - { - this.isDisposed = true; - mxStreamIOWriter = this.mxStreamIOWriter; - } + // The code in this delegate needs to happen in several branches including possibly asynchronously. + // We carefully define it here with no closure so that the C# compiler generates a static field for the delegate + // thus avoiding any extra allocations from reusing code in this way. + Action finalDisposalAction = (exOrAntecedent, state) => + { + var self = (Channel)state; + self.disposalTokenSource.Cancel(); + self.completionSource.TrySetResult(null); + self.MultiplexingStream.OnChannelDisposed(self); + }; - // Complete writing so that the mxstream cannot write to this channel any more. - // We must also cancel a pending flush since no one is guaranteed to be reading this any more - // and we don't want to deadlock on a full buffer in a disposed channel's pipe. - mxStreamIOWriter?.Complete(); - mxStreamIOWriter?.CancelPendingFlush(); - this.mxStreamIOWriterCompleted.Set(); + this.acceptanceSource.TrySetCanceled(); + this.optionsAppliedTaskSource?.TrySetCanceled(); - if (this.channelIO != null) - { - // We're using our own Pipe to relay user messages, so we can shutdown writing and allow for our reader to propagate what was already written - // before actually shutting down. - this.channelIO.Output.Complete(); - } - else - { - // We don't own the user's PipeWriter to complete it (so they can't write anything more to this channel). - // We can't know whether there is or will be more bytes written to the user's PipeWriter, - // but we need to terminate our reader for their writer as part of reclaiming resources. - // We want to complete reading immediately and cancel any pending read. - this.mxStreamIOReader?.Complete(); - this.mxStreamIOReader?.CancelPendingRead(); - } + PipeWriter? mxStreamIOWriter; + lock (this.SyncObject) + { + mxStreamIOWriter = this.mxStreamIOWriter; + } - // Unblock the reader that might be waiting on this. - this.remoteWindowHasCapacity.Set(); + // Complete writing so that the mxstream cannot write to this channel any more. + // We must also cancel a pending flush since no one is guaranteed to be reading this any more + // and we don't want to deadlock on a full buffer in a disposed channel's pipe. + mxStreamIOWriter?.Complete(); + mxStreamIOWriter?.CancelPendingFlush(); + this.mxStreamIOWriterCompleted.Set(); - // As a minor perf optimization, avoid allocating a continuation task if the antecedent is already completed. - if (this.mxStreamIOReaderCompleted?.IsCompleted ?? true) - { - finalDisposalAction(null, this); - } - else - { - this.mxStreamIOReaderCompleted!.ContinueWith(finalDisposalAction!, this, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Forget(); - } + if (this.channelIO != null) + { + // We're using our own Pipe to relay user messages, so we can shutdown writing and allow for our reader to propagate what was already written + // before actually shutting down. + this.channelIO.Output.Complete(); + } + else + { + // We don't own the user's PipeWriter to complete it (so they can't write anything more to this channel). + // We can't know whether there is or will be more bytes written to the user's PipeWriter, + // but we need to terminate our reader for their writer as part of reclaiming resources. + // We want to complete reading immediately and cancel any pending read. + this.mxStreamIOReader?.Complete(); + this.mxStreamIOReader?.CancelPendingRead(); + } + + // Unblock the reader that might be waiting on this. + this.remoteWindowHasCapacity.Set(); + + // As a minor perf optimization, avoid allocating a continuation task if the antecedent is already completed. + if (this.mxStreamIOReaderCompleted?.IsCompleted ?? true) + { + finalDisposalAction(null, this); + } + else + { + this.mxStreamIOReaderCompleted!.ContinueWith(finalDisposalAction!, this, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Forget(); } + } internal async Task OnChannelTerminatedAsync() @@ -460,6 +470,11 @@ internal void OnContentWritingCompleted(Exception? error = null) } } + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) + { + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Calling disposed self in {0} for content writing completed", this.QualifiedId); + } + this.DisposeSelfOnFailure(Task.Run(async delegate { if (!this.IsDisposed) @@ -618,10 +633,8 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) Assumes.NotNull(this.channelIO); this.existingPipe = channelOptions.ExistingPipe; this.existingPipeGiven = true; - // We always want to write ALL received data to the user's ExistingPipe, rather than truncating it on disposal, so don't use a cancellation token in that direction. this.DisposeSelfOnFailure(this.channelIO.Input.LinkToAsync(channelOptions.ExistingPipe.Output)); - // Upon disposal, we no longer want to continue reading from the user's ExistingPipe into our buffer since we won't be propagating it any further, so use our DisposalToken. this.DisposeSelfOnFailure(channelOptions.ExistingPipe.Input.LinkToAsync(this.channelIO.Output, this.DisposalToken)); } @@ -873,22 +886,36 @@ private async Task AutoCloseOnPipesClosureAsync() private void Fault(Exception exception) { - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) + + bool hasBeenDisposed; + lock (this.SyncObject) { - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel Closing self due to exception: {0}", exception); + hasBeenDisposed = this.IsDisposed; } - this.mxStreamIOReader?.Complete(exception); - if (!this.IsDisposed) + if (!hasBeenDisposed && (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false)) { - this.Dispose(); + string callerName = new StackTrace().GetFrame(1) + .GetMethod().Name; + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Fault called in {0} by {1}", this.QualifiedId, callerName); + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel Closing self due to exception: {0}", exception); } + + this.mxStreamIOReader?.Complete(exception); + this.Dispose(); } private void DisposeSelfOnFailure(Task task) { Requires.NotNull(task, nameof(task)); + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) + { + string callerName = new StackTrace().GetFrame(1) + .GetMethod().Name; + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "DisposeSelfOnFailure called in {0} by {1}", this.QualifiedId, callerName); + } + if (task.IsCompleted) { if (task.IsFaulted) diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs index 08de0066..cc5b3375 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -14,7 +14,6 @@ public partial class MultiplexingStream /// A class containing information about a write error and which is sent to the /// remote alongside . /// - [MessagePackObject] public class WriteError { /// @@ -29,7 +28,6 @@ public WriteError(string message) /// /// Gets the error message that we want to send to receiver. /// - [Key(0)] public string ErrorMessage { get; } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 750f744e..e0a8bb0f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -941,8 +941,7 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc } // First close the channel and then throw the exception - WriteError errorClass = MessagePackSerializer.Deserialize(message); - string errorMessage = errorClass.ErrorMessage; + string errorMessage = Encoding.Unicode.GetString(message.ToArray()); Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); if (!this.channelsPendingTermination.Contains(channelId)) @@ -1173,8 +1172,7 @@ private void OnChannelWritingError(Channel channel, Exception error) if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) { string errorMessage = error.Message; - WriteError errorClass = new WriteError(errorMessage); - byte[] messageBytes = MessagePackSerializer.Serialize(errorClass); + byte[] messageBytes = Encoding.Unicode.GetBytes(errorMessage); ReadOnlySequence messageToSend = new ReadOnlySequence(messageBytes); FrameHeader header = new FrameHeader { diff --git a/src/Nerdbank.Streams/Nerdbank.Streams.csproj b/src/Nerdbank.Streams/Nerdbank.Streams.csproj index d895bab5..c223bca1 100644 --- a/src/Nerdbank.Streams/Nerdbank.Streams.csproj +++ b/src/Nerdbank.Streams/Nerdbank.Streams.csproj @@ -9,8 +9,6 @@ - - diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 336c374d..b51b5765 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -153,10 +153,7 @@ public async Task OfferPipeWithError() catch (MultiplexingProtocolException error) { errorThrown = error.Message.Contains(errorMessage); - if (errorThrown) - { - continueReading = false; - } + continueReading = false; } } From d2b6a00457bdbdfc8d4471ef786ee0c5d7e062ff Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 9 Jun 2022 15:20:06 -0700 Subject: [PATCH 07/77] Updated SDK version locally --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 5 ++--- src/Nerdbank.Streams/MultiplexingStream.cs | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 687679fc..f11f6317 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -317,7 +317,6 @@ private long RemoteWindowRemaining /// public void Dispose() { - bool hasBeenDisposed; lock (this.SyncObject) { @@ -385,7 +384,6 @@ public void Dispose() { this.mxStreamIOReaderCompleted!.ContinueWith(finalDisposalAction!, this, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Forget(); } - } internal async Task OnChannelTerminatedAsync() @@ -633,8 +631,10 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) Assumes.NotNull(this.channelIO); this.existingPipe = channelOptions.ExistingPipe; this.existingPipeGiven = true; + // We always want to write ALL received data to the user's ExistingPipe, rather than truncating it on disposal, so don't use a cancellation token in that direction. this.DisposeSelfOnFailure(this.channelIO.Input.LinkToAsync(channelOptions.ExistingPipe.Output)); + // Upon disposal, we no longer want to continue reading from the user's ExistingPipe into our buffer since we won't be propagating it any further, so use our DisposalToken. this.DisposeSelfOnFailure(channelOptions.ExistingPipe.Input.LinkToAsync(this.channelIO.Output, this.DisposalToken)); } @@ -886,7 +886,6 @@ private async Task AutoCloseOnPipesClosureAsync() private void Fault(Exception exception) { - bool hasBeenDisposed; lock (this.SyncObject) { diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index e0a8bb0f..0ca94111 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -12,7 +12,6 @@ namespace Nerdbank.Streams using System.Text; using System.Threading; using System.Threading.Tasks; - using MessagePack; using Microsoft; using Microsoft.VisualStudio.Threading; From 4cc7a2dd6a109d3cef883eff8626695e58d9ea1d Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 13 Jun 2022 22:56:07 -0700 Subject: [PATCH 08/77] Implemented custom serialization using messagepack --- .../MultiplexingStream.Formatters.cs | 35 +++++++++++++++++ src/Nerdbank.Streams/MultiplexingStream.cs | 39 ++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index c0550733..bab5d7d1 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -295,6 +295,41 @@ internal override ReadOnlySequence SerializeContentProcessed(long bytesPro { throw new NotSupportedException(); } + + /// + /// Method to serialize a write error object using . + /// + /// The object that we want to serialize. + /// The serialized version of the error object. + internal ReadOnlySequence SerializeWritingError(WriteError error) + { + var sequence = new Sequence(); + var writer = new MessagePackWriter(sequence); + writer.WriteArrayHeader(1); + writer.Write(error.ErrorMessage); + writer.Flush(); + return sequence.AsReadOnlySequence; + } + + /// + /// Method to deserialize a write error object using . + /// + /// The serialized sequence that we are trying to deserialize. + /// The deserialized object. + /// Thrown if payload can't be deserialized. + internal WriteError DeserializeWritingError(ReadOnlySequence payload) + { + var reader = new MessagePackReader(payload); + int elementsCount = reader.ReadArrayHeader(); + if (elementsCount != 1) + { + throw new MultiplexingProtocolException("Improper number of elements in writing error payload in " + elementsCount); + } + + string errorMessage = reader.ReadString(); + return new WriteError(errorMessage); + } + } internal class V2Formatter : Formatter diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 0ca94111..652978e9 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -939,9 +939,23 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc throw new MultiplexingProtocolException($"Remote party indicated error writing to channel {channelId} before accepting it."); } - // First close the channel and then throw the exception - string errorMessage = Encoding.Unicode.GetString(message.ToArray()); - Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); + if (!(this.formatter is V1Formatter)) + { + // TODO: Handle the case that we are not using the V2Formatter, currently don't process the message + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not processing error message due to invalid formatter"); + } + + return; + } + + // Extract the exception from the payload + V1Formatter formatterWithError = (V1Formatter)this.formatter; + WriteError errorClass = formatterWithError.DeserializeWritingError(message); + + // Close the channel with the exception + Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorClass.ErrorMessage}"); if (!this.channelsPendingTermination.Contains(channelId)) { @@ -1165,14 +1179,27 @@ private void OnChannelWritingCompleted(Channel channel) private void OnChannelWritingError(Channel channel, Exception error) { Requires.NotNull(channel, nameof(channel)); + + if (!(this.formatter is V1Formatter)) + { + // TODO: Handle the case that we are not using the V2Formatter, currently don't process the message + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending error message due to invalid formatter"); + } + + return; + } + lock (this.syncObject) { // Only inform the remote side if this channel has not already been terminated. if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) { - string errorMessage = error.Message; - byte[] messageBytes = Encoding.Unicode.GetBytes(errorMessage); - ReadOnlySequence messageToSend = new ReadOnlySequence(messageBytes); + WriteError errorClass = new WriteError(error.Message); + V1Formatter formatterWithError = (V1Formatter)this.formatter; + ReadOnlySequence messageToSend = formatterWithError.SerializeWritingError(errorClass); + FrameHeader header = new FrameHeader { Code = ControlCode.ContentWritingError, From d3560e1e5860588c81b459aa29b144a320f9793f Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 18 Jun 2022 11:30:53 -0700 Subject: [PATCH 09/77] Added support for write error only in V2 and V3 --- .../MultiplexingStream.Channel.cs | 78 +++++++++++++++---- .../MultiplexingStream.Formatters.cs | 77 +++++++++--------- src/Nerdbank.Streams/MultiplexingStream.cs | 33 ++++---- .../MultiplexingStreamTests.cs | 4 +- .../MultiplexingStreamV2Tests.cs | 43 ++++++++++ 5 files changed, 169 insertions(+), 66 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index f11f6317..539d623e 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -129,6 +129,11 @@ public class Channel : IDisposableObservable, IDuplexPipe /// private bool? existingPipeGiven; + /// + /// A value indicating whether the was closed with an error. + /// + private bool writerCompletedWithError; + /// /// Initializes a new instance of the class. /// @@ -144,6 +149,7 @@ internal Channel(MultiplexingStream multiplexingStream, QualifiedChannelId chann this.MultiplexingStream = multiplexingStream; this.channelId = channelId; this.OfferParams = offerParameters; + this.writerCompletedWithError = false; switch (channelId.Source) { @@ -460,45 +466,79 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequenceIf we are closing the writing channel due to us receiving an error, defaults to null. internal void OnContentWritingCompleted(Exception? error = null) { - if (error != null) + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Called Content Writing Complete with error {0}", error); + } + + // Ensure that we don't complete the writer if we previously completed with an error + bool alreadyCompletedWithError = this.writerCompletedWithError; + this.writerCompletedWithError = error != null; + + if (alreadyCompletedWithError) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Exception {0} passed into writing complete", error); + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Previously called Content Writing Completed with error message so don't process this"); } - } - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) - { - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Calling disposed self in {0} for content writing completed", this.QualifiedId); + return; } this.DisposeSelfOnFailure(Task.Run(async delegate { + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) + { + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Content Writing Completed had disposed of {0}", this.IsDisposed); + } + if (!this.IsDisposed) { try { PipeWriter? writer = this.GetReceivedMessagePipeWriter(); - await writer.CompleteAsync(error).ConfigureAwait(false); + if (writer != null) + { + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) + { + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.WriteError, "Closing pipe writer {0} with error {1}", writer, error); + } + + await writer.CompleteAsync(error).ConfigureAwait(false); + } } catch (ObjectDisposedException) { + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) + { + this.TraceSource!.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "ObjectDisposedException when closing pipe writer"); + } + if (this.mxStreamIOWriter != null) { + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) + { + this.TraceSource!.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Closing general writer {0} with error {1}", this.mxStreamIOWriter, error); + } + await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); } } } - else + else if (this.mxStreamIOWriter != null) { - if (this.mxStreamIOWriter != null) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) { - await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Closing general writer {0} with error {1}", this.mxStreamIOWriter, error); } + + await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); } - this.mxStreamIOWriterCompleted.Set(); + if (this.mxStreamIOWriter != null) + { + this.mxStreamIOWriterCompleted.Set(); + } })); } @@ -887,20 +927,23 @@ private async Task AutoCloseOnPipesClosureAsync() private void Fault(Exception exception) { bool hasBeenDisposed; + lock (this.SyncObject) { - hasBeenDisposed = this.IsDisposed; + hasBeenDisposed = this.isDisposed; + if (!this.isDisposed) + { + this.mxStreamIOReader?.Complete(exception); + } } if (!hasBeenDisposed && (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false)) { - string callerName = new StackTrace().GetFrame(1) - .GetMethod().Name; + string callerName = new StackTrace().GetFrame(1).GetMethod().Name; this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Fault called in {0} by {1}", this.QualifiedId, callerName); this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel Closing self due to exception: {0}", exception); } - this.mxStreamIOReader?.Complete(exception); this.Dispose(); } @@ -917,6 +960,11 @@ private void DisposeSelfOnFailure(Task task) if (task.IsCompleted) { + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) + { + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "DisposeSelfOnFailure completed with faulted value of {0}", task.IsFaulted); + } + if (task.IsFaulted) { this.Fault(task.Exception!.InnerException ?? task.Exception); diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index bab5d7d1..2a28cea5 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -295,41 +295,6 @@ internal override ReadOnlySequence SerializeContentProcessed(long bytesPro { throw new NotSupportedException(); } - - /// - /// Method to serialize a write error object using . - /// - /// The object that we want to serialize. - /// The serialized version of the error object. - internal ReadOnlySequence SerializeWritingError(WriteError error) - { - var sequence = new Sequence(); - var writer = new MessagePackWriter(sequence); - writer.WriteArrayHeader(1); - writer.Write(error.ErrorMessage); - writer.Flush(); - return sequence.AsReadOnlySequence; - } - - /// - /// Method to deserialize a write error object using . - /// - /// The serialized sequence that we are trying to deserialize. - /// The deserialized object. - /// Thrown if payload can't be deserialized. - internal WriteError DeserializeWritingError(ReadOnlySequence payload) - { - var reader = new MessagePackReader(payload); - int elementsCount = reader.ReadArrayHeader(); - if (elementsCount != 1) - { - throw new MultiplexingProtocolException("Improper number of elements in writing error payload in " + elementsCount); - } - - string errorMessage = reader.ReadString(); - return new WriteError(errorMessage); - } - } internal class V2Formatter : Formatter @@ -530,6 +495,48 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R return new Channel.AcceptanceParameters(remoteWindowSize); } + /// + /// Method to serialize a write error object using . + /// + /// The object that we want to serialize. + /// The serialized version of the error object. + internal ReadOnlySequence SerializeWritingError(WriteError error) + { + var sequence = new Sequence(); + var writer = new MessagePackWriter(sequence); + writer.WriteArrayHeader(2); + writer.WriteInt32(ProtocolVersion.Major); + writer.Write(error.ErrorMessage); + writer.Flush(); + return sequence.AsReadOnlySequence; + } + + /// + /// Method to deserialize a write error object using . + /// + /// The serialized sequence that we are trying to deserialize. + /// The deserialized object if the sender's version matches our version, null if it doesn't. + /// Thrown if payload can't be deserialized. + internal WriteError? DeserializeWritingError(ReadOnlySequence payload) + { + var reader = new MessagePackReader(payload); + int elementsCount = reader.ReadArrayHeader(); + if (elementsCount != 2) + { + throw new MultiplexingProtocolException("Improper number of elements in writing error payload in " + elementsCount); + } + + int senderVersion = reader.ReadInt32(); + if (senderVersion != ProtocolVersion.Major) + { + // TODO: For the time being use the strict requirement that the versions need to line up but need to look into this + return null; + } + + string errorMessage = reader.ReadString(); + return new WriteError(errorMessage); + } + protected virtual (FrameHeader Header, ReadOnlySequence Payload) DeserializeFrame(ReadOnlySequence frameSequence) { var reader = new MessagePackReader(frameSequence); diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 652978e9..a0155832 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -939,9 +939,9 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc throw new MultiplexingProtocolException($"Remote party indicated error writing to channel {channelId} before accepting it."); } - if (!(this.formatter is V1Formatter)) + if (this.formatter is V1Formatter) { - // TODO: Handle the case that we are not using the V2Formatter, currently don't process the message + // If we are using a V1 Formatter then ignore the write error message if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not processing error message due to invalid formatter"); @@ -951,8 +951,18 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc } // Extract the exception from the payload - V1Formatter formatterWithError = (V1Formatter)this.formatter; - WriteError errorClass = formatterWithError.DeserializeWritingError(message); + V2Formatter formatterWithError = (V2Formatter)this.formatter; + WriteError? errorClass = formatterWithError.DeserializeWritingError(message); + + if (errorClass == null) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not processing error message due to version numbers not matching up"); + } + + return; + } // Close the channel with the exception Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorClass.ErrorMessage}"); @@ -961,7 +971,7 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Calling write complete for {0}", channel); + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Calling write complete for {0} with error {1}", channel, remoteException); } // We haven't already sent a termination frame so close the channel @@ -1180,9 +1190,9 @@ private void OnChannelWritingError(Channel channel, Exception error) { Requires.NotNull(channel, nameof(channel)); - if (!(this.formatter is V1Formatter)) + if (this.formatter is V1Formatter) { - // TODO: Handle the case that we are not using the V2Formatter, currently don't process the message + // Ignore error if we are using a V1 Formatter if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending error message due to invalid formatter"); @@ -1197,7 +1207,7 @@ private void OnChannelWritingError(Channel channel, Exception error) if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) { WriteError errorClass = new WriteError(error.Message); - V1Formatter formatterWithError = (V1Formatter)this.formatter; + V2Formatter formatterWithError = (V2Formatter)this.formatter; ReadOnlySequence messageToSend = formatterWithError.SerializeWritingError(errorClass); FrameHeader header = new FrameHeader @@ -1207,12 +1217,7 @@ private void OnChannelWritingError(Channel channel, Exception error) }; if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Sending write error header {0} for channel {1}", - header, - channel); + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Sending write error header {0} for channel {1}", header, channel); } this.SendFrame(header, messageToSend, CancellationToken.None); diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index b51b5765..477f9500 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -122,7 +122,7 @@ public async Task OfferReadOnlyDuplexPipe() } [Fact] - public async Task OfferPipeWithError() + public async Task ErrorInPipeIgnoredForV1() { bool errorThrown = false; string errorMessage = "Hello World"; @@ -157,7 +157,7 @@ public async Task OfferPipeWithError() } } - Assert.True(errorThrown); + Assert.False(errorThrown); } [Fact] diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index e516f4fb..00b1ca24 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -19,6 +19,49 @@ public MultiplexingStreamV2Tests(ITestOutputHelper logger) protected override int ProtocolMajorVersion => 2; + [Fact] + public async Task OfferPipeWithError() + { + bool errorThrown = false; + string errorMessage = "Hello World"; + + // Prepare a readonly pipe that is already fully populated with data but is completed with an exception + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + var writeException = new NullReferenceException(errorMessage); + pipe.Writer.Complete(writeException); + + MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); + + bool continueReading = true; + while (continueReading) + { + try + { + ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); + if (readResult.IsCompleted) + { + this.Logger.WriteLine("Setting continue Reading to false due to read result in channel " + ch2.QualifiedId); + continueReading = false; + } + else + { + ch2.Input.AdvanceTo(readResult.Buffer.End); + } + } + catch (Exception error) + { + this.Logger.WriteLine("Caught error " + error.Message + " in OfferPipeWithError"); + errorThrown = error.Message.Contains(errorMessage); + continueReading = false; + } + } + + Assert.True(errorThrown); + } + [Fact] public async Task Backpressure() { From 33be3747183be504ea037919b73b4acc563fe11f Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Wed, 22 Jun 2022 22:26:11 -0700 Subject: [PATCH 10/77] Removed all changes in current branch --- .../MultiplexingStream.Channel.cs | 103 ++------------ .../MultiplexingStream.ControlCode.cs | 7 - .../MultiplexingStream.Formatters.cs | 42 ------ src/Nerdbank.Streams/MultiplexingStream.cs | 127 +----------------- .../netstandard2.0/PublicAPI.Unshipped.txt | 3 - test/IsolatedTestHost/IsolatedTestHost.csproj | 2 - .../MultiplexingStreamTests.cs | 39 ------ .../MultiplexingStreamV2Tests.cs | 43 ------ 8 files changed, 16 insertions(+), 350 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index a604941a..91bfdf77 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -142,11 +142,6 @@ public class Channel : IDisposableObservable, IDuplexPipe /// private bool? existingPipeGiven; - /// - /// A value indicating whether the was closed with an error. - /// - private bool writerCompletedWithError; - /// /// Initializes a new instance of the class. /// @@ -162,7 +157,6 @@ internal Channel(MultiplexingStream multiplexingStream, QualifiedChannelId chann this.MultiplexingStream = multiplexingStream; this.channelId = channelId; this.OfferParams = offerParameters; - this.writerCompletedWithError = false; switch (channelId.Source) { @@ -336,22 +330,17 @@ private long RemoteWindowRemaining /// public void Dispose() { - bool hasBeenDisposed; - lock (this.SyncObject) + if (!this.IsDisposed) { this.acceptanceSource.TrySetCanceled(); this.optionsAppliedTaskSource?.TrySetCanceled(); - // The code in this delegate needs to happen in several branches including possibly asynchronously. - // We carefully define it here with no closure so that the C# compiler generates a static field for the delegate - // thus avoiding any extra allocations from reusing code in this way. - Action finalDisposalAction = (exOrAntecedent, state) => - { - var self = (Channel)state; - self.disposalTokenSource.Cancel(); - self.completionSource.TrySetResult(null); - self.MultiplexingStream.OnChannelDisposed(self); - }; + PipeWriter? mxStreamIOWriter; + lock (this.SyncObject) + { + this.isDisposed = true; + mxStreamIOWriter = this.mxStreamIOWriter; + } // Complete writing so that the mxstream cannot write to this channel any more. // We must also cancel a pending flush since no one is guaranteed to be reading this any more @@ -408,12 +397,8 @@ public void Dispose() mxStreamIOReader?.Complete(); } - // Complete writing so that the mxstream cannot write to this channel any more. - // We must also cancel a pending flush since no one is guaranteed to be reading this any more - // and we don't want to deadlock on a full buffer in a disposed channel's pipe. - mxStreamIOWriter?.Complete(); - mxStreamIOWriter?.CancelPendingFlush(); - this.mxStreamIOWriterCompleted.Set(); + // Unblock the reader that might be waiting on this. + this.remoteWindowHasCapacity.Set(); this.disposalTokenSource.Cancel(); this.completionSource.TrySetResult(null); @@ -490,35 +475,10 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence /// Called by the when when it will not be writing any more data to the channel. /// - /// If we are closing the writing channel due to us receiving an error, defaults to null. - internal void OnContentWritingCompleted(Exception? error = null) + internal void OnContentWritingCompleted() { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Called Content Writing Complete with error {0}", error); - } - - // Ensure that we don't complete the writer if we previously completed with an error - bool alreadyCompletedWithError = this.writerCompletedWithError; - this.writerCompletedWithError = error != null; - - if (alreadyCompletedWithError) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Previously called Content Writing Completed with error message so don't process this"); - } - - return; - } - this.DisposeSelfOnFailure(Task.Run(async delegate { - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) - { - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Content Writing Completed had disposed of {0}", this.IsDisposed); - } - if (!this.IsDisposed) { try @@ -528,11 +488,6 @@ internal void OnContentWritingCompleted(Exception? error = null) } catch (ObjectDisposedException) { - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) - { - this.TraceSource!.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "ObjectDisposedException when closing pipe writer"); - } - if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); @@ -540,21 +495,16 @@ internal void OnContentWritingCompleted(Exception? error = null) } } } - else if (this.mxStreamIOWriter != null) + else { - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) + if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); await this.mxStreamIOWriter.CompleteAsync().ConfigureAwait(false); } - - await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); } - if (this.mxStreamIOWriter != null) - { - this.mxStreamIOWriterCompleted.Set(); - } + this.mxStreamIOWriterCompleted.Set(); })); } @@ -944,21 +894,8 @@ private async Task AutoCloseOnPipesClosureAsync() private void Fault(Exception exception) { - bool hasBeenDisposed; - - lock (this.SyncObject) - { - hasBeenDisposed = this.isDisposed; - if (!this.isDisposed) - { - this.mxStreamIOReader?.Complete(exception); - } - } - - if (!hasBeenDisposed && (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false)) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) { - string callerName = new StackTrace().GetFrame(1).GetMethod().Name; - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Fault called in {0} by {1}", this.QualifiedId, callerName); this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel Closing self due to exception: {0}", exception); } @@ -975,20 +912,8 @@ private void DisposeSelfOnFailure(Task task) { Requires.NotNull(task, nameof(task)); - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) - { - string callerName = new StackTrace().GetFrame(1) - .GetMethod().Name; - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "DisposeSelfOnFailure called in {0} by {1}", this.QualifiedId, callerName); - } - if (task.IsCompleted) { - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) - { - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "DisposeSelfOnFailure completed with faulted value of {0}", task.IsFaulted); - } - if (task.IsFaulted) { this.Fault(task.Exception!.InnerException ?? task.Exception); diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index 94df33d3..91a9b364 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -44,13 +44,6 @@ internal enum ControlCode : byte /// allowing them to send more data. /// ContentProcessed, - - /// - /// Sent when we encounter error writing data on a given channel and is sent before a - /// to indicate the reason - /// for the content writing closure. - /// - ContentWritingError, } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 2a28cea5..c0550733 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -495,48 +495,6 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R return new Channel.AcceptanceParameters(remoteWindowSize); } - /// - /// Method to serialize a write error object using . - /// - /// The object that we want to serialize. - /// The serialized version of the error object. - internal ReadOnlySequence SerializeWritingError(WriteError error) - { - var sequence = new Sequence(); - var writer = new MessagePackWriter(sequence); - writer.WriteArrayHeader(2); - writer.WriteInt32(ProtocolVersion.Major); - writer.Write(error.ErrorMessage); - writer.Flush(); - return sequence.AsReadOnlySequence; - } - - /// - /// Method to deserialize a write error object using . - /// - /// The serialized sequence that we are trying to deserialize. - /// The deserialized object if the sender's version matches our version, null if it doesn't. - /// Thrown if payload can't be deserialized. - internal WriteError? DeserializeWritingError(ReadOnlySequence payload) - { - var reader = new MessagePackReader(payload); - int elementsCount = reader.ReadArrayHeader(); - if (elementsCount != 2) - { - throw new MultiplexingProtocolException("Improper number of elements in writing error payload in " + elementsCount); - } - - int senderVersion = reader.ReadInt32(); - if (senderVersion != ProtocolVersion.Major) - { - // TODO: For the time being use the strict requirement that the versions need to line up but need to look into this - return null; - } - - string errorMessage = reader.ReadString(); - return new WriteError(errorMessage); - } - protected virtual (FrameHeader Header, ReadOnlySequence Payload) DeserializeFrame(ReadOnlySequence frameSequence) { var reader = new MessagePackReader(frameSequence); diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 418662b7..55572d1e 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -12,6 +12,7 @@ namespace Nerdbank.Streams using System.Text; using System.Threading; using System.Threading.Tasks; + using MessagePack; using Microsoft; using Microsoft.VisualStudio.Threading; @@ -187,7 +188,6 @@ private enum TraceEventId FrameReceived, FrameSentPayload, FrameReceivedPayload, - WriteError, /// /// Raised when content arrives for a channel that has been disposed locally, resulting in discarding the content. @@ -828,9 +828,6 @@ private async Task ReadStreamAsync() case ControlCode.ContentWritingCompleted: this.OnContentWritingCompleted(header.RequiredChannelId); break; - case ControlCode.ContentWritingError: - this.OnContentWritingError(header.RequiredChannelId, frame.Value.Payload); - break; case ControlCode.ChannelTerminated: await this.OnChannelTerminatedAsync(header.RequiredChannelId).ConfigureAwait(false); break; @@ -904,82 +901,6 @@ private void OnContentWritingCompleted(QualifiedChannelId channelId) channel.OnContentWritingCompleted(); } - private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence message) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Received Write Error from channel {0}", channelId); - } - - Channel? channel; - lock (this.syncObject) - { - if (this.openChannels.ContainsKey(channelId)) - { - channel = this.openChannels[channelId]; - } - else - { - channel = null; - } - } - - if (channel == null) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Found no open channels {0}", channelId); - } - - // This is not an open channel so ignore the error message - return; - } - - if (channelId.Source == ChannelSource.Local && !channel.IsAccepted) - { - throw new MultiplexingProtocolException($"Remote party indicated error writing to channel {channelId} before accepting it."); - } - - if (this.formatter is V1Formatter) - { - // If we are using a V1 Formatter then ignore the write error message - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not processing error message due to invalid formatter"); - } - - return; - } - - // Extract the exception from the payload - V2Formatter formatterWithError = (V2Formatter)this.formatter; - WriteError? errorClass = formatterWithError.DeserializeWritingError(message); - - if (errorClass == null) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not processing error message due to version numbers not matching up"); - } - - return; - } - - // Close the channel with the exception - Exception remoteException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorClass.ErrorMessage}"); - - if (!this.channelsPendingTermination.Contains(channelId)) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Calling write complete for {0} with error {1}", channel, remoteException); - } - - // We haven't already sent a termination frame so close the channel - channel.OnContentWritingCompleted(remoteException); - } - } - private async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence payload, CancellationToken cancellationToken) { Channel channel; @@ -1168,6 +1089,7 @@ private void OnChannelDisposed(Channel channel) /// Indicates that the local end will not be writing any more data to this channel, /// leading to the transmission of a frame being sent for this channel. /// + /// The channel whose writing has finished. private void OnChannelWritingCompleted(Channel channel) { Requires.NotNull(channel, nameof(channel)); @@ -1181,51 +1103,6 @@ private void OnChannelWritingCompleted(Channel channel) } } - /// - /// Indicate that the local end encountered an error writing data to this channel, - /// leading to the transmission of a frame being sent to this channel. - /// - /// The channel we encountered writing the message to. - /// The error we encountered when trying to write to the channel. - private void OnChannelWritingError(Channel channel, Exception error) - { - Requires.NotNull(channel, nameof(channel)); - - if (this.formatter is V1Formatter) - { - // Ignore error if we are using a V1 Formatter - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending error message due to invalid formatter"); - } - - return; - } - - lock (this.syncObject) - { - // Only inform the remote side if this channel has not already been terminated. - if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) - { - WriteError errorClass = new WriteError(error.Message); - V2Formatter formatterWithError = (V2Formatter)this.formatter; - ReadOnlySequence messageToSend = formatterWithError.SerializeWritingError(errorClass); - - FrameHeader header = new FrameHeader - { - Code = ControlCode.ContentWritingError, - ChannelId = channel.QualifiedId, - }; - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Sending write error header {0} for channel {1}", header, channel); - } - - this.SendFrame(header, messageToSend, CancellationToken.None); - } - } - } - private void SendFrame(ControlCode code, QualifiedChannelId channelId) { var header = new FrameHeader diff --git a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt index 207e2a19..b00dc10d 100644 --- a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,7 +1,4 @@ Nerdbank.Streams.BufferWriterExtensions -Nerdbank.Streams.MultiplexingStream.WriteError -Nerdbank.Streams.MultiplexingStream.WriteError.ErrorMessage.get -> string! -Nerdbank.Streams.MultiplexingStream.WriteError.WriteError(string! message) -> void Nerdbank.Streams.ReadOnlySequenceExtensions Nerdbank.Streams.StreamPipeReader Nerdbank.Streams.StreamPipeReader.Read() -> System.IO.Pipelines.ReadResult diff --git a/test/IsolatedTestHost/IsolatedTestHost.csproj b/test/IsolatedTestHost/IsolatedTestHost.csproj index ac1cd87f..e6b5dd16 100644 --- a/test/IsolatedTestHost/IsolatedTestHost.csproj +++ b/test/IsolatedTestHost/IsolatedTestHost.csproj @@ -6,8 +6,6 @@ - - diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 98bfeb47..5391c218 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -121,45 +121,6 @@ public async Task OfferReadOnlyDuplexPipe() await Task.WhenAll(ch1.Completion, ch2.Completion).WithCancellation(this.TimeoutToken); } - [Fact] - public async Task ErrorInPipeIgnoredForV1() - { - bool errorThrown = false; - string errorMessage = "Hello World"; - - // Prepare a readonly pipe that is already fully populated with data but is completed with an exception - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - var writeException = new NullReferenceException(errorMessage); - pipe.Writer.Complete(writeException); - - MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); - await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); - - bool continueReading = true; - while (continueReading) - { - try - { - ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); - if (readResult.IsCanceled || readResult.IsCompleted) - { - continueReading = false; - } - - ch2.Input.AdvanceTo(readResult.Buffer.End); - } - catch (MultiplexingProtocolException error) - { - errorThrown = error.Message.Contains(errorMessage); - continueReading = false; - } - } - - Assert.False(errorThrown); - } - [Fact] public async Task OfferReadOnlyPipe() { diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index 00b1ca24..e516f4fb 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -19,49 +19,6 @@ public MultiplexingStreamV2Tests(ITestOutputHelper logger) protected override int ProtocolMajorVersion => 2; - [Fact] - public async Task OfferPipeWithError() - { - bool errorThrown = false; - string errorMessage = "Hello World"; - - // Prepare a readonly pipe that is already fully populated with data but is completed with an exception - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - var writeException = new NullReferenceException(errorMessage); - pipe.Writer.Complete(writeException); - - MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); - await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); - - bool continueReading = true; - while (continueReading) - { - try - { - ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); - if (readResult.IsCompleted) - { - this.Logger.WriteLine("Setting continue Reading to false due to read result in channel " + ch2.QualifiedId); - continueReading = false; - } - else - { - ch2.Input.AdvanceTo(readResult.Buffer.End); - } - } - catch (Exception error) - { - this.Logger.WriteLine("Caught error " + error.Message + " in OfferPipeWithError"); - errorThrown = error.Message.Contains(errorMessage); - continueReading = false; - } - } - - Assert.True(errorThrown); - } - [Fact] public async Task Backpressure() { From 19fc8f352f8fa871a9b689e4fecd744154f51b13 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Wed, 22 Jun 2022 22:54:31 -0700 Subject: [PATCH 11/77] Reimplemented formatter code for messpack to write error interaction --- .../MultiplexingStream.ControlCode.cs | 7 ++++ .../MultiplexingStream.Formatters.cs | 36 +++++++++++++++++++ .../MultiplexingStream.WriteError.cs | 2 -- src/Nerdbank.Streams/MultiplexingStream.cs | 5 +++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index 91a9b364..104f4619 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -44,6 +44,13 @@ internal enum ControlCode : byte /// allowing them to send more data. /// ContentProcessed, + + /// + /// Sent when a channel encountered an error writin data on a given channel. This is sent right before a + /// to indicate that all data can't be transmitted and the cause of + /// that error. + /// + ContentWritingError, } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index c0550733..493bb617 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -495,6 +495,42 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R return new Channel.AcceptanceParameters(remoteWindowSize); } + /// + /// Serializes an object using . + /// + /// An instance of that we want to seralize. + /// A which is the serialized version of the error. + internal ReadOnlySequence SerializeWriteError(WriteError error) + { + var errorSequence = new Sequence(); + var writer = new MessagePackWriter(errorSequence); + writer.WriteArrayHeader(2); + writer.WriteInt32(ProtocolVersion.Major); + writer.Write(error.ErrorMessage); + writer.Flush(); + return errorSequence.AsReadOnlySequence; + } + + internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError) + { + var reader = new MessagePackReader(serializedError); + if (reader.ReadArrayHeader() != 2) + { + // For now the error sequence will only contain the major version and the error message + return null; + } + + int senderVersion = reader.ReadInt32(); + if (senderVersion != ProtocolVersion.Major) + { + // For now a channel should only process write errors from channels with the same major version + return null; + } + + string errorMessage = reader.ReadString(); + return new WriteError(errorMessage); + } + protected virtual (FrameHeader Header, ReadOnlySequence Payload) DeserializeFrame(ReadOnlySequence frameSequence) { var reader = new MessagePackReader(frameSequence); diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs index cc5b3375..1ad229d9 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -3,8 +3,6 @@ namespace Nerdbank.Streams { - using MessagePack; - /// /// Contains the nested type. /// diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 55572d1e..f37ce9fc 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -208,6 +208,11 @@ private enum TraceEventId /// Raised when the protocol handshake is starting, to annouce the major version being used. /// HandshakeStarted, + + /// + /// Raised when we are tracing an event related to . + /// + WriteError, } /// From 8d3e2b8cd96b117cd17e1b1a912936f9dda367cf Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Wed, 22 Jun 2022 23:42:38 -0700 Subject: [PATCH 12/77] Rewrote client side code of dealing content write error --- .../MultiplexingStream.Channel.cs | 15 ++++- .../MultiplexingStream.Formatters.cs | 2 +- src/Nerdbank.Streams/MultiplexingStream.cs | 59 +++++++++++++++++++ .../MultiplexingStreamV2Tests.cs | 38 ++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 91bfdf77..51aa14f8 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -816,7 +816,13 @@ private async Task ProcessOutboundTransmissionsAsync() } else { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Completing channel {0} with exception {1}", this.QualifiedId, ex.Message); + } + await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); + this.MultiplexingStream.OnChannelWritingError(this, ex); } throw; @@ -896,14 +902,21 @@ private void Fault(Exception exception) { if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) { - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel Closing self due to exception: {0}", exception); + this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Fault called in {0} with exception message {1}", this.QualifiedId, exception.Message); } + bool alreadyFaulted = false; lock (this.SyncObject) { + alreadyFaulted = this.faultingException != null; this.faultingException ??= exception; } + if (!alreadyFaulted && (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false)) + { + this.TraceSource.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel {0} closing self due to exception: {1}", this.QualifiedId, exception); + } + this.mxStreamIOReader?.CancelPendingRead(); this.Dispose(); } diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 493bb617..09b73a1f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -499,7 +499,7 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R /// Serializes an object using . /// /// An instance of that we want to seralize. - /// A which is the serialized version of the error. + /// A which is the serialized version of the error. internal ReadOnlySequence SerializeWriteError(WriteError error) { var errorSequence = new Sequence(); diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index f37ce9fc..2bec806e 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1090,6 +1090,65 @@ private void OnChannelDisposed(Channel channel) } } + private void OnChannelWritingError(Channel channel, Exception exception) + { + Requires.NotNull(channel, nameof(channel)); + + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "onChannelWritingError called for {0} with exception {1}", channel.QualifiedId, exception); + } + + if (this.formatter is V1Formatter) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending WriteError frame from {0} since we are using a V1 Formatter", channel.QualifiedId); + } + + return; + } + + lock (this.syncObject) + { + // Only inform the remote side if this channel has not already been terminated. + if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) + { + WriteError error = new WriteError(exception.Message); + V2Formatter wrappedFormatter = (V2Formatter)this.formatter; + ReadOnlySequence serializedError = wrappedFormatter.SerializeWriteError(error); + + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Generated MessagePack object {0} for write error with message {1} in channel {2}", + serializedError, + error.ErrorMessage, + channel.QualifiedId); + } + + FrameHeader header = new FrameHeader + { + Code = ControlCode.ContentWritingError, + ChannelId = channel.QualifiedId, + }; + + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Sending write error with frame header {0} in channel {1}", header, channel.QualifiedId); + } + + this.SendFrame(header, serializedError, CancellationToken.None); + } + else if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending WriteError frame since channel {0} is terminated", channel.QualifiedId); + } + } + } + /// /// Indicates that the local end will not be writing any more data to this channel, /// leading to the transmission of a frame being sent for this channel. diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index e516f4fb..a2c0e035 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -19,6 +19,44 @@ public MultiplexingStreamV2Tests(ITestOutputHelper logger) protected override int ProtocolMajorVersion => 2; + [Fact] + public async Task OfferPipeWithError() + { + bool errorThrown = false; + string errorMessage = "Hello World"; + + // Prepare a readonly pipe that is already fully populated with data but also with an error + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + pipe.Writer.Complete(new NullReferenceException(errorMessage)); + + MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); + + bool readMoreData = true; + while (readMoreData) + { + try + { + ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); + if (readResult.IsCompleted || readResult.IsCanceled) + { + readMoreData = false; + } + + ch2.Input.AdvanceTo(readResult.Buffer.End); + } + catch (MultiplexingProtocolException exception) + { + errorThrown = exception.Message.Contains(errorMessage); + readMoreData = !errorThrown; + } + } + + Assert.True(errorThrown); + } + [Fact] public async Task Backpressure() { From b379da9886c35dd19eb55e9d82e7533907b1a332 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 23 Jun 2022 00:08:54 -0700 Subject: [PATCH 13/77] Rewrote code on remote to process write error but not close stream --- src/Nerdbank.Streams/MultiplexingStream.cs | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 2bec806e..0bc6d959 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -836,6 +836,9 @@ private async Task ReadStreamAsync() case ControlCode.ChannelTerminated: await this.OnChannelTerminatedAsync(header.RequiredChannelId).ConfigureAwait(false); break; + case ControlCode.ContentWritingError: + this.OnContentWritingError(header.RequiredChannelId, frame.Value.Payload); + break; default: break; } @@ -890,6 +893,94 @@ private async Task OnChannelTerminatedAsync(QualifiedChannelId channelId) } } + /// + /// Called when the channel receives a frame with code from the remote. + /// + /// The channel id of the sender of the frame. + /// The payload that the sender sent in the frame. + private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence payload) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "OnContentWritingError received from remote party {0}", channelId); + } + + if (this.formatter is V1Formatter) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting writing error from {0} as stream uses V1Formatter", channelId); + } + + return; + } + + Channel? channel; + lock (this.syncObject) + { + if (this.openChannels.ContainsKey(channelId)) + { + channel = this.openChannels[channelId]; + } + else + { + channel = null; + } + } + + if (channel == null) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from non open channel {0}", channelId); + } + + return; + } + + if (channelId.Source == ChannelSource.Local && !channel.IsAccepted) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from non accepted channel {0}", channelId); + } + + throw new MultiplexingProtocolException($"Remote party indicated they're done writing to channel {channelId} before accepting it."); + } + + if (channel.IsRejectedOrCanceled || this.channelsPendingTermination.Contains(channelId)) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from channel {0} as it is in an unwanted state", channelId); + } + + return; + } + + // Deserialize the payload + V2Formatter wrappedFormatter = (V2Formatter)this.formatter; + WriteError? error = wrappedFormatter.DeserializeWriteError(payload); + + if (error == null) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from channel {0} as can't deserialize payload {1} using {2}", channelId, payload, this.formatter); + } + + return; + } + + string errorMessage = error.ErrorMessage; + MultiplexingProtocolException channelClosingException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); + + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Content Writing Error closing channel {0} using error {1}", channelId, channelClosingException.Message); + } + } + private void OnContentWritingCompleted(QualifiedChannelId channelId) { Channel channel; @@ -1090,6 +1181,12 @@ private void OnChannelDisposed(Channel channel) } } + /// + /// Called when the local end was not able to completely write all the data to this channel due to an error, + /// leading to the transmission of a frame being sent for this channel. + /// + /// The channel whose writing was halted. + /// The exception that caused the writing to be haulted. private void OnChannelWritingError(Channel channel, Exception exception) { Requires.NotNull(channel, nameof(channel)); From 738e67b88a3eaeb330cd6c1296ecee13a7856cd2 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 23 Jun 2022 01:15:34 -0700 Subject: [PATCH 14/77] Implemented sketch solution to get OfferPipeWithError to pass --- .../MultiplexingStream.Channel.cs | 102 +++++++++++++++++- src/Nerdbank.Streams/MultiplexingStream.cs | 4 +- .../MultiplexingStreamV2Tests.cs | 4 +- 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 51aa14f8..3122c379 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -321,6 +321,16 @@ private long RemoteWindowRemaining /// private bool BackpressureSupportEnabled => this.MultiplexingStream.protocolMajorVersion > 1; + /// + /// Gets or sets a value indicating whether the open writers have been requested to be closed by onComplete. + /// + private bool WriterCompletedWithException { get; set; } + + /// + /// Gets or sets a value indicating whether the writers that need to be closed have been closed by onComplete. + /// + private bool WriterCompleteFinished { get; set; } + /// /// Closes this channel and releases all resources associated with it. /// @@ -330,6 +340,17 @@ private long RemoteWindowRemaining /// public void Dispose() { + if (this.WriterCompletedWithException && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Dispose called on {0} with writer excepted to close due to exception", this.QualifiedId); + + // Temp solution: Block until we have finished executing on Complete + while (!this.WriterCompleteFinished) + { + continue; + } + } + if (!this.IsDisposed) { this.acceptanceSource.TrySetCanceled(); @@ -415,6 +436,20 @@ internal async Task OnChannelTerminatedAsync() try { + if (this.WriterCompletedWithException && !this.IsDisposed) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Not completing rental writer inside OnChannelTerminatedAsync on channel {0} as we expect writer rental to be completed with error", + this.QualifiedId); + } + + return; + } + // We Complete the writer because only the writing (logical) thread should complete it // to avoid race conditions, and Channel.Dispose can be called from any thread. using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); @@ -466,6 +501,20 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence /// Called by the when when it will not be writing any more data to the channel. /// - internal void OnContentWritingCompleted() + /// Optional param used to indicate if we are stopping writing due to an error. + internal void OnContentWritingCompleted(Exception? error = null) { + if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Called OnContentWritingCompleted on {0} with exception {1}", this.QualifiedId, error.Message); + } + + if (this.WriterCompletedWithException) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting OnContentWritingCompleted call on {0} due to previous call with error", this.QualifiedId); + } + + return; + } + + this.WriterCompletedWithException = error != null; + this.DisposeSelfOnFailure(Task.Run(async delegate { if (!this.IsDisposed) @@ -484,14 +551,24 @@ internal void OnContentWritingCompleted() try { using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync().ConfigureAwait(false); + await writerRental.Writer.CompleteAsync(error).ConfigureAwait(false); + + if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Writing Completed closed writer rental on {0} with error {1}", this.QualifiedId, error.Message); + } } catch (ObjectDisposedException) { if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync().ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); + + if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Writing Completed closed mxStreamIOWriter on {0} with error {1}", this.QualifiedId, error.Message); + } } } } @@ -500,10 +577,17 @@ internal void OnContentWritingCompleted() if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync().ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(error).ConfigureAwait(false); + + if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Writing Completed closed mxStreamIOWriter on {0} with error {1}", this.QualifiedId, error.Message); + } } } + this.WriterCompleteFinished = true; + this.mxStreamIOWriterCompleted.Set(); })); } @@ -895,7 +979,14 @@ private async Task AutoCloseOnPipesClosureAsync() this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.ChannelAutoClosing, "Channel {0} \"{1}\" self-closing because both reader and writer are complete.", this.QualifiedId, this.Name); } - this.Dispose(); + if (!this.WriterCompletedWithException) + { + this.Dispose(); + } + else if (this.WriterCompletedWithException && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not calling Dispose inside AutoClose method on channel {0} as we except writer to be completed with error", this.QualifiedId); + } } private void Fault(Exception exception) @@ -918,6 +1009,7 @@ private void Fault(Exception exception) } this.mxStreamIOReader?.CancelPendingRead(); + this.WriterCompleteFinished = true; this.Dispose(); } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 0bc6d959..fbc05411 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -979,6 +979,8 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc { this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Content Writing Error closing channel {0} using error {1}", channelId, channelClosingException.Message); } + + channel.OnContentWritingCompleted(channelClosingException); } private void OnContentWritingCompleted(QualifiedChannelId channelId) @@ -1193,7 +1195,7 @@ private void OnChannelWritingError(Channel channel, Exception exception) if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "onChannelWritingError called for {0} with exception {1}", channel.QualifiedId, exception); + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "onChannelWritingError called for {0} with exception {1}", channel.QualifiedId, exception.Message); } if (this.formatter is V1Formatter) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index a2c0e035..ea25483b 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -43,14 +43,16 @@ public async Task OfferPipeWithError() if (readResult.IsCompleted || readResult.IsCanceled) { readMoreData = false; + this.Logger.WriteLine("Set readMoreData to False based on readResult fields"); } ch2.Input.AdvanceTo(readResult.Buffer.End); } - catch (MultiplexingProtocolException exception) + catch (Exception exception) { errorThrown = exception.Message.Contains(errorMessage); readMoreData = !errorThrown; + this.Logger.WriteLine("Set readMoreData to " + readMoreData + " based on catching error with message " + exception.Message); } } From 61a6eddd9941740c89d41d9c59da5011fc1fefca Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 25 Jun 2022 13:36:31 -0700 Subject: [PATCH 15/77] Changed dispose to close writer with exception if exception passed in onComplete --- .../MultiplexingStream.Channel.cs | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 3122c379..54866140 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -142,6 +142,11 @@ public class Channel : IDisposableObservable, IDuplexPipe /// private bool? existingPipeGiven; + /// + /// The exception to close any that sends data to the user, defaults to null. + /// + private Exception? writerCompletionException = null; + /// /// Initializes a new instance of the class. /// @@ -321,16 +326,6 @@ private long RemoteWindowRemaining /// private bool BackpressureSupportEnabled => this.MultiplexingStream.protocolMajorVersion > 1; - /// - /// Gets or sets a value indicating whether the open writers have been requested to be closed by onComplete. - /// - private bool WriterCompletedWithException { get; set; } - - /// - /// Gets or sets a value indicating whether the writers that need to be closed have been closed by onComplete. - /// - private bool WriterCompleteFinished { get; set; } - /// /// Closes this channel and releases all resources associated with it. /// @@ -340,17 +335,6 @@ private long RemoteWindowRemaining /// public void Dispose() { - if (this.WriterCompletedWithException && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Dispose called on {0} with writer excepted to close due to exception", this.QualifiedId); - - // Temp solution: Block until we have finished executing on Complete - while (!this.WriterCompleteFinished) - { - continue; - } - } - if (!this.IsDisposed) { this.acceptanceSource.TrySetCanceled(); @@ -382,7 +366,7 @@ public void Dispose() mxStreamIOWriter = self.mxStreamIOWriter; } - mxStreamIOWriter?.Complete(); + mxStreamIOWriter?.Complete(self.writerCompletionException); self.mxStreamIOWriterCompleted.Set(); } finally @@ -436,7 +420,7 @@ internal async Task OnChannelTerminatedAsync() try { - if (this.WriterCompletedWithException && !this.IsDisposed) + if (this.writerCompletionException != null) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -501,7 +485,7 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence Date: Mon, 27 Jun 2022 23:28:24 -0700 Subject: [PATCH 16/77] Changed to using faulting exception instead of custom created field --- .../MultiplexingStream.Channel.cs | 85 +++++++------------ .../MultiplexingStream.WriteError.cs | 2 +- .../MultiplexingStreamTests.cs | 40 +++++++++ .../MultiplexingStreamV2Tests.cs | 40 --------- 4 files changed, 74 insertions(+), 93 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 54866140..d6ab0016 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -143,9 +143,9 @@ public class Channel : IDisposableObservable, IDuplexPipe private bool? existingPipeGiven; /// - /// The exception to close any that sends data to the user, defaults to null. + /// A value indicating whether this received an error from a remote party in . /// - private Exception? writerCompletionException = null; + private bool receivedRemoteException; /// /// Initializes a new instance of the class. @@ -366,7 +366,7 @@ public void Dispose() mxStreamIOWriter = self.mxStreamIOWriter; } - mxStreamIOWriter?.Complete(self.writerCompletionException); + mxStreamIOWriter?.Complete(self.GetWriterException()); self.mxStreamIOWriterCompleted.Set(); } finally @@ -420,24 +420,10 @@ internal async Task OnChannelTerminatedAsync() try { - if (this.writerCompletionException != null) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Not completing rental writer inside OnChannelTerminatedAsync on channel {0} as we expect writer rental to be completed with error", - this.QualifiedId); - } - - return; - } - // We Complete the writer because only the writing (logical) thread should complete it // to avoid race conditions, and Channel.Dispose can be called from any thread. using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync().ConfigureAwait(false); + await writerRental.Writer.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -485,23 +471,9 @@ internal async ValueTask OnContentAsync(FrameHeader header, ReadOnlySequence when when it will not be writing any more data to the channel. /// /// Optional param used to indicate if we are stopping writing due to an error. - internal void OnContentWritingCompleted(Exception? error = null) + internal void OnContentWritingCompleted(MultiplexingProtocolException? error = null) { - if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + if (this.receivedRemoteException) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Called OnContentWritingCompleted on {0} with exception {1}", this.QualifiedId, error.Message); + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Current call to onContentWritingCompleted ignored due to previous call with error on {0}", this.QualifiedId); + } + + return; } - if (this.writerCompletionException != null) + if (error != null) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting OnContentWritingCompleted call on {0} due to previous call with error", this.QualifiedId); + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Processed error from remote on {0} with message {1}", this.QualifiedId, error.Message); } - return; + lock (this.SyncObject) + { + this.faultingException = error; + this.receivedRemoteException = true; + } } - this.writerCompletionException = error; - this.DisposeSelfOnFailure(Task.Run(async delegate { if (!this.IsDisposed) @@ -535,7 +514,7 @@ internal void OnContentWritingCompleted(Exception? error = null) try { using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync(this.writerCompletionException).ConfigureAwait(false); + await writerRental.Writer.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -547,7 +526,7 @@ internal void OnContentWritingCompleted(Exception? error = null) if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync(this.writerCompletionException).ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -561,7 +540,7 @@ internal void OnContentWritingCompleted(Exception? error = null) if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync(this.writerCompletionException).ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -688,6 +667,15 @@ private async ValueTask GetReceivedMessagePipeWriterAsync(Canc } } + /// + /// Gets the to close any that the channel is managing. + /// + /// The exception sent from the remote if there is one, null otherwise. + private Exception? GetWriterException() + { + return this.receivedRemoteException ? this.faultingException : null; + } + /// /// Apply channel options to this channel, including setting up or linking to an user-supplied pipe writer/reader pair. /// @@ -961,14 +949,7 @@ private async Task AutoCloseOnPipesClosureAsync() this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.ChannelAutoClosing, "Channel {0} \"{1}\" self-closing because both reader and writer are complete.", this.QualifiedId, this.Name); } - if (this.writerCompletionException == null) - { - this.Dispose(); - } - else if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not calling Dispose inside AutoClose method on channel {0} as we except writer to be completed with error", this.QualifiedId); - } + this.Dispose(); } private void Fault(Exception exception) diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs index 1ad229d9..628bd4dc 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -12,7 +12,7 @@ public partial class MultiplexingStream /// A class containing information about a write error and which is sent to the /// remote alongside . /// - public class WriteError + internal class WriteError { /// /// Initializes a new instance of the class. diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 5391c218..a6f6d68d 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -36,6 +36,46 @@ public MultiplexingStreamTests(ITestOutputHelper logger) protected virtual int ProtocolMajorVersion { get; } = 1; + [Fact] + public async Task OfferPipeWithError() + { + bool errorThrown = false; + string errorMessage = "Hello World"; + + // Prepare a readonly pipe that is already fully populated with data but also with an error + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + pipe.Writer.Complete(new NullReferenceException(errorMessage)); + + MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); + + bool readMoreData = true; + while (readMoreData) + { + try + { + ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); + if (readResult.IsCompleted || readResult.IsCanceled) + { + readMoreData = false; + this.Logger.WriteLine("Set readMoreData to False based on readResult fields"); + } + + ch2.Input.AdvanceTo(readResult.Buffer.End); + } + catch (Exception exception) + { + errorThrown = exception.Message.Contains(errorMessage); + readMoreData = !errorThrown; + this.Logger.WriteLine("Set readMoreData to " + readMoreData + " based on catching error with message " + exception.Message); + } + } + + Assert.Equal(this.ProtocolMajorVersion > 1, errorThrown); + } + public async Task InitializeAsync() { var mx1TraceSource = new TraceSource(nameof(this.mx1), SourceLevels.All); diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index ea25483b..e516f4fb 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -19,46 +19,6 @@ public MultiplexingStreamV2Tests(ITestOutputHelper logger) protected override int ProtocolMajorVersion => 2; - [Fact] - public async Task OfferPipeWithError() - { - bool errorThrown = false; - string errorMessage = "Hello World"; - - // Prepare a readonly pipe that is already fully populated with data but also with an error - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - pipe.Writer.Complete(new NullReferenceException(errorMessage)); - - MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); - await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); - - bool readMoreData = true; - while (readMoreData) - { - try - { - ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); - if (readResult.IsCompleted || readResult.IsCanceled) - { - readMoreData = false; - this.Logger.WriteLine("Set readMoreData to False based on readResult fields"); - } - - ch2.Input.AdvanceTo(readResult.Buffer.End); - } - catch (Exception exception) - { - errorThrown = exception.Message.Contains(errorMessage); - readMoreData = !errorThrown; - this.Logger.WriteLine("Set readMoreData to " + readMoreData + " based on catching error with message " + exception.Message); - } - } - - Assert.True(errorThrown); - } - [Fact] public async Task Backpressure() { From 1cc32722225e83e1f91e7063bf3b38be50ca9356 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sun, 24 Jul 2022 19:53:20 -0700 Subject: [PATCH 17/77] Updated formatter for error message --- src/nerdbank-streams/src/MultiplexingStreamFormatters.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index af625669..96be8a1f 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -293,6 +293,14 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } + serializeErrorMessage(errorMessage : string) : Buffer { + return msgpack.encode([errorMessage]) + } + + deserializeErrorMessage(payload : Buffer) : string { + return msgpack.decode(payload)[0] + } + protected async readMessagePackAsync(cancellationToken: CancellationToken): Promise<{} | [] | null> { const streamEnded = new Deferred(); while (true) { From 425a208be1e111073f393dcbc1fb58c6d8c3b6ec Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sun, 24 Jul 2022 20:25:35 -0700 Subject: [PATCH 18/77] Modified formatter to handle errors --- src/nerdbank-streams/src/ControlCode.ts | 6 ++++ .../src/MultiplexingStream.ts | 33 +++++++++++++++++++ .../src/MultiplexingStreamFormatters.ts | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/nerdbank-streams/src/ControlCode.ts b/src/nerdbank-streams/src/ControlCode.ts index 3ae71fed..be49e0e5 100644 --- a/src/nerdbank-streams/src/ControlCode.ts +++ b/src/nerdbank-streams/src/ControlCode.ts @@ -32,4 +32,10 @@ export enum ControlCode { * Sent when a channel has finished processing data received from the remote party, allowing them to send more data. */ ContentProcessed, + + /** + * Sent if there was an error transmitting message on the given channel. This frame only gets sent for ProtocolVersion >= 1 + * and the receiving end must also be >= 1 + */ + ContentWritingError, } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 1f6eafcc..d6d04ef0 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -585,6 +585,20 @@ export class MultiplexingStreamClass extends MultiplexingStream { } } + public onChannelWritingError(channel : ChannelClass, errorMessage? : string) { + // Perform error check to ensure that we can send the frame + if(this.formatter instanceof MultiplexingStreamV1Formatter) { + return; + } + if(channel.isDisposed || !this.getOpenChannel(channel.qualifiedId)) { + return; + } + // Convert the error message into a payload and send that + let errorFormatter = this.formatter as MultiplexingStreamV2Formatter; + let errorPayload = errorFormatter.serializeErrorMessage(errorMessage); + this.rejectOnFailure(this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload)); + } + public async onChannelDisposed(channel: ChannelClass) { if (!this._completionSource.isCompleted) { try { @@ -632,6 +646,9 @@ export class MultiplexingStreamClass extends MultiplexingStream { case ControlCode.ChannelTerminated: this.onChannelTerminated(frame.header.requiredChannel); break; + case ControlCode.ContentWritingError: + this.onContentWritingError(frame.header.requiredChannel, frame.payload); + break; default: break; } @@ -716,6 +733,22 @@ export class MultiplexingStreamClass extends MultiplexingStream { channel.onContentProcessed(bytesProcessed); } + private onContentWritingError(channelId : QualifiedChannelId, payload : Buffer) { + // Ensure that we should process the enrror message + if (this.formatter as MultiplexingStreamV1Formatter) { + return; + } + const channel = this.getOpenChannel(channelId); + if (!channel) { + throw new Error(`No channel with id ${channelId} found.`); + } + // Convert the payload into an error + let errorFormatter = this.formatter as MultiplexingStreamV2Formatter; + let errorMessage = errorFormatter.deserializeErrorMessage(payload); + let remoteError = new Error(errorMessage); + throw remoteError; + } + private onContentWritingCompleted(channelId: QualifiedChannelId) { const channel = this.getOpenChannel(channelId); if (!channel) { diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index 96be8a1f..8947c794 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -293,7 +293,7 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } - serializeErrorMessage(errorMessage : string) : Buffer { + serializeErrorMessage(errorMessage? : string) : Buffer { return msgpack.encode([errorMessage]) } From 90f0fd3ab73e13d310bd785e0d4c118b5564a2d0 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sun, 24 Jul 2022 20:43:30 -0700 Subject: [PATCH 19/77] Implemented error handling in the channel --- src/nerdbank-streams/src/Channel.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index eae435dd..0e8c8161 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -71,6 +71,7 @@ export class ChannelClass extends Channel { private readonly _completion = new Deferred(); public localWindowSize?: number; private remoteWindowSize?: number; + private remoteError?: Error; /** * The number of bytes transmitted from here but not yet acknowledged as processed from there, @@ -214,7 +215,16 @@ export class ChannelClass extends Channel { return this._acceptance.resolve(); } - public onContent(buffer: Buffer | null) { + public onContent(buffer: Buffer | null, error?: Error) { + // Already received remote error so don't process any future messages + if (this.remoteError) { + return; + } + + if (error) { + this.remoteError = error; + } + this._duplex.push(buffer); // We should find a way to detect when we *actually* share the received buffer with the Channel's user @@ -244,15 +254,23 @@ export class ChannelClass extends Channel { } } - public async dispose() { + public async dispose(errorToSend? : Error) { if (!this.isDisposed) { super.dispose(); + if (errorToSend) { + await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); + } + this._acceptance.reject(new CancellationToken.CancellationError("disposed")); // For the pipes, we Complete *our* ends, and leave the user's ends alone. // The completion will propagate when it's ready to. - this._duplex.end(); + if (this.remoteError) { + this._duplex.destroy(this.remoteError); + } else { + this._duplex.end(); + } this._duplex.push(null); this._completion.resolve(); From d3d270d0a1a773f78aeb70693f9dd17cda2c3730 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sun, 24 Jul 2022 21:02:58 -0700 Subject: [PATCH 20/77] Wrote unit test --- src/nerdbank-streams/src/Channel.ts | 15 ++++++++---- .../src/tests/MultiplexingStream.spec.ts | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 0e8c8161..01029942 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -54,9 +54,10 @@ export abstract class Channel implements IDisposableObservable { } /** - * Closes this channel. + * Closes this channel. If an error is passed in then that error message is sent + * to the remote side. */ - public dispose() { + public async dispose(error? : Error) { // The interesting stuff is in the derived class. this._isDisposed = true; } @@ -272,8 +273,14 @@ export class ChannelClass extends Channel { this._duplex.end(); } this._duplex.push(null); - - this._completion.resolve(); + + // Reject or Resolve the completion based on the remote error + if (this.remoteError) { + this._completion.reject(this.remoteError); + } else { + this._completion.resolve(); + } + await this._multiplexingStream.onChannelDisposed(this); } } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index e78cc49a..2e76477f 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -35,6 +35,29 @@ import * as assert from "assert"; } }); + it("Encountered error writing content", async() => { + const errorMessage = "Sending error to the remote that there was an writing error"; + const errorToSend = new Error(errorMessage); + + const channels = await Promise.all([ + mx1.offerChannelAsync("test"), + mx2.acceptChannelAsync("test"), + ]); + channels[0].stream.write("abc"); + await channels[0].dispose(errorToSend); + + // Ensure that the message is completed with an error + let caughtError = false; + try { + await channels[1].completion; + } catch(error) { + caughtError = true; + } + + assert.deepStrictEqual(protocolMajorVersion > 1, caughtError); + + }); + it("CreateAsync rejects null stream", async () => { expectThrow(MultiplexingStream.CreateAsync(null!)); expectThrow(MultiplexingStream.CreateAsync(undefined!)); From 783068c693c03d3b3fcf9c62f67359ac14ea5d4a Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 25 Jul 2022 07:20:12 -0700 Subject: [PATCH 21/77] Passing basic error test --- src/nerdbank-streams/src/Channel.ts | 8 +++++--- src/nerdbank-streams/src/MultiplexingStream.ts | 11 ++++++----- .../src/tests/MultiplexingStream.spec.ts | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 01029942..a3cd97c7 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -257,11 +257,12 @@ export class ChannelClass extends Channel { public async dispose(errorToSend? : Error) { if (!this.isDisposed) { + super.dispose(); if (errorToSend) { await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); - } + } this._acceptance.reject(new CancellationToken.CancellationError("disposed")); @@ -275,13 +276,14 @@ export class ChannelClass extends Channel { this._duplex.push(null); // Reject or Resolve the completion based on the remote error - if (this.remoteError) { - this._completion.reject(this.remoteError); + if (errorToSend?? this.remoteError) { + this._completion.reject(errorToSend?? this.remoteError); } else { this._completion.resolve(); } await this._multiplexingStream.onChannelDisposed(this); + } } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index d6d04ef0..24bf524b 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -585,18 +585,18 @@ export class MultiplexingStreamClass extends MultiplexingStream { } } - public onChannelWritingError(channel : ChannelClass, errorMessage? : string) { + public async onChannelWritingError(channel : ChannelClass, errorMessage? : string) { // Perform error check to ensure that we can send the frame if(this.formatter instanceof MultiplexingStreamV1Formatter) { return; } - if(channel.isDisposed || !this.getOpenChannel(channel.qualifiedId)) { + if(!this.getOpenChannel(channel.qualifiedId)) { return; } // Convert the error message into a payload and send that let errorFormatter = this.formatter as MultiplexingStreamV2Formatter; let errorPayload = errorFormatter.serializeErrorMessage(errorMessage); - this.rejectOnFailure(this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload)); + await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); } public async onChannelDisposed(channel: ChannelClass) { @@ -735,7 +735,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { private onContentWritingError(channelId : QualifiedChannelId, payload : Buffer) { // Ensure that we should process the enrror message - if (this.formatter as MultiplexingStreamV1Formatter) { + if (this.formatter instanceof MultiplexingStreamV1Formatter) { return; } const channel = this.getOpenChannel(channelId); @@ -746,7 +746,8 @@ export class MultiplexingStreamClass extends MultiplexingStream { let errorFormatter = this.formatter as MultiplexingStreamV2Formatter; let errorMessage = errorFormatter.deserializeErrorMessage(payload); let remoteError = new Error(errorMessage); - throw remoteError; + channel.onContent(null, remoteError); + } private onContentWritingCompleted(channelId: QualifiedChannelId) { diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 2e76477f..892d2d45 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -43,7 +43,7 @@ import * as assert from "assert"; mx1.offerChannelAsync("test"), mx2.acceptChannelAsync("test"), ]); - channels[0].stream.write("abc"); + await channels[0].dispose(errorToSend); // Ensure that the message is completed with an error From 9a28a50b5854819e7e927429213b938b1e60b5aa Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 25 Jul 2022 10:50:17 -0700 Subject: [PATCH 22/77] Updated documentation --- src/nerdbank-streams/src/Channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index a3cd97c7..29b68beb 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -55,7 +55,7 @@ export abstract class Channel implements IDisposableObservable { /** * Closes this channel. If an error is passed in then that error message is sent - * to the remote side. + * to the remote side before dispoing the channel. */ public async dispose(error? : Error) { // The interesting stuff is in the derived class. From d2ed5433ee27b40a70042b26d4c478c69c793c57 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:20:28 -0700 Subject: [PATCH 23/77] Channels get completed with errors Errors sent between streams are thrown when the respective channels are completed --- .../MultiplexingStream.Channel.cs | 16 +++++++++- .../MultiplexingStream.ChannelOptions.cs | 3 ++ .../MultiplexingStreamTests.cs | 30 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 750a624a..3d16b982 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -406,7 +406,16 @@ public void Dispose() this.remoteWindowHasCapacity.Set(); this.disposalTokenSource.Cancel(); - this.completionSource.TrySetResult(null); + + if (this.faultingException != null) + { + this.completionSource.TrySetException(this.faultingException); + } + else + { + this.completionSource.TrySetResult(null); + } + this.MultiplexingStream.OnChannelDisposed(this); } } @@ -897,6 +906,11 @@ private async Task ProcessOutboundTransmissionsAsync() this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Completing channel {0} with exception {1}", this.QualifiedId, ex.Message); } + lock (this.SyncObject) + { + this.faultingException = ex; + } + await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); this.MultiplexingStream.OnChannelWritingError(this, ex); } diff --git a/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs b/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs index 8adddd80..58002c73 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs @@ -50,6 +50,9 @@ public ChannelOptions() /// The specified in *must* be created with that *exceeds* /// the value of and . /// + /// + /// If set to an pipe that was completed with an error, then that error gets sents to the remote using a frame. + /// /// /// Thrown if set to an that returns null for either of its properties. public IDuplexPipe? ExistingPipe diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 67a049ee..92105b4d 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -74,6 +74,36 @@ public async Task OfferPipeWithError() } Assert.Equal(this.ProtocolMajorVersion > 1, errorThrown); + + // Ensure that both the reader and write are completed with an error if we are using protocol version > 1 + string expectedWriterErrorMessage = errorMessage; + string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; + + bool writeCompletedWithError = false; + try + { + await ch1.Completion; + } + catch (Exception writeException) + { + this.Logger.WriteLine($"Caught error {writeException.Message} in the completion of the writer"); + writeCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); + } + + Assert.Equal(this.ProtocolMajorVersion > 1, writeCompletedWithError); + + bool readCompletedWithError = false; + try + { + await ch2.Completion; + } + catch (Exception readException) + { + this.Logger.WriteLine($"Caught error {readException.Message} in the completion of the reader"); + readCompletedWithError = readException.Message.Contains(expectedReaderErrorMessage); + } + + Assert.Equal(this.ProtocolMajorVersion > 1, readCompletedWithError); } public async Task InitializeAsync() From 7d23ff7aa73871e1e1e467fd654b5b2f3039c5b9 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 30 Jul 2022 15:08:33 -0700 Subject: [PATCH 24/77] Changed C# Stream to complete with errors --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 3 ++- test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 3d16b982..84ea2989 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -110,7 +110,8 @@ public class Channel : IDisposableObservable, IDuplexPipe private Exception? faultingException; /// - /// The to use to get data to be transmitted over the . + /// The to use to get data to be transmitted over the . Any errors passed to this + /// are transmitted to the remote side. /// private PipeReader? mxStreamIOReader; diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 92105b4d..cb42b423 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -75,11 +75,10 @@ public async Task OfferPipeWithError() Assert.Equal(this.ProtocolMajorVersion > 1, errorThrown); - // Ensure that both the reader and write are completed with an error if we are using protocol version > 1 + // Ensure that the writer of the error completes with that error, no matter what version of the protocol they are using string expectedWriterErrorMessage = errorMessage; - string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; - bool writeCompletedWithError = false; + try { await ch1.Completion; @@ -90,9 +89,12 @@ public async Task OfferPipeWithError() writeCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); } - Assert.Equal(this.ProtocolMajorVersion > 1, writeCompletedWithError); + Assert.True(writeCompletedWithError); + // Ensure that the reader only completes with an error if we are using a protocol version > 1 + string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; bool readCompletedWithError = false; + try { await ch2.Completion; From 75496f76185223244ecd5d176b031b1bd6297f6b Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 30 Jul 2022 16:11:09 -0700 Subject: [PATCH 25/77] Added public field of remote exception to easily access remote error --- .../MultiplexingStream.Channel.cs | 6 ++++++ .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + test/Nerdbank.Streams.Interop.Tests/Program.cs | 16 ++++++++++++++-- .../MultiplexingStreamTests.cs | 4 ++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 84ea2989..9345bba9 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -271,6 +271,12 @@ public PipeWriter Output /// public Task Completion => this.completionSource.Task; + /// + /// Gets the duue to which the remote shut down communication due to. + /// Returns null if the remote channel didn't send any errors. + /// + public Exception? RemoteException => this.GetWriterException(); + /// /// Gets the underlying instance. /// diff --git a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt index b00dc10d..1f491d39 100644 --- a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ Nerdbank.Streams.BufferWriterExtensions +Nerdbank.Streams.MultiplexingStream.Channel.RemoteException.get -> System.Exception? Nerdbank.Streams.ReadOnlySequenceExtensions Nerdbank.Streams.StreamPipeReader Nerdbank.Streams.StreamPipeReader.Read() -> System.IO.Pipelines.ReadResult diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index d5b5a46e..292977bd 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -75,8 +75,20 @@ private async Task ClientOfferAsync() { MultiplexingStream.Channel? channel = await this.mx.AcceptChannelAsync("clientOffer"); (StreamReader r, StreamWriter w) = CreateStreamIO(channel); - string? line = await r.ReadLineAsync(); - await w.WriteLineAsync($"recv: {line}"); + + // Determine the response to send back based on whether an exception was sent + string? response; + if (channel.RemoteException == null) + { + string? line = await r.ReadLineAsync(); + response = "recv: " + line; + } + else + { + response = "rece: " + channel.RemoteException?.Message; + } + + await w.WriteLineAsync(response); } private async Task ServerOfferAsync() diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index cb42b423..3d468443 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -106,6 +106,10 @@ public async Task OfferPipeWithError() } Assert.Equal(this.ProtocolMajorVersion > 1, readCompletedWithError); + + // Also ensure that the remote error field gets set properly on both the channels + Assert.Null(ch1.RemoteException); + Assert.Equal(this.ProtocolMajorVersion > 1, ch2.RemoteException != null); } public async Task InitializeAsync() From 9116de1b879d1e143a892f17feb59771666a1566 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 30 Jul 2022 16:21:35 -0700 Subject: [PATCH 26/77] Added interop test to ensure that error is sent properly --- .../src/tests/MultiplexingStream.Interop.spec.ts | 12 ++++++++++++ .../src/tests/MultiplexingStream.spec.ts | 2 +- test/Nerdbank.Streams.Interop.Tests/Program.cs | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index 15341867..41a50884 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -86,6 +86,18 @@ import { ChannelOptions } from "../ChannelOptions"; expect(recv).toEqual(`recv: ${bigdata}`); }); + if (protocolMajorVersion > 1) { + it("Can send error to remote", async () => { + const errorMessage = "Couldn't write all the data"; + const errorToSend = new Error(errorMessage); + + const channel = await mx.offerChannelAsync("clientOffer"); + await channel.dispose(errorToSend); + const recv = await readLineAsync(channel.stream); + expect(recv).toEqual(`Received error: ${errorMessage}`); + }) + } + if (protocolMajorVersion >= 3) { it("Can communicate over seeded channel", async () => { const channel = mx.acceptChannel(0); diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 892d2d45..e4f5571e 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -36,7 +36,7 @@ import * as assert from "assert"; }); it("Encountered error writing content", async() => { - const errorMessage = "Sending error to the remote that there was an writing error"; + const errorMessage = "Couldn't write all the data"; const errorToSend = new Error(errorMessage); const channels = await Promise.all([ diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index 292977bd..2a48acdf 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -85,7 +85,7 @@ private async Task ClientOfferAsync() } else { - response = "rece: " + channel.RemoteException?.Message; + response = "Received error: " + channel.RemoteException?.Message; } await w.WriteLineAsync(response); From 7932c26e14ff3902c008e4b0ebc881a7f2329e5a Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Tue, 2 Aug 2022 22:55:04 -0700 Subject: [PATCH 27/77] Changed C# code to not have public field --- .../MultiplexingStream.Channel.cs | 6 ----- .../Nerdbank.Streams.Interop.Tests/Program.cs | 27 +++++++++++++------ .../MultiplexingStreamTests.cs | 4 --- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 9345bba9..84ea2989 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -271,12 +271,6 @@ public PipeWriter Output /// public Task Completion => this.completionSource.Task; - /// - /// Gets the duue to which the remote shut down communication due to. - /// Returns null if the remote channel didn't send any errors. - /// - public Exception? RemoteException => this.GetWriterException(); - /// /// Gets the underlying instance. /// diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index 2a48acdf..29262454 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -61,6 +61,7 @@ private static (StreamReader Reader, StreamWriter Writer) CreateStreamIO(Multipl private async Task RunAsync(int protocolMajorVersion) { this.ClientOfferAsync().Forget(); + this.ClientOfferErrorAsync().Forget(); this.ServerOfferAsync().Forget(); if (protocolMajorVersion >= 3) @@ -75,20 +76,30 @@ private async Task ClientOfferAsync() { MultiplexingStream.Channel? channel = await this.mx.AcceptChannelAsync("clientOffer"); (StreamReader r, StreamWriter w) = CreateStreamIO(channel); + string? line = await r.ReadLineAsync(); + await w.WriteLineAsync($"recv: {line}"); + } + + private async Task ClientOfferErrorAsync() + { + // Await both of the channels from the sender, one to read the error and the other to return the response + MultiplexingStream.Channel? incomingChannel = await this.mx.AcceptChannelAsync("clientOffer"); + MultiplexingStream.Channel? outgoingChannel = await this.mx.AcceptChannelAsync("clientResponseOffer"); - // Determine the response to send back based on whether an exception was sent - string? response; - if (channel.RemoteException == null) + // Determine the response to send back on the whether the incoming channel completed with an exception + string? responseMessage = "didn't receive any errors"; + try { - string? line = await r.ReadLineAsync(); - response = "recv: " + line; + await incomingChannel.Completion; } - else + catch (Exception error) { - response = "Received error: " + channel.RemoteException?.Message; + responseMessage = "received error: " + error.Message; } - await w.WriteLineAsync(response); + // Create a writer using the outgoing channel and send the response to the sender + (StreamReader _, StreamWriter writer) = CreateStreamIO(outgoingChannel); + await writer.WriteLineAsync(responseMessage); } private async Task ServerOfferAsync() diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 3d468443..cb42b423 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -106,10 +106,6 @@ public async Task OfferPipeWithError() } Assert.Equal(this.ProtocolMajorVersion > 1, readCompletedWithError); - - // Also ensure that the remote error field gets set properly on both the channels - Assert.Null(ch1.RemoteException); - Assert.Equal(this.ProtocolMajorVersion > 1, ch2.RemoteException != null); } public async Task InitializeAsync() From e6ffbec9f1d4b7e5bcda894a321c0afa59850af5 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:40:49 -0700 Subject: [PATCH 28/77] Code push --- src/nerdbank-streams/src/Channel.ts | 6 ++- src/nerdbank-streams/src/Deferred.ts | 10 +++- .../src/MultiplexingStream.ts | 37 ++++++++++----- .../src/tests/MultiplexingStream.spec.ts | 47 ++++++++++++------- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 29b68beb..c5a7948d 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -271,7 +271,11 @@ export class ChannelClass extends Channel { if (this.remoteError) { this._duplex.destroy(this.remoteError); } else { - this._duplex.end(); + try { + this._duplex.end(); + } catch(error) { + console.log(`Caught error in call to duplex end of ${error}`) + } } this._duplex.push(null); diff --git a/src/nerdbank-streams/src/Deferred.ts b/src/nerdbank-streams/src/Deferred.ts index 7e53c8aa..c6046b0e 100644 --- a/src/nerdbank-streams/src/Deferred.ts +++ b/src/nerdbank-streams/src/Deferred.ts @@ -66,8 +66,14 @@ export class Deferred { if (this.isCompleted) { return false; } - - this.rejectPromise(reason); + + console.log(`Entering try/catch block inside reject`); + try { + this.rejectPromise(reason); + } catch(error) { + console.log(`Reject Promise threw err inside of deffered: ${error}`); + } + this._error = reason; this._isRejected = true; return true; diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 24bf524b..dec194cf 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -376,9 +376,11 @@ export abstract class MultiplexingStream implements IDisposableObservable { * Disposes the stream. */ public dispose() { + console.log(`Going to dispose multiplexing stream`); this.disposalTokenSource.cancel(); this._completionSource.resolve(); this.formatter.end(); + console.log(`Going to iterae through the channels`); [this.locallyOfferedOpenChannels, this.remotelyOfferedOpenChannels].forEach(cb => { for (const channelId in cb) { if (cb.hasOwnProperty(channelId)) { @@ -386,8 +388,12 @@ export abstract class MultiplexingStream implements IDisposableObservable { // Acceptance gets rejected when a channel is disposed. // Avoid a node.js crash or test failure for unobserved channels (e.g. offers for channels from the other party that no one cared to receive on this side). - caught(channel.acceptance); - channel.dispose(); + try { + caught(channel.acceptance); + channel.dispose(); + } catch(error) { + console.log(`Caught error when disposing channel ${channelId} `); + } } } }); @@ -542,17 +548,24 @@ export class MultiplexingStreamClass extends MultiplexingStream { header: FrameHeader, payload?: Buffer, cancellationToken: CancellationToken = CancellationToken.CONTINUE): Promise { - - if (!header) { - throw new Error("Header is required."); + try { + if (!header) { + throw new Error("Header is required."); + } + + await this.sendingSemaphore.use(async () => { + cancellationToken.throwIfCancelled(); + throwIfDisposed(this); + try { + await this.formatter.writeFrameAsync(header, payload); + } catch(error) { + console.log(`Caught error in writing using formatter: ${error}`) + } + + }); + } catch(error) { + console.log(`Caught error in send frame async: ${error}`) } - - await this.sendingSemaphore.use(async () => { - cancellationToken.throwIfCancelled(); - throwIfDisposed(this); - - await this.formatter.writeFrameAsync(header, payload); - }); } /** diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index e4f5571e..1da17c98 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -25,13 +25,21 @@ import * as assert from "assert"; afterEach(async () => { if (mx1) { - mx1.dispose(); - await mx1.completion; + try { + mx1.dispose(); + await mx1.completion; + } catch(error) { + console.log(`Multplexing Stream 1 after each error: ${error}`) + } } if (mx2) { - mx2.dispose(); - await mx2.completion; + try { + mx2.dispose(); + await mx2.completion; + } catch(error) { + console.log(`Multplexing Stream 1 after each error: ${error}`) + } } }); @@ -39,23 +47,30 @@ import * as assert from "assert"; const errorMessage = "Couldn't write all the data"; const errorToSend = new Error(errorMessage); - const channels = await Promise.all([ - mx1.offerChannelAsync("test"), - mx2.acceptChannelAsync("test"), - ]); + try { + console.log(`Going to create channels`); + const channels = await Promise.all([ + mx1.offerChannelAsync("test"), + mx2.acceptChannelAsync("test"), + ]); - await channels[0].dispose(errorToSend); + console.log(`Going to dispose first channel`); + await channels[0].dispose(errorToSend); - // Ensure that the message is completed with an error - let caughtError = false; - try { - await channels[1].completion; + // Ensure that the message is completed with an error + console.log(`Going to check second channel`); + let caughtError = false; + try { + await channels[1].completion; + } catch(error) { + caughtError = true; + } + + assert.deepStrictEqual(protocolMajorVersion > 1, caughtError); } catch(error) { - caughtError = true; + console.log(`Caught error in the main method: ${error}`) } - assert.deepStrictEqual(protocolMajorVersion > 1, caughtError); - }); it("CreateAsync rejects null stream", async () => { From 9ba19a7b057a1589e7f0904102ff7760f80d1c93 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:55:00 -0700 Subject: [PATCH 29/77] Code save --- src/nerdbank-streams/src/Channel.ts | 145 +++++++++--------- .../src/tests/MultiplexingStream.spec.ts | 4 +- 2 files changed, 76 insertions(+), 73 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index c5a7948d..7769a2b2 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -74,16 +74,16 @@ export class ChannelClass extends Channel { private remoteWindowSize?: number; private remoteError?: Error; - /** - * The number of bytes transmitted from here but not yet acknowledged as processed from there, - * and thus occupying some portion of the full AcceptanceParameters.RemoteWindowSize. - */ +/** + * The number of bytes transmitted from here but not yet acknowledged as processed from there, + * and thus occupying some portion of the full AcceptanceParameters.RemoteWindowSize. + */ private remoteWindowFilled: number = 0; /** A signal which indicates when the is non-zero. */ private remoteWindowHasCapacity: Deferred; - - constructor( + + constructor( multiplexingStream: MultiplexingStreamClass, id: QualifiedChannelId, offerParameters: OfferParameters) { @@ -163,29 +163,29 @@ export class ChannelClass extends Channel { // Nothing to do here since data is pushed to us. }, }); - } - - public get stream(): NodeJS.ReadWriteStream { + } + + public get stream(): NodeJS.ReadWriteStream { return this._duplex; - } - - public get acceptance(): Promise { + } + + public get acceptance(): Promise { return this._acceptance.promise; - } - - public get isAccepted() { + } + + public get isAccepted() { return this._acceptance.isResolved; - } - - public get isRejectedOrCanceled() { + } + + public get isRejectedOrCanceled() { return this._acceptance.isRejected; - } - - public get completion(): Promise { + } + + public get completion(): Promise { return this._completion.promise; - } - - public tryAcceptOffer(options?: ChannelOptions): boolean { + } + + public tryAcceptOffer(options?: ChannelOptions): boolean { if (this._acceptance.resolve()) { this.localWindowSize = options?.channelReceivingWindowSize !== undefined ? Math.max(this._multiplexingStream.defaultChannelReceivingWindowSize, options?.channelReceivingWindowSize) @@ -194,9 +194,9 @@ export class ChannelClass extends Channel { } return false; - } - - public tryCancelOffer(reason: any) { + } + + public tryCancelOffer(reason: any) { const cancellationReason = new CancellationToken.CancellationError(reason); this._acceptance.reject(cancellationReason); this._completion.reject(cancellationReason); @@ -205,26 +205,20 @@ export class ChannelClass extends Channel { // or even expect it to be recognized by anyone else. // The acceptance promise rejection is observed by the offer channel method. caught(this._completion.promise); - } - - public onAccepted(acceptanceParameter: AcceptanceParameters): boolean { + } + + public onAccepted(acceptanceParameter: AcceptanceParameters): boolean { if (this._multiplexingStream.backpressureSupportEnabled) { this.remoteWindowSize = acceptanceParameter.remoteWindowSize; this.remoteWindowHasCapacity.resolve(); } return this._acceptance.resolve(); - } - - public onContent(buffer: Buffer | null, error?: Error) { - // Already received remote error so don't process any future messages - if (this.remoteError) { - return; - } + } + + public onContent(buffer: Buffer | null, error? : Error) { - if (error) { - this.remoteError = error; - } + this.remoteError = error this._duplex.push(buffer); @@ -234,9 +228,9 @@ export class ChannelClass extends Channel { if (this._multiplexingStream.backpressureSupportEnabled && buffer) { this._multiplexingStream.localContentExamined(this, buffer.length); } - } - - public onContentProcessed(bytesProcessed: number) { + } + + public onContentProcessed(bytesProcessed: number) { if (bytesProcessed < 0) { throw new Error("A non-negative number is required."); } @@ -253,41 +247,48 @@ export class ChannelClass extends Channel { if (this.remoteWindowFilled < this.remoteWindowSize) { this.remoteWindowHasCapacity.resolve(); } - } + } public async dispose(errorToSend? : Error) { if (!this.isDisposed) { + try { + super.dispose(); + + if (errorToSend) { + await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); + } - super.dispose(); - - if (errorToSend) { - await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); - } - - this._acceptance.reject(new CancellationToken.CancellationError("disposed")); - - // For the pipes, we Complete *our* ends, and leave the user's ends alone. - // The completion will propagate when it's ready to. - if (this.remoteError) { - this._duplex.destroy(this.remoteError); - } else { - try { - this._duplex.end(); - } catch(error) { - console.log(`Caught error in call to duplex end of ${error}`) + this._acceptance.reject(new CancellationToken.CancellationError("disposed")); + + if (this.remoteError) { + console.log(`Calling destroy on duplex in ${this.id}`); + this._duplex.destroy(this.remoteError); + } + + /* + // For the pipes, we Complete *our* ends, and leave the user's ends alone. + // The completion will propagate when it's ready to. + else { + try { + this._duplex.end(); + } catch(error) { + console.log(`Caught error in call to duplex end of ${error}`) + } } + this._duplex.push(null); + + // Reject or Resolve the completion based on the remote error + if (errorToSend?? this.remoteError) { + this._completion.reject(errorToSend?? this.remoteError); + } else { + this._completion.resolve(); + } + + await this._multiplexingStream.onChannelDisposed(this); + */ + } catch(error) { + console.log(`Caught error in dispose: ${error}`); } - this._duplex.push(null); - - // Reject or Resolve the completion based on the remote error - if (errorToSend?? this.remoteError) { - this._completion.reject(errorToSend?? this.remoteError); - } else { - this._completion.resolve(); - } - - await this._multiplexingStream.onChannelDisposed(this); - } } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 1da17c98..25170d8c 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -259,11 +259,14 @@ import * as assert from "assert"; }); it("channel terminated", async () => { + console.log(`Starting channel terminated test`); const channels = await Promise.all([ mx1.offerChannelAsync("test"), mx2.acceptChannelAsync("test"), ]); + console.log(`Calling dispose on created channel in channel terminated test`); channels[0].dispose(); + console.log(`Finished dispose on created channel in channel terminated test`); expect(await getBufferFrom(channels[1].stream, 1, true)).toBeNull(); await channels[1].completion; }); @@ -274,7 +277,6 @@ import * as assert from "assert"; mx2.acceptChannelAsync("test"), ]); mx1.dispose(); - // Verify that both mxstream's complete when one does. await mx1.completion; await mx2.completion; From 1f6e2526395379e09cf05ab3332286d468a3169d Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:03:18 -0700 Subject: [PATCH 30/77] Changed back to basic state --- src/nerdbank-streams/src/Channel.ts | 132 +++++++++++----------------- 1 file changed, 50 insertions(+), 82 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 7769a2b2..62da0e03 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -54,10 +54,9 @@ export abstract class Channel implements IDisposableObservable { } /** - * Closes this channel. If an error is passed in then that error message is sent - * to the remote side before dispoing the channel. + * Closes this channel. */ - public async dispose(error? : Error) { + public dispose(error? : Error) { // The interesting stuff is in the derived class. this._isDisposed = true; } @@ -72,18 +71,17 @@ export class ChannelClass extends Channel { private readonly _completion = new Deferred(); public localWindowSize?: number; private remoteWindowSize?: number; - private remoteError?: Error; -/** - * The number of bytes transmitted from here but not yet acknowledged as processed from there, - * and thus occupying some portion of the full AcceptanceParameters.RemoteWindowSize. - */ + /** + * The number of bytes transmitted from here but not yet acknowledged as processed from there, + * and thus occupying some portion of the full AcceptanceParameters.RemoteWindowSize. + */ private remoteWindowFilled: number = 0; /** A signal which indicates when the is non-zero. */ private remoteWindowHasCapacity: Deferred; - - constructor( + + constructor( multiplexingStream: MultiplexingStreamClass, id: QualifiedChannelId, offerParameters: OfferParameters) { @@ -163,29 +161,29 @@ export class ChannelClass extends Channel { // Nothing to do here since data is pushed to us. }, }); - } - - public get stream(): NodeJS.ReadWriteStream { + } + + public get stream(): NodeJS.ReadWriteStream { return this._duplex; - } - - public get acceptance(): Promise { + } + + public get acceptance(): Promise { return this._acceptance.promise; - } - - public get isAccepted() { + } + + public get isAccepted() { return this._acceptance.isResolved; - } - - public get isRejectedOrCanceled() { + } + + public get isRejectedOrCanceled() { return this._acceptance.isRejected; - } - - public get completion(): Promise { + } + + public get completion(): Promise { return this._completion.promise; - } - - public tryAcceptOffer(options?: ChannelOptions): boolean { + } + + public tryAcceptOffer(options?: ChannelOptions): boolean { if (this._acceptance.resolve()) { this.localWindowSize = options?.channelReceivingWindowSize !== undefined ? Math.max(this._multiplexingStream.defaultChannelReceivingWindowSize, options?.channelReceivingWindowSize) @@ -194,9 +192,9 @@ export class ChannelClass extends Channel { } return false; - } - - public tryCancelOffer(reason: any) { + } + + public tryCancelOffer(reason: any) { const cancellationReason = new CancellationToken.CancellationError(reason); this._acceptance.reject(cancellationReason); this._completion.reject(cancellationReason); @@ -205,21 +203,18 @@ export class ChannelClass extends Channel { // or even expect it to be recognized by anyone else. // The acceptance promise rejection is observed by the offer channel method. caught(this._completion.promise); - } - - public onAccepted(acceptanceParameter: AcceptanceParameters): boolean { + } + + public onAccepted(acceptanceParameter: AcceptanceParameters): boolean { if (this._multiplexingStream.backpressureSupportEnabled) { this.remoteWindowSize = acceptanceParameter.remoteWindowSize; this.remoteWindowHasCapacity.resolve(); } return this._acceptance.resolve(); - } - - public onContent(buffer: Buffer | null, error? : Error) { - - this.remoteError = error + } + public onContent(buffer: Buffer | null, error? : Error) { this._duplex.push(buffer); // We should find a way to detect when we *actually* share the received buffer with the Channel's user @@ -228,9 +223,9 @@ export class ChannelClass extends Channel { if (this._multiplexingStream.backpressureSupportEnabled && buffer) { this._multiplexingStream.localContentExamined(this, buffer.length); } - } - - public onContentProcessed(bytesProcessed: number) { + } + + public onContentProcessed(bytesProcessed: number) { if (bytesProcessed < 0) { throw new Error("A non-negative number is required."); } @@ -247,48 +242,21 @@ export class ChannelClass extends Channel { if (this.remoteWindowFilled < this.remoteWindowSize) { this.remoteWindowHasCapacity.resolve(); } - } + } - public async dispose(errorToSend? : Error) { + public async dispose(error? : Error) { if (!this.isDisposed) { - try { - super.dispose(); - - if (errorToSend) { - await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); - } + super.dispose(); - this._acceptance.reject(new CancellationToken.CancellationError("disposed")); - - if (this.remoteError) { - console.log(`Calling destroy on duplex in ${this.id}`); - this._duplex.destroy(this.remoteError); - } - - /* - // For the pipes, we Complete *our* ends, and leave the user's ends alone. - // The completion will propagate when it's ready to. - else { - try { - this._duplex.end(); - } catch(error) { - console.log(`Caught error in call to duplex end of ${error}`) - } - } - this._duplex.push(null); - - // Reject or Resolve the completion based on the remote error - if (errorToSend?? this.remoteError) { - this._completion.reject(errorToSend?? this.remoteError); - } else { - this._completion.resolve(); - } - - await this._multiplexingStream.onChannelDisposed(this); - */ - } catch(error) { - console.log(`Caught error in dispose: ${error}`); - } + this._acceptance.reject(new CancellationToken.CancellationError("disposed")); + + // For the pipes, we Complete *our* ends, and leave the user's ends alone. + // The completion will propagate when it's ready to. + this._duplex.end(); + this._duplex.push(null); + + this._completion.resolve(); + await this._multiplexingStream.onChannelDisposed(this); } } @@ -305,4 +273,4 @@ export class ChannelClass extends Channel { } } } -} +} \ No newline at end of file From 317d1eb7f5b66fbbdc0843cacaecfd7cf4d8764a Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:10:39 -0700 Subject: [PATCH 31/77] Complete restore try --- src/nerdbank-streams/src/Channel.ts | 6 +- .../src/MultiplexingStream.ts | 73 ++++--------------- .../tests/MultiplexingStream.Interop.spec.ts | 12 --- .../src/tests/MultiplexingStream.spec.ts | 30 -------- 4 files changed, 16 insertions(+), 105 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 62da0e03..b6352d71 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -56,7 +56,7 @@ export abstract class Channel implements IDisposableObservable { /** * Closes this channel. */ - public dispose(error? : Error) { + public dispose() { // The interesting stuff is in the derived class. this._isDisposed = true; } @@ -214,7 +214,7 @@ export class ChannelClass extends Channel { return this._acceptance.resolve(); } - public onContent(buffer: Buffer | null, error? : Error) { + public onContent(buffer: Buffer | null) { this._duplex.push(buffer); // We should find a way to detect when we *actually* share the received buffer with the Channel's user @@ -244,7 +244,7 @@ export class ChannelClass extends Channel { } } - public async dispose(error? : Error) { + public async dispose() { if (!this.isDisposed) { super.dispose(); diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index dec194cf..bd471057 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -376,11 +376,9 @@ export abstract class MultiplexingStream implements IDisposableObservable { * Disposes the stream. */ public dispose() { - console.log(`Going to dispose multiplexing stream`); this.disposalTokenSource.cancel(); this._completionSource.resolve(); this.formatter.end(); - console.log(`Going to iterae through the channels`); [this.locallyOfferedOpenChannels, this.remotelyOfferedOpenChannels].forEach(cb => { for (const channelId in cb) { if (cb.hasOwnProperty(channelId)) { @@ -388,12 +386,8 @@ export abstract class MultiplexingStream implements IDisposableObservable { // Acceptance gets rejected when a channel is disposed. // Avoid a node.js crash or test failure for unobserved channels (e.g. offers for channels from the other party that no one cared to receive on this side). - try { - caught(channel.acceptance); - channel.dispose(); - } catch(error) { - console.log(`Caught error when disposing channel ${channelId} `); - } + caught(channel.acceptance); + channel.dispose(); } } }); @@ -548,24 +542,17 @@ export class MultiplexingStreamClass extends MultiplexingStream { header: FrameHeader, payload?: Buffer, cancellationToken: CancellationToken = CancellationToken.CONTINUE): Promise { - try { - if (!header) { - throw new Error("Header is required."); - } - - await this.sendingSemaphore.use(async () => { - cancellationToken.throwIfCancelled(); - throwIfDisposed(this); - try { - await this.formatter.writeFrameAsync(header, payload); - } catch(error) { - console.log(`Caught error in writing using formatter: ${error}`) - } - - }); - } catch(error) { - console.log(`Caught error in send frame async: ${error}`) + + if (!header) { + throw new Error("Header is required."); } + + await this.sendingSemaphore.use(async () => { + cancellationToken.throwIfCancelled(); + throwIfDisposed(this); + + await this.formatter.writeFrameAsync(header, payload); + }); } /** @@ -598,20 +585,6 @@ export class MultiplexingStreamClass extends MultiplexingStream { } } - public async onChannelWritingError(channel : ChannelClass, errorMessage? : string) { - // Perform error check to ensure that we can send the frame - if(this.formatter instanceof MultiplexingStreamV1Formatter) { - return; - } - if(!this.getOpenChannel(channel.qualifiedId)) { - return; - } - // Convert the error message into a payload and send that - let errorFormatter = this.formatter as MultiplexingStreamV2Formatter; - let errorPayload = errorFormatter.serializeErrorMessage(errorMessage); - await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); - } - public async onChannelDisposed(channel: ChannelClass) { if (!this._completionSource.isCompleted) { try { @@ -659,9 +632,6 @@ export class MultiplexingStreamClass extends MultiplexingStream { case ControlCode.ChannelTerminated: this.onChannelTerminated(frame.header.requiredChannel); break; - case ControlCode.ContentWritingError: - this.onContentWritingError(frame.header.requiredChannel, frame.payload); - break; default: break; } @@ -746,23 +716,6 @@ export class MultiplexingStreamClass extends MultiplexingStream { channel.onContentProcessed(bytesProcessed); } - private onContentWritingError(channelId : QualifiedChannelId, payload : Buffer) { - // Ensure that we should process the enrror message - if (this.formatter instanceof MultiplexingStreamV1Formatter) { - return; - } - const channel = this.getOpenChannel(channelId); - if (!channel) { - throw new Error(`No channel with id ${channelId} found.`); - } - // Convert the payload into an error - let errorFormatter = this.formatter as MultiplexingStreamV2Formatter; - let errorMessage = errorFormatter.deserializeErrorMessage(payload); - let remoteError = new Error(errorMessage); - channel.onContent(null, remoteError); - - } - private onContentWritingCompleted(channelId: QualifiedChannelId) { const channel = this.getOpenChannel(channelId); if (!channel) { @@ -784,4 +737,4 @@ export class MultiplexingStreamClass extends MultiplexingStream { channel.dispose(); } } -} +} \ No newline at end of file diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index 41a50884..15341867 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -86,18 +86,6 @@ import { ChannelOptions } from "../ChannelOptions"; expect(recv).toEqual(`recv: ${bigdata}`); }); - if (protocolMajorVersion > 1) { - it("Can send error to remote", async () => { - const errorMessage = "Couldn't write all the data"; - const errorToSend = new Error(errorMessage); - - const channel = await mx.offerChannelAsync("clientOffer"); - await channel.dispose(errorToSend); - const recv = await readLineAsync(channel.stream); - expect(recv).toEqual(`Received error: ${errorMessage}`); - }) - } - if (protocolMajorVersion >= 3) { it("Can communicate over seeded channel", async () => { const channel = mx.acceptChannel(0); diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 25170d8c..257e2d1f 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -43,36 +43,6 @@ import * as assert from "assert"; } }); - it("Encountered error writing content", async() => { - const errorMessage = "Couldn't write all the data"; - const errorToSend = new Error(errorMessage); - - try { - console.log(`Going to create channels`); - const channels = await Promise.all([ - mx1.offerChannelAsync("test"), - mx2.acceptChannelAsync("test"), - ]); - - console.log(`Going to dispose first channel`); - await channels[0].dispose(errorToSend); - - // Ensure that the message is completed with an error - console.log(`Going to check second channel`); - let caughtError = false; - try { - await channels[1].completion; - } catch(error) { - caughtError = true; - } - - assert.deepStrictEqual(protocolMajorVersion > 1, caughtError); - } catch(error) { - console.log(`Caught error in the main method: ${error}`) - } - - }); - it("CreateAsync rejects null stream", async () => { expectThrow(MultiplexingStream.CreateAsync(null!)); expectThrow(MultiplexingStream.CreateAsync(undefined!)); From 1a64f908881ceaedbb44392f74cce9eccad1353a Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:22:18 -0700 Subject: [PATCH 32/77] Downloaded code from upstream master --- src/nerdbank-streams/src/Channel.ts | 2 +- src/nerdbank-streams/src/ControlCode.ts | 6 ------ src/nerdbank-streams/src/Deferred.ts | 10 ++-------- .../src/MultiplexingStream.ts | 2 +- .../src/MultiplexingStreamFormatters.ts | 8 -------- .../src/tests/MultiplexingStream.spec.ts | 20 +++++-------------- 6 files changed, 9 insertions(+), 39 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index b6352d71..eae435dd 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -273,4 +273,4 @@ export class ChannelClass extends Channel { } } } -} \ No newline at end of file +} diff --git a/src/nerdbank-streams/src/ControlCode.ts b/src/nerdbank-streams/src/ControlCode.ts index be49e0e5..3ae71fed 100644 --- a/src/nerdbank-streams/src/ControlCode.ts +++ b/src/nerdbank-streams/src/ControlCode.ts @@ -32,10 +32,4 @@ export enum ControlCode { * Sent when a channel has finished processing data received from the remote party, allowing them to send more data. */ ContentProcessed, - - /** - * Sent if there was an error transmitting message on the given channel. This frame only gets sent for ProtocolVersion >= 1 - * and the receiving end must also be >= 1 - */ - ContentWritingError, } diff --git a/src/nerdbank-streams/src/Deferred.ts b/src/nerdbank-streams/src/Deferred.ts index c6046b0e..7e53c8aa 100644 --- a/src/nerdbank-streams/src/Deferred.ts +++ b/src/nerdbank-streams/src/Deferred.ts @@ -66,14 +66,8 @@ export class Deferred { if (this.isCompleted) { return false; } - - console.log(`Entering try/catch block inside reject`); - try { - this.rejectPromise(reason); - } catch(error) { - console.log(`Reject Promise threw err inside of deffered: ${error}`); - } - + + this.rejectPromise(reason); this._error = reason; this._isRejected = true; return true; diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index bd471057..1f6eafcc 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -737,4 +737,4 @@ export class MultiplexingStreamClass extends MultiplexingStream { channel.dispose(); } } -} \ No newline at end of file +} diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index 8947c794..af625669 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -293,14 +293,6 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } - serializeErrorMessage(errorMessage? : string) : Buffer { - return msgpack.encode([errorMessage]) - } - - deserializeErrorMessage(payload : Buffer) : string { - return msgpack.decode(payload)[0] - } - protected async readMessagePackAsync(cancellationToken: CancellationToken): Promise<{} | [] | null> { const streamEnded = new Deferred(); while (true) { diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 257e2d1f..e78cc49a 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -25,21 +25,13 @@ import * as assert from "assert"; afterEach(async () => { if (mx1) { - try { - mx1.dispose(); - await mx1.completion; - } catch(error) { - console.log(`Multplexing Stream 1 after each error: ${error}`) - } + mx1.dispose(); + await mx1.completion; } if (mx2) { - try { - mx2.dispose(); - await mx2.completion; - } catch(error) { - console.log(`Multplexing Stream 1 after each error: ${error}`) - } + mx2.dispose(); + await mx2.completion; } }); @@ -229,14 +221,11 @@ import * as assert from "assert"; }); it("channel terminated", async () => { - console.log(`Starting channel terminated test`); const channels = await Promise.all([ mx1.offerChannelAsync("test"), mx2.acceptChannelAsync("test"), ]); - console.log(`Calling dispose on created channel in channel terminated test`); channels[0].dispose(); - console.log(`Finished dispose on created channel in channel terminated test`); expect(await getBufferFrom(channels[1].stream, 1, true)).toBeNull(); await channels[1].completion; }); @@ -247,6 +236,7 @@ import * as assert from "assert"; mx2.acceptChannelAsync("test"), ]); mx1.dispose(); + // Verify that both mxstream's complete when one does. await mx1.completion; await mx2.completion; From 0e9656cfff163b8726e55c664ed3a3304d4b8fcd Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 8 Aug 2022 22:45:02 -0700 Subject: [PATCH 33/77] Changed formatter to support formatting error messages --- src/nerdbank-streams/src/ControlCode.ts | 6 +++++ .../src/MultiplexingStreamFormatters.ts | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/nerdbank-streams/src/ControlCode.ts b/src/nerdbank-streams/src/ControlCode.ts index 3ae71fed..8bac56b9 100644 --- a/src/nerdbank-streams/src/ControlCode.ts +++ b/src/nerdbank-streams/src/ControlCode.ts @@ -32,4 +32,10 @@ export enum ControlCode { * Sent when a channel has finished processing data received from the remote party, allowing them to send more data. */ ContentProcessed, + + /** + * Sent when the sender is unable to send the complete message to the remote side on the given channel. This frame + * only gets sent where the sender and receiver are both running protocol version >= 1 + */ + ContentWritingError, } diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index af625669..58666daf 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -78,6 +78,15 @@ export abstract class MultiplexingStreamFormatter { } } +export function getFormatterVersion(formatter : MultiplexingStreamFormatter) : number { + if (formatter instanceof MultiplexingStreamV3Formatter) { + return 3 + } else if (formatter instanceof MultiplexingStreamV2Formatter) { + return 2 + } + return 1 +} + // tslint:disable-next-line: max-classes-per-file export class MultiplexingStreamV1Formatter extends MultiplexingStreamFormatter { /** @@ -293,6 +302,21 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } + serializeContentWritingEror(version: number, writingError: string) : Buffer { + const payload: any[] = [version, writingError]; + return msgpack.encode([payload]); + } + + deserializeContentWritingError(payload: Buffer, expectedVersion: number) : string | null { + const msgpackObject = msgpack.decode(payload); + const sentVersion : number = msgpackObject[0] as number; + if (sentVersion != expectedVersion) { + // For now, a channel should communicate with channels of the same version + return null + } + return (msgpackObject[1] as string); + } + protected async readMessagePackAsync(cancellationToken: CancellationToken): Promise<{} | [] | null> { const streamEnded = new Deferred(); while (true) { From 9448156848fb54b5762d70e619f4e2902f3542c4 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 8 Aug 2022 22:57:41 -0700 Subject: [PATCH 34/77] Added code on the remote to receive exceptions from the sender --- src/nerdbank-streams/src/Channel.ts | 8 ++++- .../src/MultiplexingStream.ts | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index eae435dd..ca3d8073 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -71,6 +71,7 @@ export class ChannelClass extends Channel { private readonly _completion = new Deferred(); public localWindowSize?: number; private remoteWindowSize?: number; + private remoteError?: Error; /** * The number of bytes transmitted from here but not yet acknowledged as processed from there, @@ -214,7 +215,12 @@ export class ChannelClass extends Channel { return this._acceptance.resolve(); } - public onContent(buffer: Buffer | null) { + public onContent(buffer: Buffer | null, error? : Error) { + // If we have already received an error from remote then don't process any future messages + if (this.remoteError) { + return + } + this._duplex.push(buffer); // We should find a way to detect when we *actually* share the received buffer with the Channel's user diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 1f6eafcc..8c026cf2 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -17,6 +17,7 @@ import "./MultiplexingStreamOptions"; import { MultiplexingStreamOptions } from "./MultiplexingStreamOptions"; import { removeFromQueue, throwIfDisposed } from "./Utilities"; import { + getFormatterVersion, MultiplexingStreamFormatter, MultiplexingStreamV1Formatter, MultiplexingStreamV2Formatter, @@ -629,6 +630,9 @@ export class MultiplexingStreamClass extends MultiplexingStream { case ControlCode.ContentWritingCompleted: this.onContentWritingCompleted(frame.header.requiredChannel); break; + case ControlCode.ContentWritingError: + this.onContentWritingError(frame.header.requiredChannel, frame.payload); + break; case ControlCode.ChannelTerminated: this.onChannelTerminated(frame.header.requiredChannel); break; @@ -716,6 +720,31 @@ export class MultiplexingStreamClass extends MultiplexingStream { channel.onContentProcessed(bytesProcessed); } + private onContentWritingError(channelId: QualifiedChannelId, payload: Buffer) { + // Make sure that the channel has the proper formatter to process the output + const formatterVersion = getFormatterVersion(this.formatter); + if (formatterVersion == 1) { + return + } + + // Ensure that we received the message on an open channel + const channel = this.getOpenChannel(channelId); + if (!channel) { + throw new Error(`No channel with id ${channelId} found.`); + } + + // Ensure that we are able to get a error message from a casted formatter + const castedFormatter = (this.formatter as MultiplexingStreamV2Formatter); + const errorMessage = castedFormatter.deserializeContentWritingError(payload, formatterVersion); + if (!errorMessage) { + throw new Error("Couldn't process content writing error payload received from remote"); + } + + // Create the error received from the remote and pass it to the channel + const remoteErr = new Error(`Received error message from remote: ${errorMessage}`); + channel.onContent(null, remoteErr) + } + private onContentWritingCompleted(channelId: QualifiedChannelId) { const channel = this.getOpenChannel(channelId); if (!channel) { From 26add641e0e4a79813cafc640cfdb384bc44193f Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 8 Aug 2022 23:32:48 -0700 Subject: [PATCH 35/77] Remote side completes with an error test passing --- src/nerdbank-streams/src/Channel.ts | 25 ++++++++++++++----- .../src/MultiplexingStream.ts | 20 +++++++++++++++ .../src/MultiplexingStreamFormatters.ts | 8 +++--- .../src/tests/MultiplexingStream.spec.ts | 22 ++++++++++++++++ 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index ca3d8073..73cf5dae 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -54,9 +54,10 @@ export abstract class Channel implements IDisposableObservable { } /** - * Closes this channel. + * Closes this channel. If the an error is passed into the method then that error + * gets sent to the remote before the disposing of the channel. */ - public dispose() { + public async dispose(error? : Error) { // The interesting stuff is in the derived class. this._isDisposed = true; } @@ -218,9 +219,11 @@ export class ChannelClass extends Channel { public onContent(buffer: Buffer | null, error? : Error) { // If we have already received an error from remote then don't process any future messages if (this.remoteError) { - return + return; } - + + this.remoteError = error; + this._duplex.push(buffer); // We should find a way to detect when we *actually* share the received buffer with the Channel's user @@ -250,9 +253,14 @@ export class ChannelClass extends Channel { } } - public async dispose() { + public async dispose(errorToSend? : Error) { if (!this.isDisposed) { super.dispose(); + + // If the caller passed in an error then send that error to the remote + if (errorToSend) { + await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); + } this._acceptance.reject(new CancellationToken.CancellationError("disposed")); @@ -261,7 +269,12 @@ export class ChannelClass extends Channel { this._duplex.end(); this._duplex.push(null); - this._completion.resolve(); + if (this.remoteError) { + this._completion.reject(this.remoteError); + } else { + this._completion.resolve(); + } + await this._multiplexingStream.onChannelDisposed(this); } } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 8c026cf2..22abd3e8 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -579,6 +579,26 @@ export class MultiplexingStreamClass extends MultiplexingStream { } } + public async onChannelWritingError(channel: ChannelClass, errorMessage: string) { + // If the formatter version is 1 then don't send the error message + const formatterVersion = getFormatterVersion(this.formatter); + if (formatterVersion == 1) { + return; + } + + // Make sure we can send error messages on this channel + if (!this.getOpenChannel(channel.qualifiedId)) { + throw new Error(`Sending writing error from invalid channel ${channel.qualifiedId}`); + } + + // Convert the error message into a payload into a formatter + const castedFormatter = (this.formatter as MultiplexingStreamV2Formatter); + const errorPayload = castedFormatter.serializeContentWritingEror(formatterVersion, errorMessage); + + // Sent the payload as a frame to the sender of the error message + await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); + } + public onChannelWritingCompleted(channel: ChannelClass) { // Only inform the remote side if this channel has not already been terminated. if (!channel.isDisposed && this.getOpenChannel(channel.qualifiedId)) { diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index 58666daf..c5c63400 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -307,12 +307,12 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.encode([payload]); } - deserializeContentWritingError(payload: Buffer, expectedVersion: number) : string | null { - const msgpackObject = msgpack.decode(payload); - const sentVersion : number = msgpackObject[0] as number; + deserializeContentWritingError(payload: Buffer, expectedVersion: number) : string { + const msgpackObject = msgpack.decode(payload)[0]; + const sentVersion : number = msgpackObject[0]; if (sentVersion != expectedVersion) { // For now, a channel should communicate with channels of the same version - return null + throw new Error(`Sender has version ${sentVersion} but expected version ${expectedVersion}`); } return (msgpackObject[1] as string); } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index e78cc49a..c06a0a04 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -230,6 +230,28 @@ import * as assert from "assert"; await channels[1].completion; }); + it("channel disposes with an error", async() => { + const errorMessage = "couldn't send all of the data"; + const errorToSend = new Error(errorMessage); + + const channels = await Promise.all([ + mx1.offerChannelAsync("test"), + mx2.acceptChannelAsync("test"), + ]); + + await channels[0].dispose(errorToSend); + + let caughtError = false; + try { + await channels[1].completion; + } catch(error) { + caughtError = true; + } + + assert.deepStrictEqual(protocolMajorVersion > 1, caughtError); + + }) + it("channels complete when mxstream is disposed", async () => { const channels = await Promise.all([ mx1.offerChannelAsync("test"), From 27d56a7e9019a4512e4e8929f6420c024d8fe60a Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Wed, 10 Aug 2022 00:51:10 -0700 Subject: [PATCH 36/77] All MultiplexingStream tests passing --- src/nerdbank-streams/src/Channel.ts | 9 +++++-- .../src/MultiplexingStream.ts | 2 +- .../src/MultiplexingStreamFormatters.ts | 6 ++--- .../tests/MultiplexingStream.Interop.spec.ts | 18 +++++++++++++ .../src/tests/MultiplexingStream.spec.ts | 25 ++++++++++++++++--- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 73cf5dae..1bb6e58d 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -268,9 +268,14 @@ export class ChannelClass extends Channel { // The completion will propagate when it's ready to. this._duplex.end(); this._duplex.push(null); + + let errorToUse = errorToSend; + if (!errorToUse) { + errorToUse = this.remoteError; + } - if (this.remoteError) { - this._completion.reject(this.remoteError); + if (errorToUse) { + this._completion.reject(errorToUse); } else { this._completion.resolve(); } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 22abd3e8..abaab38c 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -593,7 +593,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { // Convert the error message into a payload into a formatter const castedFormatter = (this.formatter as MultiplexingStreamV2Formatter); - const errorPayload = castedFormatter.serializeContentWritingEror(formatterVersion, errorMessage); + const errorPayload = castedFormatter.serializeContentWritingError(formatterVersion, errorMessage); // Sent the payload as a frame to the sender of the error message await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index c5c63400..fd53d6d3 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -302,13 +302,13 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } - serializeContentWritingEror(version: number, writingError: string) : Buffer { + serializeContentWritingError(version: number, writingError: string) : Buffer { const payload: any[] = [version, writingError]; - return msgpack.encode([payload]); + return msgpack.encode(payload); } deserializeContentWritingError(payload: Buffer, expectedVersion: number) : string { - const msgpackObject = msgpack.decode(payload)[0]; + const msgpackObject = msgpack.decode(payload); const sentVersion : number = msgpackObject[0]; if (sentVersion != expectedVersion) { // For now, a channel should communicate with channels of the same version diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index 15341867..41dcfede 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -4,6 +4,7 @@ import { Deferred } from "../Deferred"; import { FullDuplexStream } from "../FullDuplexStream"; import { MultiplexingStream } from "../MultiplexingStream"; import { ChannelOptions } from "../ChannelOptions"; +import * as assert from "assert"; [1, 2, 3].forEach(protocolMajorVersion => { describe(`MultiplexingStream v${protocolMajorVersion} (interop) `, () => { @@ -86,6 +87,23 @@ import { ChannelOptions } from "../ChannelOptions"; expect(recv).toEqual(`recv: ${bigdata}`); }); + it("Can send error to remote", async() => { + const errorWriteChannel = await mx.acceptChannelAsync("clientOffer"); + const responseReceiveChannel = await mx.acceptChannelAsync("clientResponseOffer"); + + const errorMessage = "couldn't send all of the data"; + const errorToSend = new Error(errorMessage); + errorWriteChannel.dispose(errorToSend); + + let expectedMessage = `received error: ${errorMessage}`; + if (protocolMajorVersion == 1) { + expectedMessage = "didn't receive any errors"; + } + + const receivedMessage = await readLineAsync(responseReceiveChannel.stream); + assert.deepStrictEqual(expectedMessage, receivedMessage); + }) + if (protocolMajorVersion >= 3) { it("Can communicate over seeded channel", async () => { const channel = mx.acceptChannel(0); diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index c06a0a04..7f5f7ed2 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -240,15 +240,34 @@ import * as assert from "assert"; ]); await channels[0].dispose(errorToSend); + + // Ensure that the current channel disposes with the error + let caughtSenderError = false; + try { + await channels[0].completion; + } catch(error) { + let completionErrMsg = String(error); + if (error instanceof Error) { + completionErrMsg = (error as Error).message; + } + caughtSenderError = completionErrMsg.includes(errorMessage); + } + + assert.deepStrictEqual(true, caughtSenderError); - let caughtError = false; + // Ensure that the remote side received the error only for version >= 1 + let caughtRemoteError = false; try { await channels[1].completion; } catch(error) { - caughtError = true; + let completionErrMsg = String(error); + if (error instanceof Error) { + completionErrMsg = (error as Error).message; + } + caughtRemoteError = completionErrMsg.includes(errorMessage); } - assert.deepStrictEqual(protocolMajorVersion > 1, caughtError); + assert.deepStrictEqual(protocolMajorVersion > 1, caughtRemoteError); }) From 61ca8ffcdb3b4a36c7f94a1c9c1cf668f86e40e7 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 18 Aug 2022 00:36:01 -0700 Subject: [PATCH 37/77] v2 Interop tests passing --- .../src/MultiplexingStream.ts | 5 +- .../tests/MultiplexingStream.Interop.spec.ts | 78 +++++++++++++++---- 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index abaab38c..4be74a5f 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -627,13 +627,16 @@ export class MultiplexingStreamClass extends MultiplexingStream { } private async readFromStream(cancellationToken: CancellationToken) { + console.log(`Call to read from stream`); while (!this.isDisposed) { const frame = await this.formatter.readFrameAsync(cancellationToken); if (frame === null) { + console.log(`Received null frame on incoming stream`); break; } - frame.header.flipChannelPerspective(); + let sender = frame.header.channel?.id; + console.log(`Received frame with id ${frame.header.code} from ${sender}`); switch (frame.header.code) { case ControlCode.Offer: this.onOffer(frame.header.requiredChannel, frame.payload); diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index 41dcfede..a7aa0bcc 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -15,6 +15,8 @@ import * as assert from "assert"; const dotnetEnvBlock: NodeJS.ProcessEnv = { DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1", // prevent warnings in stdout that corrupt our interop stream. }; + let expectedError: boolean; + beforeAll( async () => { proc = spawn( @@ -25,15 +27,24 @@ import * as assert from "assert"; procExited = new Deferred(); proc.once("error", (err) => procExited.resolve(err)); proc.once("exit", (code) => procExited.resolve(code)); - // proc.stdout!.pipe(process.stdout); + proc.stdout!.pipe(process.stdout); proc.stderr!.pipe(process.stderr); - expect(await procExited.promise).toEqual(0); + let buildExitVal = await procExited.promise; + console.log(`Build exited with code ${buildExitVal}`); + expect(buildExitVal).toEqual(0); + } catch(error) { + let errorMessage = String(error); + if (error instanceof Error) { + errorMessage = (error as Error).message; + } + console.log(`Before all failed due to error ${errorMessage}`); } finally { proc.kill(); proc = null; } }, - 20000); // leave time for package restore and build + 2000000); // leave time for package restore and build + beforeEach(async () => { proc = spawn( "dotnet", @@ -45,18 +56,33 @@ import * as assert from "assert"; proc.once("exit", (code) => procExited.resolve(code)); proc.stderr!.pipe(process.stderr); const seededChannels: ChannelOptions[] | undefined = protocolMajorVersion >= 3 ? [{}] : undefined; + console.log(`Going to create multiplexing stream`); mx = await MultiplexingStream.CreateAsync(FullDuplexStream.Splice(proc.stdout!, proc.stdin!), { protocolMajorVersion, seededChannels }); - } catch (e) { + console.log(`Finished creating multiplexing stream`); + } catch(error) { + let errorMessage = String(error); + if (error instanceof Error) { + errorMessage = (error as Error).message; + } + console.log(`Before each failed due to error ${errorMessage}`); proc.kill(); proc = null; - throw e; - } - }, 10000); // leave time for dotnet to start. + throw error; + } + expectedError = false; + }, 10000000); // leave time for dotnet to start. afterEach(async () => { if (mx) { mx.dispose(); - await mx.completion; + try { + await mx.completion; + } catch(error) { + if (!expectedError) { + throw error; + } + } + } if (proc) { @@ -88,21 +114,45 @@ import * as assert from "assert"; }); it("Can send error to remote", async() => { - const errorWriteChannel = await mx.acceptChannelAsync("clientOffer"); - const responseReceiveChannel = await mx.acceptChannelAsync("clientResponseOffer"); + expectedError = true; + const errorWriteChannel = await mx.offerChannelAsync("clientErrorOffer"); + const responseReceiveChannel = await mx.offerChannelAsync("clientResponseOffer"); const errorMessage = "couldn't send all of the data"; const errorToSend = new Error(errorMessage); - errorWriteChannel.dispose(errorToSend); - let expectedMessage = `received error: ${errorMessage}`; + let caughtCompletionErr = false; + let caughtAcceptanceErr = false; + + errorWriteChannel.completion.catch(err => { + console.log(`Caught completion rejection ${err}`); + caughtCompletionErr = true; + }); + + errorWriteChannel.acceptance.catch(err => { + caughtAcceptanceErr = true; + console.log(`Caught acceptance rejection ${err}`); + }); + + try { + await errorWriteChannel.dispose(errorToSend); + } catch(error) { + console.log(`Caught error during dispose call ${error}`); + } + + assert.deepStrictEqual(caughtAcceptanceErr, false); + assert.deepStrictEqual(caughtCompletionErr, true); + + let expectedMessage = `received error: Remote party indicated writing error: ${errorMessage}`; if (protocolMajorVersion == 1) { expectedMessage = "didn't receive any errors"; } const receivedMessage = await readLineAsync(responseReceiveChannel.stream); - assert.deepStrictEqual(expectedMessage, receivedMessage); - }) + assert.deepStrictEqual(receivedMessage?.trim(), expectedMessage); + + console.log("Reached end of error sending test"); + }, 100000000) if (protocolMajorVersion >= 3) { it("Can communicate over seeded channel", async () => { From b8721f4f4a5ff1b8b3d83d55dc66167a6be1e781 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 18 Aug 2022 00:51:09 -0700 Subject: [PATCH 38/77] Passing all typescript tests --- src/nerdbank-streams/src/MultiplexingStream.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 4be74a5f..60a08332 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -580,9 +580,12 @@ export class MultiplexingStreamClass extends MultiplexingStream { } public async onChannelWritingError(channel: ChannelClass, errorMessage: string) { + console.log(`Got call to channel writing error with error: ${errorMessage}`); + // If the formatter version is 1 then don't send the error message const formatterVersion = getFormatterVersion(this.formatter); if (formatterVersion == 1) { + console.log(`Ignoring channel writing error since in v1`); return; } @@ -596,6 +599,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { const errorPayload = castedFormatter.serializeContentWritingError(formatterVersion, errorMessage); // Sent the payload as a frame to the sender of the error message + console.log(`Sending error writing frame to remote`); await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); } From e4f8cea3b7ee2f94cbfa2778a86dc5e7374c8e79 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 18 Aug 2022 00:51:53 -0700 Subject: [PATCH 39/77] Including changes in Nerdbank.Stream --- src/Nerdbank.Streams/MultiplexingStream.Formatters.cs | 4 ++-- src/Nerdbank.Streams/MultiplexingStream.cs | 4 ++-- .../netstandard2.0/PublicAPI.Unshipped.txt | 1 - test/Nerdbank.Streams.Interop.Tests/Program.cs | 9 +++++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 09b73a1f..1d1c834f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -511,7 +511,7 @@ internal ReadOnlySequence SerializeWriteError(WriteError error) return errorSequence.AsReadOnlySequence; } - internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError) + internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, int expectedVersion) { var reader = new MessagePackReader(serializedError); if (reader.ReadArrayHeader() != 2) @@ -521,7 +521,7 @@ internal ReadOnlySequence SerializeWriteError(WriteError error) } int senderVersion = reader.ReadInt32(); - if (senderVersion != ProtocolVersion.Major) + if (senderVersion != expectedVersion) { // For now a channel should only process write errors from channels with the same major version return null; diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 5e961fca..f9d07ecf 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -939,7 +939,7 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "OnContentWritingError received from remote party {0}", channelId); } - if (this.formatter is V1Formatter) + if (this.protocolMajorVersion == 1) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -994,7 +994,7 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc // Deserialize the payload V2Formatter wrappedFormatter = (V2Formatter)this.formatter; - WriteError? error = wrappedFormatter.DeserializeWriteError(payload); + WriteError? error = wrappedFormatter.DeserializeWriteError(payload, this.protocolMajorVersion); if (error == null) { diff --git a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt index 1f491d39..b00dc10d 100644 --- a/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Nerdbank.Streams/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,5 +1,4 @@ Nerdbank.Streams.BufferWriterExtensions -Nerdbank.Streams.MultiplexingStream.Channel.RemoteException.get -> System.Exception? Nerdbank.Streams.ReadOnlySequenceExtensions Nerdbank.Streams.StreamPipeReader Nerdbank.Streams.StreamPipeReader.Read() -> System.IO.Pipelines.ReadResult diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index 29262454..8014c898 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -25,7 +25,7 @@ private Program(MultiplexingStream mx) private static async Task Main(string[] args) { - ////System.Diagnostics.Debugger.Launch(); + // System.Diagnostics.Debugger.Launch(); int protocolMajorVersion = int.Parse(args[0]); var options = new MultiplexingStream.Options { @@ -61,6 +61,7 @@ private static (StreamReader Reader, StreamWriter Writer) CreateStreamIO(Multipl private async Task RunAsync(int protocolMajorVersion) { this.ClientOfferAsync().Forget(); + this.ClientOfferErrorAsync().Forget(); this.ServerOfferAsync().Forget(); @@ -83,13 +84,15 @@ private async Task ClientOfferAsync() private async Task ClientOfferErrorAsync() { // Await both of the channels from the sender, one to read the error and the other to return the response - MultiplexingStream.Channel? incomingChannel = await this.mx.AcceptChannelAsync("clientOffer"); + MultiplexingStream.Channel? incomingChannel = await this.mx.AcceptChannelAsync("clientErrorOffer"); + MultiplexingStream.Channel? outgoingChannel = await this.mx.AcceptChannelAsync("clientResponseOffer"); // Determine the response to send back on the whether the incoming channel completed with an exception string? responseMessage = "didn't receive any errors"; try { + // sConsole.WriteLine("Waiting for the channel to complete"); await incomingChannel.Completion; } catch (Exception error) @@ -99,6 +102,8 @@ private async Task ClientOfferErrorAsync() // Create a writer using the outgoing channel and send the response to the sender (StreamReader _, StreamWriter writer) = CreateStreamIO(outgoingChannel); + + // Console.WriteLine("Writing response back to the caller"); await writer.WriteLineAsync(responseMessage); } From 626242ea05e25089ae5f3d01b532d2e065eddfce Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 18 Aug 2022 01:04:50 -0700 Subject: [PATCH 40/77] All C# tests passsing --- src/Nerdbank.Streams/MultiplexingStream.Formatters.cs | 5 +++-- src/Nerdbank.Streams/MultiplexingStream.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 1d1c834f..63923913 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -498,14 +498,15 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R /// /// Serializes an object using . /// + /// The protocol version to include in the serialized error buffer. /// An instance of that we want to seralize. /// A which is the serialized version of the error. - internal ReadOnlySequence SerializeWriteError(WriteError error) + internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteError error) { var errorSequence = new Sequence(); var writer = new MessagePackWriter(errorSequence); writer.WriteArrayHeader(2); - writer.WriteInt32(ProtocolVersion.Major); + writer.WriteInt32(protocolVersion); writer.Write(error.ErrorMessage); writer.Flush(); return errorSequence.AsReadOnlySequence; diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index f9d07ecf..14540a89 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1226,7 +1226,7 @@ private void OnChannelWritingError(Channel channel, Exception exception) this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "onChannelWritingError called for {0} with exception {1}", channel.QualifiedId, exception.Message); } - if (this.formatter is V1Formatter) + if (this.protocolMajorVersion == 1) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -1243,7 +1243,7 @@ private void OnChannelWritingError(Channel channel, Exception exception) { WriteError error = new WriteError(exception.Message); V2Formatter wrappedFormatter = (V2Formatter)this.formatter; - ReadOnlySequence serializedError = wrappedFormatter.SerializeWriteError(error); + ReadOnlySequence serializedError = wrappedFormatter.SerializeWriteError(this.protocolMajorVersion, error); if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { From 00dd45c1ea0cca8cee35fa65b41b60f0cbf39ff4 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:05:18 -0700 Subject: [PATCH 41/77] Fixed style issues in C# code --- .../MultiplexingStream.Channel.cs | 77 +++------- .../MultiplexingStream.Formatters.cs | 20 ++- src/Nerdbank.Streams/MultiplexingStream.cs | 140 +++++++----------- 3 files changed, 88 insertions(+), 149 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 84ea2989..7c2671ef 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -367,7 +367,7 @@ public void Dispose() mxStreamIOWriter = self.mxStreamIOWriter; } - mxStreamIOWriter?.Complete(self.GetWriterException()); + mxStreamIOWriter?.Complete(self.GetRemoteException()); self.mxStreamIOWriterCompleted.Set(); } finally @@ -408,6 +408,7 @@ public void Dispose() this.disposalTokenSource.Cancel(); + // If we are disposing due to receiving or sending an exception, the relay that to our clients if (this.faultingException != null) { this.completionSource.TrySetException(this.faultingException); @@ -433,7 +434,7 @@ internal async Task OnChannelTerminatedAsync() // We Complete the writer because only the writing (logical) thread should complete it // to avoid race conditions, and Channel.Dispose can be called from any thread. using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); + await writerRental.Writer.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -512,31 +513,20 @@ internal async ValueTask OnContentAsync(ReadOnlySequence payload, Cancella /// /// Called by the when when it will not be writing any more data to the channel. /// - /// Optional param used to indicate if we are stopping writing due to an error. + /// Optional param used to indicate if we are stopping writing due to an error on the remote side. internal void OnContentWritingCompleted(MultiplexingProtocolException? error = null) { + // If we have already received an error from the remote side then no need to complete the channel again if (this.receivedRemoteException) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Current call to onContentWritingCompleted ignored due to previous call with error on {0}", this.QualifiedId); - } - return; } - if (error != null) + // Set the state of the channel based on whether we are completing due to an error. + lock (this.SyncObject) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Processed error from remote on {0} with message {1}", this.QualifiedId, error.Message); - } - - lock (this.SyncObject) - { - this.faultingException = error; - this.receivedRemoteException = true; - } + this.faultingException ??= error; + this.receivedRemoteException = error != null; } this.DisposeSelfOnFailure(Task.Run(async delegate @@ -545,39 +535,27 @@ internal void OnContentWritingCompleted(MultiplexingProtocolException? error = n { try { + // If the channel is not disposed, then first try to close the writer used by the channel owner using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); - - if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Writing Completed closed writer rental on {0} with error {1}", this.QualifiedId, error.Message); - } + await writerRental.Writer.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); } catch (ObjectDisposedException) { + // If not, try to close the underlying writer. if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); - - if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Writing Completed closed mxStreamIOWriter on {0} with error {1}", this.QualifiedId, error.Message); - } + await this.mxStreamIOWriter.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); } } } else { + // If the channel has not been disposed then just close the underlying writer if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync(this.GetWriterException()).ConfigureAwait(false); - - if (error != null && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Writing Completed closed mxStreamIOWriter on {0} with error {1}", this.QualifiedId, error.Message); - } + await this.mxStreamIOWriter.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); } } @@ -700,10 +678,10 @@ private async ValueTask GetReceivedMessagePipeWriterAsync(Canc } /// - /// Gets the to close any that the channel is managing. + /// Gets the exception that we received from the remote side when completing this channel. /// /// The exception sent from the remote if there is one, null otherwise. - private Exception? GetWriterException() + private Exception? GetRemoteException() { return this.receivedRemoteException ? this.faultingException : null; } @@ -896,22 +874,20 @@ private async Task ProcessOutboundTransmissionsAsync() } catch (Exception ex) { + // If the operation had been cancelled then we are expecting to receive this error so don't transmit it if (ex is OperationCanceledException && this.DisposalToken.IsCancellationRequested) { await mxStreamIOReader!.CompleteAsync().ConfigureAwait(false); } else { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Completing channel {0} with exception {1}", this.QualifiedId, ex.Message); - } - + // If not record it as the error to dispose this channel with lock (this.SyncObject) { this.faultingException = ex; } + // Since were not expecting to receive this error, transmit the error to the remote side await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); this.MultiplexingStream.OnChannelWritingError(this, ex); } @@ -991,23 +967,12 @@ private async Task AutoCloseOnPipesClosureAsync() private void Fault(Exception exception) { - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) - { - this.TraceSource!.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Fault called in {0} with exception message {1}", this.QualifiedId, exception.Message); - } - - bool alreadyFaulted = false; + // If the reason why are faulting isn't already set then do so before disposing the channel lock (this.SyncObject) { - alreadyFaulted = this.faultingException != null; this.faultingException ??= exception; } - if (!alreadyFaulted && (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false)) - { - this.TraceSource.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel {0} closing self due to exception: {1}", this.QualifiedId, exception); - } - this.mxStreamIOReader?.CancelPendingRead(); this.Dispose(); } diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 63923913..7cf2cf58 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -496,38 +496,52 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R } /// - /// Serializes an object using . + /// Returns the serializaed representation of a object using . /// /// The protocol version to include in the serialized error buffer. /// An instance of that we want to seralize. /// A which is the serialized version of the error. internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteError error) { + // Create the payload var errorSequence = new Sequence(); var writer = new MessagePackWriter(errorSequence); + + // Write the error message and the protocol version to the payload writer.WriteArrayHeader(2); writer.WriteInt32(protocolVersion); writer.Write(error.ErrorMessage); + + // Return the payload to the caller writer.Flush(); return errorSequence.AsReadOnlySequence; } + /// + /// Extracts an object from the payload using . + /// + /// The payload we are trying to extract the error object from. + /// The protocol version we expect to be associated with the error object. + /// A object if the payload is correctly formatted and has the expected protocol version, + /// null otherwise. internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, int expectedVersion) { var reader = new MessagePackReader(serializedError); + + // The payload should only have the error message and the protocol version if (reader.ReadArrayHeader() != 2) { - // For now the error sequence will only contain the major version and the error message return null; } + // Verify that the protocol version of the payload matches our expected value int senderVersion = reader.ReadInt32(); if (senderVersion != expectedVersion) { - // For now a channel should only process write errors from channels with the same major version return null; } + // Extract the error message and use that to create the write error object string errorMessage = reader.ReadString(); return new WriteError(errorMessage); } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 14540a89..a8ca253e 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -934,86 +934,55 @@ private async Task OnChannelTerminatedAsync(QualifiedChannelId channelId) /// The payload that the sender sent in the frame. private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence payload) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "OnContentWritingError received from remote party {0}", channelId); - } - + // Make sure this MultiplexingStream is qualified to received content writing error messages if (this.protocolMajorVersion == 1) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting writing error from {0} as stream uses V1Formatter", channelId); + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Rejecting writing error from channel {0} as MultiplexingStream has protocol version of {1}", + channelId, + this.protocolMajorVersion); } return; } - Channel? channel; + // Get the channel that send this frame + Channel channel; lock (this.syncObject) { - if (this.openChannels.ContainsKey(channelId)) - { - channel = this.openChannels[channelId]; - } - else - { - channel = null; - } - } - - if (channel == null) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from non open channel {0}", channelId); - } - - return; + channel = this.openChannels[channelId]; } + // Verify that the channel is in a state that it can receive communication if (channelId.Source == ChannelSource.Local && !channel.IsAccepted) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from non accepted channel {0}", channelId); - } - - throw new MultiplexingProtocolException($"Remote party indicated they're done writing to channel {channelId} before accepting it."); - } - - if (channel.IsRejectedOrCanceled || this.channelsPendingTermination.Contains(channelId)) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from channel {0} as it is in an unwanted state", channelId); - } - - return; + throw new MultiplexingProtocolException($"Remote party indicated they encountered errors writing to channel {channelId} before accepting it."); } - // Deserialize the payload - V2Formatter wrappedFormatter = (V2Formatter)this.formatter; - WriteError? error = wrappedFormatter.DeserializeWriteError(payload, this.protocolMajorVersion); - + // Deserialize the payload and verify that it was in an expected state + V2Formatter errorDeserializingFormattter = (V2Formatter)this.formatter; + WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.protocolMajorVersion); if (error == null) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Rejecting content writing error from channel {0} as can't deserialize payload {1} using {2}", channelId, payload, this.formatter); + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Rejecting content writing error from channel {0} due to invalid payload", + channelId); } return; } + // Get the error message and complete the channel using it string errorMessage = error.ErrorMessage; MultiplexingProtocolException channelClosingException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); - - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Content Writing Error closing channel {0} using error {1}", channelId, channelClosingException.Message); - } - channel.OnContentWritingCompleted(channelClosingException); } @@ -1221,59 +1190,50 @@ private void OnChannelWritingError(Channel channel, Exception exception) { Requires.NotNull(channel, nameof(channel)); - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "onChannelWritingError called for {0} with exception {1}", channel.QualifiedId, exception.Message); - } - + // Make sure that we are allowed to send error frames on this protocol version if (this.protocolMajorVersion == 1) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending WriteError frame from {0} since we are using a V1 Formatter", channel.QualifiedId); + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Not informing remote side of write error on channel {0} since MultiplexingStream has protocol version of {1}", + channel.QualifiedId, + this.protocolMajorVersion); } return; } - lock (this.syncObject) + // Verify that we are able to communicate to the remote side on this channel + if (this.channelsPendingTermination.Contains(channel.QualifiedId) || !this.openChannels.ContainsKey(channel.QualifiedId)) { - // Only inform the remote side if this channel has not already been terminated. - if (!this.channelsPendingTermination.Contains(channel.QualifiedId) && this.openChannels.ContainsKey(channel.QualifiedId)) + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - WriteError error = new WriteError(exception.Message); - V2Formatter wrappedFormatter = (V2Formatter)this.formatter; - ReadOnlySequence serializedError = wrappedFormatter.SerializeWriteError(this.protocolMajorVersion, error); + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Not informing remote side of write error on channel {0} as it is an improper state"); + } - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Generated MessagePack object {0} for write error with message {1} in channel {2}", - serializedError, - error.ErrorMessage, - channel.QualifiedId); - } + return; + } - FrameHeader header = new FrameHeader - { - Code = ControlCode.ContentWritingError, - ChannelId = channel.QualifiedId, - }; + // Create the payload to send to the remote side + WriteError error = new WriteError(exception.Message); + V2Formatter errorSerializationFormatter = (V2Formatter)this.formatter; + ReadOnlySequence serializedError = errorSerializationFormatter.SerializeWriteError(this.protocolMajorVersion, error); - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Sending write error with frame header {0} in channel {1}", header, channel.QualifiedId); - } + // Create the frame header indicating that we encountered a content writing error + FrameHeader header = new FrameHeader + { + Code = ControlCode.ContentWritingError, + ChannelId = channel.QualifiedId, + }; - this.SendFrame(header, serializedError, CancellationToken.None); - } - else if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Not sending WriteError frame since channel {0} is terminated", channel.QualifiedId); - } - } + // Send the frame alongside the payload to the remote side + this.SendFrame(header, serializedError, CancellationToken.None); } /// From a8c5fbd0f6b2d65e011963d8cda887f3f8f17506 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:40:34 -0700 Subject: [PATCH 42/77] Fixed typescript style issue --- src/nerdbank-streams/src/Channel.ts | 14 +++--- .../src/MultiplexingStream.ts | 31 +++++-------- .../src/MultiplexingStreamFormatters.ts | 11 +++-- .../tests/MultiplexingStream.Interop.spec.ts | 44 ++++--------------- 4 files changed, 33 insertions(+), 67 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 1bb6e58d..754a0478 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -265,17 +265,15 @@ export class ChannelClass extends Channel { this._acceptance.reject(new CancellationToken.CancellationError("disposed")); // For the pipes, we Complete *our* ends, and leave the user's ends alone. - // The completion will propagate when it's ready to. + // The completion will propagate when it's ready to. No need to destroy the duplex + // as the frame containing the error message has already been sent. this._duplex.end(); this._duplex.push(null); - - let errorToUse = errorToSend; - if (!errorToUse) { - errorToUse = this.remoteError; - } - if (errorToUse) { - this._completion.reject(errorToUse); + // If we are sending an error to the remote side or received an error from the remote, + // relay that information to the clients. + if (errorToSend ?? this.remoteError) { + this._completion.reject(errorToSend ?? this.remoteError); } else { this._completion.resolve(); } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 60a08332..3b137bec 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -17,7 +17,6 @@ import "./MultiplexingStreamOptions"; import { MultiplexingStreamOptions } from "./MultiplexingStreamOptions"; import { removeFromQueue, throwIfDisposed } from "./Utilities"; import { - getFormatterVersion, MultiplexingStreamFormatter, MultiplexingStreamV1Formatter, MultiplexingStreamV2Formatter, @@ -580,26 +579,21 @@ export class MultiplexingStreamClass extends MultiplexingStream { } public async onChannelWritingError(channel: ChannelClass, errorMessage: string) { - console.log(`Got call to channel writing error with error: ${errorMessage}`); - - // If the formatter version is 1 then don't send the error message - const formatterVersion = getFormatterVersion(this.formatter); - if (formatterVersion == 1) { - console.log(`Ignoring channel writing error since in v1`); + // Make sure that we are in a protocol version in which we can write errors + if (this.protocolMajorVersion == 1) { return; } // Make sure we can send error messages on this channel if (!this.getOpenChannel(channel.qualifiedId)) { - throw new Error(`Sending writing error from invalid channel ${channel.qualifiedId}`); + return; } // Convert the error message into a payload into a formatter - const castedFormatter = (this.formatter as MultiplexingStreamV2Formatter); - const errorPayload = castedFormatter.serializeContentWritingError(formatterVersion, errorMessage); + const errorSerializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); + const errorPayload = errorSerializingFormatter.serializeContentWritingError(this.protocolMajorVersion, errorMessage); - // Sent the payload as a frame to the sender of the error message - console.log(`Sending error writing frame to remote`); + // Sent the error to the remote side await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); } @@ -749,9 +743,8 @@ export class MultiplexingStreamClass extends MultiplexingStream { private onContentWritingError(channelId: QualifiedChannelId, payload: Buffer) { // Make sure that the channel has the proper formatter to process the output - const formatterVersion = getFormatterVersion(this.formatter); - if (formatterVersion == 1) { - return + if (this.protocolMajorVersion == 1) { + return; } // Ensure that we received the message on an open channel @@ -760,14 +753,14 @@ export class MultiplexingStreamClass extends MultiplexingStream { throw new Error(`No channel with id ${channelId} found.`); } - // Ensure that we are able to get a error message from a casted formatter - const castedFormatter = (this.formatter as MultiplexingStreamV2Formatter); - const errorMessage = castedFormatter.deserializeContentWritingError(payload, formatterVersion); + // Extract the error message from the payload + const errorDeserializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); + const errorMessage = errorDeserializingFormatter.deserializeContentWritingError(payload, this.protocolMajorVersion); if (!errorMessage) { throw new Error("Couldn't process content writing error payload received from remote"); } - // Create the error received from the remote and pass it to the channel + // Pass the error received from the remote to the channel const remoteErr = new Error(`Received error message from remote: ${errorMessage}`); channel.onContent(null, remoteErr) } diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index fd53d6d3..eb9801d2 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -309,11 +309,14 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { deserializeContentWritingError(payload: Buffer, expectedVersion: number) : string { const msgpackObject = msgpack.decode(payload); - const sentVersion : number = msgpackObject[0]; - if (sentVersion != expectedVersion) { - // For now, a channel should communicate with channels of the same version - throw new Error(`Sender has version ${sentVersion} but expected version ${expectedVersion}`); + const payloadVersion : number = msgpackObject[0]; + + // Make sure the version of the payload matches the expected version + if (payloadVersion != expectedVersion) { + throw new Error(`Payload has version ${payloadVersion} but expected version ${expectedVersion}`); } + + // Return the error message to the caller return (msgpackObject[1] as string); } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index a7aa0bcc..d657a130 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -15,7 +15,7 @@ import * as assert from "assert"; const dotnetEnvBlock: NodeJS.ProcessEnv = { DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1", // prevent warnings in stdout that corrupt our interop stream. }; - let expectedError: boolean; + let expectedDisposeError: boolean; beforeAll( async () => { @@ -30,14 +30,7 @@ import * as assert from "assert"; proc.stdout!.pipe(process.stdout); proc.stderr!.pipe(process.stderr); let buildExitVal = await procExited.promise; - console.log(`Build exited with code ${buildExitVal}`); expect(buildExitVal).toEqual(0); - } catch(error) { - let errorMessage = String(error); - if (error instanceof Error) { - errorMessage = (error as Error).message; - } - console.log(`Before all failed due to error ${errorMessage}`); } finally { proc.kill(); proc = null; @@ -56,29 +49,24 @@ import * as assert from "assert"; proc.once("exit", (code) => procExited.resolve(code)); proc.stderr!.pipe(process.stderr); const seededChannels: ChannelOptions[] | undefined = protocolMajorVersion >= 3 ? [{}] : undefined; - console.log(`Going to create multiplexing stream`); mx = await MultiplexingStream.CreateAsync(FullDuplexStream.Splice(proc.stdout!, proc.stdin!), { protocolMajorVersion, seededChannels }); - console.log(`Finished creating multiplexing stream`); } catch(error) { - let errorMessage = String(error); - if (error instanceof Error) { - errorMessage = (error as Error).message; - } - console.log(`Before each failed due to error ${errorMessage}`); proc.kill(); proc = null; throw error; } - expectedError = false; + expectedDisposeError = false; }, 10000000); // leave time for dotnet to start. afterEach(async () => { if (mx) { mx.dispose(); + + // See if we encounter any errors in the multplexing stream and rethrow them if they are unexpected try { await mx.completion; } catch(error) { - if (!expectedError) { + if(!expectedDisposeError) { throw error; } } @@ -114,7 +102,7 @@ import * as assert from "assert"; }); it("Can send error to remote", async() => { - expectedError = true; + expectedDisposeError = true; const errorWriteChannel = await mx.offerChannelAsync("clientErrorOffer"); const responseReceiveChannel = await mx.offerChannelAsync("clientResponseOffer"); @@ -122,25 +110,11 @@ import * as assert from "assert"; const errorToSend = new Error(errorMessage); let caughtCompletionErr = false; - let caughtAcceptanceErr = false; - errorWriteChannel.completion.catch(err => { - console.log(`Caught completion rejection ${err}`); caughtCompletionErr = true; }); - errorWriteChannel.acceptance.catch(err => { - caughtAcceptanceErr = true; - console.log(`Caught acceptance rejection ${err}`); - }); - - try { - await errorWriteChannel.dispose(errorToSend); - } catch(error) { - console.log(`Caught error during dispose call ${error}`); - } - - assert.deepStrictEqual(caughtAcceptanceErr, false); + await errorWriteChannel.dispose(errorToSend); assert.deepStrictEqual(caughtCompletionErr, true); let expectedMessage = `received error: Remote party indicated writing error: ${errorMessage}`; @@ -150,9 +124,7 @@ import * as assert from "assert"; const receivedMessage = await readLineAsync(responseReceiveChannel.stream); assert.deepStrictEqual(receivedMessage?.trim(), expectedMessage); - - console.log("Reached end of error sending test"); - }, 100000000) + }); if (protocolMajorVersion >= 3) { it("Can communicate over seeded channel", async () => { From f9ed5a2fe7333b457e6f5029c92fbee4f1866d11 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:41:11 -0700 Subject: [PATCH 43/77] Added comments to C# test --- .../MultiplexingStreamTests.cs | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index cb42b423..58c42bdf 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -42,34 +42,35 @@ public async Task OfferPipeWithError() bool errorThrown = false; string errorMessage = "Hello World"; - // Prepare a readonly pipe that is already fully populated with data but also with an error + // Prepare a readonly pipe that is already populated with data an an error var pipe = new Pipe(); await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); pipe.Writer.Complete(new NullReferenceException(errorMessage)); - MultiplexingStream.Channel? ch1 = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + // Create a sending and receiving channel using the channel + MultiplexingStream.Channel? localChannel = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? ch2 = this.mx2.AcceptChannel(ch1.QualifiedId.Id); + MultiplexingStream.Channel? remoteChannel = this.mx2.AcceptChannel(localChannel.QualifiedId.Id); - bool readMoreData = true; - while (readMoreData) + bool continueReading = true; + while (continueReading) { try { - ReadResult readResult = await ch2.Input.ReadAsync(this.TimeoutToken); + // Read the latest input from the local channel and determine if we should continue reading + ReadResult readResult = await remoteChannel.Input.ReadAsync(this.TimeoutToken); if (readResult.IsCompleted || readResult.IsCanceled) { - readMoreData = false; - this.Logger.WriteLine("Set readMoreData to False based on readResult fields"); + continueReading = false; } - ch2.Input.AdvanceTo(readResult.Buffer.End); + remoteChannel.Input.AdvanceTo(readResult.Buffer.End); } catch (Exception exception) { + // Check not only that we caught an exception but that it was the expected exception. errorThrown = exception.Message.Contains(errorMessage); - readMoreData = !errorThrown; - this.Logger.WriteLine("Set readMoreData to " + readMoreData + " based on catching error with message " + exception.Message); + continueReading = !errorThrown; } } @@ -77,35 +78,33 @@ public async Task OfferPipeWithError() // Ensure that the writer of the error completes with that error, no matter what version of the protocol they are using string expectedWriterErrorMessage = errorMessage; - bool writeCompletedWithError = false; + bool localChannelCompletedWithError = false; try { - await ch1.Completion; + await localChannel.Completion; } catch (Exception writeException) { - this.Logger.WriteLine($"Caught error {writeException.Message} in the completion of the writer"); - writeCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); + localChannelCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); } - Assert.True(writeCompletedWithError); + Assert.True(localChannelCompletedWithError); // Ensure that the reader only completes with an error if we are using a protocol version > 1 string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; - bool readCompletedWithError = false; + bool remoteChannelCompletedWithError = false; try { - await ch2.Completion; + await remoteChannel.Completion; } catch (Exception readException) { - this.Logger.WriteLine($"Caught error {readException.Message} in the completion of the reader"); - readCompletedWithError = readException.Message.Contains(expectedReaderErrorMessage); + remoteChannelCompletedWithError = readException.Message.Contains(expectedReaderErrorMessage); } - Assert.Equal(this.ProtocolMajorVersion > 1, readCompletedWithError); + Assert.Equal(this.ProtocolMajorVersion > 1, remoteChannelCompletedWithError); } public async Task InitializeAsync() From ed2d5d6170252b2f14c091b22e02f7d695ea0d4e Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:58:37 -0700 Subject: [PATCH 44/77] Added a writing error class --- src/nerdbank-streams/src/MultiplexingStream.ts | 12 +++++++----- .../src/MultiplexingStreamFormatters.ts | 12 +++++++----- src/nerdbank-streams/src/WriteError.ts | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 src/nerdbank-streams/src/WriteError.ts diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 3b137bec..4a12ab07 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -25,6 +25,7 @@ import { import { OfferParameters } from "./OfferParameters"; import { Semaphore } from 'await-semaphore'; import { QualifiedChannelId, ChannelSource } from "./QualifiedChannelId"; +import { WriteError } from "./WriteError"; export abstract class MultiplexingStream implements IDisposableObservable { /** @@ -590,8 +591,9 @@ export class MultiplexingStreamClass extends MultiplexingStream { } // Convert the error message into a payload into a formatter + const writingError = new WriteError(errorMessage); const errorSerializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); - const errorPayload = errorSerializingFormatter.serializeContentWritingError(this.protocolMajorVersion, errorMessage); + const errorPayload = errorSerializingFormatter.serializeContentWritingError(this.protocolMajorVersion, writingError); // Sent the error to the remote side await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); @@ -753,15 +755,15 @@ export class MultiplexingStreamClass extends MultiplexingStream { throw new Error(`No channel with id ${channelId} found.`); } - // Extract the error message from the payload + // Extract the error from the payload const errorDeserializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); - const errorMessage = errorDeserializingFormatter.deserializeContentWritingError(payload, this.protocolMajorVersion); - if (!errorMessage) { + const writingError = errorDeserializingFormatter.deserializeContentWritingError(payload, this.protocolMajorVersion); + if (!writingError) { throw new Error("Couldn't process content writing error payload received from remote"); } // Pass the error received from the remote to the channel - const remoteErr = new Error(`Received error message from remote: ${errorMessage}`); + const remoteErr = new Error(`Received error message from remote: ${writingError.getErrorMessage()}`); channel.onContent(null, remoteErr) } diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index eb9801d2..a4a441e8 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -8,6 +8,7 @@ import * as msgpack from 'msgpack-lite'; import { Deferred } from "./Deferred"; import { FrameHeader } from "./FrameHeader"; import { ControlCode } from "./ControlCode"; +import {WriteError} from "./WriteError"; import { ChannelSource } from "./QualifiedChannelId"; export interface Version { @@ -302,22 +303,23 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } - serializeContentWritingError(version: number, writingError: string) : Buffer { - const payload: any[] = [version, writingError]; + serializeContentWritingError(version: number, writingError: WriteError) : Buffer { + const payload: any[] = [version, writingError.getErrorMessage()]; return msgpack.encode(payload); } - deserializeContentWritingError(payload: Buffer, expectedVersion: number) : string { + deserializeContentWritingError(payload: Buffer, expectedVersion: number) : WriteError | null { const msgpackObject = msgpack.decode(payload); const payloadVersion : number = msgpackObject[0]; // Make sure the version of the payload matches the expected version if (payloadVersion != expectedVersion) { - throw new Error(`Payload has version ${payloadVersion} but expected version ${expectedVersion}`); + return null; } // Return the error message to the caller - return (msgpackObject[1] as string); + const errorMsg : string = msgpackObject[1]; + return new WriteError(errorMsg); } protected async readMessagePackAsync(cancellationToken: CancellationToken): Promise<{} | [] | null> { diff --git a/src/nerdbank-streams/src/WriteError.ts b/src/nerdbank-streams/src/WriteError.ts new file mode 100644 index 00000000..93c542e0 --- /dev/null +++ b/src/nerdbank-streams/src/WriteError.ts @@ -0,0 +1,16 @@ +// WriteError is a class that is used to store information related to ContentWritingError. +// It is used by both the sending and receiving streams to transmit errors encountered while +// writing content. +export class WriteError { + + private _errorMessage: string; + + constructor(errorMsg: string) { + this._errorMessage = errorMsg; + } + + // Returns the error message associated with this error + getErrorMessage() : string { + return this._errorMessage; + } +} \ No newline at end of file From 270a484dd1ec9b284ed2d5c781cf7bb1717ba0f6 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 20 Aug 2022 13:59:00 -0700 Subject: [PATCH 45/77] Updated writing error class in C# --- src/Nerdbank.Streams/MultiplexingStream.WriteError.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs index 628bd4dc..89832e89 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -24,7 +24,7 @@ public WriteError(string message) } /// - /// Gets the error message that we want to send to receiver. + /// Gets the error message associated with this error. /// public string ErrorMessage { get; } } From 6cd7a625b22f5e8278dada9a65815fdf69ccbe76 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 20 Aug 2022 14:35:16 -0700 Subject: [PATCH 46/77] Fixed linter issues --- src/nerdbank-streams/src/Channel.ts | 15 +++++++-------- src/nerdbank-streams/src/MultiplexingStream.ts | 10 +++------- .../src/MultiplexingStreamFormatters.ts | 4 ++-- src/nerdbank-streams/src/WriteError.ts | 4 ++-- .../src/tests/MultiplexingStream.Interop.spec.ts | 9 ++++----- .../src/tests/MultiplexingStream.spec.ts | 3 +-- 6 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 754a0478..aad3b35a 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -55,7 +55,7 @@ export abstract class Channel implements IDisposableObservable { /** * Closes this channel. If the an error is passed into the method then that error - * gets sent to the remote before the disposing of the channel. + * gets sent to the remote before the disposing of the channel. */ public async dispose(error? : Error) { // The interesting stuff is in the derived class. @@ -219,11 +219,10 @@ export class ChannelClass extends Channel { public onContent(buffer: Buffer | null, error? : Error) { // If we have already received an error from remote then don't process any future messages if (this.remoteError) { - return; + return; } this.remoteError = error; - this._duplex.push(buffer); // We should find a way to detect when we *actually* share the received buffer with the Channel's user @@ -256,8 +255,8 @@ export class ChannelClass extends Channel { public async dispose(errorToSend? : Error) { if (!this.isDisposed) { super.dispose(); - - // If the caller passed in an error then send that error to the remote + + // If the channel is disposed with an error, transmit it to the remote side if (errorToSend) { await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); } @@ -266,18 +265,18 @@ export class ChannelClass extends Channel { // For the pipes, we Complete *our* ends, and leave the user's ends alone. // The completion will propagate when it's ready to. No need to destroy the duplex - // as the frame containing the error message has already been sent. + // as the frame containing the error message has already been sent. this._duplex.end(); this._duplex.push(null); // If we are sending an error to the remote side or received an error from the remote, - // relay that information to the clients. + // relay that information to the clients. if (errorToSend ?? this.remoteError) { this._completion.reject(errorToSend ?? this.remoteError); } else { this._completion.resolve(); } - + await this._multiplexingStream.onChannelDisposed(this); } } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 4a12ab07..3740b2c1 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -581,7 +581,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { public async onChannelWritingError(channel: ChannelClass, errorMessage: string) { // Make sure that we are in a protocol version in which we can write errors - if (this.protocolMajorVersion == 1) { + if (this.protocolMajorVersion === 1) { return; } @@ -627,16 +627,12 @@ export class MultiplexingStreamClass extends MultiplexingStream { } private async readFromStream(cancellationToken: CancellationToken) { - console.log(`Call to read from stream`); while (!this.isDisposed) { const frame = await this.formatter.readFrameAsync(cancellationToken); if (frame === null) { - console.log(`Received null frame on incoming stream`); break; } frame.header.flipChannelPerspective(); - let sender = frame.header.channel?.id; - console.log(`Received frame with id ${frame.header.code} from ${sender}`); switch (frame.header.code) { case ControlCode.Offer: this.onOffer(frame.header.requiredChannel, frame.payload); @@ -745,10 +741,10 @@ export class MultiplexingStreamClass extends MultiplexingStream { private onContentWritingError(channelId: QualifiedChannelId, payload: Buffer) { // Make sure that the channel has the proper formatter to process the output - if (this.protocolMajorVersion == 1) { + if (this.protocolMajorVersion === 1) { return; } - + // Ensure that we received the message on an open channel const channel = this.getOpenChannel(channelId); if (!channel) { diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index a4a441e8..83e1eda0 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -84,7 +84,7 @@ export function getFormatterVersion(formatter : MultiplexingStreamFormatter) : n return 3 } else if (formatter instanceof MultiplexingStreamV2Formatter) { return 2 - } + } return 1 } @@ -313,7 +313,7 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { const payloadVersion : number = msgpackObject[0]; // Make sure the version of the payload matches the expected version - if (payloadVersion != expectedVersion) { + if (payloadVersion !== expectedVersion) { return null; } diff --git a/src/nerdbank-streams/src/WriteError.ts b/src/nerdbank-streams/src/WriteError.ts index 93c542e0..0d21bfad 100644 --- a/src/nerdbank-streams/src/WriteError.ts +++ b/src/nerdbank-streams/src/WriteError.ts @@ -1,10 +1,10 @@ // WriteError is a class that is used to store information related to ContentWritingError. // It is used by both the sending and receiving streams to transmit errors encountered while -// writing content. +// writing content. export class WriteError { private _errorMessage: string; - + constructor(errorMsg: string) { this._errorMessage = errorMsg; } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index d657a130..f6b2a614 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -29,7 +29,7 @@ import * as assert from "assert"; proc.once("exit", (code) => procExited.resolve(code)); proc.stdout!.pipe(process.stdout); proc.stderr!.pipe(process.stderr); - let buildExitVal = await procExited.promise; + const buildExitVal = await procExited.promise; expect(buildExitVal).toEqual(0); } finally { proc.kill(); @@ -54,7 +54,7 @@ import * as assert from "assert"; proc.kill(); proc = null; throw error; - } + } expectedDisposeError = false; }, 10000000); // leave time for dotnet to start. @@ -70,7 +70,6 @@ import * as assert from "assert"; throw error; } } - } if (proc) { @@ -114,11 +113,11 @@ import * as assert from "assert"; caughtCompletionErr = true; }); - await errorWriteChannel.dispose(errorToSend); + await errorWriteChannel.dispose(errorToSend); assert.deepStrictEqual(caughtCompletionErr, true); let expectedMessage = `received error: Remote party indicated writing error: ${errorMessage}`; - if (protocolMajorVersion == 1) { + if (protocolMajorVersion === 1) { expectedMessage = "didn't receive any errors"; } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 7f5f7ed2..c2d1d24d 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -240,7 +240,7 @@ import * as assert from "assert"; ]); await channels[0].dispose(errorToSend); - + // Ensure that the current channel disposes with the error let caughtSenderError = false; try { @@ -268,7 +268,6 @@ import * as assert from "assert"; } assert.deepStrictEqual(protocolMajorVersion > 1, caughtRemoteError); - }) it("channels complete when mxstream is disposed", async () => { From 1c92e329c22387f5fd3147aec764fbeaefdaa5f0 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 24 Aug 2022 19:33:14 -0600 Subject: [PATCH 47/77] Some doc and syntax touch-ups --- .../MultiplexingStream.Channel.cs | 12 +++++----- .../MultiplexingStream.ChannelOptions.cs | 3 ++- .../MultiplexingStream.ControlCode.cs | 5 ++--- .../MultiplexingStream.Formatters.cs | 10 ++++----- .../MultiplexingStream.WriteError.cs | 4 ++-- src/Nerdbank.Streams/MultiplexingStream.cs | 22 +++++++++---------- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 7c2671ef..0f46f939 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -408,7 +408,7 @@ public void Dispose() this.disposalTokenSource.Cancel(); - // If we are disposing due to receiving or sending an exception, the relay that to our clients + // If we are disposing due to receiving or sending an exception, relay that to our client. if (this.faultingException != null) { this.completionSource.TrySetException(this.faultingException); @@ -513,10 +513,10 @@ internal async ValueTask OnContentAsync(ReadOnlySequence payload, Cancella /// /// Called by the when when it will not be writing any more data to the channel. /// - /// Optional param used to indicate if we are stopping writing due to an error on the remote side. + /// The error in writing that originated on the remote side, if applicable. internal void OnContentWritingCompleted(MultiplexingProtocolException? error = null) { - // If we have already received an error from the remote side then no need to complete the channel again + // If we have already received an error from the remote side then no need to complete the channel again. if (this.receivedRemoteException) { return; @@ -874,7 +874,7 @@ private async Task ProcessOutboundTransmissionsAsync() } catch (Exception ex) { - // If the operation had been cancelled then we are expecting to receive this error so don't transmit it + // If the operation had been cancelled then we are expecting to receive this error so don't transmit it. if (ex is OperationCanceledException && this.DisposalToken.IsCancellationRequested) { await mxStreamIOReader!.CompleteAsync().ConfigureAwait(false); @@ -887,7 +887,7 @@ private async Task ProcessOutboundTransmissionsAsync() this.faultingException = ex; } - // Since were not expecting to receive this error, transmit the error to the remote side + // Since we're not expecting to receive this error, transmit the error to the remote side. await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); this.MultiplexingStream.OnChannelWritingError(this, ex); } @@ -967,7 +967,7 @@ private async Task AutoCloseOnPipesClosureAsync() private void Fault(Exception exception) { - // If the reason why are faulting isn't already set then do so before disposing the channel + // Record the faulting exception unless it is not the original exception. lock (this.SyncObject) { this.faultingException ??= exception; diff --git a/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs b/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs index 58002c73..e53c7b46 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ChannelOptions.cs @@ -51,7 +51,8 @@ public ChannelOptions() /// the value of and . /// /// - /// If set to an pipe that was completed with an error, then that error gets sents to the remote using a frame. + /// A faulted (one where is called with an exception) + /// will have its exception relayed to the remote party before closing the channel. /// /// /// Thrown if set to an that returns null for either of its properties. diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index 104f4619..1bf80c03 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -46,9 +46,8 @@ internal enum ControlCode : byte ContentProcessed, /// - /// Sent when a channel encountered an error writin data on a given channel. This is sent right before a - /// to indicate that all data can't be transmitted and the cause of - /// that error. + /// Sent when one party experiences an exception related to a particular channel and carries details regarding the error. + /// This is sent right before a frame closes that channel. /// ContentWritingError, } diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 7cf2cf58..848f018d 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -496,7 +496,7 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R } /// - /// Returns the serializaed representation of a object using . + /// Returns the serialized representation of a object using . /// /// The protocol version to include in the serialized error buffer. /// An instance of that we want to seralize. @@ -504,8 +504,8 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteError error) { // Create the payload - var errorSequence = new Sequence(); - var writer = new MessagePackWriter(errorSequence); + using Sequence errorSequence = new(ArrayPool.Shared); + MessagePackWriter writer = new(errorSequence); // Write the error message and the protocol version to the payload writer.WriteArrayHeader(2); @@ -526,9 +526,9 @@ internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteEr /// null otherwise. internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, int expectedVersion) { - var reader = new MessagePackReader(serializedError); + MessagePackReader reader = new(serializedError); - // The payload should only have the error message and the protocol version + // The payload should only have the error message and the protocol version. if (reader.ReadArrayHeader() != 2) { return null; diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs index 89832e89..12ac9673 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -18,7 +18,7 @@ internal class WriteError /// Initializes a new instance of the class. /// /// The error message we want to send to the receiver. - public WriteError(string message) + internal WriteError(string message) { this.ErrorMessage = message; } @@ -26,7 +26,7 @@ public WriteError(string message) /// /// Gets the error message associated with this error. /// - public string ErrorMessage { get; } + internal string ErrorMessage { get; } } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index a8ca253e..bb8da1f4 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -211,7 +211,7 @@ private enum TraceEventId HandshakeStarted, /// - /// Raised when we are tracing an event related to . + /// Raised when receiving or sending a . /// WriteError, } @@ -928,19 +928,19 @@ private async Task OnChannelTerminatedAsync(QualifiedChannelId channelId) } /// - /// Called when the channel receives a frame with code from the remote. + /// Occurs when the channel receives a frame with code from the remote. /// /// The channel id of the sender of the frame. /// The payload that the sender sent in the frame. private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence payload) { - // Make sure this MultiplexingStream is qualified to received content writing error messages + // Make sure this MultiplexingStream is qualified to receive content writing error messages. if (this.protocolMajorVersion == 1) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Error)) { this.TraceSource.TraceEvent( - TraceEventType.Information, + TraceEventType.Error, (int)TraceEventId.WriteError, "Rejecting writing error from channel {0} as MultiplexingStream has protocol version of {1}", channelId, @@ -968,10 +968,10 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.protocolMajorVersion); if (error == null) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Error)) { this.TraceSource.TraceEvent( - TraceEventType.Information, + TraceEventType.Error, (int)TraceEventId.WriteError, "Rejecting content writing error from channel {0} due to invalid payload", channelId); @@ -1181,8 +1181,8 @@ private void OnChannelDisposed(Channel channel) } /// - /// Called when the local end was not able to completely write all the data to this channel due to an error, - /// leading to the transmission of a frame being sent for this channel. + /// Informs the remote party of a local error that prevents sending all the required data to this channel + /// by transmitting a frame. /// /// The channel whose writing was halted. /// The exception that caused the writing to be haulted. @@ -1214,14 +1214,14 @@ private void OnChannelWritingError(Channel channel, Exception exception) this.TraceSource.TraceEvent( TraceEventType.Information, (int)TraceEventId.WriteError, - "Not informing remote side of write error on channel {0} as it is an improper state"); + "Not informing remote side of write error on channel {0} as it is already terminated or unknown."); } return; } // Create the payload to send to the remote side - WriteError error = new WriteError(exception.Message); + WriteError error = new(exception.Message); V2Formatter errorSerializationFormatter = (V2Formatter)this.formatter; ReadOnlySequence serializedError = errorSerializationFormatter.SerializeWriteError(this.protocolMajorVersion, error); From 6b3691ef7a6a16d4f6145f9f4c878f039b6c1cb3 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 24 Aug 2022 19:55:24 -0600 Subject: [PATCH 48/77] More syntax touch-ups --- .../MultiplexingStream.ControlCode.cs | 3 ++- src/nerdbank-streams/src/Channel.ts | 2 +- src/nerdbank-streams/src/ControlCode.ts | 5 ++-- .../src/MultiplexingStream.ts | 17 +++++++------ .../src/MultiplexingStreamFormatters.ts | 8 +++--- src/nerdbank-streams/src/WriteError.ts | 25 ++++++++----------- .../tests/MultiplexingStream.Interop.spec.ts | 13 +++++----- .../src/tests/MultiplexingStream.spec.ts | 2 +- .../Nerdbank.Streams.Interop.Tests/Program.cs | 8 ++---- 9 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index 1bf80c03..ee188963 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -46,7 +46,8 @@ internal enum ControlCode : byte ContentProcessed, /// - /// Sent when one party experiences an exception related to a particular channel and carries details regarding the error. + /// Sent when one party experiences an exception related to a particular channel and carries details regarding the error, + /// when using protocol version 2 or later. /// This is sent right before a frame closes that channel. /// ContentWritingError, diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index aad3b35a..57c2cd3a 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -217,7 +217,7 @@ export class ChannelClass extends Channel { } public onContent(buffer: Buffer | null, error? : Error) { - // If we have already received an error from remote then don't process any future messages + // If we have already received an error from the remote party, then don't process any future messages. if (this.remoteError) { return; } diff --git a/src/nerdbank-streams/src/ControlCode.ts b/src/nerdbank-streams/src/ControlCode.ts index 8bac56b9..5459cda6 100644 --- a/src/nerdbank-streams/src/ControlCode.ts +++ b/src/nerdbank-streams/src/ControlCode.ts @@ -34,8 +34,9 @@ export enum ControlCode { ContentProcessed, /** - * Sent when the sender is unable to send the complete message to the remote side on the given channel. This frame - * only gets sent where the sender and receiver are both running protocol version >= 1 + * Sent when one party experiences an exception related to a particular channel and carries details regarding the error, + * when using protocol version 2 or later. + * This is sent right before a ContentWritingCompleted frame closes that channel. */ ContentWritingError, } diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 3740b2c1..27e3e78b 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -580,19 +580,19 @@ export class MultiplexingStreamClass extends MultiplexingStream { } public async onChannelWritingError(channel: ChannelClass, errorMessage: string) { - // Make sure that we are in a protocol version in which we can write errors + // Make sure that we are in a protocol version in which we can write errors. if (this.protocolMajorVersion === 1) { return; } - // Make sure we can send error messages on this channel + // Make sure we can send error messages on this channel. if (!this.getOpenChannel(channel.qualifiedId)) { return; } - // Convert the error message into a payload into a formatter + // Convert the error message into a payload into a formatter. const writingError = new WriteError(errorMessage); - const errorSerializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); + const errorSerializingFormatter = this.formatter as MultiplexingStreamV2Formatter; const errorPayload = errorSerializingFormatter.serializeContentWritingError(this.protocolMajorVersion, writingError); // Sent the error to the remote side @@ -632,6 +632,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { if (frame === null) { break; } + frame.header.flipChannelPerspective(); switch (frame.header.code) { case ControlCode.Offer: @@ -740,12 +741,12 @@ export class MultiplexingStreamClass extends MultiplexingStream { } private onContentWritingError(channelId: QualifiedChannelId, payload: Buffer) { - // Make sure that the channel has the proper formatter to process the output + // Make sure that the channel has the proper formatter to process the output. if (this.protocolMajorVersion === 1) { return; } - // Ensure that we received the message on an open channel + // Ensure that we received the message on an open channel. const channel = this.getOpenChannel(channelId); if (!channel) { throw new Error(`No channel with id ${channelId} found.`); @@ -759,8 +760,8 @@ export class MultiplexingStreamClass extends MultiplexingStream { } // Pass the error received from the remote to the channel - const remoteErr = new Error(`Received error message from remote: ${writingError.getErrorMessage()}`); - channel.onContent(null, remoteErr) + const remoteErr = new Error(`Received error message from remote: ${writingError.errorMessage}`); + channel.onContent(null, remoteErr); } private onContentWritingCompleted(channelId: QualifiedChannelId) { diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index 83e1eda0..54becd84 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -8,7 +8,7 @@ import * as msgpack from 'msgpack-lite'; import { Deferred } from "./Deferred"; import { FrameHeader } from "./FrameHeader"; import { ControlCode } from "./ControlCode"; -import {WriteError} from "./WriteError"; +import { WriteError } from "./WriteError"; import { ChannelSource } from "./QualifiedChannelId"; export interface Version { @@ -304,7 +304,7 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { } serializeContentWritingError(version: number, writingError: WriteError) : Buffer { - const payload: any[] = [version, writingError.getErrorMessage()]; + const payload: any[] = [version, writingError.errorMessage]; return msgpack.encode(payload); } @@ -312,12 +312,12 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { const msgpackObject = msgpack.decode(payload); const payloadVersion : number = msgpackObject[0]; - // Make sure the version of the payload matches the expected version + // Make sure the version of the payload matches the expected version. if (payloadVersion !== expectedVersion) { return null; } - // Return the error message to the caller + // Return the error message to the caller. const errorMsg : string = msgpackObject[1]; return new WriteError(errorMsg); } diff --git a/src/nerdbank-streams/src/WriteError.ts b/src/nerdbank-streams/src/WriteError.ts index 0d21bfad..3a8b57ee 100644 --- a/src/nerdbank-streams/src/WriteError.ts +++ b/src/nerdbank-streams/src/WriteError.ts @@ -1,16 +1,13 @@ -// WriteError is a class that is used to store information related to ContentWritingError. -// It is used by both the sending and receiving streams to transmit errors encountered while -// writing content. +/** + * A class that is used to store information related to ContentWritingError. + * It is used by both the sending and receiving streams to transmit errors encountered while + * writing content. + */ export class WriteError { - - private _errorMessage: string; - - constructor(errorMsg: string) { - this._errorMessage = errorMsg; + /** + * Initializes a new instance of the WriteError class. + * @param errorMsg The error message. + */ + constructor(public readonly errorMessage: string) { } - - // Returns the error message associated with this error - getErrorMessage() : string { - return this._errorMessage; - } -} \ No newline at end of file +} diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index f6b2a614..71aaf24a 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -36,8 +36,7 @@ import * as assert from "assert"; proc = null; } }, - 2000000); // leave time for package restore and build - + 20000); // leave time for package restore and build beforeEach(async () => { proc = spawn( "dotnet", @@ -50,13 +49,13 @@ import * as assert from "assert"; proc.stderr!.pipe(process.stderr); const seededChannels: ChannelOptions[] | undefined = protocolMajorVersion >= 3 ? [{}] : undefined; mx = await MultiplexingStream.CreateAsync(FullDuplexStream.Splice(proc.stdout!, proc.stdin!), { protocolMajorVersion, seededChannels }); - } catch(error) { + } catch (e) { proc.kill(); proc = null; - throw error; + throw e; } expectedDisposeError = false; - }, 10000000); // leave time for dotnet to start. + }, 10000); // leave time for dotnet to start. afterEach(async () => { if (mx) { @@ -65,7 +64,7 @@ import * as assert from "assert"; // See if we encounter any errors in the multplexing stream and rethrow them if they are unexpected try { await mx.completion; - } catch(error) { + } catch (error) { if(!expectedDisposeError) { throw error; } @@ -100,7 +99,7 @@ import * as assert from "assert"; expect(recv).toEqual(`recv: ${bigdata}`); }); - it("Can send error to remote", async() => { + it("Can send error to remote", async () => { expectedDisposeError = true; const errorWriteChannel = await mx.offerChannelAsync("clientErrorOffer"); const responseReceiveChannel = await mx.offerChannelAsync("clientResponseOffer"); diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index c2d1d24d..90d0ca50 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -230,7 +230,7 @@ import * as assert from "assert"; await channels[1].completion; }); - it("channel disposes with an error", async() => { + it("channel disposes with an error", async () => { const errorMessage = "couldn't send all of the data"; const errorToSend = new Error(errorMessage); diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index 8014c898..e55a6933 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -25,7 +25,7 @@ private Program(MultiplexingStream mx) private static async Task Main(string[] args) { - // System.Diagnostics.Debugger.Launch(); + ////System.Diagnostics.Debugger.Launch(); int protocolMajorVersion = int.Parse(args[0]); var options = new MultiplexingStream.Options { @@ -61,7 +61,6 @@ private static (StreamReader Reader, StreamWriter Writer) CreateStreamIO(Multipl private async Task RunAsync(int protocolMajorVersion) { this.ClientOfferAsync().Forget(); - this.ClientOfferErrorAsync().Forget(); this.ServerOfferAsync().Forget(); @@ -85,14 +84,12 @@ private async Task ClientOfferErrorAsync() { // Await both of the channels from the sender, one to read the error and the other to return the response MultiplexingStream.Channel? incomingChannel = await this.mx.AcceptChannelAsync("clientErrorOffer"); - MultiplexingStream.Channel? outgoingChannel = await this.mx.AcceptChannelAsync("clientResponseOffer"); // Determine the response to send back on the whether the incoming channel completed with an exception string? responseMessage = "didn't receive any errors"; try { - // sConsole.WriteLine("Waiting for the channel to complete"); await incomingChannel.Completion; } catch (Exception error) @@ -100,10 +97,9 @@ private async Task ClientOfferErrorAsync() responseMessage = "received error: " + error.Message; } - // Create a writer using the outgoing channel and send the response to the sender + // Create a writer using the outgoing channel and send the response to the sender. (StreamReader _, StreamWriter writer) = CreateStreamIO(outgoingChannel); - // Console.WriteLine("Writing response back to the caller"); await writer.WriteLineAsync(responseMessage); } From 2021f29248bbc80558cbc1922866b0f5735b2ea4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 24 Aug 2022 20:25:00 -0600 Subject: [PATCH 49/77] Fix a regression my recent syntax changes made --- src/Nerdbank.Streams/MultiplexingStream.Formatters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 848f018d..3c922192 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -504,7 +504,7 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteError error) { // Create the payload - using Sequence errorSequence = new(ArrayPool.Shared); + Sequence errorSequence = new(); MessagePackWriter writer = new(errorSequence); // Write the error message and the protocol version to the payload From 06adacf1ff8c9010ed7f13e52f1e47cb56fdd757 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 24 Aug 2022 20:29:55 -0600 Subject: [PATCH 50/77] Fix doc comment --- src/nerdbank-streams/src/WriteError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nerdbank-streams/src/WriteError.ts b/src/nerdbank-streams/src/WriteError.ts index 3a8b57ee..4c0c132a 100644 --- a/src/nerdbank-streams/src/WriteError.ts +++ b/src/nerdbank-streams/src/WriteError.ts @@ -6,7 +6,7 @@ export class WriteError { /** * Initializes a new instance of the WriteError class. - * @param errorMsg The error message. + * @param errorMessage The error message. */ constructor(public readonly errorMessage: string) { } From 1e87e009fcf2ce11df8ac160c4d75013e1188b97 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 25 Aug 2022 00:19:19 -0700 Subject: [PATCH 51/77] Fixed style issues --- .../MultiplexingStream.Channel.cs | 46 +++++++++++++------ .../MultiplexingStream.ControlCode.cs | 2 +- .../MultiplexingStream.Formatters.cs | 25 +++++----- src/Nerdbank.Streams/MultiplexingStream.cs | 29 ++++++------ .../MultiplexingStreamTests.cs | 2 + .../MultiplexingStreamV2Tests.cs | 2 + .../MultiplexingStreamV3Tests.cs | 2 + 7 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 0f46f939..f2c18e83 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -322,6 +322,11 @@ private long RemoteWindowRemaining } } + /// + /// Gets the exception sent from the remote side over this channel, null otherwise. + /// + private Exception? RemoteException => this.receivedRemoteException ? this.faultingException : null; + /// /// Gets a value indicating whether backpressure support is enabled. /// @@ -367,7 +372,7 @@ public void Dispose() mxStreamIOWriter = self.mxStreamIOWriter; } - mxStreamIOWriter?.Complete(self.GetRemoteException()); + mxStreamIOWriter?.Complete(self.RemoteException); self.mxStreamIOWriterCompleted.Set(); } finally @@ -434,7 +439,7 @@ internal async Task OnChannelTerminatedAsync() // We Complete the writer because only the writing (logical) thread should complete it // to avoid race conditions, and Channel.Dispose can be called from any thread. using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); + await writerRental.Writer.CompleteAsync(this.RemoteException).ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -537,7 +542,7 @@ internal void OnContentWritingCompleted(MultiplexingProtocolException? error = n { // If the channel is not disposed, then first try to close the writer used by the channel owner using PipeWriterRental writerRental = await this.GetReceivedMessagePipeWriterAsync().ConfigureAwait(false); - await writerRental.Writer.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); + await writerRental.Writer.CompleteAsync(this.RemoteException).ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -545,7 +550,7 @@ internal void OnContentWritingCompleted(MultiplexingProtocolException? error = n if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(this.RemoteException).ConfigureAwait(false); } } } @@ -555,7 +560,7 @@ internal void OnContentWritingCompleted(MultiplexingProtocolException? error = n if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); - await this.mxStreamIOWriter.CompleteAsync(this.GetRemoteException()).ConfigureAwait(false); + await this.mxStreamIOWriter.CompleteAsync(this.RemoteException).ConfigureAwait(false); } } @@ -677,15 +682,6 @@ private async ValueTask GetReceivedMessagePipeWriterAsync(Canc } } - /// - /// Gets the exception that we received from the remote side when completing this channel. - /// - /// The exception sent from the remote if there is one, null otherwise. - private Exception? GetRemoteException() - { - return this.receivedRemoteException ? this.faultingException : null; - } - /// /// Apply channel options to this channel, including setting up or linking to an user-supplied pipe writer/reader pair. /// @@ -884,7 +880,7 @@ private async Task ProcessOutboundTransmissionsAsync() // If not record it as the error to dispose this channel with lock (this.SyncObject) { - this.faultingException = ex; + this.faultingException ??= ex; } // Since we're not expecting to receive this error, transmit the error to the remote side. @@ -974,6 +970,26 @@ private void Fault(Exception exception) } this.mxStreamIOReader?.CancelPendingRead(); + + // Determine if the channel has already been disposed + bool alreadyDisposed = false; + lock (this.SyncObject) + { + alreadyDisposed = this.isDisposed; + } + + // If the channel has already been disposed then no need to call dispose again + if (alreadyDisposed) + { + return; + } + + // Record the fact that we are about to close the channel due to a fault + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) + { + this.TraceSource.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel {0} closing self due to exception: {1}", this.QualifiedId, exception); + } + this.Dispose(); } diff --git a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs index ee188963..5b01fd34 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.ControlCode.cs @@ -48,7 +48,7 @@ internal enum ControlCode : byte /// /// Sent when one party experiences an exception related to a particular channel and carries details regarding the error, /// when using protocol version 2 or later. - /// This is sent right before a frame closes that channel. + /// This is sent before a frame closes that channel. /// ContentWritingError, } diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 3c922192..9cfa97aa 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -5,6 +5,7 @@ namespace Nerdbank.Streams { using System; using System.Buffers; + using System.Diagnostics; using System.IO; using System.IO.Pipelines; using System.Threading; @@ -300,6 +301,7 @@ internal override ReadOnlySequence SerializeContentProcessed(long bytesPro internal class V2Formatter : Formatter { private static readonly Version ProtocolVersion = new Version(2, 0); + private static readonly int WriteErrorPayloadSize = 1; private readonly MessagePackStreamReader reader; private readonly AsyncSemaphore readingSemaphore = new AsyncSemaphore(1); @@ -498,18 +500,16 @@ internal override Channel.AcceptanceParameters DeserializeAcceptanceParameters(R /// /// Returns the serialized representation of a object using . /// - /// The protocol version to include in the serialized error buffer. /// An instance of that we want to seralize. /// A which is the serialized version of the error. - internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteError error) + internal ReadOnlySequence SerializeWriteError(WriteError error) { // Create the payload Sequence errorSequence = new(); MessagePackWriter writer = new(errorSequence); // Write the error message and the protocol version to the payload - writer.WriteArrayHeader(2); - writer.WriteInt32(protocolVersion); + writer.WriteArrayHeader(WriteErrorPayloadSize); writer.Write(error.ErrorMessage); // Return the payload to the caller @@ -521,23 +521,24 @@ internal ReadOnlySequence SerializeWriteError(int protocolVersion, WriteEr /// Extracts an object from the payload using . /// /// The payload we are trying to extract the error object from. - /// The protocol version we expect to be associated with the error object. + /// The tracer to use when tracing errors to deserialize a received payload. /// A object if the payload is correctly formatted and has the expected protocol version, /// null otherwise. - internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, int expectedVersion) + internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, TraceSource? TraceSource) { MessagePackReader reader = new(serializedError); + int numElements = reader.ReadArrayHeader(); - // The payload should only have the error message and the protocol version. - if (reader.ReadArrayHeader() != 2) + // If received an unexpected number of fields, report that to the users + if (numElements != WriteErrorPayloadSize && TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) { - return null; + TraceSource.TraceEvent(TraceEventType.Warning, 0, "Expected error payload to have {0} elements, found {1} elements", WriteErrorPayloadSize, numElements); } - // Verify that the protocol version of the payload matches our expected value - int senderVersion = reader.ReadInt32(); - if (senderVersion != expectedVersion) + // The payload should have enough elements that we can process all the critical fields + if (numElements < WriteErrorPayloadSize) { + return null; } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index bb8da1f4..2da8540b 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -935,7 +935,7 @@ private async Task OnChannelTerminatedAsync(QualifiedChannelId channelId) private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence payload) { // Make sure this MultiplexingStream is qualified to receive content writing error messages. - if (this.protocolMajorVersion == 1) + if (!(this.formatter is V2Formatter errorDeserializingFormattter)) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Error)) { @@ -964,8 +964,7 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc } // Deserialize the payload and verify that it was in an expected state - V2Formatter errorDeserializingFormattter = (V2Formatter)this.formatter; - WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.protocolMajorVersion); + WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.TraceSource); if (error == null) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Error)) @@ -1191,7 +1190,7 @@ private void OnChannelWritingError(Channel channel, Exception exception) Requires.NotNull(channel, nameof(channel)); // Make sure that we are allowed to send error frames on this protocol version - if (this.protocolMajorVersion == 1) + if (!(this.formatter is V2Formatter errorSerializationFormatter)) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { @@ -1207,23 +1206,25 @@ private void OnChannelWritingError(Channel channel, Exception exception) } // Verify that we are able to communicate to the remote side on this channel - if (this.channelsPendingTermination.Contains(channel.QualifiedId) || !this.openChannels.ContainsKey(channel.QualifiedId)) + lock (this.syncObject) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + if (this.channelsPendingTermination.Contains(channel.QualifiedId) || !this.openChannels.ContainsKey(channel.QualifiedId)) { - this.TraceSource.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Not informing remote side of write error on channel {0} as it is already terminated or unknown."); - } + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Not informing remote side of write error on channel {0} as it is already terminated or unknown."); + } - return; + return; + } } // Create the payload to send to the remote side WriteError error = new(exception.Message); - V2Formatter errorSerializationFormatter = (V2Formatter)this.formatter; - ReadOnlySequence serializedError = errorSerializationFormatter.SerializeWriteError(this.protocolMajorVersion, error); + ReadOnlySequence serializedError = errorSerializationFormatter.SerializeWriteError(error); // Create the frame header indicating that we encountered a content writing error FrameHeader header = new FrameHeader diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 58c42bdf..8b4d441d 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +namespace Nerdbank.Streams.Tests; + using System; using System.Buffers; using System.Diagnostics; diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index a0fd8bb9..767a0edd 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -1,6 +1,8 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +namespace Nerdbank.Streams.Tests; + using System.Buffers; using System.IO.Pipelines; using System.Linq; diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs index 660b7e51..fa0c30ce 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs @@ -1,6 +1,8 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +namespace Nerdbank.Streams.Tests; + using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; From 782ad6d8ed214a430747ceb2b6027fabecc5c417 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 25 Aug 2022 00:20:50 -0700 Subject: [PATCH 52/77] Revert namespace change --- test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs | 2 -- test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs | 2 -- test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs | 2 -- 3 files changed, 6 deletions(-) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 8b4d441d..58c42bdf 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.Streams.Tests; - using System; using System.Buffers; using System.Diagnostics; diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs index 767a0edd..a0fd8bb9 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV2Tests.cs @@ -1,8 +1,6 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.Streams.Tests; - using System.Buffers; using System.IO.Pipelines; using System.Linq; diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs index fa0c30ce..660b7e51 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamV3Tests.cs @@ -1,8 +1,6 @@ // Copyright (c) Andrew Arnott. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Nerdbank.Streams.Tests; - using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; From d6ec961ca30b0e73143cbeaa6953cba967350687 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Thu, 25 Aug 2022 00:35:25 -0700 Subject: [PATCH 53/77] Fixed build errors --- src/Nerdbank.Streams/MultiplexingStream.Formatters.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 9cfa97aa..98a8a81f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -521,24 +521,23 @@ internal ReadOnlySequence SerializeWriteError(WriteError error) /// Extracts an object from the payload using . /// /// The payload we are trying to extract the error object from. - /// The tracer to use when tracing errors to deserialize a received payload. + /// The tracer to use when tracing errors to deserialize a received payload. /// A object if the payload is correctly formatted and has the expected protocol version, /// null otherwise. - internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, TraceSource? TraceSource) + internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, TraceSource? traceSource) { MessagePackReader reader = new(serializedError); int numElements = reader.ReadArrayHeader(); // If received an unexpected number of fields, report that to the users - if (numElements != WriteErrorPayloadSize && TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) + if (numElements != WriteErrorPayloadSize && traceSource!.Switch.ShouldTrace(TraceEventType.Warning)) { - TraceSource.TraceEvent(TraceEventType.Warning, 0, "Expected error payload to have {0} elements, found {1} elements", WriteErrorPayloadSize, numElements); + traceSource.TraceEvent(TraceEventType.Warning, 0, "Expected error payload to have {0} elements, found {1} elements", WriteErrorPayloadSize, numElements); } // The payload should have enough elements that we can process all the critical fields if (numElements < WriteErrorPayloadSize) { - return null; } From 0586aeb798ab2793d80dbaca7d308163a0a79bab Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 27 Aug 2022 11:10:57 -0700 Subject: [PATCH 54/77] Switched to using protocol version for cast checking --- src/Nerdbank.Streams/MultiplexingStream.cs | 143 +++++++++++---------- 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 2da8540b..b68a32ff 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -249,6 +249,11 @@ private enum TraceEventId /// private Func? DefaultChannelTraceSourceFactory { get; } + /// + /// Gets a value indicating whether this stream can send or receive frames of type. + /// + private bool ContentWritingErrorSupported => this.protocolMajorVersion > 1; + /// /// Initializes a new instance of the class /// with set to 3. @@ -934,22 +939,6 @@ private async Task OnChannelTerminatedAsync(QualifiedChannelId channelId) /// The payload that the sender sent in the frame. private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequence payload) { - // Make sure this MultiplexingStream is qualified to receive content writing error messages. - if (!(this.formatter is V2Formatter errorDeserializingFormattter)) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Error)) - { - this.TraceSource.TraceEvent( - TraceEventType.Error, - (int)TraceEventId.WriteError, - "Rejecting writing error from channel {0} as MultiplexingStream has protocol version of {1}", - channelId, - this.protocolMajorVersion); - } - - return; - } - // Get the channel that send this frame Channel channel; lock (this.syncObject) @@ -957,32 +946,56 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc channel = this.openChannels[channelId]; } - // Verify that the channel is in a state that it can receive communication - if (channelId.Source == ChannelSource.Local && !channel.IsAccepted) + // Determines if the channel is in a state to receive messages + bool channelInValidState = channelId.Source != ChannelSource.Local || channel.IsAccepted; + + // If the channel is in a valid state and we have a valid protocol version, then process the message + if (channelInValidState && this.ContentWritingErrorSupported) { - throw new MultiplexingProtocolException($"Remote party indicated they encountered errors writing to channel {channelId} before accepting it."); - } + // Deserialize the payload and verify that it was in an expected state + V2Formatter errorDeserializingFormattter = (V2Formatter)this.formatter; + WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.TraceSource); - // Deserialize the payload and verify that it was in an expected state - WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.TraceSource); - if (error == null) + if (error == null) + { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) + { + this.TraceSource.TraceEvent( + TraceEventType.Warning, + (int)TraceEventId.WriteError, + "Rejecting content writing error from channel {0} due to invalid payload", + channelId); + } + + return; + } + + // Get the error message and complete the channel using it + string errorMessage = error.ErrorMessage; + MultiplexingProtocolException channelClosingException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); + channel.OnContentWritingCompleted(channelClosingException); + } + else if (channelInValidState && !this.ContentWritingErrorSupported) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Error)) + // The channel is in a valid state but we have a protocol version that doesn't support processing errrors + // so don't do anything. + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) { this.TraceSource.TraceEvent( - TraceEventType.Error, + TraceEventType.Warning, (int)TraceEventId.WriteError, - "Rejecting content writing error from channel {0} due to invalid payload", - channelId); + "Rejecting writing error from channel {0} as MultiplexingStream has protocol version of {1}", + channelId, + this.protocolMajorVersion); } return; } - - // Get the error message and complete the channel using it - string errorMessage = error.ErrorMessage; - MultiplexingProtocolException channelClosingException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); - channel.OnContentWritingCompleted(channelClosingException); + else + { + // The channel is in an invalid state so throw an error indicating so + throw new MultiplexingProtocolException($"Remote party indicated they encountered errors writing to channel {channelId} before accepting it."); + } } private void OnContentWritingCompleted(QualifiedChannelId channelId) @@ -1189,13 +1202,39 @@ private void OnChannelWritingError(Channel channel, Exception exception) { Requires.NotNull(channel, nameof(channel)); - // Make sure that we are allowed to send error frames on this protocol version - if (!(this.formatter is V2Formatter errorSerializationFormatter)) + // Verify that we can send a message over this channel + bool channelInValidState = true; + lock (this.syncObject) + { + channelInValidState = !this.channelsPendingTermination.Contains(channel.QualifiedId) + && this.openChannels.ContainsKey(channel.QualifiedId); + } + + // If we can send messages over this channel and we have the correct protocol version then send the error + if (channelInValidState && this.ContentWritingErrorSupported) + { + // Create the payload to send to the remote side + V2Formatter errorSerializationFormatter = (V2Formatter)this.formatter; + WriteError error = new(exception.Message); + ReadOnlySequence serializedError = errorSerializationFormatter.SerializeWriteError(error); + + // Create the frame header indicating that we encountered a content writing error + FrameHeader header = new FrameHeader + { + Code = ControlCode.ContentWritingError, + ChannelId = channel.QualifiedId, + }; + + // Send the frame alongside the payload to the remote side + this.SendFrame(header, serializedError, CancellationToken.None); + } + else if (channelInValidState && !this.ContentWritingErrorSupported) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + // The channel is in a valid state but our protocol version doesn't support writing errors so don't do anything + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) { this.TraceSource.TraceEvent( - TraceEventType.Information, + TraceEventType.Warning, (int)TraceEventId.WriteError, "Not informing remote side of write error on channel {0} since MultiplexingStream has protocol version of {1}", channel.QualifiedId, @@ -1204,37 +1243,11 @@ private void OnChannelWritingError(Channel channel, Exception exception) return; } - - // Verify that we are able to communicate to the remote side on this channel - lock (this.syncObject) + else { - if (this.channelsPendingTermination.Contains(channel.QualifiedId) || !this.openChannels.ContainsKey(channel.QualifiedId)) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Not informing remote side of write error on channel {0} as it is already terminated or unknown."); - } - - return; - } + // The channel is not in a valid state to send any messages so throw an error indicating so + throw new MultiplexingProtocolException($"Can't write content writing error to channel {channel.QualifiedId} as it is terminated or isn't open"); } - - // Create the payload to send to the remote side - WriteError error = new(exception.Message); - ReadOnlySequence serializedError = errorSerializationFormatter.SerializeWriteError(error); - - // Create the frame header indicating that we encountered a content writing error - FrameHeader header = new FrameHeader - { - Code = ControlCode.ContentWritingError, - ChannelId = channel.QualifiedId, - }; - - // Send the frame alongside the payload to the remote side - this.SendFrame(header, serializedError, CancellationToken.None); } /// From 562caf228340536ffce1de6546c86a602a052e06 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 27 Aug 2022 11:37:04 -0700 Subject: [PATCH 55/77] Added fault method --- src/nerdbank-streams/src/Channel.ts | 39 +++++++++++++------ .../tests/MultiplexingStream.Interop.spec.ts | 2 +- .../src/tests/MultiplexingStream.spec.ts | 2 +- .../Nerdbank.Streams.Interop.Tests/Program.cs | 2 +- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index 57c2cd3a..c9d6d69a 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -53,11 +53,16 @@ export abstract class Channel implements IDisposableObservable { return this._isDisposed; } + // Sends the passed in error to the remote side and then closes the channel. + // dispose can be called after calling async even though it is not necessary. + public fault(error: Error) { + this._isDisposed = true; + } + /** - * Closes this channel. If the an error is passed into the method then that error - * gets sent to the remote before the disposing of the channel. + * Closes this channel. */ - public async dispose(error? : Error) { + public dispose() { // The interesting stuff is in the derived class. this._isDisposed = true; } @@ -252,15 +257,27 @@ export class ChannelClass extends Channel { } } - public async dispose(errorToSend? : Error) { + public async fault(error: Error) { + // If the channel is already disposed then don't do anything + if(this.isDisposed) { + return; + } + + // Send the error message to the remote side + await this._multiplexingStream.onChannelWritingError(this, error.message); + + // Set the remote exception to the passed in error so that the channel is + // completed with this error + this.remoteError = error; + + // Dispose of the channel + await this.dispose(); + } + + public async dispose() { if (!this.isDisposed) { super.dispose(); - // If the channel is disposed with an error, transmit it to the remote side - if (errorToSend) { - await this._multiplexingStream.onChannelWritingError(this, errorToSend.message); - } - this._acceptance.reject(new CancellationToken.CancellationError("disposed")); // For the pipes, we Complete *our* ends, and leave the user's ends alone. @@ -271,8 +288,8 @@ export class ChannelClass extends Channel { // If we are sending an error to the remote side or received an error from the remote, // relay that information to the clients. - if (errorToSend ?? this.remoteError) { - this._completion.reject(errorToSend ?? this.remoteError); + if (this.remoteError) { + this._completion.reject(this.remoteError); } else { this._completion.resolve(); } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index 71aaf24a..7733714f 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -112,7 +112,7 @@ import * as assert from "assert"; caughtCompletionErr = true; }); - await errorWriteChannel.dispose(errorToSend); + await errorWriteChannel.fault(errorToSend); assert.deepStrictEqual(caughtCompletionErr, true); let expectedMessage = `received error: Remote party indicated writing error: ${errorMessage}`; diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 90d0ca50..281898e3 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -239,7 +239,7 @@ import * as assert from "assert"; mx2.acceptChannelAsync("test"), ]); - await channels[0].dispose(errorToSend); + await channels[0].fault(errorToSend); // Ensure that the current channel disposes with the error let caughtSenderError = false; diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index e55a6933..55bf0292 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -25,7 +25,7 @@ private Program(MultiplexingStream mx) private static async Task Main(string[] args) { - ////System.Diagnostics.Debugger.Launch(); + // System.Diagnostics.Debugger.Launch(); int protocolMajorVersion = int.Parse(args[0]); var options = new MultiplexingStream.Options { From 3e4baf8861b2a45cdf6e9c1a38a54b907fbbb2a3 Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Sat, 27 Aug 2022 11:43:12 -0700 Subject: [PATCH 56/77] Fixed formatter mismatch --- src/nerdbank-streams/src/MultiplexingStream.ts | 4 ++-- .../src/MultiplexingStreamFormatters.ts | 14 ++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 27e3e78b..443b1a55 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -593,7 +593,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { // Convert the error message into a payload into a formatter. const writingError = new WriteError(errorMessage); const errorSerializingFormatter = this.formatter as MultiplexingStreamV2Formatter; - const errorPayload = errorSerializingFormatter.serializeContentWritingError(this.protocolMajorVersion, writingError); + const errorPayload = errorSerializingFormatter.serializeContentWritingError(writingError); // Sent the error to the remote side await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); @@ -754,7 +754,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { // Extract the error from the payload const errorDeserializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); - const writingError = errorDeserializingFormatter.deserializeContentWritingError(payload, this.protocolMajorVersion); + const writingError = errorDeserializingFormatter.deserializeContentWritingError(payload); if (!writingError) { throw new Error("Couldn't process content writing error payload received from remote"); } diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index 54becd84..54b5ca95 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -303,22 +303,16 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } - serializeContentWritingError(version: number, writingError: WriteError) : Buffer { - const payload: any[] = [version, writingError.errorMessage]; + serializeContentWritingError(writingError: WriteError) : Buffer { + const payload: any[] = [writingError.errorMessage]; return msgpack.encode(payload); } - deserializeContentWritingError(payload: Buffer, expectedVersion: number) : WriteError | null { + deserializeContentWritingError(payload: Buffer) : WriteError | null { const msgpackObject = msgpack.decode(payload); - const payloadVersion : number = msgpackObject[0]; - - // Make sure the version of the payload matches the expected version. - if (payloadVersion !== expectedVersion) { - return null; - } // Return the error message to the caller. - const errorMsg : string = msgpackObject[1]; + const errorMsg : string = msgpackObject[0]; return new WriteError(errorMsg); } From d8dcb0cd67ce0efde383d9e7da3f3a314dced2ec Mon Sep 17 00:00:00 2001 From: Devesh Sarda <32046390+sarda-devesh@users.noreply.github.com> Date: Mon, 29 Aug 2022 01:21:10 -0700 Subject: [PATCH 57/77] Moved newly added tests with other tests --- .../MultiplexingStreamTests.cs | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 58c42bdf..71707928 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -36,6 +36,70 @@ public MultiplexingStreamTests(ITestOutputHelper logger) protected virtual int ProtocolMajorVersion { get; } = 1; + public async Task InitializeAsync() + { + var mx1TraceSource = new TraceSource(nameof(this.mx1), SourceLevels.All); + var mx2TraceSource = new TraceSource(nameof(this.mx2), SourceLevels.All); + + mx1TraceSource.Listeners.Add(new XunitTraceListener(this.Logger, this.TestId, this.TestTimer)); + mx2TraceSource.Listeners.Add(new XunitTraceListener(this.Logger, this.TestId, this.TestTimer)); + + Func traceSourceFactory = (string mxInstanceName, MultiplexingStream.QualifiedChannelId id, string name) => + { + var traceSource = new TraceSource(mxInstanceName + " channel " + id, SourceLevels.All); + traceSource.Listeners.Clear(); // remove DefaultTraceListener + traceSource.Listeners.Add(new XunitTraceListener(this.Logger, this.TestId, this.TestTimer)); + return traceSource; + }; + + Func mx1TraceSourceFactory = (MultiplexingStream.QualifiedChannelId id, string name) => traceSourceFactory(nameof(this.mx1), id, name); + Func mx2TraceSourceFactory = (MultiplexingStream.QualifiedChannelId id, string name) => traceSourceFactory(nameof(this.mx2), id, name); + + (this.transport1, this.transport2) = FullDuplexStream.CreatePair(new PipeOptions(pauseWriterThreshold: 2 * 1024 * 1024)); + Task? mx1 = MultiplexingStream.CreateAsync(this.transport1, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion, TraceSource = mx1TraceSource, DefaultChannelTraceSourceFactoryWithQualifier = mx1TraceSourceFactory }, this.TimeoutToken); + Task? mx2 = MultiplexingStream.CreateAsync(this.transport2, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion, TraceSource = mx2TraceSource, DefaultChannelTraceSourceFactoryWithQualifier = mx2TraceSourceFactory }, this.TimeoutToken); + this.mx1 = await mx1; + this.mx2 = await mx2; + } + + public async Task DisposeAsync() + { + await (this.mx1?.DisposeAsync() ?? default); + await (this.mx2?.DisposeAsync() ?? default); + AssertNoFault(this.mx1); + AssertNoFault(this.mx2); + + this.mx1?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); + this.mx2?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); + } + + [Fact, Obsolete] + public async Task DefaultChannelTraceSourceFactory() + { + var factoryArgs = new TaskCompletionSource<(int, string)>(); + var obsoleteFactory = new Func((id, name) => + { + factoryArgs.SetResult((id, name)); + return null; + }); + (this.transport1, this.transport2) = FullDuplexStream.CreatePair(); + Task? mx1Task = MultiplexingStream.CreateAsync(this.transport1, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion, DefaultChannelTraceSourceFactory = obsoleteFactory }, this.TimeoutToken); + Task? mx2Task = MultiplexingStream.CreateAsync(this.transport2, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion }, this.TimeoutToken); + MultiplexingStream? mx1 = await mx1Task; + MultiplexingStream? mx2 = await mx2Task; + + MultiplexingStream.Channel[]? ch = await Task.WhenAll(mx1.OfferChannelAsync("myname"), mx2.AcceptChannelAsync("myname")); + (int, string) args = await factoryArgs.Task; + Assert.Equal(ch[0].QualifiedId.Id, (ulong)args.Item1); + Assert.Equal("myname", args.Item2); + } + + [Fact] + public void DefaultMajorProtocolVersion() + { + Assert.Equal(1, new MultiplexingStream.Options().ProtocolMajorVersion); + } + [Fact] public async Task OfferPipeWithError() { @@ -45,7 +109,7 @@ public async Task OfferPipeWithError() // Prepare a readonly pipe that is already populated with data an an error var pipe = new Pipe(); await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - pipe.Writer.Complete(new NullReferenceException(errorMessage)); + pipe.Writer.Complete(new Exception(errorMessage)); // Create a sending and receiving channel using the channel MultiplexingStream.Channel? localChannel = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); @@ -107,70 +171,6 @@ public async Task OfferPipeWithError() Assert.Equal(this.ProtocolMajorVersion > 1, remoteChannelCompletedWithError); } - public async Task InitializeAsync() - { - var mx1TraceSource = new TraceSource(nameof(this.mx1), SourceLevels.All); - var mx2TraceSource = new TraceSource(nameof(this.mx2), SourceLevels.All); - - mx1TraceSource.Listeners.Add(new XunitTraceListener(this.Logger, this.TestId, this.TestTimer)); - mx2TraceSource.Listeners.Add(new XunitTraceListener(this.Logger, this.TestId, this.TestTimer)); - - Func traceSourceFactory = (string mxInstanceName, MultiplexingStream.QualifiedChannelId id, string name) => - { - var traceSource = new TraceSource(mxInstanceName + " channel " + id, SourceLevels.All); - traceSource.Listeners.Clear(); // remove DefaultTraceListener - traceSource.Listeners.Add(new XunitTraceListener(this.Logger, this.TestId, this.TestTimer)); - return traceSource; - }; - - Func mx1TraceSourceFactory = (MultiplexingStream.QualifiedChannelId id, string name) => traceSourceFactory(nameof(this.mx1), id, name); - Func mx2TraceSourceFactory = (MultiplexingStream.QualifiedChannelId id, string name) => traceSourceFactory(nameof(this.mx2), id, name); - - (this.transport1, this.transport2) = FullDuplexStream.CreatePair(new PipeOptions(pauseWriterThreshold: 2 * 1024 * 1024)); - Task? mx1 = MultiplexingStream.CreateAsync(this.transport1, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion, TraceSource = mx1TraceSource, DefaultChannelTraceSourceFactoryWithQualifier = mx1TraceSourceFactory }, this.TimeoutToken); - Task? mx2 = MultiplexingStream.CreateAsync(this.transport2, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion, TraceSource = mx2TraceSource, DefaultChannelTraceSourceFactoryWithQualifier = mx2TraceSourceFactory }, this.TimeoutToken); - this.mx1 = await mx1; - this.mx2 = await mx2; - } - - public async Task DisposeAsync() - { - await (this.mx1?.DisposeAsync() ?? default); - await (this.mx2?.DisposeAsync() ?? default); - AssertNoFault(this.mx1); - AssertNoFault(this.mx2); - - this.mx1?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); - this.mx2?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); - } - - [Fact, Obsolete] - public async Task DefaultChannelTraceSourceFactory() - { - var factoryArgs = new TaskCompletionSource<(int, string)>(); - var obsoleteFactory = new Func((id, name) => - { - factoryArgs.SetResult((id, name)); - return null; - }); - (this.transport1, this.transport2) = FullDuplexStream.CreatePair(); - Task? mx1Task = MultiplexingStream.CreateAsync(this.transport1, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion, DefaultChannelTraceSourceFactory = obsoleteFactory }, this.TimeoutToken); - Task? mx2Task = MultiplexingStream.CreateAsync(this.transport2, new MultiplexingStream.Options { ProtocolMajorVersion = this.ProtocolMajorVersion }, this.TimeoutToken); - MultiplexingStream? mx1 = await mx1Task; - MultiplexingStream? mx2 = await mx2Task; - - MultiplexingStream.Channel[]? ch = await Task.WhenAll(mx1.OfferChannelAsync("myname"), mx2.AcceptChannelAsync("myname")); - (int, string) args = await factoryArgs.Task; - Assert.Equal(ch[0].QualifiedId.Id, (ulong)args.Item1); - Assert.Equal("myname", args.Item2); - } - - [Fact] - public void DefaultMajorProtocolVersion() - { - Assert.Equal(1, new MultiplexingStream.Options().ProtocolMajorVersion); - } - [Fact] public async Task OfferReadOnlyDuplexPipe() { From 2aaea24e044ae5b4e550b27a1f8c0876c5ef14d1 Mon Sep 17 00:00:00 2001 From: Devesh Sarda Date: Sat, 17 Sep 2022 12:49:28 -0700 Subject: [PATCH 58/77] Added check for channel dispoal before sending of error --- .../MultiplexingStream.Channel.cs | 40 ++++++++++++++++++- src/Nerdbank.Streams/MultiplexingStream.cs | 17 +++++--- .../Nerdbank.Streams.Tests.csproj | 2 +- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index f2c18e83..d103f15f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -582,7 +582,9 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) this.localWindowSize ??= channelOptions.ChannelReceivingWindowSize is long windowSize ? Math.Max(windowSize, this.MultiplexingStream.DefaultChannelReceivingWindowSize) : this.MultiplexingStream.DefaultChannelReceivingWindowSize; } + TraceSource traceSrc = new TraceSource($"{nameof(Streams.MultiplexingStream)}.{nameof(Channel)} {this.QualifiedId} ({this.Name}) TryAcceptOffer", SourceLevels.Critical); var acceptanceParameters = new AcceptanceParameters(this.localWindowSize.Value); + if (this.acceptanceSource.TrySetResult(acceptanceParameters)) { if (this.QualifiedId.Source != ChannelSource.Seeded) @@ -607,8 +609,16 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) { // A (harmless) race condition was hit. // Swallow it and return false below. + if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) + { + traceSrc.TraceEvent(TraceEventType.Critical, (int)TraceEventId.WriteError, "Rejecting channel offer due to ObjectDisposedException exception"); + } } } + else if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) + { + traceSrc.TraceEvent(TraceEventType.Critical, (int)TraceEventId.WriteError, "Rejecting channel offer due to trySetResult failure"); + } return false; } @@ -870,6 +880,11 @@ private async Task ProcessOutboundTransmissionsAsync() } catch (Exception ex) { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Caught exception relaying message to remote side: {0}", ex.Message); + } + // If the operation had been cancelled then we are expecting to receive this error so don't transmit it. if (ex is OperationCanceledException && this.DisposalToken.IsCancellationRequested) { @@ -885,14 +900,35 @@ private async Task ProcessOutboundTransmissionsAsync() // Since we're not expecting to receive this error, transmit the error to the remote side. await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); - this.MultiplexingStream.OnChannelWritingError(this, ex); + + // Send error message to the remote if this channel hasn't been disposed + bool canSendErrorMessage; + lock (this.SyncObject) + { + canSendErrorMessage = !this.isDisposed; + } + + if (canSendErrorMessage) + { + this.MultiplexingStream.OnChannelWritingError(this, ex); + } } throw; } finally { - this.MultiplexingStream.OnChannelWritingCompleted(this); + // Send the completion message to the remote if the channel hasn't been disposed + bool canSendCompletionMessage; + lock (this.SyncObject) + { + canSendCompletionMessage = !this.isDisposed; + } + + if (canSendCompletionMessage) + { + this.MultiplexingStream.OnChannelWritingCompleted(this); + } // Restore the PipeReader to the field. lock (this.SyncObject) diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index b68a32ff..e5e98f5c 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1143,12 +1143,7 @@ private bool TryAcceptChannel(Channel channel, ChannelOptions options) Requires.NotNull(channel, nameof(channel)); Requires.NotNull(options, nameof(options)); - if (channel.TryAcceptOffer(options)) - { - return true; - } - - return false; + return channel.TryAcceptOffer(options); } private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) @@ -1158,6 +1153,11 @@ private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) if (!this.TryAcceptChannel(channel, options)) { + if (System.Environment.StackTrace.Contains("OfferPipeWithError")) + { + System.Diagnostics.Debugger.Launch(); + } + if (channel.IsAccepted) { throw new InvalidOperationException("Channel is already accepted."); @@ -1202,6 +1202,11 @@ private void OnChannelWritingError(Channel channel, Exception exception) { Requires.NotNull(channel, nameof(channel)); + if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information)) + { + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Local channel {0} encountered write error {1}", channel.QualifiedId, exception.Message); + } + // Verify that we can send a message over this channel bool channelInValidState = true; lock (this.syncObject) diff --git a/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj b/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj index 469b0614..e37d81ba 100644 --- a/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj +++ b/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj @@ -1,6 +1,6 @@  - net6.0;netcoreapp3.1;net472 + net6.0;netcoreapp3.1 true From d0a63e5016220d333082ec180b58fcc3d8269243 Mon Sep 17 00:00:00 2001 From: Devesh Sarda Date: Sat, 17 Sep 2022 13:22:06 -0700 Subject: [PATCH 59/77] Code push to see error in pipeline --- run_test_until_failure.sh | 9 +++++++++ .../Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100755 run_test_until_failure.sh diff --git a/run_test_until_failure.sh b/run_test_until_failure.sh new file mode 100755 index 00000000..d9012753 --- /dev/null +++ b/run_test_until_failure.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +dotnet build +status=$? +until [[ $status -ne 0 ]] +do + dotnet test + status=$? +done diff --git a/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj b/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj index e37d81ba..469b0614 100644 --- a/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj +++ b/test/Nerdbank.Streams.Tests/Nerdbank.Streams.Tests.csproj @@ -1,6 +1,6 @@  - net6.0;netcoreapp3.1 + net6.0;netcoreapp3.1;net472 true From bafac8aaa7a30352795ae1ffccdd399861b4d182 Mon Sep 17 00:00:00 2001 From: Devesh Date: Sun, 18 Sep 2022 11:07:18 -0700 Subject: [PATCH 60/77] Fixed uncessary lock acquisiton --- .../MultiplexingStream.Channel.cs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index d103f15f..9cf02269 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -582,9 +582,7 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) this.localWindowSize ??= channelOptions.ChannelReceivingWindowSize is long windowSize ? Math.Max(windowSize, this.MultiplexingStream.DefaultChannelReceivingWindowSize) : this.MultiplexingStream.DefaultChannelReceivingWindowSize; } - TraceSource traceSrc = new TraceSource($"{nameof(Streams.MultiplexingStream)}.{nameof(Channel)} {this.QualifiedId} ({this.Name}) TryAcceptOffer", SourceLevels.Critical); var acceptanceParameters = new AcceptanceParameters(this.localWindowSize.Value); - if (this.acceptanceSource.TrySetResult(acceptanceParameters)) { if (this.QualifiedId.Source != ChannelSource.Seeded) @@ -609,16 +607,8 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) { // A (harmless) race condition was hit. // Swallow it and return false below. - if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) - { - traceSrc.TraceEvent(TraceEventType.Critical, (int)TraceEventId.WriteError, "Rejecting channel offer due to ObjectDisposedException exception"); - } } } - else if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) - { - traceSrc.TraceEvent(TraceEventType.Critical, (int)TraceEventId.WriteError, "Rejecting channel offer due to trySetResult failure"); - } return false; } @@ -920,8 +910,12 @@ private async Task ProcessOutboundTransmissionsAsync() { // Send the completion message to the remote if the channel hasn't been disposed bool canSendCompletionMessage; + + // Restore the PipeReader to the field. lock (this.SyncObject) { + this.mxStreamIOReader = mxStreamIOReader; + mxStreamIOReader = null; canSendCompletionMessage = !this.isDisposed; } @@ -929,13 +923,6 @@ private async Task ProcessOutboundTransmissionsAsync() { this.MultiplexingStream.OnChannelWritingCompleted(this); } - - // Restore the PipeReader to the field. - lock (this.SyncObject) - { - this.mxStreamIOReader = mxStreamIOReader; - mxStreamIOReader = null; - } } } From 3b7352b7416a8e0698d252eef24e8f83aa0b637e Mon Sep 17 00:00:00 2001 From: Devesh Date: Sun, 18 Sep 2022 17:34:30 -0700 Subject: [PATCH 61/77] Tried to get error to show up locally --- .../MultiplexingStream.Channel.cs | 24 +++++++++++++++---- src/Nerdbank.Streams/MultiplexingStream.cs | 5 ---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 9cf02269..c637b1a5 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -583,6 +583,8 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) } var acceptanceParameters = new AcceptanceParameters(this.localWindowSize.Value); + string errorCause = "TrySetResult failure"; + if (this.acceptanceSource.TrySetResult(acceptanceParameters)) { if (this.QualifiedId.Source != ChannelSource.Seeded) @@ -607,10 +609,18 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) { // A (harmless) race condition was hit. // Swallow it and return false below. + errorCause = "Object Disposed Exception"; } } - return false; + /* + if (errorCause != string.Empty && System.Environment.StackTrace.Contains("OfferPipeWithError")) + { + System.Diagnostics.Debugger.Launch(); + } + */ + + return false && errorCause.Length == 0; } /// @@ -910,12 +920,8 @@ private async Task ProcessOutboundTransmissionsAsync() { // Send the completion message to the remote if the channel hasn't been disposed bool canSendCompletionMessage; - - // Restore the PipeReader to the field. lock (this.SyncObject) { - this.mxStreamIOReader = mxStreamIOReader; - mxStreamIOReader = null; canSendCompletionMessage = !this.isDisposed; } @@ -923,6 +929,14 @@ private async Task ProcessOutboundTransmissionsAsync() { this.MultiplexingStream.OnChannelWritingCompleted(this); } + + // Restore the PipeReader to the field. + lock (this.SyncObject) + { + this.mxStreamIOReader = mxStreamIOReader; + mxStreamIOReader = null; + canSendCompletionMessage = !this.isDisposed; + } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index e5e98f5c..731442f8 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1153,11 +1153,6 @@ private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) if (!this.TryAcceptChannel(channel, options)) { - if (System.Environment.StackTrace.Contains("OfferPipeWithError")) - { - System.Diagnostics.Debugger.Launch(); - } - if (channel.IsAccepted) { throw new InvalidOperationException("Channel is already accepted."); From ebf09033168806bc319937b0d7f9cb4d434057f6 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 10:39:44 -0700 Subject: [PATCH 62/77] Added trace statements for better error visibility --- .../MultiplexingStream.Channel.cs | 99 +++++++++----- src/Nerdbank.Streams/MultiplexingStream.cs | 29 +++- .../MultiplexingStreamTests.cs | 126 ++++++++++-------- 3 files changed, 165 insertions(+), 89 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index c637b1a5..95c806f1 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -343,6 +343,16 @@ public void Dispose() { if (!this.IsDisposed) { + TraceSource traceSrc = this.GetTraceSource(); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Calling TrySetCanceled of acceptanceSource in Dispose method of channel {0}", + this.QualifiedId); + } + this.acceptanceSource.TrySetCanceled(); this.optionsAppliedTaskSource?.TrySetCanceled(); @@ -583,7 +593,16 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) } var acceptanceParameters = new AcceptanceParameters(this.localWindowSize.Value); - string errorCause = "TrySetResult failure"; + TraceSource traceSrc = this.GetTraceSource(); + + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Calling TrySetResult of acceptanceSource in TryAcceptOffer method of channel {0}", + this.QualifiedId); + } if (this.acceptanceSource.TrySetResult(acceptanceParameters)) { @@ -609,18 +628,24 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) { // A (harmless) race condition was hit. // Swallow it and return false below. - errorCause = "Object Disposed Exception"; + if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) + { + traceSrc.TraceEvent( + TraceEventType.Critical, + (int)TraceEventId.WriteError, + "Rejecting channel offer due to ObjectDisposedException exception"); + } } } - - /* - if (errorCause != string.Empty && System.Environment.StackTrace.Contains("OfferPipeWithError")) + else if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) { - System.Diagnostics.Debugger.Launch(); + traceSrc.TraceEvent( + TraceEventType.Critical, + (int)TraceEventId.WriteError, + "Rejecting channel offer due to trySetResult failure"); } - */ - return false && errorCause.Length == 0; + return false; } /// @@ -632,6 +657,16 @@ internal bool OnAccepted(AcceptanceParameters acceptanceParameters) { lock (this.SyncObject) { + TraceSource traceSrc = this.GetTraceSource(); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Calling TrySetResult of acceptanceSource in OnAccepted method of channel {0}", + this.QualifiedId); + } + if (this.acceptanceSource.TrySetResult(acceptanceParameters)) { this.remoteWindowSize = acceptanceParameters.RemoteWindowSize; @@ -692,6 +727,17 @@ private async ValueTask GetReceivedMessagePipeWriterAsync(Canc } } + /// + /// GetTraceSource gets the trace source to use when emitting trace messages. + /// + /// The to use when emitting messages. + private TraceSource GetTraceSource() + { + return this.TraceSource + ?? this.MultiplexingStream.DefaultChannelTraceSourceFactory?.Invoke(this.QualifiedId, this.Name) + ?? new TraceSource($"{nameof(Streams.MultiplexingStream)}.{nameof(Channel)} {this.QualifiedId} ({this.Name})", SourceLevels.All); + } + /// /// Apply channel options to this channel, including setting up or linking to an user-supplied pipe writer/reader pair. /// @@ -735,6 +781,16 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) } catch (Exception ex) { + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Critical)) + { + this.TraceSource.TraceEvent( + TraceEventType.Critical, + (int)TraceEventId.WriteError, + "Caught ApplyChannelOptions error on channel {0}: {1}", + this.QualifiedId, + ex.Message); + } + this.optionsAppliedTaskSource?.TrySetException(ex); throw; } @@ -900,42 +956,21 @@ private async Task ProcessOutboundTransmissionsAsync() // Since we're not expecting to receive this error, transmit the error to the remote side. await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); - - // Send error message to the remote if this channel hasn't been disposed - bool canSendErrorMessage; - lock (this.SyncObject) - { - canSendErrorMessage = !this.isDisposed; - } - - if (canSendErrorMessage) - { - this.MultiplexingStream.OnChannelWritingError(this, ex); - } + this.MultiplexingStream.OnChannelWritingError(this, ex); } throw; } finally { - // Send the completion message to the remote if the channel hasn't been disposed - bool canSendCompletionMessage; - lock (this.SyncObject) - { - canSendCompletionMessage = !this.isDisposed; - } - - if (canSendCompletionMessage) - { - this.MultiplexingStream.OnChannelWritingCompleted(this); - } + // Send the completion message to the remote + this.MultiplexingStream.OnChannelWritingCompleted(this); // Restore the PipeReader to the field. lock (this.SyncObject) { this.mxStreamIOReader = mxStreamIOReader; mxStreamIOReader = null; - canSendCompletionMessage = !this.isDisposed; } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 731442f8..09aef7b6 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -225,7 +225,7 @@ private enum TraceEventId /// Gets the logger used by this instance. /// /// Never null. - public TraceSource TraceSource { get; } + public TraceSource TraceSource { get; private set; } /// /// Gets the default window size used for new channels that do not specify a value for . @@ -451,6 +451,16 @@ public Channel AcceptChannel(ulong id, ChannelOptions? options = default) } } + TraceSource traceSrc = this.GetTraceSource(); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Calling AcceptChannelOrThrow inside AcceptChannel for channel {0}", + channel.QualifiedId); + } + this.AcceptChannelOrThrow(channel, options); return channel; } @@ -998,6 +1008,11 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc } } + private TraceSource GetTraceSource() + { + return this.TraceSource ?? new TraceSource($"{nameof(Streams.MultiplexingStream)}", SourceLevels.All); + } + private void OnContentWritingCompleted(QualifiedChannelId channelId) { Channel channel; @@ -1131,6 +1146,16 @@ private void OnOffer(QualifiedChannelId channelId, ReadOnlySequence payloa if (acceptingChannelAlreadyPresent) { + TraceSource traceSrc = this.GetTraceSource(); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Calling AcceptChannelOrThrow inside OnOffer method for channel {0}", + channel.QualifiedId); + } + this.AcceptChannelOrThrow(channel, options); } @@ -1328,7 +1353,7 @@ private async Task SendFrameAsync(FrameHeader header, ReadOnlySequence pay // In such cases, we should just suppress transmission of the frame because the other side does not care. // ContentWritingCompleted can be sent to SendFrame after a ChannelTerminated message such that neither have been transmitted yet // and thus wasn't in the termination collection until later, so forgive that too. - if (header.Code is ControlCode.ContentProcessed or ControlCode.ContentWritingCompleted) + if (header.Code is ControlCode.ContentProcessed or ControlCode.ContentWritingCompleted or ControlCode.ContentWritingError) { this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.FrameSendSkipped, "Skipping {0} frame for channel {1} because we're about to terminate it.", header.Code, header.ChannelId); return; diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 71707928..6330dcd1 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -64,13 +64,21 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - await (this.mx1?.DisposeAsync() ?? default); - await (this.mx2?.DisposeAsync() ?? default); - AssertNoFault(this.mx1); - AssertNoFault(this.mx2); + try + { + await (this.mx1?.DisposeAsync() ?? default); + await (this.mx2?.DisposeAsync() ?? default); + AssertNoFault(this.mx1); + AssertNoFault(this.mx2); - this.mx1?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); - this.mx2?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); + this.mx1?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); + this.mx2?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); + } + catch (Exception err) + { + this.Logger.WriteLine("Caught error in DisposeAsync: {0}", err.Message); + throw; + } } [Fact, Obsolete] @@ -103,72 +111,80 @@ public void DefaultMajorProtocolVersion() [Fact] public async Task OfferPipeWithError() { - bool errorThrown = false; - string errorMessage = "Hello World"; + try + { + bool errorThrown = false; + string errorMessage = "Hello World"; - // Prepare a readonly pipe that is already populated with data an an error - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - pipe.Writer.Complete(new Exception(errorMessage)); + // Prepare a readonly pipe that is already populated with data an an error + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + pipe.Writer.Complete(new Exception(errorMessage)); - // Create a sending and receiving channel using the channel - MultiplexingStream.Channel? localChannel = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); - await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? remoteChannel = this.mx2.AcceptChannel(localChannel.QualifiedId.Id); + // Create a sending and receiving channel using the channel + MultiplexingStream.Channel? localChannel = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? remoteChannel = this.mx2.AcceptChannel(localChannel.QualifiedId.Id); - bool continueReading = true; - while (continueReading) - { - try + bool continueReading = true; + while (continueReading) { - // Read the latest input from the local channel and determine if we should continue reading - ReadResult readResult = await remoteChannel.Input.ReadAsync(this.TimeoutToken); - if (readResult.IsCompleted || readResult.IsCanceled) + try + { + // Read the latest input from the local channel and determine if we should continue reading + ReadResult readResult = await remoteChannel.Input.ReadAsync(this.TimeoutToken); + if (readResult.IsCompleted || readResult.IsCanceled) + { + continueReading = false; + } + + remoteChannel.Input.AdvanceTo(readResult.Buffer.End); + } + catch (Exception exception) { - continueReading = false; + // Check not only that we caught an exception but that it was the expected exception. + errorThrown = exception.Message.Contains(errorMessage); + continueReading = !errorThrown; } + } + + Assert.Equal(this.ProtocolMajorVersion > 1, errorThrown); - remoteChannel.Input.AdvanceTo(readResult.Buffer.End); + // Ensure that the writer of the error completes with that error, no matter what version of the protocol they are using + string expectedWriterErrorMessage = errorMessage; + bool localChannelCompletedWithError = false; + + try + { + await localChannel.Completion; } - catch (Exception exception) + catch (Exception writeException) { - // Check not only that we caught an exception but that it was the expected exception. - errorThrown = exception.Message.Contains(errorMessage); - continueReading = !errorThrown; + localChannelCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); } - } - - Assert.Equal(this.ProtocolMajorVersion > 1, errorThrown); - // Ensure that the writer of the error completes with that error, no matter what version of the protocol they are using - string expectedWriterErrorMessage = errorMessage; - bool localChannelCompletedWithError = false; - - try - { - await localChannel.Completion; - } - catch (Exception writeException) - { - localChannelCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); - } + Assert.True(localChannelCompletedWithError); - Assert.True(localChannelCompletedWithError); + // Ensure that the reader only completes with an error if we are using a protocol version > 1 + string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; + bool remoteChannelCompletedWithError = false; - // Ensure that the reader only completes with an error if we are using a protocol version > 1 - string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; - bool remoteChannelCompletedWithError = false; + try + { + await remoteChannel.Completion; + } + catch (Exception readException) + { + remoteChannelCompletedWithError = readException.Message.Contains(expectedReaderErrorMessage); + } - try - { - await remoteChannel.Completion; + Assert.Equal(this.ProtocolMajorVersion > 1, remoteChannelCompletedWithError); } - catch (Exception readException) + catch (Exception err) { - remoteChannelCompletedWithError = readException.Message.Contains(expectedReaderErrorMessage); + this.Logger.WriteLine("Caught error in OfferPipeWithError: {0}", err.Message); + throw; } - - Assert.Equal(this.ProtocolMajorVersion > 1, remoteChannelCompletedWithError); } [Fact] From b33e34e226086499642d2ac35e581da7ee6fc32c Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 10:53:52 -0700 Subject: [PATCH 63/77] Changed channel options error to be more verbose --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 95c806f1..2d01ed8a 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -779,19 +779,19 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) this.DisposeSelfOnFailure(this.mxStreamIOReaderCompleted); this.DisposeSelfOnFailure(this.AutoCloseOnPipesClosureAsync()); } - catch (Exception ex) + catch (Exception exception) { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Critical)) { this.TraceSource.TraceEvent( TraceEventType.Critical, (int)TraceEventId.WriteError, - "Caught ApplyChannelOptions error on channel {0}: {1}", + "Caught ApplyChannelOptions error on channel {0}:\n{1}", this.QualifiedId, - ex.Message); + exception); } - this.optionsAppliedTaskSource?.TrySetException(ex); + this.optionsAppliedTaskSource?.TrySetException(exception); throw; } finally From fd44d7e1125dad9f28573928bf0601c56a660c94 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 20:43:38 -0700 Subject: [PATCH 64/77] Tried to fix race condition --- .../MultiplexingStream.Channel.cs | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index a6ca5da9..7ef11425 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -604,8 +604,13 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) this.QualifiedId); } - if (this.acceptanceSource.TrySetResult(acceptanceParameters)) + try { + // Set up the channel options and ensure that the channel is still valid after applying the options + this.ApplyChannelOptions(channelOptions); + Verify.NotDisposed(this); + + // If we aren't a seeded channel then send an offer accepted frame if (this.QualifiedId.Source != ChannelSource.Seeded) { ReadOnlySequence payload = this.MultiplexingStream.formatter.Serialize(acceptanceParameters); @@ -619,32 +624,24 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) CancellationToken.None); } - try - { - this.ApplyChannelOptions(channelOptions); - return true; - } - catch (ObjectDisposedException) - { - // A (harmless) race condition was hit. - // Swallow it and return false below. - if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) - { - traceSrc.TraceEvent( - TraceEventType.Critical, - (int)TraceEventId.WriteError, - "Rejecting channel offer due to ObjectDisposedException exception"); - } - } + return this.acceptanceSource.TrySetResult(acceptanceParameters); } - else if (traceSrc.Switch.ShouldTrace(TraceEventType.Critical)) + catch (Exception exception) { - traceSrc.TraceEvent( - TraceEventType.Critical, - (int)TraceEventId.WriteError, - "Rejecting channel offer due to trySetResult failure"); + // Record the exception in the acceptance source + this.acceptanceSource.TrySetException(exception); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Caught exception in TryAcceptOffer method of channel {0}: \n {1}", + this.QualifiedId, + exception); + } } + // We caught an exception so return false return false; } @@ -792,7 +789,6 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) } this.optionsAppliedTaskSource?.TrySetException(exception); - throw; } finally { From 738add2913d1d9bb0df3c6e46ad1aa6793b06a4f Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 21:00:43 -0700 Subject: [PATCH 65/77] Only swallow exception if it was not user specified --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 7ef11425..d9677319 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -606,7 +606,8 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) try { - // Set up the channel options and ensure that the channel is still valid after applying the options + // Set up the channel options and ensure that the channel is still valid + // before we transition to an accepted state this.ApplyChannelOptions(channelOptions); Verify.NotDisposed(this); @@ -641,8 +642,8 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) } } - // We caught an exception so return false - return false; + // Swallow the exception if we faulted overself rather than an a user specified dispose. + return this.faultingException != null; } /// @@ -788,7 +789,9 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) exception); } + // Record that we caught an exception this.optionsAppliedTaskSource?.TrySetException(exception); + throw; } finally { From a2eae37697eb32ba0cb9eb3fb6842dbab6582920 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 21:37:26 -0700 Subject: [PATCH 66/77] Retrigger build pipeline From 53a142b70882eb34f0bbd99644614e0c5805dafb Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 21:39:03 -0700 Subject: [PATCH 67/77] Add check in TryAcceptOffer that channel is disposed --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index d9677319..1405508b 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -643,7 +643,7 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) } // Swallow the exception if we faulted overself rather than an a user specified dispose. - return this.faultingException != null; + return this.IsDisposed && this.faultingException != null; } /// From 29fc1f884ac69ef9a7aa2bfeed0b87ea8cc43eb8 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 21:42:00 -0700 Subject: [PATCH 68/77] Verify that it was an object disposed exception --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 1405508b..eb920f7d 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -604,6 +604,7 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) this.QualifiedId); } + bool expectedExceptionType; try { // Set up the channel options and ensure that the channel is still valid @@ -631,6 +632,8 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) { // Record the exception in the acceptance source this.acceptanceSource.TrySetException(exception); + expectedExceptionType = exception is ObjectDisposedException; + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) { traceSrc.TraceEvent( @@ -642,8 +645,9 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) } } - // Swallow the exception if we faulted overself rather than an a user specified dispose. - return this.IsDisposed && this.faultingException != null; + // Swallow the exception if it is an objectDisposeException due to a self triggered + // fault rather than a user triggered dispose. + return expectedExceptionType && this.faultingException != null; } /// From 3ab4f466296d422d4abb52cec0f1227628279370 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 22:05:38 -0700 Subject: [PATCH 69/77] Revert to old state to check for disposal --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index eb920f7d..8732523f 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -604,7 +604,6 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) this.QualifiedId); } - bool expectedExceptionType; try { // Set up the channel options and ensure that the channel is still valid @@ -632,7 +631,6 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) { // Record the exception in the acceptance source this.acceptanceSource.TrySetException(exception); - expectedExceptionType = exception is ObjectDisposedException; if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) { @@ -647,7 +645,7 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) // Swallow the exception if it is an objectDisposeException due to a self triggered // fault rather than a user triggered dispose. - return expectedExceptionType && this.faultingException != null; + return this.IsDisposed && this.faultingException != null; } /// From b38d67bb4cec8da7a984d8fcec65076663274654 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 22:16:47 -0700 Subject: [PATCH 70/77] Retrigger build pipeline try 2 From b0248f5222a1bd8e5e5a3ceee52f220f779c22f8 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 23:06:20 -0700 Subject: [PATCH 71/77] Checked for acceptance transition to faulted --- .../MultiplexingStream.Channel.cs | 24 +++++++++++++++---- src/Nerdbank.Streams/MultiplexingStream.cs | 7 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 8732523f..6a019b64 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -625,7 +625,18 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) CancellationToken.None); } - return this.acceptanceSource.TrySetResult(acceptanceParameters); + // Update the acceptance source to the acceptance parameters + bool setResult = this.acceptanceSource.TrySetResult(acceptanceParameters); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Result of call to set acceptanceSource in TryAcceptOffer is {0}", + setResult); + } + + return setResult; } catch (Exception exception) { @@ -641,11 +652,16 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) this.QualifiedId, exception); } + + // If we caught an disposal error due to the channel self faulting then swallow + // the exception + if (exception is ObjectDisposedException && this.faultingException != null) + { + return true; + } } - // Swallow the exception if it is an objectDisposeException due to a self triggered - // fault rather than a user triggered dispose. - return this.IsDisposed && this.faultingException != null; + return false; } /// diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index 09aef7b6..bb3e5bf9 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1178,7 +1178,12 @@ private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) if (!this.TryAcceptChannel(channel, options)) { - if (channel.IsAccepted) + // If we disposed of the channel due to an user passed error + if (channel.IsDisposed && channel.Acceptance.IsFaulted) + { + return; + } + else if (channel.IsAccepted) { throw new InvalidOperationException("Channel is already accepted."); } From d9f49333fc6240c1dfd9beb74af496713cee7553 Mon Sep 17 00:00:00 2001 From: Devesh Date: Mon, 19 Sep 2022 23:36:08 -0700 Subject: [PATCH 72/77] Added log message inside AcceptChannelOrThrow --- .../MultiplexingStream.Channel.cs | 1 - src/Nerdbank.Streams/MultiplexingStream.cs | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 6a019b64..658d4e31 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -609,7 +609,6 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) // Set up the channel options and ensure that the channel is still valid // before we transition to an accepted state this.ApplyChannelOptions(channelOptions); - Verify.NotDisposed(this); // If we aren't a seeded channel then send an offer accepted frame if (this.QualifiedId.Source != ChannelSource.Seeded) diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index bb3e5bf9..dd775881 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1178,8 +1178,21 @@ private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) if (!this.TryAcceptChannel(channel, options)) { - // If we disposed of the channel due to an user passed error - if (channel.IsDisposed && channel.Acceptance.IsFaulted) + TraceSource traceSrc = this.GetTraceSource(); + if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) + { + traceSrc.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "State of channel {0} of tryAcceptChannel failure in AcceptChannelOrThrow: \n IsDisposed - {1}, Acceptance - {2}, Completion - {3}", + channel.QualifiedId, + channel.IsDisposed, + channel.Acceptance.Status, + channel.Completion.Status); + } + + // If we disposed of the channel due to an user passed error then ignore the error + if (channel.IsDisposed && channel.Completion.IsFaulted) { return; } From 9c3360b4d3bde3e496bcddfedd41cb13120d7f94 Mon Sep 17 00:00:00 2001 From: Devesh Date: Tue, 20 Sep 2022 00:03:38 -0700 Subject: [PATCH 73/77] Retrigger build pipeline try 3 From 9b40d8383d6822c97021b2097608126b19535488 Mon Sep 17 00:00:00 2001 From: Devesh Date: Tue, 20 Sep 2022 00:28:32 -0700 Subject: [PATCH 74/77] Check for both acceptance and completion --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 658d4e31..01122b93 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -353,8 +353,17 @@ public void Dispose() this.QualifiedId); } - this.acceptanceSource.TrySetCanceled(); - this.optionsAppliedTaskSource?.TrySetCanceled(); + // If we are disposing due to an faulting error, transition the acceptanceSource to an error state + if (this.faultingException != null) + { + this.acceptanceSource?.TrySetException(this.faultingException); + this.optionsAppliedTaskSource?.TrySetException(this.faultingException); + } + else + { + this.acceptanceSource.TrySetCanceled(); + this.optionsAppliedTaskSource?.TrySetCanceled(); + } PipeWriter? mxStreamIOWriter; lock (this.SyncObject) From 125e2fac5d62d98ecdc83b46f309859d94f71edf Mon Sep 17 00:00:00 2001 From: Devesh Date: Tue, 20 Sep 2022 00:44:33 -0700 Subject: [PATCH 75/77] Pipeline rebuild --- src/Nerdbank.Streams/MultiplexingStream.Channel.cs | 2 +- src/Nerdbank.Streams/MultiplexingStream.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 01122b93..28966e06 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -963,7 +963,7 @@ private async Task ProcessOutboundTransmissionsAsync() { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Caught exception relaying message to remote side: {0}", ex.Message); + this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Caught exception relaying message to remote side:\n{0}", ex); } // If the operation had been cancelled then we are expecting to receive this error so don't transmit it. diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index dd775881..b53f9e21 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -1192,7 +1192,7 @@ private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) } // If we disposed of the channel due to an user passed error then ignore the error - if (channel.IsDisposed && channel.Completion.IsFaulted) + if (channel.IsDisposed && (channel.Completion.IsFaulted || channel.Acceptance.IsFaulted)) { return; } From f6819e923315969a8dfceef0d8438b1051efc651 Mon Sep 17 00:00:00 2001 From: Devesh Date: Tue, 20 Sep 2022 09:48:50 -0700 Subject: [PATCH 76/77] Keep track of channel acceptance in process outbound --- .../MultiplexingStream.Channel.cs | 90 +++++++++++++------ 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 28966e06..273da110 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -343,6 +343,13 @@ public void Dispose() { if (!this.IsDisposed) { + PipeWriter? mxStreamIOWriter; + lock (this.SyncObject) + { + this.isDisposed = true; + mxStreamIOWriter = this.mxStreamIOWriter; + } + TraceSource traceSrc = this.GetTraceSource(); if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) { @@ -354,22 +361,18 @@ public void Dispose() } // If we are disposing due to an faulting error, transition the acceptanceSource to an error state - if (this.faultingException != null) - { - this.acceptanceSource?.TrySetException(this.faultingException); - this.optionsAppliedTaskSource?.TrySetException(this.faultingException); - } - else - { - this.acceptanceSource.TrySetCanceled(); - this.optionsAppliedTaskSource?.TrySetCanceled(); - } - - PipeWriter? mxStreamIOWriter; lock (this.SyncObject) { - this.isDisposed = true; - mxStreamIOWriter = this.mxStreamIOWriter; + if (this.faultingException != null) + { + this.acceptanceSource?.TrySetException(this.faultingException); + this.optionsAppliedTaskSource?.TrySetException(this.faultingException); + } + else + { + this.acceptanceSource.TrySetCanceled(); + this.optionsAppliedTaskSource?.TrySetCanceled(); + } } // Complete writing so that the mxstream cannot write to this channel any more. @@ -433,13 +436,16 @@ public void Dispose() this.disposalTokenSource.Cancel(); // If we are disposing due to receiving or sending an exception, relay that to our client. - if (this.faultingException != null) - { - this.completionSource.TrySetException(this.faultingException); - } - else + lock (this.SyncObject) { - this.completionSource.TrySetResult(null); + if (this.faultingException != null) + { + this.completionSource.TrySetException(this.faultingException); + } + else + { + this.completionSource.TrySetResult(null); + } } this.MultiplexingStream.OnChannelDisposed(this); @@ -618,6 +624,7 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) // Set up the channel options and ensure that the channel is still valid // before we transition to an accepted state this.ApplyChannelOptions(channelOptions); + Verify.NotDisposed(this); // If we aren't a seeded channel then send an offer accepted frame if (this.QualifiedId.Source != ChannelSource.Seeded) @@ -873,14 +880,33 @@ private async Task ProcessOutboundTransmissionsAsync() this.mxStreamIOReader = new UnownedPipeReader(mxStreamIOReader); } + bool channelAccepted = true; try { // Don't transmit data on the channel until the remote party has accepted it. // This is not just a courtesy: it ensure we don't transmit data from the offering party before the offer frame itself. // Likewise: it may help prevent transmitting data from the accepting party before the acceptance frame itself. - await this.Acceptance.ConfigureAwait(false); + try + { + await this.Acceptance.ConfigureAwait(false); + } + catch (Exception exception) + { + // This await will only throw an exception if the channel has been disposed and thus we can swallow + if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) + { + this.TraceSource.TraceEvent( + TraceEventType.Verbose, + (int)TraceEventId.ChannelDisposed, + "Channel {0} swalled acceptance exception in ProcessOutbound: {0}", + this.QualifiedId, + exception); + } + + channelAccepted = false; + } - while (!this.Completion.IsCompleted) + while (channelAccepted && !this.Completion.IsCompleted) { if (!this.remoteWindowHasCapacity.IsSet && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) { @@ -963,7 +989,12 @@ private async Task ProcessOutboundTransmissionsAsync() { if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Caught exception relaying message to remote side:\n{0}", ex); + this.TraceSource.TraceEvent( + TraceEventType.Information, + (int)TraceEventId.WriteError, + "Caught exception relaying message to remote side: {0} with stack trace:\n{1}", + ex, + ex.StackTrace); } // If the operation had been cancelled then we are expecting to receive this error so don't transmit it. @@ -981,15 +1012,22 @@ private async Task ProcessOutboundTransmissionsAsync() // Since we're not expecting to receive this error, transmit the error to the remote side. await mxStreamIOReader!.CompleteAsync(ex).ConfigureAwait(false); - this.MultiplexingStream.OnChannelWritingError(this, ex); + + if (channelAccepted) + { + this.MultiplexingStream.OnChannelWritingError(this, ex); + } } throw; } finally { - // Send the completion message to the remote - this.MultiplexingStream.OnChannelWritingCompleted(this); + // Send the completion message to the remote if the channel was accepted + if (channelAccepted) + { + this.MultiplexingStream.OnChannelWritingCompleted(this); + } // Restore the PipeReader to the field. lock (this.SyncObject) From a2aef171a0d3050e0841bbc730b06834097f6961 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Tue, 20 Sep 2022 15:37:54 -0600 Subject: [PATCH 77/77] Touch-ups --- run_test_until_failure.sh | 9 - .../MultiplexingStream.Channel.cs | 166 ++++-------------- .../MultiplexingStream.Formatters.cs | 35 ++-- .../MultiplexingStream.WriteError.cs | 6 +- src/Nerdbank.Streams/MultiplexingStream.cs | 98 ++--------- src/nerdbank-streams/src/Channel.ts | 17 +- .../src/MultiplexingStream.ts | 23 ++- .../src/MultiplexingStreamFormatters.ts | 19 +- src/nerdbank-streams/src/WriteError.ts | 4 +- .../tests/MultiplexingStream.Interop.spec.ts | 4 +- .../src/tests/MultiplexingStream.spec.ts | 8 +- .../Nerdbank.Streams.Interop.Tests/Program.cs | 6 +- .../MultiplexingStreamTests.cs | 133 ++++++-------- 13 files changed, 164 insertions(+), 364 deletions(-) delete mode 100755 run_test_until_failure.sh diff --git a/run_test_until_failure.sh b/run_test_until_failure.sh deleted file mode 100755 index d9012753..00000000 --- a/run_test_until_failure.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -dotnet build -status=$? -until [[ $status -ne 0 ]] -do - dotnet test - status=$? -done diff --git a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs index 273da110..6a9072f5 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Channel.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Channel.cs @@ -110,8 +110,8 @@ public class Channel : IDisposableObservable, IDuplexPipe private Exception? faultingException; /// - /// The to use to get data to be transmitted over the . Any errors passed to this - /// are transmitted to the remote side. + /// The to use to get data to be transmitted over the . + /// Any errors passed to this are transmitted to the remote side. /// private PipeReader? mxStreamIOReader; @@ -350,22 +350,12 @@ public void Dispose() mxStreamIOWriter = this.mxStreamIOWriter; } - TraceSource traceSrc = this.GetTraceSource(); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Calling TrySetCanceled of acceptanceSource in Dispose method of channel {0}", - this.QualifiedId); - } - - // If we are disposing due to an faulting error, transition the acceptanceSource to an error state + // If we are disposing due to a faulting error, transition the acceptanceSource to an error state lock (this.SyncObject) { if (this.faultingException != null) { - this.acceptanceSource?.TrySetException(this.faultingException); + this.acceptanceSource.TrySetException(this.faultingException); this.optionsAppliedTaskSource?.TrySetException(this.faultingException); } else @@ -436,16 +426,13 @@ public void Dispose() this.disposalTokenSource.Cancel(); // If we are disposing due to receiving or sending an exception, relay that to our client. - lock (this.SyncObject) + if (this.faultingException is Exception faultingException) { - if (this.faultingException != null) - { - this.completionSource.TrySetException(this.faultingException); - } - else - { - this.completionSource.TrySetResult(null); - } + this.completionSource.TrySetException(faultingException); + } + else + { + this.completionSource.TrySetResult(null); } this.MultiplexingStream.OnChannelDisposed(this); @@ -581,7 +568,7 @@ internal void OnContentWritingCompleted(MultiplexingProtocolException? error = n } else { - // If the channel has not been disposed then just close the underlying writer + // If the channel has not been disposed then just close the underlying writer. if (this.mxStreamIOWriter != null) { using AsyncSemaphore.Releaser releaser = await this.mxStreamIOWriterSemaphore.EnterAsync().ConfigureAwait(false); @@ -608,25 +595,15 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) } var acceptanceParameters = new AcceptanceParameters(this.localWindowSize.Value); - TraceSource traceSrc = this.GetTraceSource(); - - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Calling TrySetResult of acceptanceSource in TryAcceptOffer method of channel {0}", - this.QualifiedId); - } try { // Set up the channel options and ensure that the channel is still valid - // before we transition to an accepted state + // before we transition to an accepted state. this.ApplyChannelOptions(channelOptions); Verify.NotDisposed(this); - // If we aren't a seeded channel then send an offer accepted frame + // If we aren't a seeded channel then send an offer accepted frame. if (this.QualifiedId.Source != ChannelSource.Seeded) { ReadOnlySequence payload = this.MultiplexingStream.formatter.Serialize(acceptanceParameters); @@ -640,36 +617,16 @@ internal bool TryAcceptOffer(ChannelOptions channelOptions) CancellationToken.None); } - // Update the acceptance source to the acceptance parameters - bool setResult = this.acceptanceSource.TrySetResult(acceptanceParameters); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Result of call to set acceptanceSource in TryAcceptOffer is {0}", - setResult); - } - - return setResult; + // Update the acceptance source to the acceptance parameters. + return this.acceptanceSource.TrySetResult(acceptanceParameters); } catch (Exception exception) { - // Record the exception in the acceptance source + // Record the exception in the acceptance source. this.acceptanceSource.TrySetException(exception); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Caught exception in TryAcceptOffer method of channel {0}: \n {1}", - this.QualifiedId, - exception); - } - // If we caught an disposal error due to the channel self faulting then swallow - // the exception + // the exception. if (exception is ObjectDisposedException && this.faultingException != null) { return true; @@ -688,16 +645,6 @@ internal bool OnAccepted(AcceptanceParameters acceptanceParameters) { lock (this.SyncObject) { - TraceSource traceSrc = this.GetTraceSource(); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Calling TrySetResult of acceptanceSource in OnAccepted method of channel {0}", - this.QualifiedId); - } - if (this.acceptanceSource.TrySetResult(acceptanceParameters)) { this.remoteWindowSize = acceptanceParameters.RemoteWindowSize; @@ -758,17 +705,6 @@ private async ValueTask GetReceivedMessagePipeWriterAsync(Canc } } - /// - /// GetTraceSource gets the trace source to use when emitting trace messages. - /// - /// The to use when emitting messages. - private TraceSource GetTraceSource() - { - return this.TraceSource - ?? this.MultiplexingStream.DefaultChannelTraceSourceFactory?.Invoke(this.QualifiedId, this.Name) - ?? new TraceSource($"{nameof(Streams.MultiplexingStream)}.{nameof(Channel)} {this.QualifiedId} ({this.Name})", SourceLevels.All); - } - /// /// Apply channel options to this channel, including setting up or linking to an user-supplied pipe writer/reader pair. /// @@ -810,20 +746,9 @@ private void ApplyChannelOptions(ChannelOptions channelOptions) this.DisposeSelfOnFailure(this.mxStreamIOReaderCompleted); this.DisposeSelfOnFailure(this.AutoCloseOnPipesClosureAsync()); } - catch (Exception exception) + catch (Exception ex) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Critical)) - { - this.TraceSource.TraceEvent( - TraceEventType.Critical, - (int)TraceEventId.WriteError, - "Caught ApplyChannelOptions error on channel {0}:\n{1}", - this.QualifiedId, - exception); - } - - // Record that we caught an exception - this.optionsAppliedTaskSource?.TrySetException(exception); + this.optionsAppliedTaskSource?.TrySetException(ex); throw; } finally @@ -880,7 +805,7 @@ private async Task ProcessOutboundTransmissionsAsync() this.mxStreamIOReader = new UnownedPipeReader(mxStreamIOReader); } - bool channelAccepted = true; + bool channelAccepted = false; try { // Don't transmit data on the channel until the remote party has accepted it. @@ -889,26 +814,26 @@ private async Task ProcessOutboundTransmissionsAsync() try { await this.Acceptance.ConfigureAwait(false); + channelAccepted = true; } catch (Exception exception) { - // This await will only throw an exception if the channel has been disposed and thus we can swallow - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) + // This await will only throw an exception if the channel has been disposed and thus we can swallow. + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Error) ?? false) { this.TraceSource.TraceEvent( - TraceEventType.Verbose, - (int)TraceEventId.ChannelDisposed, - "Channel {0} swalled acceptance exception in ProcessOutbound: {0}", + TraceEventType.Error, + (int)TraceEventId.NonFatalInternalError, + "Channel {0} swalled acceptance exception in {1}: {2}", this.QualifiedId, + nameof(this.ProcessOutboundTransmissionsAsync), exception); } - - channelAccepted = false; } while (channelAccepted && !this.Completion.IsCompleted) { - if (!this.remoteWindowHasCapacity.IsSet && this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) + if (!this.remoteWindowHasCapacity.IsSet && (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Verbose) ?? false)) { this.TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Remote window is full. Waiting for remote party to process data before sending more."); } @@ -916,7 +841,7 @@ private async Task ProcessOutboundTransmissionsAsync() await this.remoteWindowHasCapacity.WaitAsync().ConfigureAwait(false); if (this.IsRemotelyTerminated) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Verbose) ?? false) { this.TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Transmission on channel {0} \"{1}\" terminated the remote party terminated the channel.", this.QualifiedId, this.Name); } @@ -929,7 +854,7 @@ private async Task ProcessOutboundTransmissionsAsync() if (result.IsCanceled) { // We've been asked to cancel. Presumably the channel has faulted or been disposed. - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Verbose) ?? false) { this.TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Transmission terminated because the read was canceled."); } @@ -948,7 +873,7 @@ private async Task ProcessOutboundTransmissionsAsync() ReadOnlySequence bufferToRelay = result.Buffer.Slice(0, bytesToSend); this.OnTransmittingBytes(bufferToRelay.Length); bool isCompleted = result.IsCompleted && result.Buffer.Length == bufferToRelay.Length; - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Verbose)) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Verbose) ?? false) { this.TraceSource.TraceEvent(TraceEventType.Verbose, 0, "{0} of {1} bytes will be transmitted.", bufferToRelay.Length, result.Buffer.Length); } @@ -974,7 +899,7 @@ private async Task ProcessOutboundTransmissionsAsync() if (isCompleted) { - if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information)) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Information) ?? false) { this.TraceSource.TraceEvent(TraceEventType.Information, 0, "Transmission terminated because the writer completed."); } @@ -987,16 +912,6 @@ private async Task ProcessOutboundTransmissionsAsync() } catch (Exception ex) { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Caught exception relaying message to remote side: {0} with stack trace:\n{1}", - ex, - ex.StackTrace); - } - // If the operation had been cancelled then we are expecting to receive this error so don't transmit it. if (ex is OperationCanceledException && this.DisposalToken.IsCancellationRequested) { @@ -1106,23 +1021,16 @@ private void Fault(Exception exception) this.mxStreamIOReader?.CancelPendingRead(); - // Determine if the channel has already been disposed - bool alreadyDisposed = false; - lock (this.SyncObject) - { - alreadyDisposed = this.isDisposed; - } - - // If the channel has already been disposed then no need to call dispose again - if (alreadyDisposed) + // Only dispose if not already disposed. + if (this.IsDisposed) { return; } - // Record the fact that we are about to close the channel due to a fault - if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Critical) ?? false) + // Record the fact that we are about to close the channel due to a fault. + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Error) ?? false) { - this.TraceSource.TraceEvent(TraceEventType.Critical, (int)TraceEventId.FatalError, "Channel {0} closing self due to exception: {1}", this.QualifiedId, exception); + this.TraceSource.TraceEvent(TraceEventType.Error, (int)TraceEventId.FatalError, "Channel {0} closing self due to exception: {1}", this.QualifiedId, exception); } this.Dispose(); diff --git a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs index 98a8a81f..c5cf9472 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.Formatters.cs @@ -301,7 +301,6 @@ internal override ReadOnlySequence SerializeContentProcessed(long bytesPro internal class V2Formatter : Formatter { private static readonly Version ProtocolVersion = new Version(2, 0); - private static readonly int WriteErrorPayloadSize = 1; private readonly MessagePackStreamReader reader; private readonly AsyncSemaphore readingSemaphore = new AsyncSemaphore(1); @@ -509,8 +508,8 @@ internal ReadOnlySequence SerializeWriteError(WriteError error) MessagePackWriter writer = new(errorSequence); // Write the error message and the protocol version to the payload - writer.WriteArrayHeader(WriteErrorPayloadSize); - writer.Write(error.ErrorMessage); + writer.WriteArrayHeader(1); + writer.Write(error.Message); // Return the payload to the caller writer.Flush(); @@ -521,29 +520,27 @@ internal ReadOnlySequence SerializeWriteError(WriteError error) /// Extracts an object from the payload using . /// /// The payload we are trying to extract the error object from. - /// The tracer to use when tracing errors to deserialize a received payload. - /// A object if the payload is correctly formatted and has the expected protocol version, - /// null otherwise. - internal WriteError? DeserializeWriteError(ReadOnlySequence serializedError, TraceSource? traceSource) + /// A object. + internal WriteError DeserializeWriteError(ReadOnlySequence serializedError) { MessagePackReader reader = new(serializedError); int numElements = reader.ReadArrayHeader(); - // If received an unexpected number of fields, report that to the users - if (numElements != WriteErrorPayloadSize && traceSource!.Switch.ShouldTrace(TraceEventType.Warning)) - { - traceSource.TraceEvent(TraceEventType.Warning, 0, "Expected error payload to have {0} elements, found {1} elements", WriteErrorPayloadSize, numElements); - } - - // The payload should have enough elements that we can process all the critical fields - if (numElements < WriteErrorPayloadSize) + string? errorMessage = null; + for (int i = 0; i < numElements; i++) { - return null; + switch (i) + { + case 0: + errorMessage = reader.ReadString(); + break; + default: + reader.Skip(); + break; + } } - // Extract the error message and use that to create the write error object - string errorMessage = reader.ReadString(); - return new WriteError(errorMessage); + return new WriteError(errorMessage ?? string.Empty); } protected virtual (FrameHeader Header, ReadOnlySequence Payload) DeserializeFrame(ReadOnlySequence frameSequence) diff --git a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs index 12ac9673..b46801d0 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.WriteError.cs @@ -18,15 +18,15 @@ internal class WriteError /// Initializes a new instance of the class. /// /// The error message we want to send to the receiver. - internal WriteError(string message) + internal WriteError(string? message) { - this.ErrorMessage = message; + this.Message = message; } /// /// Gets the error message associated with this error. /// - internal string ErrorMessage { get; } + internal string? Message { get; } } } } diff --git a/src/Nerdbank.Streams/MultiplexingStream.cs b/src/Nerdbank.Streams/MultiplexingStream.cs index b53f9e21..3244674c 100644 --- a/src/Nerdbank.Streams/MultiplexingStream.cs +++ b/src/Nerdbank.Streams/MultiplexingStream.cs @@ -214,6 +214,11 @@ private enum TraceEventId /// Raised when receiving or sending a . /// WriteError, + + /// + /// An error occurred that is likely not fatal. + /// + NonFatalInternalError, } /// @@ -451,16 +456,6 @@ public Channel AcceptChannel(ulong id, ChannelOptions? options = default) } } - TraceSource traceSrc = this.GetTraceSource(); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Calling AcceptChannelOrThrow inside AcceptChannel for channel {0}", - channel.QualifiedId); - } - this.AcceptChannelOrThrow(channel, options); return channel; } @@ -964,24 +959,10 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc { // Deserialize the payload and verify that it was in an expected state V2Formatter errorDeserializingFormattter = (V2Formatter)this.formatter; - WriteError? error = errorDeserializingFormattter.DeserializeWriteError(payload, this.TraceSource); - - if (error == null) - { - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) - { - this.TraceSource.TraceEvent( - TraceEventType.Warning, - (int)TraceEventId.WriteError, - "Rejecting content writing error from channel {0} due to invalid payload", - channelId); - } - - return; - } + WriteError error = errorDeserializingFormattter.DeserializeWriteError(payload); // Get the error message and complete the channel using it - string errorMessage = error.ErrorMessage; + string errorMessage = error.Message ?? ""; MultiplexingProtocolException channelClosingException = new MultiplexingProtocolException($"Remote party indicated writing error: {errorMessage}"); channel.OnContentWritingCompleted(channelClosingException); } @@ -989,7 +970,7 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc { // The channel is in a valid state but we have a protocol version that doesn't support processing errrors // so don't do anything. - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) + if (this.TraceSource?.Switch.ShouldTrace(TraceEventType.Warning) ?? false) { this.TraceSource.TraceEvent( TraceEventType.Warning, @@ -998,8 +979,6 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc channelId, this.protocolMajorVersion); } - - return; } else { @@ -1008,11 +987,6 @@ private void OnContentWritingError(QualifiedChannelId channelId, ReadOnlySequenc } } - private TraceSource GetTraceSource() - { - return this.TraceSource ?? new TraceSource($"{nameof(Streams.MultiplexingStream)}", SourceLevels.All); - } - private void OnContentWritingCompleted(QualifiedChannelId channelId) { Channel channel; @@ -1146,16 +1120,6 @@ private void OnOffer(QualifiedChannelId channelId, ReadOnlySequence payloa if (acceptingChannelAlreadyPresent) { - TraceSource traceSrc = this.GetTraceSource(); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "Calling AcceptChannelOrThrow inside OnOffer method for channel {0}", - channel.QualifiedId); - } - this.AcceptChannelOrThrow(channel, options); } @@ -1178,20 +1142,7 @@ private void AcceptChannelOrThrow(Channel channel, ChannelOptions options) if (!this.TryAcceptChannel(channel, options)) { - TraceSource traceSrc = this.GetTraceSource(); - if (traceSrc.Switch.ShouldTrace(TraceEventType.Information)) - { - traceSrc.TraceEvent( - TraceEventType.Information, - (int)TraceEventId.WriteError, - "State of channel {0} of tryAcceptChannel failure in AcceptChannelOrThrow: \n IsDisposed - {1}, Acceptance - {2}, Completion - {3}", - channel.QualifiedId, - channel.IsDisposed, - channel.Acceptance.Status, - channel.Completion.Status); - } - - // If we disposed of the channel due to an user passed error then ignore the error + // If we disposed of the channel due to a user provided error then ignore the error. if (channel.IsDisposed && (channel.Completion.IsFaulted || channel.Acceptance.IsFaulted)) { return; @@ -1238,14 +1189,15 @@ private void OnChannelDisposed(Channel channel) /// The exception that caused the writing to be haulted. private void OnChannelWritingError(Channel channel, Exception exception) { - Requires.NotNull(channel, nameof(channel)); - - if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information)) + if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Error)) { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)TraceEventId.WriteError, "Local channel {0} encountered write error {1}", channel.QualifiedId, exception.Message); + this.TraceSource.TraceEvent(TraceEventType.Error, (int)TraceEventId.WriteError, "Local channel {0} encountered write error {1}", channel.QualifiedId, exception.Message); } - // Verify that we can send a message over this channel + // Verify that we can send a message over this channel. + // The race condition here is handled within SendFrameAsync which will drop the frame + // if the conditions we're checking for here change after we check them, so our check here + // is just an optimization to avoid work when we can predict its failure. bool channelInValidState = true; lock (this.syncObject) { @@ -1271,26 +1223,6 @@ private void OnChannelWritingError(Channel channel, Exception exception) // Send the frame alongside the payload to the remote side this.SendFrame(header, serializedError, CancellationToken.None); } - else if (channelInValidState && !this.ContentWritingErrorSupported) - { - // The channel is in a valid state but our protocol version doesn't support writing errors so don't do anything - if (this.TraceSource!.Switch.ShouldTrace(TraceEventType.Warning)) - { - this.TraceSource.TraceEvent( - TraceEventType.Warning, - (int)TraceEventId.WriteError, - "Not informing remote side of write error on channel {0} since MultiplexingStream has protocol version of {1}", - channel.QualifiedId, - this.protocolMajorVersion); - } - - return; - } - else - { - // The channel is not in a valid state to send any messages so throw an error indicating so - throw new MultiplexingProtocolException($"Can't write content writing error to channel {channel.QualifiedId} as it is terminated or isn't open"); - } } /// diff --git a/src/nerdbank-streams/src/Channel.ts b/src/nerdbank-streams/src/Channel.ts index c9d6d69a..257e83c7 100644 --- a/src/nerdbank-streams/src/Channel.ts +++ b/src/nerdbank-streams/src/Channel.ts @@ -53,10 +53,13 @@ export abstract class Channel implements IDisposableObservable { return this._isDisposed; } - // Sends the passed in error to the remote side and then closes the channel. - // dispose can be called after calling async even though it is not necessary. - public fault(error: Error) { - this._isDisposed = true; + /** + * Closes this channel after transmitting an error to the remote party. + * @param error The error to transmit to the remote party. + */ + public fault(error: Error): Promise { + // The interesting stuff is in the derived class. + return Promise.resolve(); } /** @@ -221,7 +224,7 @@ export class ChannelClass extends Channel { return this._acceptance.resolve(); } - public onContent(buffer: Buffer | null, error? : Error) { + public onContent(buffer: Buffer | null, error?: Error) { // If we have already received an error from the remote party, then don't process any future messages. if (this.remoteError) { return; @@ -259,12 +262,12 @@ export class ChannelClass extends Channel { public async fault(error: Error) { // If the channel is already disposed then don't do anything - if(this.isDisposed) { + if (this.isDisposed) { return; } // Send the error message to the remote side - await this._multiplexingStream.onChannelWritingError(this, error.message); + await this._multiplexingStream.onChannelWritingError(this, error); // Set the remote exception to the passed in error so that the channel is // completed with this error diff --git a/src/nerdbank-streams/src/MultiplexingStream.ts b/src/nerdbank-streams/src/MultiplexingStream.ts index 443b1a55..4265a32b 100644 --- a/src/nerdbank-streams/src/MultiplexingStream.ts +++ b/src/nerdbank-streams/src/MultiplexingStream.ts @@ -107,9 +107,9 @@ export abstract class MultiplexingStream implements IDisposableObservable { * @param options Options to customize the behavior of the stream. * @returns The multiplexing stream. */ - public static Create( + public static Create( stream: NodeJS.ReadWriteStream, - options?: MultiplexingStreamOptions) : MultiplexingStream { + options?: MultiplexingStreamOptions): MultiplexingStream { options ??= { protocolMajorVersion: 3 }; options.protocolMajorVersion ??= 3; @@ -579,7 +579,7 @@ export class MultiplexingStreamClass extends MultiplexingStream { } } - public async onChannelWritingError(channel: ChannelClass, errorMessage: string) { + public async onChannelWritingError(channel: ChannelClass, error: Error) { // Make sure that we are in a protocol version in which we can write errors. if (this.protocolMajorVersion === 1) { return; @@ -587,15 +587,15 @@ export class MultiplexingStreamClass extends MultiplexingStream { // Make sure we can send error messages on this channel. if (!this.getOpenChannel(channel.qualifiedId)) { - return; + return; } // Convert the error message into a payload into a formatter. - const writingError = new WriteError(errorMessage); + const writingError = new WriteError(error.message); const errorSerializingFormatter = this.formatter as MultiplexingStreamV2Formatter; const errorPayload = errorSerializingFormatter.serializeContentWritingError(writingError); - // Sent the error to the remote side + // Sent the error to the remote side. await this.sendFrameAsync(new FrameHeader(ControlCode.ContentWritingError, channel.qualifiedId), errorPayload); } @@ -752,15 +752,12 @@ export class MultiplexingStreamClass extends MultiplexingStream { throw new Error(`No channel with id ${channelId} found.`); } - // Extract the error from the payload - const errorDeserializingFormatter = (this.formatter as MultiplexingStreamV2Formatter); + // Extract the error from the payload. + const errorDeserializingFormatter = this.formatter as MultiplexingStreamV2Formatter; const writingError = errorDeserializingFormatter.deserializeContentWritingError(payload); - if (!writingError) { - throw new Error("Couldn't process content writing error payload received from remote"); - } - // Pass the error received from the remote to the channel - const remoteErr = new Error(`Received error message from remote: ${writingError.errorMessage}`); + // Pass the error received from the remote to the channel. + const remoteErr = new Error(`Received error message from remote: ${writingError.message}`); channel.onContent(null, remoteErr); } diff --git a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts index 54b5ca95..ce4ec7e0 100644 --- a/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts +++ b/src/nerdbank-streams/src/MultiplexingStreamFormatters.ts @@ -79,15 +79,6 @@ export abstract class MultiplexingStreamFormatter { } } -export function getFormatterVersion(formatter : MultiplexingStreamFormatter) : number { - if (formatter instanceof MultiplexingStreamV3Formatter) { - return 3 - } else if (formatter instanceof MultiplexingStreamV2Formatter) { - return 2 - } - return 1 -} - // tslint:disable-next-line: max-classes-per-file export class MultiplexingStreamV1Formatter extends MultiplexingStreamFormatter { /** @@ -303,17 +294,17 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter { return msgpack.decode(payload)[0]; } - serializeContentWritingError(writingError: WriteError) : Buffer { - const payload: any[] = [writingError.errorMessage]; + serializeContentWritingError(writingError: WriteError): Buffer { + const payload: any[] = [writingError.message]; return msgpack.encode(payload); } - deserializeContentWritingError(payload: Buffer) : WriteError | null { + deserializeContentWritingError(payload: Buffer): WriteError { const msgpackObject = msgpack.decode(payload); // Return the error message to the caller. - const errorMsg : string = msgpackObject[0]; - return new WriteError(errorMsg); + const errorMsg: string | undefined = msgpackObject[0]; + return new WriteError(errorMsg ?? ""); } protected async readMessagePackAsync(cancellationToken: CancellationToken): Promise<{} | [] | null> { diff --git a/src/nerdbank-streams/src/WriteError.ts b/src/nerdbank-streams/src/WriteError.ts index 4c0c132a..81970f05 100644 --- a/src/nerdbank-streams/src/WriteError.ts +++ b/src/nerdbank-streams/src/WriteError.ts @@ -6,8 +6,8 @@ export class WriteError { /** * Initializes a new instance of the WriteError class. - * @param errorMessage The error message. + * @param message The error message. */ - constructor(public readonly errorMessage: string) { + constructor(public readonly message: string) { } } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts index 7733714f..108aa344 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts @@ -27,7 +27,7 @@ import * as assert from "assert"; procExited = new Deferred(); proc.once("error", (err) => procExited.resolve(err)); proc.once("exit", (code) => procExited.resolve(code)); - proc.stdout!.pipe(process.stdout); + // proc.stdout!.pipe(process.stdout); proc.stderr!.pipe(process.stderr); const buildExitVal = await procExited.promise; expect(buildExitVal).toEqual(0); @@ -65,7 +65,7 @@ import * as assert from "assert"; try { await mx.completion; } catch (error) { - if(!expectedDisposeError) { + if (!expectedDisposeError) { throw error; } } diff --git a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts index 281898e3..08db0365 100644 --- a/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts +++ b/src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts @@ -241,11 +241,11 @@ import * as assert from "assert"; await channels[0].fault(errorToSend); - // Ensure that the current channel disposes with the error + // Ensure that the current channel disposes with the error. let caughtSenderError = false; try { await channels[0].completion; - } catch(error) { + } catch (error) { let completionErrMsg = String(error); if (error instanceof Error) { completionErrMsg = (error as Error).message; @@ -253,13 +253,13 @@ import * as assert from "assert"; caughtSenderError = completionErrMsg.includes(errorMessage); } - assert.deepStrictEqual(true, caughtSenderError); + assert.strictEqual(true, caughtSenderError); // Ensure that the remote side received the error only for version >= 1 let caughtRemoteError = false; try { await channels[1].completion; - } catch(error) { + } catch (error) { let completionErrMsg = String(error); if (error instanceof Error) { completionErrMsg = (error as Error).message; diff --git a/test/Nerdbank.Streams.Interop.Tests/Program.cs b/test/Nerdbank.Streams.Interop.Tests/Program.cs index 55bf0292..b0c46048 100644 --- a/test/Nerdbank.Streams.Interop.Tests/Program.cs +++ b/test/Nerdbank.Streams.Interop.Tests/Program.cs @@ -25,7 +25,7 @@ private Program(MultiplexingStream mx) private static async Task Main(string[] args) { - // System.Diagnostics.Debugger.Launch(); + ////System.Diagnostics.Debugger.Launch(); int protocolMajorVersion = int.Parse(args[0]); var options = new MultiplexingStream.Options { @@ -82,11 +82,11 @@ private async Task ClientOfferAsync() private async Task ClientOfferErrorAsync() { - // Await both of the channels from the sender, one to read the error and the other to return the response + // Await both of the channels from the sender, one to read the error and the other to return the response. MultiplexingStream.Channel? incomingChannel = await this.mx.AcceptChannelAsync("clientErrorOffer"); MultiplexingStream.Channel? outgoingChannel = await this.mx.AcceptChannelAsync("clientResponseOffer"); - // Determine the response to send back on the whether the incoming channel completed with an exception + // Determine the response to send back on the whether the incoming channel completed with an exception. string? responseMessage = "didn't receive any errors"; try { diff --git a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs index 6330dcd1..c772fb0c 100644 --- a/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs +++ b/test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs @@ -64,21 +64,13 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - try - { - await (this.mx1?.DisposeAsync() ?? default); - await (this.mx2?.DisposeAsync() ?? default); - AssertNoFault(this.mx1); - AssertNoFault(this.mx2); + await (this.mx1?.DisposeAsync() ?? default); + await (this.mx2?.DisposeAsync() ?? default); + AssertNoFault(this.mx1); + AssertNoFault(this.mx2); - this.mx1?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); - this.mx2?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); - } - catch (Exception err) - { - this.Logger.WriteLine("Caught error in DisposeAsync: {0}", err.Message); - throw; - } + this.mx1?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); + this.mx2?.TraceSource.Listeners.OfType().SingleOrDefault()?.Dispose(); } [Fact, Obsolete] @@ -109,82 +101,71 @@ public void DefaultMajorProtocolVersion() } [Fact] - public async Task OfferPipeWithError() + public async Task ClosePipeWithError() { - try - { - bool errorThrown = false; - string errorMessage = "Hello World"; + (MultiplexingStream.Channel channel1, MultiplexingStream.Channel channel2) = await this.EstablishChannelsAsync("test"); + await channel1.Output.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + ReadResult readResult = await channel2.Input.ReadAtLeastAsync(3, this.TimeoutToken); + channel2.Input.AdvanceTo(readResult.Buffer.End); - // Prepare a readonly pipe that is already populated with data an an error - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); - pipe.Writer.Complete(new Exception(errorMessage)); + // Now fail one side. + const string expectedErrorMessage = "Inflicted error"; + await channel1.Output.CompleteAsync(new ApplicationException(expectedErrorMessage)); + if (this.ProtocolMajorVersion > 1) + { + MultiplexingProtocolException ex = await Assert.ThrowsAnyAsync(async () => await channel2.Input.ReadAsync(this.TimeoutToken)); + Assert.Contains(expectedErrorMessage, ex.Message); + } + else + { + await channel2.Input.ReadAsync(this.TimeoutToken); + } + } - // Create a sending and receiving channel using the channel - MultiplexingStream.Channel? localChannel = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); - await this.WaitForEphemeralChannelOfferToPropagateAsync(); - MultiplexingStream.Channel? remoteChannel = this.mx2.AcceptChannel(localChannel.QualifiedId.Id); + [Fact] + public async Task OfferPipeWithError() + { + string errorMessage = "Hello World"; - bool continueReading = true; - while (continueReading) - { - try - { - // Read the latest input from the local channel and determine if we should continue reading - ReadResult readResult = await remoteChannel.Input.ReadAsync(this.TimeoutToken); - if (readResult.IsCompleted || readResult.IsCanceled) - { - continueReading = false; - } - - remoteChannel.Input.AdvanceTo(readResult.Buffer.End); - } - catch (Exception exception) - { - // Check not only that we caught an exception but that it was the expected exception. - errorThrown = exception.Message.Contains(errorMessage); - continueReading = !errorThrown; - } - } + // Prepare a readonly pipe that is already populated with data and an error. + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(new byte[] { 1, 2, 3 }, this.TimeoutToken); + pipe.Writer.Complete(new ApplicationException(errorMessage)); - Assert.Equal(this.ProtocolMajorVersion > 1, errorThrown); + // Create a sending and receiving channel using the channel. + MultiplexingStream.Channel? localChannel = this.mx1.CreateChannel(new MultiplexingStream.ChannelOptions { ExistingPipe = new DuplexPipe(pipe.Reader) }); + await this.WaitForEphemeralChannelOfferToPropagateAsync(); + MultiplexingStream.Channel? remoteChannel = this.mx2.AcceptChannel(localChannel.QualifiedId.Id); - // Ensure that the writer of the error completes with that error, no matter what version of the protocol they are using - string expectedWriterErrorMessage = errorMessage; - bool localChannelCompletedWithError = false; + async Task ReadAllDataAsync() + { + // Read the latest input from the local channel and determine if we should continue reading. + ReadResult readResult = await remoteChannel.Input.ReadAsync(this.TimeoutToken); + remoteChannel.Input.AdvanceTo(readResult.Buffer.End); - try + if (readResult.IsCompleted || readResult.IsCanceled) { - await localChannel.Completion; + return; } - catch (Exception writeException) - { - localChannelCompletedWithError = writeException.Message.Contains(expectedWriterErrorMessage); - } - - Assert.True(localChannelCompletedWithError); - - // Ensure that the reader only completes with an error if we are using a protocol version > 1 - string expectedReaderErrorMessage = "Remote party indicated writing error: " + errorMessage; - bool remoteChannelCompletedWithError = false; + } - try - { - await remoteChannel.Completion; - } - catch (Exception readException) - { - remoteChannelCompletedWithError = readException.Message.Contains(expectedReaderErrorMessage); - } + if (this.ProtocolMajorVersion > 1) + { + MultiplexingProtocolException caughtException = await Assert.ThrowsAnyAsync(ReadAllDataAsync); + this.Logger.WriteLine(caughtException.ToString()); + Assert.Contains(errorMessage, caughtException.Message); - Assert.Equal(this.ProtocolMajorVersion > 1, remoteChannelCompletedWithError); + MultiplexingProtocolException remoteCompletionException = await Assert.ThrowsAnyAsync(() => remoteChannel.Completion); + Assert.Contains(errorMessage, remoteCompletionException.Message); } - catch (Exception err) + else { - this.Logger.WriteLine("Caught error in OfferPipeWithError: {0}", err.Message); - throw; + await ReadAllDataAsync(); } + + // Ensure that the writer of the error completes with that error, no matter what version of the protocol they are using. + ApplicationException localCompletionException = await Assert.ThrowsAnyAsync(() => localChannel.Completion); + Assert.Contains(errorMessage, localCompletionException.Message); } [Fact]