diff --git a/src/NewRemoting/MessageHandler.cs b/src/NewRemoting/MessageHandler.cs index b7705e8..5280f84 100644 --- a/src/NewRemoting/MessageHandler.cs +++ b/src/NewRemoting/MessageHandler.cs @@ -12,6 +12,7 @@ using System.Runtime.Serialization; using System.Text; using System.Text.Json; +using System.Threading; using Castle.DynamicProxy; using Microsoft.Extensions.Logging; using NewRemoting.Toolkit; @@ -321,6 +322,11 @@ public void WriteArgumentToStream(BinaryWriter w, object data, string references } } } + else if (data is CancellationTokenSource) + { + throw new InvalidOperationException( + "A CancellationTokenSource cannot be used in a remote call. Use CrossAppDomainCancellationTokenSource instead"); + } else if (TypeIsContainerWithReference(data, out Type contentType)) { var list = data as IEnumerable; @@ -583,6 +589,28 @@ internal bool TryUseFastSerialization(BinaryWriter w, Type objectType, object da w.Write(byteArray, 0, byteArray.Length); return true; } + + case CancellationToken token: + { + // Ordinary cancellationtokens are not serializable, but we can still support them by encoding their state (canceled or not) and reconstructing them on the other side + w.Write((int)RemotingReferenceType.CancellationToken); + if (token == CancellationToken.None) + { + // We allow these special cases, because they are commonly used and don't require any special handling on the receiving side + w.Write(0); + } + else if (token.IsCancellationRequested) + { + w.Write(1); + } + else + { + throw new InvalidOperationException( + "Cannot use CancellationToken's in an RPC call. Use CrossAppDomainCancellationTokenSource instead"); + } + + return true; + } } return false; @@ -940,6 +968,26 @@ public object ReadArgumentFromStream(BinaryReader r, MethodBase callingMethod, I return ret; } + case RemotingReferenceType.CancellationToken: + { + int tokenState = r.ReadInt32(); + if (tokenState == 0) + { + return CancellationToken.None; + } + else if (tokenState == 1) + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + return cts.Token; + } + else + { + throw new InvalidOperationException( + "Invalid token state received. Cannot use CancellationToken's in an RPC call. Use CrossAppDomainCancellationTokenSource instead"); + } + } + case RemotingReferenceType.AddEvent: { lock (ConcurrentOperationsLock) diff --git a/src/NewRemoting/RemotingReferenceType.cs b/src/NewRemoting/RemotingReferenceType.cs index e351421..bbc354f 100644 --- a/src/NewRemoting/RemotingReferenceType.cs +++ b/src/NewRemoting/RemotingReferenceType.cs @@ -39,6 +39,7 @@ internal enum RemotingReferenceType AddEvent, RemoveEvent, MethodPointer, - ByteArray + ByteArray, + CancellationToken, } } diff --git a/src/NewRemotingUnitTest/CrossAppDomainCancellationTokenSourceTest.cs b/src/NewRemotingUnitTest/CrossAppDomainCancellationTokenSourceTest.cs index 0c9b1ff..448d20e 100644 --- a/src/NewRemotingUnitTest/CrossAppDomainCancellationTokenSourceTest.cs +++ b/src/NewRemotingUnitTest/CrossAppDomainCancellationTokenSourceTest.cs @@ -94,7 +94,7 @@ public void CrossAppDomainCancellationWithTimeout() { Assert.That(!cts.IsCancellationRequested); cts.CancelAfter(TimeSpan.FromSeconds(0.2)); - Assert.Throws(() => dummy.DoSomethingWithNormalToken(cts.Token)); + Assert.Throws(() => dummy.WaitForToken(cts.Token)); Assert.That(cts.IsCancellationRequested); } } @@ -110,5 +110,37 @@ public void CrossAppDomainCancellationWithoutCancellation() Assert.That(cts.IsCancellationRequested, Is.False); } } + + [Test] + public void UseCancellationWithWrongTokenSource() + { + Stopwatch sw = Stopwatch.StartNew(); + var dummy = _client.CreateRemoteInstance(); + var cts = new CancellationTokenSource(); + + var token = cts.Token; + Assert.That(!cts.IsCancellationRequested); + cts.Cancel(); + Assert.Throws(() => dummy.DoSomethingWithNormalToken(token)); // No crash, since already cancelled + Assert.That(cts.IsCancellationRequested); + cts.Dispose(); + + // Note: If we use cts.Token here, it will throw ObjectDisposedException right away. But we want to see whether + // the server can handle the case where the token source is already disposed, so we use the token that we got before disposing the source. + Assert.Throws(() => dummy.DoSomethingWithNormalToken(token)); + } + + [Test] + public void UseCancellationWithWrongToken() + { + Stopwatch sw = Stopwatch.StartNew(); + var dummy = _client.CreateRemoteInstance(); + var cts = new CancellationTokenSource(); + + var token = cts.Token; + Assert.Throws(() => dummy.DoSomethingWithNormalToken(token)); + Assert.That(cts.IsCancellationRequested, Is.False); + cts.Dispose(); + } } } diff --git a/src/SampleServerClasses/DummyCancellableType.cs b/src/SampleServerClasses/DummyCancellableType.cs index 05afae9..383b8cc 100644 --- a/src/SampleServerClasses/DummyCancellableType.cs +++ b/src/SampleServerClasses/DummyCancellableType.cs @@ -20,12 +20,17 @@ public virtual void DoSomething(ICrossAppDomainCancellationToken cancellationTok cancellationToken.ThrowIfCancellationRequested(); } - public virtual void DoSomethingWithNormalToken(ICrossAppDomainCancellationToken cancellationToken) + public virtual void WaitForToken(ICrossAppDomainCancellationToken cancellationToken) { - TakeToken(cancellationToken.GetLocalCancellationToken()); + WaitForCancellation(cancellationToken.GetLocalCancellationToken()); } - private void TakeToken(CancellationToken token) + public virtual void DoSomethingWithNormalToken(CancellationToken cancellationToken) + { + WaitForCancellation(cancellationToken); + } + + private void WaitForCancellation(CancellationToken token) { Stopwatch w = Stopwatch.StartNew(); while (true) @@ -36,6 +41,7 @@ private void TakeToken(CancellationToken token) } token.ThrowIfCancellationRequested(); + Thread.Sleep(100); } } }