-
Notifications
You must be signed in to change notification settings - Fork 145
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
Allow server the option to crash on exception thrown #74
Changes from 1 commit
de75371
43d9c91
d5f7435
226db0a
0bbc033
673d727
ef74c1a
8b9678c
8152cf4
3b1efda
5994ffb
747af41
5435f72
97f2c56
89a602e
dbde851
3d04487
12c6c54
22a1a68
a0477dd
ae643fa
984c20c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,78 +5,65 @@ There are two ways to establish connection and start invoking methods remotely: | |
|
||
1. Use the static `Attach` method in `JsonRpc` class: | ||
```csharp | ||
public void ConstructRpc() | ||
public void ConstructRpc(Stream stream) | ||
{ | ||
var target = new LanguageServerTarget(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
} | ||
``` | ||
The `JsonRpc` object returned by `Attach` method would be used to invoke remote methods via JSON-RPC. | ||
|
||
2. Construct a `JsonRpc` object directly: | ||
```csharp | ||
public void ConstructRpc() | ||
public void ConstructRpc(Stream clientStream, Stream serverStream) | ||
{ | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = new JsonRpc(streams.Item1); | ||
var serverRpc = new JsonRpc(streams.Item2); | ||
var clientRpc = new JsonRpc(clientStream, serverStream); | ||
var target = new Server(); | ||
serverRpc.AddLocalRpcTarget(target); | ||
clientRpc.StartListening(); | ||
serverRpc.StartListening(); | ||
rpc.AddLocalRpcTarget(target); | ||
rpc.StartListening(); | ||
} | ||
``` | ||
|
||
## Invoking a notification | ||
To invoke a remote method named "foo" which takes one `string` parameter and does not return anything (i.e. send a notification remotely): | ||
```csharp | ||
public async Task NotifyRemote() | ||
public async Task NotifyRemote(Stream stream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
await clientRpc.NotifyAsync("foo", "param1"); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
await rpc.NotifyAsync("foo", "param1"); | ||
} | ||
``` | ||
The parameter will be passed remotely as an array of one object. | ||
|
||
To invoke a remote method named "bar" which takes one `string` parameter (but the parameter should be passed as an object instead of an array of one object): | ||
```csharp | ||
public async Task NotifyRemote() | ||
public async Task NotifyRemote(Stream stream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
await clientRpc.NotifyWithParameterObjectAsync("bar", "param1"); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
await rpc.NotifyWithParameterObjectAsync("bar", "param1"); | ||
} | ||
``` | ||
## Invoking a request | ||
To invoke a remote method named "foo" which takes two `string` parameters and returns an int: | ||
```csharp | ||
public async Task NotifyRemote() | ||
public async Task InvokeRemote(Stream stream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
var myResult = await clientRpc.InvokeAsync<int>("foo", "param1", "param2"); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
var myResult = await rpc.InvokeAsync<int>("foo", "param1", "param2"); | ||
} | ||
``` | ||
The parameters will be passed remotely as an array of objects. | ||
|
||
To invoke a remote method named "baz" which takes one `string` parameter (but the parameter should be passed as an object instead of an array of one object) and returns a string: | ||
```csharp | ||
public async Task NotifyRemote() | ||
public async Task InvokeRemote(Stream stream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
var myResult = await clientRpc.InvokeWithParameterObjectAsync<string>("baz", "param1"); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
var myResult = await rpc.InvokeWithParameterObjectAsync<string>("baz", "param1"); | ||
} | ||
``` | ||
|
||
|
@@ -98,13 +85,11 @@ public class Server | |
|
||
public class Connection | ||
{ | ||
public async Task NotifyRemote() | ||
public async Task InvokeRemote(Stream stream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
var myResult = await clientRpc.InvokeWithParameterObjectAsync<string>("baz", "param1"); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
var myResult = await rpc.InvokeWithParameterObjectAsync<string>("baz", "param1"); | ||
} | ||
} | ||
``` | ||
|
@@ -120,53 +105,43 @@ public class Server : BaseClass | |
|
||
public class Connection | ||
{ | ||
public async Task NotifyRemote() | ||
public async Task InvokeRemote(Stream stream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStream.CreateStreams(); | ||
var clientRpc = JsonRpc.Attach(streams.Item1); | ||
var serverRpc = JsonRpc.Attach(streams.Item2, target); | ||
var myResult = await clientRpc.InvokeWithParameterObjectAsync<string>("test/InvokeTestMethod"); | ||
var rpc = JsonRpc.Attach(stream, target); | ||
var myResult = await rpc.InvokeWithParameterObjectAsync<string>("test/InvokeTestMethod"); | ||
} | ||
} | ||
``` | ||
|
||
## Crashing the process on exception | ||
In some cases, you may want to immediately crash the server process if certain exceptions are thrown. In this case, overriding the `IsFatalException` method will give you the desired functionality. Through `IsFatalException` you can access and respond to exceptions as they are observed. | ||
## Close stream on fatal errors | ||
In some cases, you may want to immediately close the streams if certain exceptions are thrown. In this case, overriding the `IsFatalException` method will give you the desired functionality. Through `IsFatalException` you can access and respond to exceptions as they are observed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the subject of closing the stream, we should think about how this fits with the new |
||
```csharp | ||
public class Server : BaseClass | ||
{ | ||
public void ThrowsException() => throw new Exception("Throwing an exception"); | ||
} | ||
|
||
public class JsonRpcCrashesOnException : JsonRpc | ||
public class JsonRpcClosesStreamOnException : JsonRpc | ||
{ | ||
public JsonRpcCrashesOnException(Stream clientStream, Stream serverStream, object target = null) : base(clientSteam, serverStream, target) | ||
public JsonRpcClosesStreamOnException(Stream clientStream, Stream serverStream, object target = null) : base(clientStream, serverStream, target) | ||
{ | ||
} | ||
|
||
protected override bool IsFatalException(Exception ex) | ||
{ | ||
if !(ex is OperationCanceledException) | ||
{ | ||
Environment.FailFast(ex.message, ex); | ||
} | ||
|
||
return false; | ||
return true; | ||
} | ||
} | ||
|
||
public class Connection | ||
{ | ||
public async Task NotifyRemote() | ||
public async Task InvokeRemote(Stream clientStream, Stream serverStream) | ||
{ | ||
var target = new Server(); | ||
var streams = Nerdbank.FullDuplexStreams.CreateStreams(); | ||
var clientRpc = new JsonRpcCrashesOnException(streams.Item1); | ||
var serverRpc = new JsonRpcCrashesOnException(streams.Item2, target); | ||
clientRpc.StartListening(); | ||
serverRpc.StartListening(); | ||
await clientRpc.InvokeAsync(nameof(Server.ThrowsException)); | ||
var rpc = new JsonRpcClosesStreamOnException(clientStream, serverStream, target); | ||
rpc.StartListening(); | ||
await rpc.InvokeAsync(nameof(Server.ThrowsException)); | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,18 +38,17 @@ public async Task CanInvokeMethodOnServer() | |
} | ||
|
||
[Fact] | ||
public async Task CloseStreamsIfCancelledTaskReturned() | ||
public async Task StreamsStayOpenIfCancelledTaskReturned() | ||
{ | ||
var ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => this.clientRpc.InvokeAsync(nameof(Server.ServerMethodThatReturnsCancelledTask))); | ||
Assert.Equal(CancellationToken.None, ex.CancellationToken); | ||
Assert.NotNull(ex.StackTrace); | ||
Assert.Equal("The operation was canceled.", this.serverRpc.FaultException.Message); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
Assert.Equal(0, this.serverRpc.IsFatalExceptionCount); | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
Assert.True(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
Assert.True(((IDisposableObservable)this.serverRpc).IsDisposed); | ||
Assert.True(((DisposingMessageHandler)this.messageHandler).IsDisposed); | ||
// Assert that the JsonRpc and MessageHandler objects are not disposed | ||
Assert.False(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
Assert.False(((IDisposableObservable)this.serverRpc).IsDisposed); | ||
Assert.False(((DisposingMessageHandler)this.messageHandler).IsDisposed); | ||
} | ||
|
||
[Fact] | ||
|
@@ -74,7 +73,7 @@ public async Task CloseStreamOnAsyncYieldAndThrowException() | |
Exception exception = await Assert.ThrowsAnyAsync<TaskCanceledException>(() => this.clientRpc.InvokeAsync<string>(nameof(Server.AsyncMethodThatThrowsAfterYield), exceptionMessage)); | ||
Assert.NotNull(exception.StackTrace); | ||
Assert.Equal(exceptionMessage, this.serverRpc.FaultException.Message); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
Assert.Equal(1, this.serverRpc.IsFatalExceptionCount); | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
Assert.True(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
|
@@ -89,21 +88,7 @@ public async Task CloseStreamOnAsyncThrowExceptionandYield() | |
Exception exception = await Assert.ThrowsAnyAsync<TaskCanceledException>(() => this.clientRpc.InvokeAsync<string>(nameof(Server.AsyncMethodThatThrowsBeforeYield), exceptionMessage)); | ||
Assert.NotNull(exception.StackTrace); | ||
Assert.Equal(exceptionMessage, this.serverRpc.FaultException.Message); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
Assert.True(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
Assert.True(((IDisposableObservable)this.serverRpc).IsDisposed); | ||
Assert.True(((DisposingMessageHandler)this.clientRpc.MessageHandler).IsDisposed); | ||
} | ||
|
||
[Fact] | ||
public async Task CloseStreamOnAsyncMethodException() | ||
{ | ||
var exceptionMessage = "Exception from CloseStreamOnAsyncMethodException"; | ||
await Assert.ThrowsAsync<TaskCanceledException>(() => this.clientRpc.InvokeAsync(nameof(Server.MethodThatThrowsAsync), exceptionMessage)); | ||
Assert.Equal(exceptionMessage, this.serverRpc.FaultException.Message); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
Assert.Equal(1, this.serverRpc.IsFatalExceptionCount); | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
Assert.True(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
|
@@ -117,7 +102,7 @@ public async Task CloseStreamOnAsyncTMethodException() | |
var exceptionMessage = "Exception from CloseStreamOnAsyncTMethodException"; | ||
await Assert.ThrowsAsync<TaskCanceledException>(() => this.clientRpc.InvokeAsync<string>(nameof(Server.AsyncMethodThatReturnsStringAndThrows), exceptionMessage)); | ||
Assert.Equal(exceptionMessage, this.serverRpc.FaultException.Message); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
Assert.Equal(1, this.serverRpc.IsFatalExceptionCount); | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
Assert.True(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
|
@@ -135,7 +120,7 @@ public async Task StreamsStayOpenForNonServerException() | |
} | ||
|
||
[Fact] | ||
public async Task CloseStreamsOnOperationCanceled() | ||
public async Task StreamsStayOpenOnOperationCanceled() | ||
{ | ||
using (var cts = new CancellationTokenSource()) | ||
{ | ||
|
@@ -145,13 +130,13 @@ public async Task CloseStreamsOnOperationCanceled() | |
|
||
// Ultimately, the server throws because it was canceled. | ||
var ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => invokeTask.WithTimeout(UnexpectedTimeout)); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
Assert.Equal(0, this.serverRpc.IsFatalExceptionCount); | ||
} | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
Assert.True(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
Assert.True(((IDisposableObservable)this.serverRpc).IsDisposed); | ||
Assert.True(((DisposingMessageHandler)this.clientRpc.MessageHandler).IsDisposed); | ||
// Assert that the JsonRpc and MessageHandler objects are not disposed | ||
Assert.False(((IDisposableObservable)this.clientRpc).IsDisposed); | ||
Assert.False(((IDisposableObservable)this.serverRpc).IsDisposed); | ||
Assert.False(((DisposingMessageHandler)this.clientRpc.MessageHandler).IsDisposed); | ||
} | ||
|
||
[Fact] | ||
|
@@ -166,7 +151,7 @@ public async Task CancelMayStillReturnErrorFromServer() | |
|
||
await Assert.ThrowsAsync<TaskCanceledException>(() => invokeTask); | ||
Assert.Equal(Server.ThrowAfterCancellationMessage, this.serverRpc.FaultException.Message); | ||
Assert.Equal(2, this.serverRpc.IsFatalExceptionCount); | ||
Assert.Equal(1, this.serverRpc.IsFatalExceptionCount); | ||
} | ||
|
||
// Assert that the JsonRpc and MessageHandler objects are disposed after exception | ||
|
@@ -219,7 +204,7 @@ public async Task<string> AsyncMethodThatReturnsStringAndThrows(string message) | |
{ | ||
await Task.Run(() => throw new Exception(message)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rewrite to this to guarantee the method is always asynchronous. await Task.Yield();
throw new Exception(message); |
||
|
||
return "never will return"; | ||
return await Task.FromResult("never will return"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious why you're using |
||
} | ||
|
||
public async Task<string> AsyncMethodWithCancellation(string arg, CancellationToken cancellationToken) | ||
|
@@ -251,11 +236,6 @@ public async Task<string> AsyncMethodFaultsAfterCancellation(string arg, Cancell | |
|
||
throw new InvalidOperationException(ThrowAfterCancellationMessage); | ||
} | ||
|
||
public async Task MethodThatThrowsAsync(string message) | ||
{ | ||
await Task.Run(() => throw new Exception(message)); | ||
} | ||
} | ||
|
||
public class DisposingMessageHandler : HeaderDelimitedMessageHandler | ||
|
@@ -282,9 +262,6 @@ internal class JsonRpcWithFatalExceptions : JsonRpc | |
{ | ||
internal Exception FaultException; | ||
|
||
// If the exception arises from a task faulting or canceling, this method will be called twice: | ||
// once in the handling of the task and once when dispatching the request | ||
// If the exception arises from a synchronous method call, this method will be called once when dispatching the request | ||
internal int IsFatalExceptionCount; | ||
|
||
public JsonRpcWithFatalExceptions(DelimitedMessageHandler messageHandler, object target = null) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,5 +27,10 @@ public enum DisconnectedReason | |
/// The stream was disposed. | ||
/// </summary> | ||
Disposed, | ||
|
||
/// <summary> | ||
/// A fatal exception was thrown from the server method. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since for most readers they probably have a distinct idea of which end the "server" refers to, and yet this actually always refers to the local side, perhaps we should rephrase this to:
|
||
/// </summary> | ||
FatalException, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having just one JsonRpc instance created looks much better (more real). Thanks.
However, one can't use the same JsonRpc instance that you passed the server to to also invoke methods on that server. You'd need the JsonRpc instance that the client created to invoke methods on the server.
Can the docs show how it looks on the server, and then as a separate code snippet show "and with this attribute on the server method, the client can now invoke this method with this special name, like this..."