diff --git a/Capnp.Net.Runtime.Tests/Dtbdct.cs b/Capnp.Net.Runtime.Tests/Dtbdct.cs index 57ae39c..d67860d 100644 --- a/Capnp.Net.Runtime.Tests/Dtbdct.cs +++ b/Capnp.Net.Runtime.Tests/Dtbdct.cs @@ -176,5 +176,17 @@ namespace Capnp.Net.Runtime.Tests { NewDtbdctTestbed().RunTest(Testsuite.ReexportSenderPromise); } + + [TestMethod] + public void CallAfterFinish1() + { + NewDtbdctTestbed().RunTest(Testsuite.CallAfterFinish1); + } + + [TestMethod] + public void CallAfterFinish2() + { + NewDtbdctTestbed().RunTest(Testsuite.CallAfterFinish2); + } } } diff --git a/Capnp.Net.Runtime.Tests/EdgeCaseHandling.cs b/Capnp.Net.Runtime.Tests/EdgeCaseHandling.cs index 0986996..0e61fa5 100644 --- a/Capnp.Net.Runtime.Tests/EdgeCaseHandling.cs +++ b/Capnp.Net.Runtime.Tests/EdgeCaseHandling.cs @@ -29,6 +29,7 @@ namespace Capnp.Net.Runtime.Tests { var pipe = new Pipe(); _fromEnginePump = new FramePump(pipe.Writer.AsStream()); + _fromEnginePump.AttachTracer(new FrameTracing.RpcFrameTracer(Console.Out, false)); _reader = new BinaryReader(pipe.Reader.AsStream()); } diff --git a/Capnp.Net.Runtime.Tests/SerializationTests.cs b/Capnp.Net.Runtime.Tests/SerializationTests.cs index 64a2835..57874ef 100644 --- a/Capnp.Net.Runtime.Tests/SerializationTests.cs +++ b/Capnp.Net.Runtime.Tests/SerializationTests.cs @@ -276,7 +276,7 @@ namespace Capnp.Net.Runtime.Tests } [TestMethod] - public void ListOfUShorts() + public void ListOfUShortsWrongUse() { var b = MessageBuilder.Create(); var wrong = b.CreateObject(); @@ -289,6 +289,54 @@ namespace Capnp.Net.Runtime.Tests Assert.ThrowsException(() => list.ListWriteValue(64, (ushort)1)); } + [TestMethod] + public void ListOfUShortsFromSegment() + { + var b = MessageBuilder.Create(); + var list = b.CreateObject>(); + var array = new ushort[] { 1, 2, 3, 4, 5, 6 }; + var segment = new ArraySegment(array, 1, 3); + list.Init(segment); + CollectionAssert.AreEqual(segment.ToArray(), list.ToArray()); + } + + [TestMethod] + public void ListOfUShortsFromDeser() + { + var b = MessageBuilder.Create(); + var list0 = b.CreateObject>(); + var expected = new ushort[] { 2, 3, 4 }; + list0.Init(expected); + var deser = ((DeserializerState)list0).RequireList().CastUShort(); + var list = b.CreateObject>(); + list.Init(deser); + CollectionAssert.AreEqual(expected, list.ToArray()); + } + + [TestMethod] + public void ListOfUShortsFromSer() + { + var b = MessageBuilder.Create(); + var list0 = b.CreateObject>(); + var expected = new ushort[] { 2, 3, 4 }; + list0.Init(expected); + var list = b.CreateObject>(); + list.Init(list0); + CollectionAssert.AreEqual(expected, list.ToArray()); + } + + [TestMethod] + public void ListOfUShortsFromList() + { + var b = MessageBuilder.Create(); + var list0 = b.CreateObject>(); + var expected = new List { 2, 3, 4 }; + list0.Init(expected); + var list = b.CreateObject>(); + list.Init(list0); + CollectionAssert.AreEqual(expected, list.ToArray()); + } + [TestMethod] public void ListOfUInts() { @@ -1093,7 +1141,7 @@ namespace Capnp.Net.Runtime.Tests { var mb = MessageBuilder.Create(); var dss = mb.CreateObject(); - dss.SetStruct(0, 4); + dss.SetStruct(0, 5); var s1 = mb.CreateObject(); s1.SomeText = "foo"; s1.MoreText = "bar"; @@ -1109,7 +1157,8 @@ namespace Capnp.Net.Runtime.Tests s4.SomeText = "1"; var arr = new SomeStruct.WRITER[] { s3, s4 }; dss.LinkObject(2, arr); - Assert.ThrowsException(() => dss.LinkObject(3, new object())); + Assert.ThrowsException(() => dss.LinkObject(3, new object())); + dss.LinkObject(3, new SomeStruct() { SomeText = "corge" }); var t1 = dss.BuildPointer(0).Rewrap(); Assert.AreEqual("foo", t1.SomeText); @@ -1121,7 +1170,8 @@ namespace Capnp.Net.Runtime.Tests Assert.AreEqual(2, l.Count); Assert.AreEqual("0", l[0].SomeText); Assert.AreEqual("1", l[1].SomeText); - Assert.AreEqual(ObjectKind.Nil, dss.BuildPointer(3).Kind); + Assert.AreEqual("corge", dss.BuildPointer(3).Rewrap().SomeText); + Assert.AreEqual(ObjectKind.Nil, dss.BuildPointer(4).Kind); } [TestMethod] @@ -1138,5 +1188,23 @@ namespace Capnp.Net.Runtime.Tests Assert.IsTrue(cap.IsDisposed); dss2.Dispose(); } + + [TestMethod] + public void ProvideCapability() + { + var dss = DynamicSerializerState.CreateForRpc(); + var impl = new TestInterfaceImpl2(); + var p1 = (Proxy)Proxy.Share(impl); + var p2 = (Proxy)Proxy.Share(impl); + Assert.AreEqual(0u, dss.ProvideCapability(p1)); + Assert.AreEqual(0u, dss.ProvideCapability(p2.ConsumedCap)); + Assert.AreEqual(0u, dss.ProvideCapability(CapabilityReflection.CreateSkeleton(impl))); + Assert.IsTrue(p1.IsDisposed); + Assert.IsFalse(p2.IsDisposed); + p2.Dispose(); + Assert.IsFalse(impl.IsDisposed); + dss.Dispose(); + Assert.IsTrue(impl.IsDisposed); + } } } diff --git a/Capnp.Net.Runtime.Tests/TcpRpc.cs b/Capnp.Net.Runtime.Tests/TcpRpc.cs index 1782101..0714581 100644 --- a/Capnp.Net.Runtime.Tests/TcpRpc.cs +++ b/Capnp.Net.Runtime.Tests/TcpRpc.cs @@ -823,8 +823,9 @@ namespace Capnp.Net.Runtime.Tests server.StartAccepting(IPAddress.Any, TcpPort); var client1 = new TcpRpcClient("localhost", TcpPort); - Assert.IsTrue(client1.WhenConnected.Wait(MediumNonDbgTimeout)); - Assert.IsTrue(SpinWait.SpinUntil(() => client1.State == ConnectionState.Down, MediumNonDbgTimeout)); + Assert.IsTrue(client1.WhenConnected.Wait(MediumNonDbgTimeout), "Did not connect"); + Assert.IsTrue(SpinWait.SpinUntil(() => client1.State == ConnectionState.Down, MediumNonDbgTimeout), + $"Connection did not go down: {client1.State}"); } [TestMethod] diff --git a/Capnp.Net.Runtime.Tests/TcpRpcAdvancedStuff.cs b/Capnp.Net.Runtime.Tests/TcpRpcAdvancedStuff.cs index 1349fd5..3f55e24 100644 --- a/Capnp.Net.Runtime.Tests/TcpRpcAdvancedStuff.cs +++ b/Capnp.Net.Runtime.Tests/TcpRpcAdvancedStuff.cs @@ -285,5 +285,17 @@ namespace Capnp.Net.Runtime.Tests { NewLocalhostTcpTestbed().RunTest(Testsuite.NoTailCallMt); } + + [TestMethod] + public void CallAfterFinish1() + { + NewLocalhostTcpTestbed().RunTest(Testsuite.CallAfterFinish1); + } + + [TestMethod] + public void CallAfterFinish2() + { + NewLocalhostTcpTestbed().RunTest(Testsuite.CallAfterFinish2); + } } } diff --git a/Capnp.Net.Runtime.Tests/Testsuite.cs b/Capnp.Net.Runtime.Tests/Testsuite.cs index 77e0a59..07719f5 100644 --- a/Capnp.Net.Runtime.Tests/Testsuite.cs +++ b/Capnp.Net.Runtime.Tests/Testsuite.cs @@ -40,6 +40,10 @@ namespace Capnp.Net.Runtime.Tests // exception had to be serialized, so we receive // the wrapped version. } + catch (TaskCanceledException) + { + // Also used in some test + } catch (System.Exception exception) { Assert.Fail($"Got wrong kind of exception: {exception}"); @@ -407,7 +411,9 @@ namespace Capnp.Net.Runtime.Tests // Note that this was a bug in previous versions: // Since passing a cap has move semantics, we need to create an explicit copy. var copy = Proxy.Share(cap); + Assert.IsFalse(((Proxy)copy).IsDisposed); var ctask = main.CallFoo(copy, default); + Assert.IsTrue(((Proxy)copy).IsDisposed); testbed.MustComplete(ctask); Assert.AreEqual("bar", ctask.Result); @@ -820,5 +826,34 @@ namespace Capnp.Net.Runtime.Tests testbed.MustComplete(tcsd.Task); } } + + public static void CallAfterFinish1(ITestbed testbed) + { + var counters = new Counters(); + var impl = new TestMoreStuffImpl3(); + using (var main = testbed.ConnectMain(impl)) + { + using (var held = main.GetHeld().Eager()) + { + testbed.CloseClient(); + testbed.ExpectPromiseThrows(held.Foo(123, true)); + } + } + } + + public static void CallAfterFinish2(ITestbed testbed) + { + var counters = new Counters(); + var impl = new TestMoreStuffImpl3(); + using (var main = testbed.ConnectMain(impl)) + { + using (var cts = new CancellationTokenSource()) + using (var held = main.GetHeld(cts.Token).Eager()) + { + cts.Cancel(); + testbed.ExpectPromiseThrows(held.Foo(123, true)); + } + } + } } } diff --git a/Capnp.Net.Runtime.Tests/Util/TestBase.cs b/Capnp.Net.Runtime.Tests/Util/TestBase.cs index 5770c2c..6b089b5 100644 --- a/Capnp.Net.Runtime.Tests/Util/TestBase.cs +++ b/Capnp.Net.Runtime.Tests/Util/TestBase.cs @@ -19,6 +19,8 @@ namespace Capnp.Net.Runtime.Tests void MustComplete(params Task[] tasks); void MustNotComplete(params Task[] tasks); void FlushCommunication(); + void CloseClient(); + void CloseServer(); long ClientSendCount { get; } } @@ -84,7 +86,13 @@ namespace Capnp.Net.Runtime.Tests { var frame = _frameBuffer.Dequeue(); _recursion = true; - OtherEndpoint.Forward(frame); + try + { + OtherEndpoint.Forward(frame); + } + catch (InvalidOperationException) + { + } _recursion = false; return true; @@ -177,6 +185,16 @@ namespace Capnp.Net.Runtime.Tests { Assert.IsFalse(tasks.Any(t => t.IsCompleted)); } + + void ITestbed.CloseClient() + { + throw new NotSupportedException(); + } + + void ITestbed.CloseServer() + { + throw new NotSupportedException(); + } } protected class DtbdctTestbed : ITestbed, ITestController @@ -231,17 +249,27 @@ namespace Capnp.Net.Runtime.Tests } long ITestbed.ClientSendCount => _enginePair.Channel2SendCount; + + void ITestbed.CloseClient() + { + _enginePair.Endpoint1.Dismiss(); + } + + void ITestbed.CloseServer() + { + _enginePair.Endpoint2.Dismiss(); + } } protected class LocalhostTcpTestbed : ITestbed, ITestController { TcpRpcServer _server; TcpRpcClient _client; + bool _prematurelyClosed; public void RunTest(Action action) { (_server, _client) = SetupClientServerPair(); - //_client.WhenConnected.Wait(MediumNonDbgTimeout); Assert.IsTrue(SpinWait.SpinUntil(() => _server.ConnectionCount > 0, MediumNonDbgTimeout)); var conn = _server.Connections[0]; @@ -250,8 +278,11 @@ namespace Capnp.Net.Runtime.Tests { action(this); - Assert.IsTrue(SpinWait.SpinUntil(() => _client.SendCount == conn.RecvCount, MediumNonDbgTimeout)); - Assert.IsTrue(SpinWait.SpinUntil(() => conn.SendCount == _client.RecvCount, MediumNonDbgTimeout)); + if (!_prematurelyClosed) + { + Assert.IsTrue(SpinWait.SpinUntil(() => _client.SendCount == conn.RecvCount, MediumNonDbgTimeout)); + Assert.IsTrue(SpinWait.SpinUntil(() => conn.SendCount == _client.RecvCount, MediumNonDbgTimeout)); + } } } @@ -293,6 +324,18 @@ namespace Capnp.Net.Runtime.Tests } long ITestbed.ClientSendCount => _client.SendCount; + + void ITestbed.CloseClient() + { + _prematurelyClosed = true; + _client.Dispose(); + } + + void ITestbed.CloseServer() + { + _prematurelyClosed = true; + _server.Dispose(); + } } public static int TcpPort = 49152; diff --git a/Capnp.Net.Runtime/ListOfPrimitivesSerializer.cs b/Capnp.Net.Runtime/ListOfPrimitivesSerializer.cs index 18477f4..5729e74 100644 --- a/Capnp.Net.Runtime/ListOfPrimitivesSerializer.cs +++ b/Capnp.Net.Runtime/ListOfPrimitivesSerializer.cs @@ -31,7 +31,7 @@ namespace Capnp /// /// The list's data /// - public Span Data => MemoryMarshal.Cast(RawData); + public Span Data => MemoryMarshal.Cast(RawData).Slice(0, Count); /// /// Gets or sets the value at given index. @@ -118,7 +118,7 @@ namespace Capnp IEnumerable Enumerate() { - for (int i = 0; i < Data.Length; i++) + for (int i = 0; i < Count; i++) yield return Data[i]; } diff --git a/Capnp.Net.Runtime/Rpc/CapabilityReflection.cs b/Capnp.Net.Runtime/Rpc/CapabilityReflection.cs index 2512bda..57dfa8b 100644 --- a/Capnp.Net.Runtime/Rpc/CapabilityReflection.cs +++ b/Capnp.Net.Runtime/Rpc/CapabilityReflection.cs @@ -239,7 +239,7 @@ namespace Capnp.Rpc } /// - /// Checks whether a given type qualifies as cpapbility interface. + /// Checks whether a given type qualifies as capability interface. /// /// type to check /// true when is a capability interface diff --git a/Capnp.Net.Runtime/Rpc/LazyCapability.cs b/Capnp.Net.Runtime/Rpc/LazyCapability.cs index bd3f037..5c1f7f7 100644 --- a/Capnp.Net.Runtime/Rpc/LazyCapability.cs +++ b/Capnp.Net.Runtime/Rpc/LazyCapability.cs @@ -104,12 +104,6 @@ namespace Capnp.Rpc cancellationToken.ThrowIfCancellationRequested(); } - if (cap == null) - { - args.Dispose(); - throw new RpcException("Broken capability"); - } - using var proxy = new Proxy(cap); var call = proxy.Call(interfaceId, methodId, args, default); var whenReturned = call.WhenReturned; diff --git a/Capnp.Net.Runtime/Rpc/PendingQuestion.cs b/Capnp.Net.Runtime/Rpc/PendingQuestion.cs index c822893..f2e5917 100644 --- a/Capnp.Net.Runtime/Rpc/PendingQuestion.cs +++ b/Capnp.Net.Runtime/Rpc/PendingQuestion.cs @@ -237,28 +237,7 @@ namespace Capnp.Rpc /// Access path /// Low-level capability /// The referenced member does not exist or does not resolve to a capability pointer. - public ConsumedCapability Access(MemberAccessPath access) - { - lock (ReentrancyBlocker) - { - if ( StateFlags.HasFlag(State.Returned) && - !StateFlags.HasFlag(State.TailCall)) - { - try - { - return access.Eval(WhenReturned.Result); - } - catch (AggregateException exception) - { - throw exception.InnerException!; - } - } - else - { - return new RemoteAnswerCapability(this, access); - } - } - } + public ConsumedCapability Access(MemberAccessPath access) => new RemoteAnswerCapability(this, access); /// /// Refer to a (possibly nested) member of this question's (possibly future) result and return diff --git a/Capnp.Net.Runtime/Rpc/Proxy.cs b/Capnp.Net.Runtime/Rpc/Proxy.cs index 48657ff..57f70ce 100644 --- a/Capnp.Net.Runtime/Rpc/Proxy.cs +++ b/Capnp.Net.Runtime/Rpc/Proxy.cs @@ -161,7 +161,7 @@ namespace Capnp.Rpc // When called from the Finalizer, we must not throw. // But when reference counting goes wrong, ConsumedCapability.Release() will throw an InvalidOperationException. // The only option here is to suppress that exception. - try { _consumedCap.Release(); } + try { _consumedCap?.Release(); } catch { } } diff --git a/Capnp.Net.Runtime/Rpc/RemoteAnswerCapability.cs b/Capnp.Net.Runtime/Rpc/RemoteAnswerCapability.cs index 2a49425..53a759d 100644 --- a/Capnp.Net.Runtime/Rpc/RemoteAnswerCapability.cs +++ b/Capnp.Net.Runtime/Rpc/RemoteAnswerCapability.cs @@ -109,18 +109,13 @@ namespace Capnp.Rpc #if DebugEmbargos Logger.LogDebug("Call by proxy"); #endif - if (_question.StateFlags.HasFlag(PendingQuestion.State.CanceledByDispose)) + if (_question.StateFlags.HasFlag(PendingQuestion.State.CanceledByDispose) || + _question.StateFlags.HasFlag(PendingQuestion.State.FinishRequested)) { args.Dispose(); throw new ObjectDisposedException(nameof(PendingQuestion)); } - if (_question.StateFlags.HasFlag(PendingQuestion.State.FinishRequested)) - { - args.Dispose(); - throw new InvalidOperationException("Finish request was already sent"); - } - _question.DisallowFinish(); ++_pendingCallsOnPromise; var promisedAnswer = base.DoCall(interfaceId, methodId, args); diff --git a/Capnp.Net.Runtime/Rpc/RpcEngine.cs b/Capnp.Net.Runtime/Rpc/RpcEngine.cs index 3a8b8ed..83ee8f7 100644 --- a/Capnp.Net.Runtime/Rpc/RpcEngine.cs +++ b/Capnp.Net.Runtime/Rpc/RpcEngine.cs @@ -438,10 +438,7 @@ namespace Capnp.Rpc try { - lock (_callReturnBlocker) - { - Tx(mb.Frame); - } + Tx(mb.Frame); } catch (RpcException exception) { diff --git a/Capnp.Net.Runtime/Rpc/TcpRpcServer.cs b/Capnp.Net.Runtime/Rpc/TcpRpcServer.cs index a4d4dc0..14fb148 100644 --- a/Capnp.Net.Runtime/Rpc/TcpRpcServer.cs +++ b/Capnp.Net.Runtime/Rpc/TcpRpcServer.cs @@ -110,6 +110,7 @@ namespace Capnp.Rpc } } }); + PumpRunner.Start(); } public ConnectionState State { get; set; } = ConnectionState.Initializing; @@ -191,8 +192,6 @@ namespace Capnp.Rpc OnConnectionChanged?.Invoke(this, new ConnectionEventArgs(connection)); connection.Start(); } - - connection.PumpRunner!.Start(); } } catch (SocketException) @@ -207,38 +206,6 @@ namespace Capnp.Rpc } } - void SafeJoin(Thread? thread) - { - if (thread == null) - { - return; - } - - for (int retry = 0; retry < 5; ++retry) - { - try - { - if (!thread.Join(500)) - { - Logger.LogError($"Unable to join {thread.Name} within timeout"); - } - break; - } - catch (ThreadStateException) - { - // In rare cases it happens that despite thread.Start() was called, the thread did not actually start yet. - Logger.LogDebug("Waiting for thread to start in order to join it"); - Thread.Sleep(100); - } - catch (System.Exception exception) - { - Logger.LogError($"Unable to join {thread.Name}: {exception.Message}"); - break; - } - } - } - - /// /// Stops accepting incoming attempts and closes all existing connections. /// @@ -260,7 +227,7 @@ namespace Capnp.Rpc { connection.Client.Dispose(); connection.Pump?.Dispose(); - SafeJoin(connection.PumpRunner); + connection.PumpRunner?.Join(5000); } _rpcEngine.BootstrapCap = null; @@ -286,7 +253,8 @@ namespace Capnp.Rpc finally { _listener = null; - SafeJoin(_acceptorThread); + if (Thread.CurrentThread != _acceptorThread) + _acceptorThread?.Join(); _acceptorThread = null; } } diff --git a/Capnp.Net.Runtime/SerializerState.cs b/Capnp.Net.Runtime/SerializerState.cs index 39f7f80..293a424 100644 --- a/Capnp.Net.Runtime/SerializerState.cs +++ b/Capnp.Net.Runtime/SerializerState.cs @@ -1314,7 +1314,7 @@ namespace Capnp /// /// is out of range. /// - /// This state does neither describe a struct, nor a list of pointers + /// Object neither describes a struct, nor a list of pointers, nor a capability /// Another state is already linked to the specified position (sorry, no overwrite allowed) /// public void LinkObject(int slot, T obj) @@ -1346,10 +1346,16 @@ namespace Capnp break; default: - if (Rpc.CapabilityReflection.IsValidCapabilityInterface(typeof(T))) + try { LinkToCapability(slot, ProvideCapability(obj)); } + catch (Exception exception) when ( + exception is Rpc.InvalidCapabilityInterfaceException || + exception is InvalidOperationException) + { + throw new InvalidOperationException("Object neither describes a struct, nor a list of pointers, nor a capability", exception); + } break; } } diff --git a/Capnp.Net.Runtime/Util/StrictlyOrderedAwaitTask.cs b/Capnp.Net.Runtime/Util/StrictlyOrderedAwaitTask.cs index 714ee2f..f5a26eb 100644 --- a/Capnp.Net.Runtime/Util/StrictlyOrderedAwaitTask.cs +++ b/Capnp.Net.Runtime/Util/StrictlyOrderedAwaitTask.cs @@ -10,7 +10,7 @@ namespace Capnp.Util internal class StrictlyOrderedAwaitTask: INotifyCompletion { readonly Task _awaitedTask; - object _lock; + object? _lock; long _inOrder, _outOrder; public StrictlyOrderedAwaitTask(Task awaitedTask) @@ -26,7 +26,7 @@ namespace Capnp.Util public async void OnCompleted(Action continuation) { - object safeLock = Volatile.Read(ref _lock); + object? safeLock = Volatile.Read(ref _lock); if (safeLock == null) { @@ -69,7 +69,7 @@ namespace Capnp.Util } } - public bool IsCompleted => Volatile.Read(ref _lock) == null; + public bool IsCompleted => Volatile.Read(ref _lock) == null || (_awaitedTask.IsCompleted && Volatile.Read(ref _inOrder) == 0); public T GetResult() => _awaitedTask.GetAwaiter().GetResult(); diff --git a/scripts/measure-coverage.ps1 b/scripts/measure-coverage.ps1 index c16ed26..4a7dda5 100644 --- a/scripts/measure-coverage.ps1 +++ b/scripts/measure-coverage.ps1 @@ -20,7 +20,7 @@ If(!(test-path $coverageReportDir)) } & $openCover -target:"$vsTestConsole" ` - -targetArgs:"/inIsolation $runtimeTests /TestCaseFilter:`"TestCategory=Coverage`"" ` + -targetArgs:"/inIsolation $runtimeTests /TestCaseFilter:`"TestCategory=Coverage`" /Framework:.NETCoreApp,Version=v2.1" ` -filter:"+[Capnp.Net.Runtime]Capnp.*" ` -excludebyattribute:"System.CodeDom.Compiler.GeneratedCodeAttribute" ` -output:"$coverageOutput" `