diff --git a/src/StreamJsonRpc.Tests/CollectingTraceListener.cs b/src/StreamJsonRpc.Tests/CollectingTraceListener.cs new file mode 100644 index 00000000..d9cb9fe6 --- /dev/null +++ b/src/StreamJsonRpc.Tests/CollectingTraceListener.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; +using Microsoft.VisualStudio.Threading; + +public class CollectingTraceListener : TraceListener +{ + private readonly StringBuilder lineInProgress = new StringBuilder(); + + private readonly ImmutableList.Builder messages = ImmutableList.CreateBuilder(); + + public override bool IsThreadSafe => false; + + public IReadOnlyList Messages + { + get + { + lock (this.messages) + { + return this.messages.ToImmutable(); + } + } + } + + public AsyncAutoResetEvent MessageReceived { get; } = new AsyncAutoResetEvent(); + + public override void Write(string message) => this.lineInProgress.Append(message); + + public override void WriteLine(string message) + { + this.lineInProgress.Append(message); + lock (this.messages) + { + this.messages.Add(this.lineInProgress.ToString()); + this.MessageReceived.Set(); + } + + this.lineInProgress.Clear(); + } +} diff --git a/src/StreamJsonRpc.Tests/JsonRpcTests.cs b/src/StreamJsonRpc.Tests/JsonRpcTests.cs index 5cfe14a2..b53507ba 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -35,11 +35,13 @@ public abstract class JsonRpcTests : TestBase protected JsonRpc serverRpc; protected IJsonRpcMessageHandler serverMessageHandler; protected IJsonRpcMessageFormatter serverMessageFormatter; + protected CollectingTraceListener serverTraces; protected Nerdbank.FullDuplexStream clientStream; protected JsonRpc clientRpc; protected IJsonRpcMessageHandler clientMessageHandler; protected IJsonRpcMessageFormatter clientMessageFormatter; + protected CollectingTraceListener clientTraces; private const int CustomTaskResult = 100; @@ -940,6 +942,19 @@ public async Task InvokeWithParameterObject_Progress_InvalidParamMethod() await Assert.ThrowsAsync(() => this.clientRpc.InvokeWithParameterObjectAsync(nameof(Server.MethodWithInvalidProgressParameter), new { p = progress }, this.TimeoutToken)); } + [Fact] + public async Task ReportProgressWithUnserializableData_LeavesTraceEvidence() + { + var progress = new Progress(); + await this.clientRpc.InvokeWithCancellationAsync(nameof(Server.MethodWithUnserializableProgressType), new object[] { progress }, cancellationToken: this.TimeoutToken); + + // Verify that the trace explains what went wrong with the original exception message. + while (!this.serverTraces.Messages.Any(m => m.Contains("Can't touch this"))) + { + await this.serverTraces.MessageReceived.WaitAsync(this.TimeoutToken); + } + } + [Fact] public async Task InvokeWithParameterObject_ProgressParameterAndFields() { @@ -1883,6 +1898,9 @@ private void ReinitializeRpcWithoutListening() this.serverRpc.TraceSource.Listeners.Add(new XunitTraceListener(this.Logger)); this.clientRpc.TraceSource.Listeners.Add(new XunitTraceListener(this.Logger)); + + this.serverRpc.TraceSource.Listeners.Add(this.serverTraces = new CollectingTraceListener()); + this.clientRpc.TraceSource.Listeners.Add(this.clientTraces = new CollectingTraceListener()); } private void StartListening() @@ -2036,6 +2054,11 @@ public static int MethodWithProgressAndMoreParameters(IProgress p, int x, i return sum; } + public static void MethodWithUnserializableProgressType(IProgress progress) + { + progress.Report(new TypeThrowsWhenSerialized()); + } + public static int MethodWithInvalidProgressParameter(Progress p) { return 1; @@ -2445,6 +2468,17 @@ public class CustomSerializedType public string? Value { get; set; } } + [DataContract] + public class TypeThrowsWhenSerialized + { + [DataMember] + public string Property + { + get => throw new Exception("Can't touch this."); + set => throw new NotImplementedException(); + } + } + public class TypeThrowsWhenDeserialized { } diff --git a/src/StreamJsonRpc.Tests/XunitTraceListener.cs b/src/StreamJsonRpc.Tests/XunitTraceListener.cs index 745329bb..f1e9979a 100644 --- a/src/StreamJsonRpc.Tests/XunitTraceListener.cs +++ b/src/StreamJsonRpc.Tests/XunitTraceListener.cs @@ -1,12 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using StreamJsonRpc; -using StreamJsonRpc.Protocol; using Xunit.Abstractions; internal class XunitTraceListener : TraceListener @@ -28,7 +22,8 @@ public override void WriteLine(string message) { if (!this.disposed) { - this.logger.WriteLine(this.lineInProgress.ToString() + message); + this.lineInProgress.Append(message); + this.logger.WriteLine(this.lineInProgress.ToString()); this.lineInProgress.Clear(); } } diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index f53544d5..8a306e15 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -361,7 +361,7 @@ public enum TraceEvents LocalContractViolation, /// - /// An exception occurred when reading the $/progress notification. + /// An exception occurred when reading or writing the $/progress notification. /// ProgressNotificationError, diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs index dea7768b..29270f71 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs @@ -8,6 +8,8 @@ namespace StreamJsonRpc.Reflection using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Reflection; + using System.Threading; + using System.Threading.Tasks; using Microsoft; using Microsoft.VisualStudio.Threading; @@ -267,7 +269,12 @@ public JsonProgress(JsonRpc rpc, object token) /// The typed value that will be send in the notification to be reported by the original instance. public void Report(T value) { - this.rpc.NotifyAsync(ProgressRequestSpecialMethod, this.token, value).Forget(); + this.rpc.NotifyAsync(ProgressRequestSpecialMethod, this.token, value).ContinueWith( + (t, s) => ((JsonRpc)s).TraceSource.TraceEvent(System.Diagnostics.TraceEventType.Error, (int)JsonRpc.TraceEvents.ProgressNotificationError, "Failed to send progress update. {0}", t.Exception.InnerException ?? t.Exception), + this.rpc, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default).Forget(); } } }