Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix method dispatch failure when args fail to deserialize for ETW verbose logging #842

Merged
merged 2 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions src/StreamJsonRpc/JsonRpcEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,22 @@ internal static string GetArgumentsString(JsonRpcRequest request)
stringBuilder.Append('[');
for (int i = 0; i < request.ArgumentCount; ++i)
{
if (request.TryGetArgumentByNameOrIndex(null, i, null, out object? value))
bool formatted = false;
try
{
Format(stringBuilder, value);
stringBuilder.Append(", ");
if (request.TryGetArgumentByNameOrIndex(null, i, null, out object? value))
{
Format(stringBuilder, value);
stringBuilder.Append(", ");
formatted = true;
}
}
else
catch
{
// Swallow exceptions when deserializing args for ETW logging. It's never worth causing a functional failure.
}

if (!formatted)
{
stringBuilder.Append("<unformattable>");
}
Expand All @@ -317,14 +327,24 @@ internal static string GetArgumentsString(JsonRpcRequest request)
stringBuilder.Append('{');
foreach (string key in request.ArgumentNames)
{
if (request.TryGetArgumentByNameOrIndex(key, -1, null, out object? value))
bool formatted = false;
try
{
stringBuilder.Append(key);
stringBuilder.Append(": ");
Format(stringBuilder, value);
stringBuilder.Append(", ");
if (request.TryGetArgumentByNameOrIndex(key, -1, null, out object? value))
{
stringBuilder.Append(key);
stringBuilder.Append(": ");
Format(stringBuilder, value);
stringBuilder.Append(", ");
formatted = true;
}
}
else
catch
{
// Swallow exceptions when deserializing args for ETW logging. It's never worth causing a functional failure.
}

if (!formatted)
{
stringBuilder.Append("<unformattable>");
}
Expand Down
59 changes: 58 additions & 1 deletion test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ internal interface IMessagePackServer
Task ProgressUnionType(IProgress<UnionBaseClass> progress, CancellationToken cancellationToken);

IAsyncEnumerable<UnionBaseClass> GetAsyncEnumerableOfUnionType(CancellationToken cancellationToken);

Task<bool> IsExtensionArgNonNull(CustomExtensionType extensionValue);
}

[Fact]
Expand Down Expand Up @@ -363,14 +365,33 @@ public async Task UnionType_AsAsyncEnumerableTypeArgument()
Assert.IsType<UnionDerivedClass>(actualItem);
}

/// <summary>
/// Verifies that an argument that cannot be deserialized by the msgpack primitive formatter will not cause a failure.
/// </summary>
/// <remarks>
/// <para>This is a regression test for <see href="https://github.com/microsoft/vs-streamjsonrpc/issues/841">a bug</see> where
/// verbose ETW tracing would fail to deserialize arguments with the primitive formatter that deserialize just fine for the actual method dispatch.</para>
/// <para>This test is effective because custom msgpack extensions cause the <see cref="PrimitiveObjectFormatter"/> to throw an exception when deserializing.</para>
/// </remarks>
[Theory, PairwiseData]
public async Task VerboseLoggingDoesNotFailWhenArgsDoNotDeserializePrimitively(bool namedArguments)
{
var server = new MessagePackServer();
this.serverRpc.AllowModificationWhileListening = true;
this.serverRpc.AddLocalRpcTarget(server);
var clientProxy = this.clientRpc.Attach<IMessagePackServer>(new JsonRpcProxyOptions { ServerRequiresNamedArguments = namedArguments });

Assert.True(await clientProxy.IsExtensionArgNonNull(new CustomExtensionType()));
}

protected override void InitializeFormattersAndHandlers(bool controlledFlushingClient)
{
this.serverMessageFormatter = new MessagePackFormatter();
this.clientMessageFormatter = new MessagePackFormatter();

var options = MessagePackFormatter.DefaultUserDataSerializationOptions
.WithResolver(CompositeResolver.Create(
new IMessagePackFormatter[] { new UnserializableTypeFormatter(), new TypeThrowsWhenDeserializedFormatter() },
new IMessagePackFormatter[] { new UnserializableTypeFormatter(), new TypeThrowsWhenDeserializedFormatter(), new CustomExtensionFormatter() },
new IFormatterResolver[] { StandardResolverAllowPrivate.Instance }));
((MessagePackFormatter)this.serverMessageFormatter).SetMessagePackSerializerOptions(options);
((MessagePackFormatter)this.clientMessageFormatter).SetMessagePackSerializerOptions(options);
Expand All @@ -392,6 +413,40 @@ public class UnionDerivedClass : UnionBaseClass
{
}

internal class CustomExtensionType
{
}

private class CustomExtensionFormatter : IMessagePackFormatter<CustomExtensionType?>
{
public CustomExtensionType? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
if (reader.TryReadNil())
{
return null;
}

if (reader.ReadExtensionFormat() is { Header: { TypeCode: 1, Length: 0 } })
{
return new();
}

throw new Exception("Unexpected extension header.");
}

public void Serialize(ref MessagePackWriter writer, CustomExtensionType? value, MessagePackSerializerOptions options)
{
if (value is null)
{
writer.WriteNil();
}
else
{
writer.WriteExtensionFormat(new ExtensionResult(1, default(Memory<byte>)));
}
}
}

private class UnserializableTypeFormatter : IMessagePackFormatter<CustomSerializedType>
{
public CustomSerializedType Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
Expand Down Expand Up @@ -448,6 +503,8 @@ public async IAsyncEnumerable<UnionBaseClass> GetAsyncEnumerableOfUnionType([Enu
await Task.Yield();
yield return new UnionDerivedClass();
}

public Task<bool> IsExtensionArgNonNull(CustomExtensionType extensionValue) => Task.FromResult(extensionValue is not null);
}

private class DelayedFlushingHandler : LengthHeaderMessageHandler, IControlledFlushHandler
Expand Down