Skip to content
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
48 changes: 48 additions & 0 deletions src/NewRemoting/MessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/NewRemoting/RemotingReferenceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal enum RemotingReferenceType
AddEvent,
RemoveEvent,
MethodPointer,
ByteArray
ByteArray,
CancellationToken,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void CrossAppDomainCancellationWithTimeout()
{
Assert.That(!cts.IsCancellationRequested);
cts.CancelAfter(TimeSpan.FromSeconds(0.2));
Assert.Throws<OperationCanceledException>(() => dummy.DoSomethingWithNormalToken(cts.Token));
Assert.Throws<OperationCanceledException>(() => dummy.WaitForToken(cts.Token));
Assert.That(cts.IsCancellationRequested);
}
}
Expand All @@ -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<DummyCancellableType>();
var cts = new CancellationTokenSource();

var token = cts.Token;
Assert.That(!cts.IsCancellationRequested);
cts.Cancel();
Assert.Throws<OperationCanceledException>(() => 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<OperationCanceledException>(() => dummy.DoSomethingWithNormalToken(token));
}

[Test]
public void UseCancellationWithWrongToken()
{
Stopwatch sw = Stopwatch.StartNew();
var dummy = _client.CreateRemoteInstance<DummyCancellableType>();
var cts = new CancellationTokenSource();

var token = cts.Token;
Assert.Throws<InvalidOperationException>(() => dummy.DoSomethingWithNormalToken(token));
Assert.That(cts.IsCancellationRequested, Is.False);
cts.Dispose();
}
}
}
12 changes: 9 additions & 3 deletions src/SampleServerClasses/DummyCancellableType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -36,6 +41,7 @@ private void TakeToken(CancellationToken token)
}

token.ThrowIfCancellationRequested();
Thread.Sleep(100);
}
}
}
Expand Down