From 87d1f2480f933476f33a28dcff612e237515f678 Mon Sep 17 00:00:00 2001 From: Eduardo Salinas Date: Wed, 20 Apr 2016 12:56:11 -0700 Subject: [PATCH] Add event support to Epoxy - Add codegen for events - Add ServiceHost logic - Add relevant tests - Add event example --- .../src/Language/Bond/Codegen/Cs/Comm_cs.hs | 43 ++++- .../generated/generic_service_services.cs | 6 +- .../tests/generated/service_interfaces.cs | 8 + compiler/tests/generated/service_proxies.cs | 54 ++++++- compiler/tests/generated/service_services.cs | 60 +++++-- cs/cs.sln | 21 +++ cs/src/comm/interfaces/CodegenHelpers.cs | 13 ++ cs/src/comm/interfaces/Comm.bond | 1 + cs/src/comm/interfaces/Connections.cs | 5 +- cs/src/comm/interfaces/IService.cs | 19 ++- cs/src/comm/service/ServiceHost.cs | 153 ++++++++++++++---- cs/src/comm/tcp-transport/TcpConnection.cs | 45 +++++- cs/src/comm/tcp-transport/TcpProtocol.cs | 23 ++- cs/test/comm/Tcp/TcpProtocolTests.cs | 33 ++-- cs/test/comm/Tcp/TcpTransportTests.cs | 81 ++++++++++ examples/cs/comm/notifyevent/NotifyEvent.cs | 77 +++++++++ .../cs/comm/notifyevent/NotifyEventService.cs | 28 ++++ examples/cs/comm/notifyevent/notify.bond | 15 ++ .../cs/comm/notifyevent/notifyevent.csproj | 75 +++++++++ 19 files changed, 682 insertions(+), 78 deletions(-) create mode 100644 examples/cs/comm/notifyevent/NotifyEvent.cs create mode 100644 examples/cs/comm/notifyevent/NotifyEventService.cs create mode 100644 examples/cs/comm/notifyevent/notify.bond create mode 100644 examples/cs/comm/notifyevent/notifyevent.csproj diff --git a/compiler/src/Language/Bond/Codegen/Cs/Comm_cs.hs b/compiler/src/Language/Bond/Codegen/Cs/Comm_cs.hs index 4023a858..282ceebd 100644 --- a/compiler/src/Language/Bond/Codegen/Cs/Comm_cs.hs +++ b/compiler/src/Language/Bond/Codegen/Cs/Comm_cs.hs @@ -52,7 +52,11 @@ namespace #{csNamespace} where getMessageResultTypeName = getMessageTypeName cs methodResult getMessageInputTypeName = getMessageTypeName cs methodInput - methodDeclaration _ = mempty + + methodDeclaration Event{..} = [lt|void #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param);|] + where + getMessageInputTypeName = getMessageTypeName cs methodInput + comm _ = mempty comm_proxy_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, L.Text) @@ -81,7 +85,7 @@ namespace #{csNamespace} }|] where methodCapability Function {} = "global::Bond.Comm.IRequestResponseConnection" - methodCapability _ = "" + methodCapability Event {} = "global::Bond.Comm.IEventConnection" getCapabilities :: [Method] -> [String] getCapabilities m = nub $ map methodCapability m @@ -106,7 +110,21 @@ namespace #{csNamespace} where getMessageResultTypeName = getMessageTypeName cs methodResult getMessageInputTypeName = getMessageTypeName cs methodInput - proxyMethod _ = mempty + + proxyMethod Event{..} = [lt|public void #{methodName}Async(#{getMessageInputTypeName} param) + { + var message = new global::Bond.Comm.Message<#{getMessageInputTypeName}>(param); + #{methodName}Async(message); + } + + public void #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param) + { + m_connection.FireEventAsync<#{getMessageInputTypeName}>( + "#{getDeclTypeName idl s}.#{methodName}", + param); + }|] + where + getMessageInputTypeName = getMessageTypeName cs methodInput comm _ = mempty @@ -141,14 +159,17 @@ namespace #{csNamespace} where generics = angles $ sepBy ", " paramName declParams - methodInfo Function{..} = [lt|yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="#{getDeclTypeName idl s}.#{methodName}", Callback = #{methodName}Async_Glue};|] - methodInfo _ = mempty + methodInfo Function{..} = [lt|yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="#{getDeclTypeName idl s}.#{methodName}", Callback = #{methodName}Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse};|] + methodInfo Event{..} = [lt|yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="#{getDeclTypeName idl s}.#{methodName}", Callback = #{methodName}Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.Event};|] methodAbstract Function{..} = [lt|public abstract global::System.Threading.Tasks.Task> #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param, global::System.Threading.CancellationToken ct);|] where getMessageResultTypeName = getMessageTypeName cs methodResult getMessageInputTypeName = getMessageTypeName cs methodInput - methodAbstract _ = mempty + + methodAbstract Event{..} = [lt|public abstract void #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param);|] + where + getMessageInputTypeName = getMessageTypeName cs methodInput methodGlue Function{..} = [lt|private global::System.Threading.Tasks.Task #{methodName}Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) { @@ -159,5 +180,13 @@ namespace #{csNamespace} where getMessageResultTypeName = getMessageTypeName cs methodResult getMessageInputTypeName = getMessageTypeName cs methodInput - methodGlue _ = mempty + + methodGlue Event{..} = [lt|private global::System.Threading.Tasks.Task #{methodName}Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) + { + #{methodName}Async(param.Convert<#{getMessageInputTypeName}>()); + return global::Bond.Comm.CodegenHelpers.CompletedTask; + }|] + where + getMessageInputTypeName = getMessageTypeName cs methodInput + comm _ = mempty diff --git a/compiler/tests/generated/generic_service_services.cs b/compiler/tests/generated/generic_service_services.cs index 4ef0576a..a646fc59 100644 --- a/compiler/tests/generated/generic_service_services.cs +++ b/compiler/tests/generated/generic_service_services.cs @@ -22,9 +22,9 @@ namespace tests { get { - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo31", Callback = foo31Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo32", Callback = foo32Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo33", Callback = foo33Async_Glue}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo31", Callback = foo31Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo32", Callback = foo32Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo33", Callback = foo33Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; } } diff --git a/compiler/tests/generated/service_interfaces.cs b/compiler/tests/generated/service_interfaces.cs index 74632e5a..7d3a38fe 100644 --- a/compiler/tests/generated/service_interfaces.cs +++ b/compiler/tests/generated/service_interfaces.cs @@ -18,6 +18,14 @@ namespace tests [System.CodeDom.Compiler.GeneratedCode("gbc", "0.4.0.2")] interface IFoo { + void foo11Async(global::Bond.Comm.IMessage param); + + void foo12Async(global::Bond.Comm.IMessage param); + + void foo13Async(global::Bond.Comm.IMessage param); + + void foo14Async(global::Bond.Comm.IMessage param); + global::System.Threading.Tasks.Task> foo21Async(global::Bond.Comm.IMessage param, global::System.Threading.CancellationToken ct); global::System.Threading.Tasks.Task> foo22Async(global::Bond.Comm.IMessage param, global::System.Threading.CancellationToken ct); diff --git a/compiler/tests/generated/service_proxies.cs b/compiler/tests/generated/service_proxies.cs index b3bfb230..01c18aea 100644 --- a/compiler/tests/generated/service_proxies.cs +++ b/compiler/tests/generated/service_proxies.cs @@ -16,7 +16,7 @@ namespace tests { [System.CodeDom.Compiler.GeneratedCode("gbc", "0.4.0.2")] - public class FooProxy : IFoo where TConnection : global::Bond.Comm.IRequestResponseConnection + public class FooProxy : IFoo where TConnection : global::Bond.Comm.IEventConnection, global::Bond.Comm.IRequestResponseConnection { private readonly TConnection m_connection; @@ -25,6 +25,58 @@ namespace tests m_connection = connection; } + public void foo11Async(global::Bond.Void param) + { + var message = new global::Bond.Comm.Message(param); + foo11Async(message); + } + + public void foo11Async(global::Bond.Comm.IMessage param) + { + m_connection.FireEventAsync( + "tests.Foo.foo11", + param); + } + + public void foo12Async(global::Bond.Void param) + { + var message = new global::Bond.Comm.Message(param); + foo12Async(message); + } + + public void foo12Async(global::Bond.Comm.IMessage param) + { + m_connection.FireEventAsync( + "tests.Foo.foo12", + param); + } + + public void foo13Async(BasicTypes param) + { + var message = new global::Bond.Comm.Message(param); + foo13Async(message); + } + + public void foo13Async(global::Bond.Comm.IMessage param) + { + m_connection.FireEventAsync( + "tests.Foo.foo13", + param); + } + + public void foo14Async(dummy param) + { + var message = new global::Bond.Comm.Message(param); + foo14Async(message); + } + + public void foo14Async(global::Bond.Comm.IMessage param) + { + m_connection.FireEventAsync( + "tests.Foo.foo14", + param); + } + public global::System.Threading.Tasks.Task> foo21Async(global::Bond.Void param) { var message = new global::Bond.Comm.Message(param); diff --git a/compiler/tests/generated/service_services.cs b/compiler/tests/generated/service_services.cs index 116fad37..4ab876ec 100644 --- a/compiler/tests/generated/service_services.cs +++ b/compiler/tests/generated/service_services.cs @@ -22,21 +22,33 @@ namespace tests { get { - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo21", Callback = foo21Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo22", Callback = foo22Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo23", Callback = foo23Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo24", Callback = foo24Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo31", Callback = foo31Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo32", Callback = foo32Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo33", Callback = foo33Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo34", Callback = foo34Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo41", Callback = foo41Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo42", Callback = foo42Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo43", Callback = foo43Async_Glue}; - yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo44", Callback = foo44Async_Glue}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo11", Callback = foo11Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.Event}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo12", Callback = foo12Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.Event}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo13", Callback = foo13Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.Event}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo14", Callback = foo14Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.Event}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo21", Callback = foo21Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo22", Callback = foo22Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo23", Callback = foo23Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo24", Callback = foo24Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo31", Callback = foo31Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo32", Callback = foo32Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo33", Callback = foo33Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo34", Callback = foo34Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo41", Callback = foo41Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo42", Callback = foo42Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo43", Callback = foo43Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; + yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="tests.Foo.foo44", Callback = foo44Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse}; } } + public abstract void foo11Async(global::Bond.Comm.IMessage param); + + public abstract void foo12Async(global::Bond.Comm.IMessage param); + + public abstract void foo13Async(global::Bond.Comm.IMessage param); + + public abstract void foo14Async(global::Bond.Comm.IMessage param); + public abstract global::System.Threading.Tasks.Task> foo21Async(global::Bond.Comm.IMessage param, global::System.Threading.CancellationToken ct); public abstract global::System.Threading.Tasks.Task> foo22Async(global::Bond.Comm.IMessage param, global::System.Threading.CancellationToken ct); @@ -61,6 +73,30 @@ namespace tests public abstract global::System.Threading.Tasks.Task> foo44Async(global::Bond.Comm.IMessage param, global::System.Threading.CancellationToken ct); + private global::System.Threading.Tasks.Task foo11Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) + { + foo11Async(param.Convert()); + return global::Bond.Comm.CodegenHelpers.CompletedTask; + } + + private global::System.Threading.Tasks.Task foo12Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) + { + foo12Async(param.Convert()); + return global::Bond.Comm.CodegenHelpers.CompletedTask; + } + + private global::System.Threading.Tasks.Task foo13Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) + { + foo13Async(param.Convert()); + return global::Bond.Comm.CodegenHelpers.CompletedTask; + } + + private global::System.Threading.Tasks.Task foo14Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) + { + foo14Async(param.Convert()); + return global::Bond.Comm.CodegenHelpers.CompletedTask; + } + private global::System.Threading.Tasks.Task foo21Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct) { return global::Bond.Comm.CodegenHelpers.Upcast, diff --git a/cs/cs.sln b/cs/cs.sln index 105e23d3..0380f8c0 100644 --- a/cs/cs.sln +++ b/cs/cs.sln @@ -202,6 +202,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "logging", "..\examples\cs\c EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "service", "src\comm\service\service.csproj", "{79D2423A-87C8-44A2-89C2-2FA94521747E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "notifyevent", "..\examples\cs\comm\notifyevent\notifyevent.csproj", "{12279366-F646-4FEC-8CAA-B62A8EC477BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -779,6 +781,24 @@ Global {79D2423A-87C8-44A2-89C2-2FA94521747E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {79D2423A-87C8-44A2-89C2-2FA94521747E}.Release|Win32.ActiveCfg = Release|Any CPU {79D2423A-87C8-44A2-89C2-2FA94521747E}.Release|Win32.Build.0 = Release|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Debug|Win32.ActiveCfg = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Debug|Win32.Build.0 = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Fields|Any CPU.ActiveCfg = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Fields|Any CPU.Build.0 = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Fields|Mixed Platforms.ActiveCfg = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Fields|Mixed Platforms.Build.0 = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Fields|Win32.ActiveCfg = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Fields|Win32.Build.0 = Debug|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Release|Any CPU.Build.0 = Release|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Release|Win32.ActiveCfg = Release|Any CPU + {12279366-F646-4FEC-8CAA-B62A8EC477BB}.Release|Win32.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -816,5 +836,6 @@ Global {54A3432B-99E1-4DEB-B4EB-2D6E158ECD24} = {ED161076-6BB8-4A13-83ED-0E9C01461D5E} {5C8132A8-C4B1-45E0-BCA6-379DA23B86D3} = {621A2166-EEE0-4A27-88AA-5BE5AC996452} {79D2423A-87C8-44A2-89C2-2FA94521747E} = {ED161076-6BB8-4A13-83ED-0E9C01461D5E} + {12279366-F646-4FEC-8CAA-B62A8EC477BB} = {621A2166-EEE0-4A27-88AA-5BE5AC996452} EndGlobalSection EndGlobal diff --git a/cs/src/comm/interfaces/CodegenHelpers.cs b/cs/src/comm/interfaces/CodegenHelpers.cs index 5b145020..e4a7478d 100644 --- a/cs/src/comm/interfaces/CodegenHelpers.cs +++ b/cs/src/comm/interfaces/CodegenHelpers.cs @@ -48,5 +48,18 @@ namespace Bond.Comm return tcsOuter.Task; } + + private static Task s_completedTask = Task.FromResult(default(object)); + + /// + /// Returns a completed Task + /// + public static Task CompletedTask + { + get + { + return s_completedTask; + } + } } } diff --git a/cs/src/comm/interfaces/Comm.bond b/cs/src/comm/interfaces/Comm.bond index a0430a66..eb8228f8 100644 --- a/cs/src/comm/interfaces/Comm.bond +++ b/cs/src/comm/interfaces/Comm.bond @@ -8,6 +8,7 @@ enum ErrorCode OK = 0x0; InternalServerError = 0xA0BD0002; MethodNotFound = 0xA0BD0003; + InvalidInvocation = 0xA0BD0004; } struct Error diff --git a/cs/src/comm/interfaces/Connections.cs b/cs/src/comm/interfaces/Connections.cs index e62daca3..515a8713 100644 --- a/cs/src/comm/interfaces/Connections.cs +++ b/cs/src/comm/interfaces/Connections.cs @@ -48,16 +48,13 @@ namespace Bond.Comm /// invoke. /// /// The message to send. - /// - /// The cancellation token for cooperative cancellation. - /// /// A task representing the asynchronous operation. /// /// Event methods cannot send responses or error. However, the returned /// task may represent an error if there was a local error sending the /// message. /// - Task FireEventAsync(string methodName, IMessage message, CancellationToken ct); + Task FireEventAsync(string methodName, IMessage message); } /// diff --git a/cs/src/comm/interfaces/IService.cs b/cs/src/comm/interfaces/IService.cs index d957a9e3..2a7b2c07 100644 --- a/cs/src/comm/interfaces/IService.cs +++ b/cs/src/comm/interfaces/IService.cs @@ -22,7 +22,23 @@ namespace Bond.Comm /// This class is primarily used by Bond's generated service /// implementations. /// - public delegate Task ServiceCallback(IMessage request, ReceiveContext context, CancellationToken ct); + public delegate Task ServiceCallback(IMessage request, ReceiveContext context, CancellationToken ct); + + /// + /// Indicates the type of ServiceCallback + /// + public enum ServiceCallbackType + { + /// + /// Method returns a Task<IMessage> + /// + RequestResponse, + + /// + /// Method returns a Task + /// + Event + }; /// /// Data about a Bond service method. @@ -38,6 +54,7 @@ namespace Bond.Comm /// The delegate to invoke for this method. /// public ServiceCallback Callback; + public ServiceCallbackType CallbackType; } /// diff --git a/cs/src/comm/service/ServiceHost.cs b/cs/src/comm/service/ServiceHost.cs index 03c66185..9722fd31 100644 --- a/cs/src/comm/service/ServiceHost.cs +++ b/cs/src/comm/service/ServiceHost.cs @@ -5,6 +5,7 @@ namespace Bond.Comm.Service { using System; using System.Collections.Generic; + using System.Reflection; using System.Threading; using System.Threading.Tasks; using Bond.Comm; @@ -13,14 +14,14 @@ namespace Bond.Comm.Service { private readonly Transport m_parentTransport; private readonly object m_lock; - private readonly Dictionary m_dispatchTable; + private readonly Dictionary m_dispatchTable; public ServiceHost(Transport parentTransport) { m_parentTransport = parentTransport; m_lock = new object(); - m_dispatchTable = new Dictionary(); + m_dispatchTable = new Dictionary(); } public bool IsRegistered(string serviceMethodName) @@ -31,9 +32,43 @@ namespace Bond.Comm.Service } } + public void ValidateServiceMethods(IEnumerable serviceMethods) + { + Type returnParameter; + + foreach (var serviceMethod in serviceMethods) + { + switch (serviceMethod.CallbackType) + { + case ServiceCallbackType.RequestResponse: + returnParameter = serviceMethod.Callback.GetMethodInfo().ReturnType; + if (returnParameter != typeof(Task)) + { + throw new ArgumentException($"{serviceMethod.MethodName} registered as " + + $"{serviceMethod.CallbackType} but callback not implemented as such."); + } + break; + case ServiceCallbackType.Event: + returnParameter = serviceMethod.Callback.GetMethodInfo().ReturnType; + if (returnParameter != typeof(Task)) + { + throw new ArgumentException($"{serviceMethod.MethodName} registered as " + + $"{serviceMethod.CallbackType} but callback not implemented as such."); + } + break; + default: + throw new ArgumentException($"{serviceMethod.MethodName} registered as invalid type " + + $"{serviceMethod.CallbackType}."); + } + } + } + public void Register(IService service) { var methodNames = new SortedSet(); + + ValidateServiceMethods(service.Methods); + lock (m_lock) { // Service methods are registerd as a unit - either register all or none. @@ -52,7 +87,7 @@ namespace Bond.Comm.Service foreach (var serviceMethod in service.Methods) { - m_dispatchTable.Add(serviceMethod.MethodName, serviceMethod.Callback); + m_dispatchTable.Add(serviceMethod.MethodName, serviceMethod); methodNames.Add(serviceMethod.MethodName); } } @@ -78,54 +113,116 @@ namespace Bond.Comm.Service { Log.Information("{0}.{1}: Got request {2} from {3}.", nameof(ServiceHost), nameof(DispatchRequest), methodName, context.Connection); - ServiceCallback callback; - IMessage result = null; + ServiceMethodInfo methodInfo; lock (m_lock) { - if (!m_dispatchTable.TryGetValue(methodName, out callback)) + if (!m_dispatchTable.TryGetValue(methodName, out methodInfo)) { var errorMessage = LogUtil.FatalAndReturnFormatted("{0}.{1}: Got request for unknown method {2}.", nameof(ServiceHost), nameof(DispatchRequest), methodName); - var error = new Error() + var error = new Error { message = errorMessage, error_code = (int)ErrorCode.MethodNotFound }; - result = Message.FromError(error); + return Message.FromError(error); } } - if (result == null) + if (methodInfo.CallbackType != ServiceCallbackType.RequestResponse) { + var errorMessage = LogUtil.FatalAndReturnFormatted("{0}.{1}: Method {2} invoked as if it were {3}, but it was registered as {4}.", + nameof(ServiceHost), nameof(DispatchRequest), methodName, ServiceCallbackType.RequestResponse, methodInfo.CallbackType); + + var error = new Error + { + message = errorMessage, + error_code = (int)ErrorCode.InvalidInvocation + }; + return Message.FromError(error); + } + + IMessage result = null; + + try + { + // Cast to appropriate return type which we validated when registering the service + result = await (Task)methodInfo.Callback(message, context, CancellationToken.None); + } + catch (Exception callbackEx) + { + Error error = null; + try { - result = await callback(message, context, CancellationToken.None); + error = m_parentTransport.UnhandledExceptionHandler(callbackEx); } - catch (Exception callbackEx) + catch (Exception handlerEx) { - Error error = null; - - try - { - error = m_parentTransport.UnhandledExceptionHandler(callbackEx); - } - catch (Exception handlerEx) - { - Transport.FailFastExceptionHandler(handlerEx); - } - - if (error == null) - { - Transport.FailFastExceptionHandler(callbackEx); - } - - result = Message.FromError(error); + Transport.FailFastExceptionHandler(handlerEx); } + + if (error == null) + { + Transport.FailFastExceptionHandler(callbackEx); + } + + result = Message.FromError(error); } return result; } + + public async Task DispatchEvent(string methodName, ReceiveContext context, IMessage message) + { + Log.Information("{0}.{1}: Got event {2} from {3}.", + nameof(ServiceHost), nameof(DispatchEvent), methodName, context.Connection); + ServiceMethodInfo methodInfo; + + lock (m_lock) + { + if (!m_dispatchTable.TryGetValue(methodName, out methodInfo)) + { + LogUtil.FatalAndReturnFormatted("{0}.{1}: Got request for unknown method {2}.", + nameof(ServiceHost), nameof(DispatchRequest), methodName); + return; + } + } + + if (methodInfo.CallbackType != ServiceCallbackType.Event) + { + LogUtil.FatalAndReturnFormatted("{0}.{1}: Method {2} invoked as if it were {3}, but it was registered as {4}.", + nameof(ServiceHost), nameof(DispatchRequest), methodName, ServiceCallbackType.Event, methodInfo.CallbackType); + return; + } + + try + { + await methodInfo.Callback(message, context, CancellationToken.None); + } + catch (Exception callbackEx) + { + Error error = null; + + try + { + error = m_parentTransport.UnhandledExceptionHandler(callbackEx); + } + catch (Exception handlerEx) + { + Transport.FailFastExceptionHandler(handlerEx); + } + + if (error == null) + { + Transport.FailFastExceptionHandler(callbackEx); + } + + LogUtil.FatalAndReturnFormatted("{0}.{1}: Failed to complete method {2}. With exception: {3}", + nameof(ServiceHost), nameof(DispatchRequest), methodName, callbackEx.ToString()); + } + } } } diff --git a/cs/src/comm/tcp-transport/TcpConnection.cs b/cs/src/comm/tcp-transport/TcpConnection.cs index ea4860e5..b8cff8c8 100644 --- a/cs/src/comm/tcp-transport/TcpConnection.cs +++ b/cs/src/comm/tcp-transport/TcpConnection.cs @@ -20,7 +20,7 @@ namespace Bond.Comm.Tcp Server } - public class TcpConnection : Connection, IRequestResponseConnection + public class TcpConnection : Connection, IRequestResponseConnection, IEventConnection { private TcpTransport m_parentTransport; @@ -188,6 +188,25 @@ namespace Bond.Comm.Tcp } } + internal async Task SendEventAsync(string methodName, IMessage message) + { + uint requestId = AllocateNextRequestId(); + var frame = MessageToFrame(requestId, methodName, PayloadType.Event, message); + + Log.Debug("{0}.{1}: Sending event {2}/{3}.", this, nameof(SendEventAsync), requestId, methodName); + using (var binWriter = new BinaryWriter(m_networkStream, encoding: Encoding.UTF8, leaveOpen: true)) + { + lock (m_networkStream) + { + frame.Write(binWriter); + binWriter.Flush(); + } + } + + await m_networkStream.FlushAsync(); + Log.Debug("{0}.{1}: Sent event {2}/{3}.", this, nameof(SendEventAsync), requestId, methodName); + } + internal void Start() { Task.Run(() => ProcessFramesAsync(m_networkStream)); @@ -221,6 +240,10 @@ namespace Bond.Comm.Tcp DispatchResponse(result.Headers, result.Payload); break; + case TcpProtocol.FrameDisposition.DeliverEventToService: + DispatchEvent(result.Headers, result.Payload); + break; + case TcpProtocol.FrameDisposition.SendProtocolError: await SendProtocolErrorAsync(result.ErrorCode ?? ProtocolErrorCode.INTERNAL_ERROR); break; @@ -278,6 +301,21 @@ namespace Bond.Comm.Tcp responseCompletionSource.SetResult(response); } + private void DispatchEvent(TcpHeaders headers, ArraySegment payload) + { + if (headers.error_code != (int)ErrorCode.OK) + { + throw new TcpProtocolErrorException("Received a request with non-zero error code. Request ID " + headers.request_id); + } + + IMessage request = Message.FromPayload(Unmarshal.From(payload)); + + Task.Run(async () => + { + await m_serviceHost.DispatchEvent(headers.method_name, new TcpReceiveContext(this), request); + }); + } + public override Task StopAsync() { Log.Debug("{0}.{1}: Shutting down.", this, nameof(StopAsync)); @@ -291,5 +329,10 @@ namespace Bond.Comm.Tcp IMessage response = await SendRequestAsync(methodName, message); return response.Convert(); } + + public Task FireEventAsync(string methodName, IMessage message) + { + return SendEventAsync(methodName, message); + } } } diff --git a/cs/src/comm/tcp-transport/TcpProtocol.cs b/cs/src/comm/tcp-transport/TcpProtocol.cs index 00f03ad4..e6aa0bf5 100644 --- a/cs/src/comm/tcp-transport/TcpProtocol.cs +++ b/cs/src/comm/tcp-transport/TcpProtocol.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.IO; using Bond.IO.Safe; using Bond.Protocols; @@ -28,6 +31,11 @@ namespace Bond.Comm.Tcp /// DeliverResponseToProxy, + /// + /// The frame was a valid Event. + /// + DeliverEventToService, + /// /// The frame was not valid, and the caller should send an error to the remote host. /// @@ -396,17 +404,12 @@ namespace Bond.Comm.Tcp { case PayloadType.Request: case PayloadType.Response: - return ClassifyState.ValidFrame; - case PayloadType.Event: - Log.Warning("{0}.{1}: Received unimplemented payload type {2}.", - nameof(TcpProtocol), nameof(TransitionFrameComplete), headers.payload_type); - errorCode = ProtocolErrorCode.NOT_SUPPORTED; - return ClassifyState.MalformedFrame; - + return ClassifyState.ValidFrame; default: Log.Warning("{0}.{1}: Received unrecognized payload type {2}.", nameof(TcpProtocol), nameof(TransitionFrameComplete), headers.payload_type); + errorCode = ProtocolErrorCode.NOT_SUPPORTED; return ClassifyState.MalformedFrame; } } @@ -429,6 +432,10 @@ namespace Bond.Comm.Tcp disposition = FrameDisposition.DeliverResponseToProxy; return ClassifyState.ClassifiedValidFrame; + case PayloadType.Event: + disposition = FrameDisposition.DeliverEventToService; + return ClassifyState.ClassifiedValidFrame; + default: return ClassifyState.InternalStateError; } diff --git a/cs/test/comm/Tcp/TcpProtocolTests.cs b/cs/test/comm/Tcp/TcpProtocolTests.cs index 6890cd08..184a0f99 100644 --- a/cs/test/comm/Tcp/TcpProtocolTests.cs +++ b/cs/test/comm/Tcp/TcpProtocolTests.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; @@ -45,6 +48,13 @@ namespace UnitTest.Tcp payload_type = PayloadType.Event, request_id = GoodRequestId }; + private static readonly TcpHeaders unknownTypeHeaders = new TcpHeaders + { + error_code = 0, + method_name = GoodMethod, + payload_type = (PayloadType)(-100), + request_id = GoodRequestId + }; private static Frame goodRequestFrame; private static Frame goodResponseFrame; @@ -411,7 +421,7 @@ namespace UnitTest.Tcp ProtocolErrorCode? errorCode = null; var after = TcpProtocol.TransitionFrameComplete( - TcpProtocol.ClassifyState.FrameComplete, goodEventHeaders, ref errorCode); + TcpProtocol.ClassifyState.FrameComplete, unknownTypeHeaders, ref errorCode); Assert.AreEqual(TcpProtocol.ClassifyState.MalformedFrame, after); Assert.AreEqual(ProtocolErrorCode.NOT_SUPPORTED, errorCode); } @@ -455,11 +465,6 @@ namespace UnitTest.Tcp var after = TcpProtocol.TransitionValidFrame(TcpProtocol.ClassifyState.ValidFrame, null, ref disposition); Assert.AreEqual(TcpProtocol.ClassifyState.InternalStateError, after); Assert.AreEqual(TcpProtocol.FrameDisposition.Indeterminate, disposition); - - after = TcpProtocol.TransitionValidFrame( - TcpProtocol.ClassifyState.ValidFrame, goodEventHeaders, ref disposition); - Assert.AreEqual(TcpProtocol.ClassifyState.InternalStateError, after); - Assert.AreEqual(TcpProtocol.FrameDisposition.Indeterminate, disposition); } [Test] @@ -538,6 +543,14 @@ namespace UnitTest.Tcp Assert.Null(protocolErrorResult.Headers); Assert.Null(protocolErrorResult.Payload.Array); Assert.AreEqual(meaninglessErrorCode, protocolErrorResult.Error.error_code); + + var eventResult = TcpProtocol.Classify(goodEventFrame); + Assert.AreEqual(TcpProtocol.FrameDisposition.DeliverEventToService, eventResult.Disposition); + Assert.AreEqual(goodEventHeaders.error_code, eventResult.Headers.error_code); + Assert.AreEqual(goodEventHeaders.method_name, eventResult.Headers.method_name); + Assert.AreEqual(goodEventHeaders.payload_type, eventResult.Headers.payload_type); + Assert.AreEqual(goodEventHeaders.request_id, eventResult.Headers.request_id); + Assert.AreEqual(goodEventFrame.Framelets[goodEventFrame.Count - 1].Contents, eventResult.Payload); } [Test] @@ -553,12 +566,6 @@ namespace UnitTest.Tcp [Test] public void Classify_MalformedFrame() { - var eventResult = TcpProtocol.Classify(goodEventFrame); - Assert.AreEqual(TcpProtocol.FrameDisposition.SendProtocolError, eventResult.Disposition); - Assert.Null(eventResult.Headers); - Assert.Null(eventResult.Payload.Array); - Assert.AreEqual(ProtocolErrorCode.NOT_SUPPORTED, eventResult.ErrorCode); - var emptyResult = TcpProtocol.Classify(emptyFrame); Assert.AreEqual(TcpProtocol.FrameDisposition.SendProtocolError, emptyResult.Disposition); Assert.Null(emptyResult.Headers); diff --git a/cs/test/comm/Tcp/TcpTransportTests.cs b/cs/test/comm/Tcp/TcpTransportTests.cs index dadf9477..c35d83d6 100644 --- a/cs/test/comm/Tcp/TcpTransportTests.cs +++ b/cs/test/comm/Tcp/TcpTransportTests.cs @@ -160,6 +160,17 @@ namespace UnitTest.Tcp await testClientServer.Transport.StopAsync(); } + [Test] + public void TestServiceMethodTypeValidation_Throws() + { + var exception = Assert.Throws(async () => await SetupTestClientServer()); + Assert.That(exception.Message, Is.StringContaining("registered as Event but callback not implemented as such")); + exception = Assert.Throws(async () => await SetupTestClientServer()); + Assert.That(exception.Message, Is.StringContaining("registered as RequestResponse but callback not implemented as such")); + exception = Assert.Throws(async () => await SetupTestClientServer()); + Assert.That(exception.Message, Is.StringContaining("registered as invalid type")); + } + private class TestClientServer { public TService Service; @@ -274,5 +285,75 @@ namespace UnitTest.Tcp return Task.FromResult>(Message.FromPayload(result)); } } + private class TestServiceEventMismatch : IService + { + public IEnumerable Methods + { + get + { + return new[] + { + new ServiceMethodInfo + { + MethodName = "TestService.RespondWithEmpty", + Callback = RespondWithEmpty, + CallbackType = ServiceCallbackType.Event + }, + }; + } + } + + private Task RespondWithEmpty(IMessage request, ReceiveContext context, CancellationToken ct) + { + var emptyMessage = Message.FromPayload(new Bond.Void()); + return Task.FromResult(emptyMessage); + } + } + private class TestServiceReqResMismatch : IService + { + public IEnumerable Methods + { + get + { + return new[] + { + new ServiceMethodInfo + { + MethodName = "TestService.DoBeep", + Callback = DoBeep, + CallbackType = ServiceCallbackType.RequestResponse + }, + }; + } + } + + private Task DoBeep(IMessage request, ReceiveContext context, CancellationToken ct) + { + return CodegenHelpers.CompletedTask; + } + } + private class TestServiceUnsupported : IService + { + public IEnumerable Methods + { + get + { + return new[] + { + new ServiceMethodInfo + { + MethodName = "TestService.DoBeep", + Callback = DoBeep, + CallbackType = (ServiceCallbackType)(-100) + }, + }; + } + } + + private Task DoBeep(IMessage request, ReceiveContext context, CancellationToken ct) + { + return CodegenHelpers.CompletedTask; + } + } } } diff --git a/examples/cs/comm/notifyevent/NotifyEvent.cs b/examples/cs/comm/notifyevent/NotifyEvent.cs new file mode 100644 index 00000000..92a70bcd --- /dev/null +++ b/examples/cs/comm/notifyevent/NotifyEvent.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Bond.Examples.NotifyEvent +{ + using System; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using Bond.Comm; + using Bond.Comm.Tcp; + using Bond.Examples.Logging; + + public static class NotifyEvent + { + private static TcpConnection s_connection; + + public static void Main() + { + var transport = SetupAsync().Result; + + MakeRequestsAndPrint(5); + + Console.WriteLine("Done with all requests."); + + // TODO: Shutdown not yet implemented. + // transport.StopAsync().Wait(); + + Console.ReadLine(); + } + + private async static Task SetupAsync() + { + var handler = new ConsoleLogger(); + Log.AddHandler(handler); + + var transport = new TcpTransportBuilder() + .SetUnhandledExceptionHandler(Transport.ToErrorExceptionHandler) + .Construct(); + + var assignAPortEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + + var notifyService = new NotifyEventService(); + TcpListener notifyListener = transport.MakeListener(assignAPortEndPoint); + notifyListener.AddService(notifyService); + + await notifyListener.StartAsync(); + + s_connection = await transport.ConnectToAsync(notifyListener.ListenEndpoint, CancellationToken.None); + + return transport; + } + + private static void MakeRequestsAndPrint(int numRequests) + { + var notifyEventProxy = new NotifyEventProxy(s_connection); + + var rnd = new Random(); + + foreach (var requestNum in Enumerable.Range(0, numRequests)) + { + UInt16 delay = (UInt16)rnd.Next(2000); + DoNotify(notifyEventProxy, requestNum, "notify" + requestNum.ToString(), delay); + } + } + + private static void DoNotify(NotifyEventProxy proxy, int requestNum, string payload, UInt16 delay) + { + var request = new PingRequest { Payload = payload, DelayMilliseconds = delay }; + proxy.NotifyAsync(request); + + Console.WriteLine($"P Event #{requestNum} Delay: {delay}"); + } + + } +} diff --git a/examples/cs/comm/notifyevent/NotifyEventService.cs b/examples/cs/comm/notifyevent/NotifyEventService.cs new file mode 100644 index 00000000..b3cb97ba --- /dev/null +++ b/examples/cs/comm/notifyevent/NotifyEventService.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Bond.Examples.NotifyEvent +{ + using Comm; + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public class NotifyEventService : NotifyEventServiceBase + { + private const UInt16 MaxDelayMilliseconds = 2000; + + public override void NotifyAsync(IMessage param) + { + PingRequest request = param.Payload.Deserialize(); + + if (request.DelayMilliseconds > 0) + { + UInt16 delayMs = Math.Min(MaxDelayMilliseconds, request.DelayMilliseconds); + Thread.Sleep(delayMs); + } + Console.WriteLine("Notified server-side, payload: " + request.Payload + " delay: "+request.DelayMilliseconds); + } + } +} diff --git a/examples/cs/comm/notifyevent/notify.bond b/examples/cs/comm/notifyevent/notify.bond new file mode 100644 index 00000000..d2c9a5b1 --- /dev/null +++ b/examples/cs/comm/notifyevent/notify.bond @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Bond.Examples.NotifyEvent; + +struct PingRequest +{ + 0: string Payload; + 1: uint16 DelayMilliseconds; +} + +service NotifyEvent +{ + nothing Notify(PingRequest); +} diff --git a/examples/cs/comm/notifyevent/notifyevent.csproj b/examples/cs/comm/notifyevent/notifyevent.csproj new file mode 100644 index 00000000..eb74a658 --- /dev/null +++ b/examples/cs/comm/notifyevent/notifyevent.csproj @@ -0,0 +1,75 @@ + + + + + + {12279366-F646-4FEC-8CAA-B62A8EC477BB} + Exe + notifyevent + notifyevent + v4.5 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + $(BOND_BINARY_PATH)\net45\Bond.Attributes.dll + + + $(BOND_BINARY_PATH)\net45\Bond.dll + + + $(BOND_BINARY_PATH)\net45\Bond.IO.dll + + + $(BOND_BINARY_PATH)\net45\Bond.Comm.Interfaces.dll + + + $(BOND_BINARY_PATH)\net45\Bond.Comm.Tcp.dll + + + + + {5c8132a8-c4b1-45e0-bca6-379da23b86d3} + logging + + + + + \ No newline at end of file