diff --git a/CoreRemoting.Tests/RpcTests.cs b/CoreRemoting.Tests/RpcTests.cs index 6001d07..3f04fb3 100644 --- a/CoreRemoting.Tests/RpcTests.cs +++ b/CoreRemoting.Tests/RpcTests.cs @@ -1,7 +1,10 @@ using System; +using System.ComponentModel; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using CoreRemoting.Channels.Websocket; +using CoreRemoting.DependencyInjection; using CoreRemoting.Serialization; using CoreRemoting.Tests.ExternalTypes; using CoreRemoting.Tests.Tools; @@ -163,7 +166,7 @@ void ClientAction() clientThread.Start(); clientThread.Join(); - _serverFixture.Server.Config.MessageEncryption = true; + _serverFixture.Server.Config.MessageEncryption = false; Assert.True(_remoteServiceCalled); Assert.Equal(0, _serverFixture.ServerErrorCount); @@ -509,5 +512,56 @@ public void NonSerializableError_method_throws_Exception() _serverFixture.ServerErrorCount = 0; } } + + [Fact] + public async Task Disposed_client_subscription_doesnt_break_other_clients() + { + async Task roundtrip(bool encryption) + { + var oldEncryption = _serverFixture.Server.Config.MessageEncryption; + _serverFixture.Server.Config.MessageEncryption = encryption; + + try + { + RemotingClient createClient() => new RemotingClient(new ClientConfig() + { + ServerPort = _serverFixture.Server.Config.NetworkPort, + MessageEncryption = encryption, + }); + + using var client1 = createClient(); + using var client2 = createClient(); + + client1.Connect(); + client2.Connect(); + + var proxy1 = client1.CreateProxy(); + var fired1 = new TaskCompletionSource(); + proxy1.ServiceEvent += () => fired1.TrySetResult(true); + + var proxy2 = client2.CreateProxy(); + var fired2 = new TaskCompletionSource(); + proxy2.ServiceEvent += () => fired2.TrySetResult(true); + + // early disposal, proxy1 subscription isn't canceled + client1.Disconnect(); + + proxy2.FireServiceEvent(); + Assert.True(await fired2.Task); + Assert.True(fired2.Task.IsCompleted); + Assert.False(fired1.Task.IsCompleted); + } + finally + { + _serverFixture.Server.Config.MessageEncryption = oldEncryption; + } + } + + // works! + await roundtrip(encryption: false); + + // fails! + await roundtrip(encryption: true); + } } } \ No newline at end of file diff --git a/CoreRemoting.Tests/ServerFixture.cs b/CoreRemoting.Tests/ServerFixture.cs index eff196f..3b2ae21 100644 --- a/CoreRemoting.Tests/ServerFixture.cs +++ b/CoreRemoting.Tests/ServerFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using CoreRemoting.DependencyInjection; using CoreRemoting.Tests.Tools; using Xunit; @@ -65,7 +66,10 @@ public ServerFixture() public void Dispose() { if (Server != null) + { + Thread.Sleep(100); // work around WatsonTcp 6.0.2 bug Server.Dispose(); + } } } diff --git a/CoreRemoting/RemotingSession.cs b/CoreRemoting/RemotingSession.cs index 0374909..dacbc2a 100644 --- a/CoreRemoting/RemotingSession.cs +++ b/CoreRemoting/RemotingSession.cs @@ -42,7 +42,7 @@ public sealed class RemotingSession : IDisposable /// Event: Fired before the session is disposed to do some clean up. /// public event Action BeforeDispose; - + #endregion #region Construction @@ -70,7 +70,7 @@ internal RemotingSession(int keySize, byte[] clientPublicKey, IRemotingServer se _delegateProxyCache = new ConcurrentDictionary(); _rawMessageTransport = rawMessageTransport ?? throw new ArgumentNullException(nameof(rawMessageTransport)); _clientPublicKeyBlob = clientPublicKey; - + _rawMessageTransport.ReceiveMessage += OnReceiveMessage; _rawMessageTransport.ErrorOccured += OnErrorOccured; @@ -122,6 +122,10 @@ internal RemotingSession(int keySize, byte[] clientPublicKey, IRemotingServer se _remoteDelegateInvocationEventAggregator.RemoteDelegateInvocationNeeded += (_, uniqueCallKey, handlerKey, arguments) => { + // handle graceful client disconnection + if (_isDisposing) + return null; + var sharedSecret = MessageEncryption ? _sessionId.ToByteArray() @@ -134,7 +138,7 @@ internal RemotingSession(int keySize, byte[] clientPublicKey, IRemotingServer se HandlerKey = handlerKey, DelegateArguments = arguments }; - + var remoteDelegateInvocationWebsocketMessage = _server.MessageEncryptionManager .CreateWireMessage( @@ -144,9 +148,19 @@ internal RemotingSession(int keySize, byte[] clientPublicKey, IRemotingServer se keyPair: _keyPair, messageType: "invoke"); - // Invoke remote delegate on client - _rawMessageTransport?.SendMessage( - _server.Serializer.Serialize(remoteDelegateInvocationWebsocketMessage)); + try + { + // Invoke remote delegate on client + _rawMessageTransport?.SendMessage( + _server.Serializer.Serialize(remoteDelegateInvocationWebsocketMessage)); + } + catch (Exception ex) + { + // handle unexpected client disconnection + OnErrorOccured("Failed to dispatch the remote event. " + + $"Session: {SessionId}, Unique call key: {uniqueCallKey}, " + + $"Handler key: {handlerKey}", ex); + } return null; }; @@ -162,7 +176,7 @@ internal RemotingSession(int keySize, byte[] clientPublicKey, IRemotingServer se private void OnErrorOccured(string errorMessage, Exception ex) { var exception = new RemotingException(errorMessage, innerEx: ex); - + ((RemotingServer)_server).OnError(exception); } @@ -705,7 +719,7 @@ private bool MapLinqExpressionArgument(Type argumentType, object argument, out o return true; } - + #endregion #region Close session