diff --git a/YaNco.sln.DotSettings b/YaNco.sln.DotSettings index 2de6434c..be89d3b3 100644 --- a/YaNco.sln.DotSettings +++ b/YaNco.sln.DotSettings @@ -1,3 +1,4 @@  True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/YaNco.Abstractions/IConnection.cs b/src/YaNco.Abstractions/IConnection.cs index e7a7a6bb..4faa8a3e 100644 --- a/src/YaNco.Abstractions/IConnection.cs +++ b/src/YaNco.Abstractions/IConnection.cs @@ -16,6 +16,8 @@ public interface IConnection : IDisposable EitherAsync CreateFunction(string name); EitherAsync InvokeFunction(IFunction function); EitherAsync InvokeFunction(IFunction function, CancellationToken cancellationToken); + + [Obsolete("Use method WithStartProgramCallback of ConnectionBuilder instead. This method will be removed in next major release.")] EitherAsync AllowStartOfPrograms(StartProgramDelegate callback); EitherAsync Cancel(); diff --git a/src/YaNco.Abstractions/IDataContainer.cs b/src/YaNco.Abstractions/IDataContainer.cs index 8678a78f..5d0740b5 100644 --- a/src/YaNco.Abstractions/IDataContainer.cs +++ b/src/YaNco.Abstractions/IDataContainer.cs @@ -12,5 +12,6 @@ public interface IDataContainer : IDisposable Either GetFieldBytes(string name); Either GetStructure(string name); Either GetTable(string name); + Either GetTypeDescription(); } } \ No newline at end of file diff --git a/src/YaNco.Abstractions/IFunctionBuilder.cs b/src/YaNco.Abstractions/IFunctionBuilder.cs new file mode 100644 index 00000000..0d1099a7 --- /dev/null +++ b/src/YaNco.Abstractions/IFunctionBuilder.cs @@ -0,0 +1,18 @@ +using LanguageExt; + +namespace Dbosoft.YaNco +{ + public interface IFunctionBuilder + { + IFunctionBuilder AddParameter(RfcParameterDescription parameter); + IFunctionBuilder AddChar(string name, RfcDirection direction, uint length, bool optional = true, string defaultValue = null); + IFunctionBuilder AddInt(string name, RfcDirection direction, bool optional = true, int defaultValue = 0); + IFunctionBuilder AddLong(string name, RfcDirection direction, bool optional = true, long defaultValue = 0); + IFunctionBuilder AddString(string name, RfcDirection direction, bool optional = true, uint length = 0, string defaultValue = null); + IFunctionBuilder AddStructure(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true); + IFunctionBuilder AddStructure(string name, RfcDirection direction, IStructure structure, bool optional = true); + IFunctionBuilder AddTable(string name, RfcDirection direction, ITable table, bool optional = true); + IFunctionBuilder AddTable(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true); + Either Build(); + } +} \ No newline at end of file diff --git a/src/YaNco.Abstractions/IRfcHandle.cs b/src/YaNco.Abstractions/IRfcHandle.cs new file mode 100644 index 00000000..f77b6674 --- /dev/null +++ b/src/YaNco.Abstractions/IRfcHandle.cs @@ -0,0 +1,6 @@ +namespace Dbosoft.YaNco +{ + public interface IRfcHandle + { + } +} \ No newline at end of file diff --git a/src/YaNco.Abstractions/IRfcRuntime.cs b/src/YaNco.Abstractions/IRfcRuntime.cs index 78929a9e..be20607d 100644 --- a/src/YaNco.Abstractions/IRfcRuntime.cs +++ b/src/YaNco.Abstractions/IRfcRuntime.cs @@ -8,6 +8,7 @@ namespace Dbosoft.YaNco { public interface IRfcRuntime { + [Obsolete("Use method AllowStartOfPrograms of ConnectionBuilder. This method will be removed in next major release.")] Either AllowStartOfPrograms(IConnectionHandle connectionHandle, StartProgramDelegate callback); Either AppendTableRow(ITableHandle tableHandle); Either CreateFunction(IFunctionDescriptionHandle descriptionHandle); @@ -16,8 +17,15 @@ public interface IRfcRuntime Either GetFunctionDescription(IFunctionHandle functionHandle); Either GetFunctionName(IFunctionDescriptionHandle descriptionHandle); Either GetFunctionParameterCount(IFunctionDescriptionHandle descriptionHandle); + Either CreateFunctionDescription(string functionName); + Either AddFunctionParameter(IFunctionDescriptionHandle descriptionHandle, RfcParameterDescription parameterDescription); Either GetFunctionParameterDescription(IFunctionDescriptionHandle descriptionHandle, int index); Either GetFunctionParameterDescription(IFunctionDescriptionHandle descriptionHandle, string name); + Either AddFunctionHandler(string sysid, + string functionName, IFunction function, Func> handler); + Either AddFunctionHandler(string sysid, string functionName, + IFunctionDescriptionHandle descriptionHandle, + Func> handler); Either GetStructure(IDataContainerHandle dataContainer, string name); Either GetTable(IDataContainerHandle dataContainer, string name); Either CloneTable(ITableHandle tableHandle); @@ -58,6 +66,7 @@ public interface IRfcRuntime Either GetFieldValue(IDataContainerHandle handle, Func> func); RfcRuntimeOptions Options { get; } + bool IsFunctionHandlerRegistered(string sysId, string functionName); } diff --git a/src/YaNco.Core/CalledFunction.cs b/src/YaNco.Core/CalledFunction.cs new file mode 100644 index 00000000..a879b669 --- /dev/null +++ b/src/YaNco.Core/CalledFunction.cs @@ -0,0 +1,63 @@ +using System; +using LanguageExt; + +namespace Dbosoft.YaNco +{ + public readonly struct CalledFunction + { + public readonly IFunction Function; + + internal CalledFunction(IFunction function) + { + Function = function; + } + + public Either> Input(Func, Either> inputFunc) + { + var function = Function; + return inputFunc(Prelude.Right(function)).Map(input => new FunctionInput(input, function)); + } + + + } + + public readonly struct FunctionInput + { + public readonly TInput Input; + public readonly IFunction Function; + + internal FunctionInput(TInput input, IFunction function) + { + Input = input; + Function = function; + } + + public FunctionProcessed Process(Func processFunc) + { + return new FunctionProcessed(processFunc(Input), Function); + } + + public void Deconstruct(out IFunction function, out TInput input) + { + function = Function; + input = Input; + } + } + + public readonly struct FunctionProcessed + { + private readonly TOutput _output; + private readonly IFunction _function; + + internal FunctionProcessed(TOutput output, IFunction function) + { + _output = output; + _function = function; + } + + public Either Reply(Func, Either> replyFunc) + { + return replyFunc(_output, Prelude.Right(_function)).Map(_ => Unit.Default); + } + } +} \ No newline at end of file diff --git a/src/YaNco.Core/Connection.cs b/src/YaNco.Core/Connection.cs index 8659aa6a..b3d607f2 100644 --- a/src/YaNco.Core/Connection.cs +++ b/src/YaNco.Core/Connection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Dbosoft.Functional; @@ -59,14 +60,6 @@ public Connection( } } - case AllowStartOfProgramsMessage allowStartOfProgramsMessage: - { - var result = rfcRuntime.AllowStartOfPrograms(handle, - allowStartOfProgramsMessage.Callback).Map(u => (object)u); - return (handle, result) ; - - } - case DisposeMessage disposeMessage: { handle.Dispose(); @@ -165,8 +158,12 @@ public EitherAsync InvokeFunction(IFunction function) public EitherAsync InvokeFunction(IFunction function, CancellationToken cancellationToken) => _stateAgent.Tell(new InvokeFunctionMessage(function, cancellationToken)).ToAsync().Map(_ => Unit.Default); - public EitherAsync AllowStartOfPrograms(StartProgramDelegate callback) => - _stateAgent.Tell(new AllowStartOfProgramsMessage(callback)).ToAsync().Map(r => Unit.Default); + [Obsolete("Use method WithStartProgramCallback of ConnectionBuilder instead. This method will be removed in next major release.")] + [ExcludeFromCodeCoverage] + public EitherAsync AllowStartOfPrograms(StartProgramDelegate callback) + { + return RfcRuntime.AllowStartOfPrograms(_connectionHandle, callback).ToAsync(); + } public EitherAsync GetAttributes() { @@ -200,15 +197,6 @@ public InvokeFunctionMessage(IFunction function, CancellationToken cancellationT } } - private class AllowStartOfProgramsMessage : AgentMessage - { - public readonly StartProgramDelegate Callback; - - public AllowStartOfProgramsMessage(StartProgramDelegate callback) - { - Callback = callback; - } - } private class DisposeMessage : AgentMessage { diff --git a/src/YaNco.Core/ConnectionBuilder.cs b/src/YaNco.Core/ConnectionBuilder.cs index d68ce30e..0f596dda 100644 --- a/src/YaNco.Core/ConnectionBuilder.cs +++ b/src/YaNco.Core/ConnectionBuilder.cs @@ -4,31 +4,114 @@ namespace Dbosoft.YaNco { + /// + /// This class is used to build connections to a SAP ABAP backend. + /// public class ConnectionBuilder { private readonly IDictionary _connectionParam; - private StartProgramDelegate _startProgramDelegate; - private Action _configureRuntime = (c) => {}; - private Func, IRfcRuntime, EitherAsync> + private Action _configureRuntime = (c) => { }; + + private Func, IRfcRuntime, EitherAsync> _connectionFactory = Connection.Create; + readonly List<(string, Action, + Func>)> _functionHandlers + = new List<(string, Action, Func>)>(); + + /// + /// Creates a new connection builder. + /// + /// Dictionary of connection parameters public ConnectionBuilder(IDictionary connectionParam) { _connectionParam = connectionParam; } + /// + /// Registers a action to configure the + /// + /// action with + /// current instance for chaining. + /// + /// Multiple calls of this method will override the previous configuration action. + /// public ConnectionBuilder ConfigureRuntime(Action configure) { _configureRuntime = configure; return this; } + /// + /// This method registers a callback of type + /// to handle backend requests to start local programs. + /// + /// + /// The SAP backend can call function RFC_START_PROGRAM on back destination to request + /// clients to start local programs. This is used a lot in KPRO applications to start saphttp and sapftp. + /// + /// Delegate to callback function implementation. + /// current instance for chaining public ConnectionBuilder WithStartProgramCallback(StartProgramDelegate startProgramDelegate) { - _startProgramDelegate = startProgramDelegate; + return WithFunctionHandler("RFC_START_PROGRAM", builder => builder + .AddChar("COMMAND", RfcDirection.Import, 512), + cf => cf + .Input(f => f.GetField("COMMAND")) + .Process(cmd => startProgramDelegate(cmd)) + .NoReply() + ); + } + + /// + /// This method registers a function handler from a SAP function name. + /// + /// Name of function + /// function handler + /// current instance for chaining + /// + /// The metadata of the function is retrieved from the backend. Therefore the function + /// must exists on the SAP backend system. + /// To register a generic function use the signature that builds from a . + /// Function handlers are registered process wide (in the SAP NW RFC Library).and mapped to backend system id. + /// Multiple registrations of same function and same backend id will therefore have no effect. + /// + public ConnectionBuilder WithFunctionHandler(string functionName, + Func> calledFunc) + { + _functionHandlers.Add((functionName, null, calledFunc)); + return this; + } + + /// + /// This method registers a function handler from a + /// + /// Name of function + /// action to configure function builder + /// function handler + /// current instance for chaining + /// + /// The metadata of the function is build in the . This allows to register + /// any kind of function. + /// To register a known function use the signature with function name + /// Function handlers are registered process wide (in the SAP NW RFC Library) and mapped to backend system id. + /// Multiple registrations of same function and same backend id will therefore have no effect. + /// + public ConnectionBuilder WithFunctionHandler(string functionName, + Action configureBuilder, + Func> calledFunc) + { + _functionHandlers.Add((functionName, configureBuilder, calledFunc)); return this; } + /// + /// Use a alternative factory method to create connection. + /// + /// factory method + /// current instance for chaining.The default implementation call . + /// public ConnectionBuilder UseFactory( Func, IRfcRuntime, EitherAsync> factory) { @@ -36,19 +119,60 @@ public ConnectionBuilder UseFactory( return this; } + /// + /// This method Builds the connection function from the settings. + /// + /// current instance for chaining. + /// + /// The connection builder first creates RfcRuntime and calls any registered runtime configure action. + /// The result is a function that first calls the connection factory (defaults to + /// and afterwards registers function handlers. + /// public Func> Build() { var runtimeConfigurer = new RfcRuntimeConfigurer(); _configureRuntime(runtimeConfigurer); var runtime = runtimeConfigurer.Create(); - - if(_startProgramDelegate == null) - return () => _connectionFactory(_connectionParam, runtime); + return () => _connectionFactory(_connectionParam, runtime) + .Bind(RegisterFunctionHandlers); + + } + + private EitherAsync RegisterFunctionHandlers(IConnection connection) + { + return connection.GetAttributes().Bind(attributes => + { + return _functionHandlers.Map(reg => + { + var (functionName, configureBuilder, callBackFunction) = reg; + + if (connection.RfcRuntime.IsFunctionHandlerRegistered(attributes.SystemId, functionName)) + return Unit.Default; + + if (configureBuilder != null) + { + var builder = new FunctionBuilder(connection.RfcRuntime, functionName); + configureBuilder(builder); + return builder.Build().ToAsync().Bind(descr => + { + return connection.RfcRuntime.AddFunctionHandler(attributes.SystemId, + functionName, + descr, + f => callBackFunction(new CalledFunction(f))).ToAsync(); + }); + + } - return () => (from c in _connectionFactory(_connectionParam, runtime) - from _ in c.AllowStartOfPrograms(_startProgramDelegate) - select c); + return connection.CreateFunction(functionName).Bind(func => + { + return connection.RfcRuntime.AddFunctionHandler(attributes.SystemId, + functionName, + func, + f => callBackFunction(new CalledFunction(f))).ToAsync(); + }); + }).Traverse(l => l).Map(eu => connection); + }); } } } diff --git a/src/YaNco.Core/DataContainer.cs b/src/YaNco.Core/DataContainer.cs index d9ed3cd2..aeb8b73b 100644 --- a/src/YaNco.Core/DataContainer.cs +++ b/src/YaNco.Core/DataContainer.cs @@ -47,7 +47,12 @@ public Either GetTable(string name) { return _rfcRuntime.GetTable(_handle, name).Map(handle => (ITable) new Table(handle, _rfcRuntime)); } - + + public Either GetTypeDescription() + { + return _rfcRuntime.GetTypeDescription(_handle); + } + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/src/YaNco.Core/Delegates.cs b/src/YaNco.Core/Delegates.cs new file mode 100644 index 00000000..d6938c17 --- /dev/null +++ b/src/YaNco.Core/Delegates.cs @@ -0,0 +1,4 @@ +using Dbosoft.YaNco; +using LanguageExt; + +public delegate Either RfcFunctionDelegate(IRfcHandle rfcHandle, IFunctionHandle functionHandle); \ No newline at end of file diff --git a/src/YaNco.Core/FunctionBuilder.cs b/src/YaNco.Core/FunctionBuilder.cs new file mode 100644 index 00000000..e0fcb3dc --- /dev/null +++ b/src/YaNco.Core/FunctionBuilder.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dbosoft.YaNco.Internal; +using LanguageExt; + +namespace Dbosoft.YaNco +{ + public class FunctionBuilder : IFunctionBuilder + { + private readonly string _functionName; + private readonly IDictionary _parameters = new Dictionary(); + private readonly IRfcRuntime _runtime; + + + public FunctionBuilder(IRfcRuntime runtime, string functionName) + { + _runtime = runtime; + _functionName = functionName; + } + + public IFunctionBuilder AddParameter(RfcParameterDescription parameter) + { + _parameters.Add(parameter.Name, parameter); + return this; + } + + public IFunctionBuilder AddChar(string name, RfcDirection direction, uint length, bool optional = true, string defaultValue = null) + { + return AddParameter(new RfcParameterDescription(name, RfcType.CHAR, direction, length, length * 2, 0, optional, defaultValue)); + } + + public IFunctionBuilder AddInt(string name, RfcDirection direction, bool optional = true, int defaultValue = 0) + { + return AddParameter(new RfcParameterDescription(name, RfcType.INT, direction, 0, 0, 0, optional, defaultValue.ToString())); + } + + public IFunctionBuilder AddLong(string name, RfcDirection direction, bool optional = true, long defaultValue = 0) + { + return AddParameter(new RfcParameterDescription(name, RfcType.INT8, direction, 0, 0, 0, optional, defaultValue.ToString())); + } + + public IFunctionBuilder AddString(string name, RfcDirection direction, bool optional = true, uint length = 0, string defaultValue = null) + { + return AddParameter(new RfcParameterDescription(name, RfcType.STRING, direction, length, length * 2, 0, optional, defaultValue)); + } + + public IFunctionBuilder AddStructure(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true) + { + return AddTyped(name, RfcType.STRUCTURE, direction, typeHandle, optional); + } + + public IFunctionBuilder AddStructure(string name, RfcDirection direction, IStructure structure, bool optional = true) + { + return structure.GetTypeDescription() + .Match( + Right: r => AddTyped(name, RfcType.STRUCTURE, direction, r, optional), + Left: l => throw new ArgumentException("Argument is not a valid type handle", nameof(structure))); + } + + public IFunctionBuilder AddTable(string name, RfcDirection direction, ITable table, bool optional = true) + { + return table.GetTypeDescription() + .Match( + Right: r => AddTyped(name, RfcType.STRUCTURE, direction, r, optional), + Left: l => throw new ArgumentException("Argument is not a valid type handle", nameof(table))); + } + + public IFunctionBuilder AddTable(string name, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true) + { + return AddTyped(name, RfcType.TABLE, direction, typeHandle, optional); + } + + private IFunctionBuilder AddTyped(string name, RfcType type, RfcDirection direction, ITypeDescriptionHandle typeHandle, bool optional = true) + { + if (!(typeHandle is TypeDescriptionHandle handle)) + throw new ArgumentException("Argument has to be of type TypeDescriptionHandle", nameof(typeHandle)); + + var ptr = handle.Ptr; + return AddParameter(new RfcParameterDescription(name, type, direction, 0, 0, 0, optional, null) { TypeDescriptionHandle = ptr }); + } + + public Either Build() + { + + return _runtime.CreateFunctionDescription(_functionName).Bind(functionHandle => + { + return _parameters.Values.Map(parameter => + _runtime.AddFunctionParameter(functionHandle, parameter)) + .Traverse(l => l).Map(e => functionHandle); + + }); + + } + + } + +} \ No newline at end of file diff --git a/src/YaNco.Core/FunctionalServerExtensions.cs b/src/YaNco.Core/FunctionalServerExtensions.cs new file mode 100644 index 00000000..a04014ec --- /dev/null +++ b/src/YaNco.Core/FunctionalServerExtensions.cs @@ -0,0 +1,42 @@ +using System; +using LanguageExt; +// ReSharper disable InconsistentNaming + +namespace Dbosoft.YaNco +{ + public static class FunctionalServerExtensions + { + + public static Either> Process( + this Either> input, + Func processFunc) + { + return input.Map(i => i.Process(processFunc)); + } + + public static Either> Process( + this Either> input, + Action processAction) + { + return input.Map(i => + { + var (function, input1) = i; + processAction(input1); + return new FunctionProcessed(Unit.Default, function); + }); + } + + public static Either Reply(this Either> self, Func, Either> replyFunc) + { + return self.Bind(p => p.Reply(replyFunc)); + } + + public static Either NoReply(this Either> self) + { + return self.Bind(p => p.Reply((o, f) => f)); + } + + + + } +} \ No newline at end of file diff --git a/src/YaNco.Core/Internal/Api.cs b/src/YaNco.Core/Internal/Api.cs index 800d43ae..dcdeeb42 100644 --- a/src/YaNco.Core/Internal/Api.cs +++ b/src/YaNco.Core/Internal/Api.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using LanguageExt; namespace Dbosoft.YaNco.Internal { @@ -42,6 +44,13 @@ public static RfcRc GetConnectionAttributes(ConnectionHandle connectionHandle, o return rc; } + public static FunctionDescriptionHandle CreateFunctionDescription(string functionName, + out RfcErrorInfo errorInfo) + { + var ptr = Interopt.RfcCreateFunctionDesc(functionName, out errorInfo); + return ptr == IntPtr.Zero ? null : new FunctionDescriptionHandle(ptr); + } + public static FunctionDescriptionHandle GetFunctionDescription(FunctionHandle functionHandle, out RfcErrorInfo errorInfo) { @@ -105,6 +114,24 @@ public static FunctionHandle CreateFunction(FunctionDescriptionHandle descriptio } + public static RfcRc AddFunctionParameter(FunctionDescriptionHandle descriptionHandle, RfcParameterDescription parameterDescription, out RfcErrorInfo errorInfo) + { + var parameterDesc = new Interopt.RFC_PARAMETER_DESC + { + Name = parameterDescription.Name, + Type = parameterDescription.Type, + Direction = parameterDescription.Direction, + Optional = parameterDescription.Optional ? 'X' : ' ', + Decimals = parameterDescription.Decimals, + NucLength = parameterDescription.NucLength, + UcLength = parameterDescription.UcLength, + TypeDescHandle = parameterDescription.TypeDescriptionHandle + }; + + return Interopt.RfcAddParameter(descriptionHandle.Ptr, ref parameterDesc, out errorInfo); + } + + public static RfcRc GetFunctionParameterCount(FunctionDescriptionHandle descriptionHandle, out int count, out RfcErrorInfo errorInfo) { @@ -171,64 +198,136 @@ public static TableHandle CloneTable(TableHandle tableHandle, out RfcErrorInfo e return ptr == IntPtr.Zero ? null : new TableHandle(ptr, true); } - public static void AllowStartOfPrograms(ConnectionHandle connectionHandle, StartProgramDelegate callback, out - RfcErrorInfo errorInfo) + public static RfcRc RegisterServerFunctionHandler(string sysId, + string functionName, + FunctionDescriptionHandle functionDescription, + RfcFunctionDelegate functionHandler, out RfcErrorInfo errorInfo) { - var descriptionHandle = new FunctionDescriptionHandle(Interopt.RfcCreateFunctionDesc("RFC_START_PROGRAM", out errorInfo)); - if (descriptionHandle.Ptr == IntPtr.Zero) + var registration = new FunctionRegistration(sysId, functionName); + + if (_registeredFunctionNames.Contains(registration)) { - return; + errorInfo = RfcErrorInfo.Ok(); + return RfcRc.RFC_OK; } + _registeredFunctionNames = _registeredFunctionNames.Add(registration); - var paramDesc = new Interopt.RFC_PARAMETER_DESC { Name = "COMMAND", Type = RfcType.CHAR, Direction = RfcDirection.Import, NucLength = 512, UcLength = 1024 }; - var rc = Interopt.RfcAddParameter(descriptionHandle.Ptr, ref paramDesc, out errorInfo); + var rc = Interopt.RfcInstallServerFunction(sysId, functionDescription.Ptr, RFC_Function_Handler, + out errorInfo); if (rc != RfcRc.RFC_OK) { - return; + _registeredFunctionNames = _registeredFunctionNames.Remove(registration); + return rc; } - rc = Interopt.RfcInstallServerFunction(null, descriptionHandle.Ptr, StartProgramHandler, out errorInfo); - if (rc != RfcRc.RFC_OK) + + RegisteredFunctions.AddOrUpdate(functionDescription.Ptr, functionHandler, (c, v) => v); + return rc; + } + + private static readonly object AllowStartOfProgramsLock = new object(); + + [Obsolete("Use method AllowStartOfPrograms of ConnectionBuilder. This method will be removed in next major release.")] + public static void AllowStartOfPrograms(ConnectionHandle connectionHandle, StartProgramDelegate callback, out + RfcErrorInfo errorInfo) + { + lock (AllowStartOfProgramsLock) { - return; + + GetConnectionAttributes(connectionHandle, out var attributes, out errorInfo); + if (errorInfo.Code != RfcRc.RFC_OK) + return; + + + RfcErrorInfo errorInfoLocal = default; + //function runtime is not available at this API level => as workaround create a new runtime -> in FunctionBuilder it is + //only used to wrap this API implementation. + new FunctionBuilder(new RfcRuntime(), "RFC_START_PROGRAM") + .AddChar("COMMAND", RfcDirection.Import, 512) + .Build() + .Match(funcDescriptionHandle => + { + RegisterServerFunctionHandler(attributes.SystemId, + "RFC_START_PROGRAM", + funcDescriptionHandle as FunctionDescriptionHandle, + (_, funcHandle) => + { + var functionHandle = funcHandle as FunctionHandle; + Debug.Assert(functionHandle != null, nameof(functionHandle) + " != null"); + + var commandBuffer = new char[513]; + var rc = Interopt.RfcGetStringByIndex(functionHandle.Ptr, 0, commandBuffer, + (uint)commandBuffer.Length - 1, out var commandLength, out var error); + + if (rc != RfcRc.RFC_OK) + return Unit.Default; + + var command = new string(commandBuffer, 0, (int)commandLength); + error = callback(command); + + if (error.Code == RfcRc.RFC_OK) + return Unit.Default; + + return error; + }, out errorInfoLocal); + }, l => errorInfoLocal = l); + + errorInfo = errorInfoLocal; } - RegisteredCallbacks.AddOrUpdate(connectionHandle.Ptr, callback, (c,v) => v ); - } - private static readonly ConcurrentDictionary RegisteredCallbacks - = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary RegisteredFunctions + = new ConcurrentDictionary(); - private static readonly Interopt.RfcServerFunction StartProgramHandler = RFC_START_PROGRAM_Handler; + private static LanguageExt.HashSet _registeredFunctionNames; - static RfcRc RFC_START_PROGRAM_Handler(IntPtr rfcHandle, IntPtr funcHandle, out RfcErrorInfo errorInfo) + public static bool IsFunctionHandlerRegistered(string sysId, string functionName) { - if (!RegisteredCallbacks.TryGetValue(rfcHandle, out var startProgramDelegate)) + var registration = new FunctionRegistration(sysId, functionName); + + return _registeredFunctionNames.Contains(registration); + } + + private static RfcRc RFC_Function_Handler(IntPtr rfcHandle, IntPtr funcHandle, out RfcErrorInfo errorInfo) + { + var descriptionHandle = Interopt.RfcDescribeFunction(funcHandle, out errorInfo); + if (descriptionHandle == IntPtr.Zero) + return errorInfo.Code; + + + if (!RegisteredFunctions.TryGetValue(descriptionHandle, out var functionDelegate)) { - errorInfo = new RfcErrorInfo(RfcRc.RFC_INVALID_HANDLE, RfcErrorGroup.EXTERNAL_APPLICATION_FAILURE, "", - "no connection registered for this callback", "", "", "", "", "", "", ""); + Interopt.RfcGetFunctionName(descriptionHandle, out var funcName, out _); + if (string.IsNullOrWhiteSpace(funcName)) + funcName = "[unknown function]"; + + errorInfo = new RfcErrorInfo(RfcRc.RFC_INVALID_HANDLE, RfcErrorGroup.EXTERNAL_APPLICATION_FAILURE, "", + $"no function handler registered for function '{funcName}'", "", "", "", "", "", "", ""); return RfcRc.RFC_INVALID_HANDLE; } - - var commandBuffer = new char[513]; - var rc = Interopt.RfcGetStringByIndex(funcHandle, 0, commandBuffer, (uint)commandBuffer.Length - 1, out var commandLength, out errorInfo); + RfcErrorInfo errorInfoLocal = default; + var rc = functionDelegate(new RfcHandle(rfcHandle), new FunctionHandle(funcHandle)).Match( + Right: r => RfcRc.RFC_OK, + l => + { + errorInfoLocal = l; + return l.Code; - if (rc != RfcRc.RFC_OK) - return rc; + }); - var command = new string(commandBuffer, 0, (int)commandLength); - errorInfo = startProgramDelegate(command); + errorInfo = errorInfoLocal; + return rc; - return errorInfo.Code; } - + [Obsolete("Callback handlers are no longer bound to connection. This method will do nothing and will be removed in next major release.")] + // ReSharper disable once UnusedParameter.Global public static void RemoveCallbackHandler(IntPtr connectionHandle) { - RegisteredCallbacks.TryRemove(connectionHandle, out var _); + } public static RfcRc GetTableRowCount(TableHandle table, out int count, out RfcErrorInfo errorInfo) @@ -379,5 +478,37 @@ public static RfcRc GetBytes(IDataContainerHandle containerHandle, string name, return rc; } + + + private struct FunctionRegistration: IEquatable + { + public readonly string SysId; + public readonly string Name; + + public FunctionRegistration(string sysId, string name) + { + SysId = sysId; + Name = name; + } + + public bool Equals(FunctionRegistration other) + { + return SysId == other.SysId && Name == other.Name; + } + + public override bool Equals(object obj) + { + return obj is FunctionRegistration other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (SysId.GetHashCode() * 397) ^ Name.GetHashCode(); + } + } + } } + } \ No newline at end of file diff --git a/src/YaNco.Core/Internal/RfcHandle.cs b/src/YaNco.Core/Internal/RfcHandle.cs new file mode 100644 index 00000000..17e555e1 --- /dev/null +++ b/src/YaNco.Core/Internal/RfcHandle.cs @@ -0,0 +1,14 @@ +using System; + +namespace Dbosoft.YaNco.Internal +{ + public class RfcHandle : IRfcHandle + { + internal IntPtr Ptr { get; set; } + + internal RfcHandle(IntPtr ptr) + { + Ptr = ptr; + } + } +} \ No newline at end of file diff --git a/src/YaNco.Core/RfcRuntime.cs b/src/YaNco.Core/RfcRuntime.cs index 8850476b..ebdae25c 100644 --- a/src/YaNco.Core/RfcRuntime.cs +++ b/src/YaNco.Core/RfcRuntime.cs @@ -32,6 +32,12 @@ public static IFieldMapper CreateDefaultFieldMapper(IEnumerable fromRfcCon public RfcRuntimeOptions Options { get; } + + public bool IsFunctionHandlerRegistered(string sysId, string functionName) + { + return Api.IsFunctionHandlerRegistered(sysId, functionName); + } + private Either ResultOrError(TResult result, RfcErrorInfo errorInfo, bool logAsError = false) { if (result == null || errorInfo.Code != RfcRc.RFC_OK) @@ -90,6 +96,19 @@ public Either OpenConnection(IDictionary CreateFunctionDescription(string functionName) + { + Logger.IfSome(l => l.LogTrace("creating function description without connection", functionName)); + IFunctionDescriptionHandle handle = Api.CreateFunctionDescription(functionName, out var errorInfo); + return ResultOrError(handle, errorInfo); + } + + public Either AddFunctionParameter(IFunctionDescriptionHandle descriptionHandle, RfcParameterDescription parameterDescription) + { + Logger.IfSome(l => l.LogTrace("adding parameter to function description", new { handle = descriptionHandle, parameter = parameterDescription })); + var rc = Api.AddFunctionParameter(descriptionHandle as FunctionDescriptionHandle, parameterDescription, out var errorInfo); + return ResultOrError(descriptionHandle, rc, errorInfo); + } public Either GetFunctionDescription(IConnectionHandle connectionHandle, string functionName) @@ -184,6 +203,32 @@ public Either GetFunctionParameterDescription( } + public Either AddFunctionHandler(string sysid, + string functionName, + IFunction function, Func> handler) + { + return GetFunctionDescription(function.Handle) + .Use(used => used.Bind(d => AddFunctionHandler(sysid, + functionName, d, handler))); + } + + public Either AddFunctionHandler(string sysid, + string functionName, + IFunctionDescriptionHandle descriptionHandle, Func> handler) + { + Api.RegisterServerFunctionHandler(sysid, + functionName, + descriptionHandle as FunctionDescriptionHandle, + (rfcHandle, functionHandle) => + { + var func = new Function(functionHandle, this); + return handler(func); + }, + out var errorInfo); + + return ResultOrError(Unit.Default, errorInfo); + } + public Either Invoke(IConnectionHandle connectionHandle, IFunctionHandle functionHandle) { Logger.IfSome(l => l.LogTrace("Invoking function", new { connectionHandle, functionHandle })); @@ -238,6 +283,7 @@ public Either CloneTable(ITableHandle tableHandle) } + [Obsolete("Use method WithStartProgramCallback of ConnectionBuilder. This method will be removed in next major release.")] public Either AllowStartOfPrograms(IConnectionHandle connectionHandle, StartProgramDelegate callback) { diff --git a/src/YaNco.Primitives/RfcParameterDescription.cs b/src/YaNco.Primitives/RfcParameterDescription.cs new file mode 100644 index 00000000..5857c2aa --- /dev/null +++ b/src/YaNco.Primitives/RfcParameterDescription.cs @@ -0,0 +1,13 @@ +using System; + +namespace Dbosoft.YaNco +{ + public class RfcParameterDescription : RfcParameterInfo + { + public IntPtr TypeDescriptionHandle { get; set; } + + public RfcParameterDescription(string name, RfcType type, RfcDirection direction, uint nucLength, uint ucLength, uint decimals, bool optional, string defaultValue) : base(name, type, direction, nucLength, ucLength, decimals, defaultValue, null, optional) + { + } + } +} \ No newline at end of file diff --git a/src/YaNco.Primitives/RfcParameterInfo.cs b/src/YaNco.Primitives/RfcParameterInfo.cs index 17b9196a..d88b3ff5 100644 --- a/src/YaNco.Primitives/RfcParameterInfo.cs +++ b/src/YaNco.Primitives/RfcParameterInfo.cs @@ -42,5 +42,4 @@ public RfcFieldInfo(string name, RfcType type, uint nucLength, uint ucLength, ui Decimals = decimals; } } - } \ No newline at end of file diff --git a/test/SAPSystemTests/Program.cs b/test/SAPSystemTests/Program.cs index 484e23fb..ea8b0410 100644 --- a/test/SAPSystemTests/Program.cs +++ b/test/SAPSystemTests/Program.cs @@ -18,6 +18,9 @@ namespace SAPSystemTests { class Program { + + private static string CallbackCommand = null; + static async Task Main(string[] args) { var configurationBuilder = @@ -53,35 +56,43 @@ static async Task Main(string[] args) StartProgramDelegate callback = command => { - var programParts = command.Split(' '); - var arguments = command.Replace(programParts[0], ""); - var p = Process.Start(AppDomain.CurrentDomain.BaseDirectory + @"\" + programParts[0] + ".exe", - arguments.TrimStart()); + CallbackCommand = command; return RfcErrorInfo.Ok(); }; + var connectionBuilder = new ConnectionBuilder(settings) .WithStartProgramCallback(callback) .ConfigureRuntime(c => c.WithLogger(new SimpleConsoleLogger())); - using (var context = new RfcContext(connectionBuilder.Build())) + var connectionFunc = connectionBuilder.Build(); + + using (var context = new RfcContext(connectionFunc)) { await context.PingAsync(); await context.GetConnection().Bind(c => c.GetAttributes()) .IfRight(attributes => - { + { Console.WriteLine("connection attributes:"); Console.WriteLine(JsonConvert.SerializeObject(attributes)); }); await RunIntegrationTests(context); + } + + using (var context = new RfcContext(connectionFunc)) + { long totalTest1 = 0; long totalTest2 = 0; + //second call back test (should still be called) + await RunCallbackTest(context); + + for (var run = 0; run < repeats; run++) { Console.WriteLine($"starting Test Run {run + 1} of {repeats}\tTest 01"); @@ -108,6 +119,7 @@ private static async Task RunIntegrationTests(IRfcContext context) await RunIntegrationTest01(context); await RunIntegrationTest02(context); await RunIntegrationTest03(context); + await RunCallbackTest(context); Console.WriteLine("*** END OF Integration Tests ***"); } @@ -414,7 +426,19 @@ from connection in context.GetConnection() }); } + private static async Task RunCallbackTest(IRfcContext context) + { + Console.WriteLine("Integration Tests 04 (callback function test)"); + var commandString = RandomString(40); + await context.CallFunctionOneWay("ZYANCO_IT_2", f => f.SetField("COMMAND", commandString)).ToEither(); + if (CallbackCommand == commandString) + Console.WriteLine("Test succeed"); + else + { + Console.WriteLine("Callback Test failed, command received: " + CallbackCommand); + } + } private static async Task RunPerformanceTest01(IRfcContext context, int rows = 0) { diff --git a/test/SAPSystemTests/Properties/launchSettings.json b/test/SAPSystemTests/Properties/launchSettings.json index 076b233c..ad122edf 100644 --- a/test/SAPSystemTests/Properties/launchSettings.json +++ b/test/SAPSystemTests/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "SAPSystemTests": { "commandName": "Project", - "commandLineArgs": "/tests:repeats=100000 /tests:rows=0", + "commandLineArgs": "/tests:repeats=10 /tests:rows=100", "nativeDebugging": true } } diff --git a/test/SAPSystemTests/SimpleConsoleLogger.cs b/test/SAPSystemTests/SimpleConsoleLogger.cs index 8aa60ac8..6953b597 100644 --- a/test/SAPSystemTests/SimpleConsoleLogger.cs +++ b/test/SAPSystemTests/SimpleConsoleLogger.cs @@ -20,7 +20,7 @@ public void LogException(Exception exception) public void LogTrace(string message, object data) { - //Console.WriteLine($"TRACE\t{message}{ObjectToString(data)}"); + // Console.WriteLine($"TRACE\t{message}{ObjectToString(data)}"); } public void LogError(string message, object data) @@ -30,6 +30,9 @@ public void LogError(string message, object data) public void LogDebug(string message, object data) { + if (data is RfcErrorInfo { Key: "RFC_TABLE_MOVE_EOF" }) + return; + Console.WriteLine($"DEBUG\t{message}{ObjectToString(data)}"); } diff --git a/test/YaNco.Core.Tests/ConnectionTests.cs b/test/YaNco.Core.Tests/ConnectionTests.cs index dca3ae6a..a376759f 100644 --- a/test/YaNco.Core.Tests/ConnectionTests.cs +++ b/test/YaNco.Core.Tests/ConnectionTests.cs @@ -136,24 +136,5 @@ from __ in c.Cancel() rfcRuntimeMock.VerifyAll(); } - - [Fact] - public async Task AllowStartOfPrograms_is_cancelled() - { - var rfcRuntimeMock = new Mock() - .SetupOpenConnection(out var connHandle); - - StartProgramDelegate callback = (c) => RfcErrorInfo.Ok(); - - rfcRuntimeMock.Setup(r => r - .AllowStartOfPrograms(connHandle.Object,callback)) - .Returns(Unit.Default); - - var conn = await rfcRuntimeMock.CreateConnection() - .Map(c => c.AllowStartOfPrograms(callback)); - - rfcRuntimeMock.VerifyAll(); - - } } } \ No newline at end of file diff --git a/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj b/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj index d57fa147..5956d808 100644 --- a/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj +++ b/test/YaNco.Core.Tests/YaNco.Core.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net471;net5.0 + netcoreapp3.1;net471;net5.0;net6.0 false