Skip to content

Commit

Permalink
chore(v0.2): Improve RPC handling
Browse files Browse the repository at this point in the history
Add IRpcMethodMarker to flag interfaces to be used for RPC,
which makes it easier to extend with new services in future.
  • Loading branch information
densogiaichned committed Sep 15, 2024
1 parent 46b56c3 commit 1e73252
Show file tree
Hide file tree
Showing 14 changed files with 152 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .Zeugwerk/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"name": "TcHaxx.Snappy",
"plcs": [
{
"version": "0.1.0.0",
"version": "0.2.0.0",
"name": "snappy",
"type": "Library",
"packages": [
Expand Down
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,5 @@ csharp_style_prefer_extended_property_pattern = true:suggestion

# SonarAnalyzer
dotnet_diagnostic.S1134.severity = suggestion
dotnet_diagnostic.S1135.severity = suggestion
dotnet_diagnostic.S1135.severity = suggestion
dotnet_diagnostic.S6602.severity = suggestion
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ Activate Configuration | Execute | `cmd /c start TcHaxx.Snappy.CLI verify -d \"%
## Source control: Received and Verified files
When dealing with source control, consider the following guidelines for handling **Received** and **Verified** files:

1. **Exclusion**:
1. **Exclude files**:
- Exclude all files with the pattern `*.received.*` from source control.
- To achieve this, add the following line to your `.gitignore` file:
```
*.received.*
```
2. **Commitment**:
- On the other hand, **commit** all files with the pattern `*.verified.*` to source control.
2. **Commit files**:
- **Commit** all files with the pattern `*.verified.*` to source control.
> See [Verify/README](https://github.com/VerifyTests/Verify?tab=readme-ov-file#source-control-received-and-verified-files)
Expand Down Expand Up @@ -169,4 +169,5 @@ Option | Required | Default | Description
* [TcUnit](https://github.com/tcunit/TcUnit) - A unit testing framework for Beckhoff's TwinCAT 3
* [CommandLineParser](https://github.com/commandlineparser/commandline) - A command line parsing library for .NET applications.
* [Verify](https://github.com/VerifyTests/Verify) - A library used for snapshot testing.
* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications.
* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications.
* [TF6000_ADS_DOTNET_V5_Samples](https://github.com/Beckhoff/TF6000_ADS_DOTNET_V5_Samples) - Sample code for the Version 6.X series of the TwinCAT ADS .NET Packages
2 changes: 1 addition & 1 deletion src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Description>A Snapshot Testing framework for TwinCAT 3</Description>
<Copyright>Copyright (c) 2024 densogiaichned</Copyright>
<Title>TwinCAT Snapshot Testing framework</Title>
<Version>0.1.0</Version>
<Version>0.2.0</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<PackAsTool>True</PackAsTool>
Expand Down
10 changes: 4 additions & 6 deletions src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using TcHaxx.Snappy.Common.Verify;

namespace TcHaxx.Snappy.Common.RPC;
namespace TcHaxx.Snappy.Common.RPC;

/// <summary>
/// Describes an descriptor for RPC methods.
Expand All @@ -14,8 +12,8 @@ public interface IRpcMethodDescriptor
public IEnumerable<RpcMethodDescription> GetRpcMethodDescription();

/// <summary>
/// Registers a <see cref="IVerifyMethod"/> implementation with <see cref="IRpcMethodDescriptor"/>.
/// Registers a <see cref="IRpcMethodMarker"/> implementation with <see cref="IRpcMethodDescriptor"/>.
/// </summary>
/// <param name="rpcVerifyMethod"></param>
public void Register(IVerifyMethod rpcVerifyMethod);
/// <param name="rpcMethod"></param>
public void Register(IRpcMethodMarker rpcMethod);
}
8 changes: 8 additions & 0 deletions src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TcHaxx.Snappy.Common.RPC;

/// <summary>
/// Empty inteface to mark RPC methods.
/// </summary>
public interface IRpcMethodMarker
{
}
6 changes: 4 additions & 2 deletions src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Reflection;
using TcHaxx.Snappy.Common.Verify;

namespace TcHaxx.Snappy.Common.RPC;

public record RpcMethodDescription(MethodInfo Method, IEnumerable<ParameterInfo> Parameters, ParameterInfo ReturnValue, IVerifyMethod RpcInvocableMethod);
public record RpcMethodDescription(MethodInfo Method, IEnumerable<ParameterInfo> Parameters, ParameterInfo ReturnValue, IRpcMethodMarker RpcInvocableMethod, string? Alias)
{
public string InstanceName => RpcInvocableMethod.GetType().FullName!;
}
59 changes: 49 additions & 10 deletions src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using TcHaxx.Snappy.Common.Verify;
using System.Data;
using System.Reflection;
using TcHaxx.Snappy.Common.RPC.Attributes;

namespace TcHaxx.Snappy.Common.RPC;

public class RpcMethodDescriptor : IRpcMethodDescriptor
{
private readonly Queue<IVerifyMethod> _verifyMethods = new();
private readonly Queue<IRpcMethodMarker> _verifyMethods = new();

public RpcMethodDescriptor()
{
Expand All @@ -18,20 +20,57 @@ public IEnumerable<RpcMethodDescription> GetRpcMethodDescription()
}
}

public void Register(IVerifyMethod rpcVerifyMethod)
public void Register(IRpcMethodMarker rpcMethod)
{
_verifyMethods.Enqueue(rpcVerifyMethod);
_verifyMethods.Enqueue(rpcMethod);
}

private RpcMethodDescription Transform(IVerifyMethod rpcVerifyMethod)
private static RpcMethodDescription Transform(IRpcMethodMarker rpcMethod)
{
var method = rpcVerifyMethod.GetType().GetMethod(nameof(IVerifyMethod.Verify)) ??
throw new RpcMethodTransformException($"Method \"{nameof(IVerifyMethod.Verify)}\" not found.");
var method = GetMethodInfo(rpcMethod);

var parameters = method.GetParameters() ??
throw new RpcMethodTransformException($"Expected method \"{nameof(IVerifyMethod.Verify)}\" to have parameters");
throw new RpcMethodTransformException($"Expected method \"{method.Name}\" to have parameters");

var retVal = method.ReturnParameter;

return new RpcMethodDescription(method, parameters, retVal, rpcVerifyMethod);
var alias = GetAliasAttriubte(rpcMethod);
return new RpcMethodDescription(method, parameters, retVal, rpcMethod, alias);
}

private static MethodInfo GetMethodInfo(IRpcMethodMarker rpcMethod)
{
var typeName = rpcMethod.GetType().Name;

var methodInfos = GetMethodInfos(rpcMethod);

if (methodInfos is null || methodInfos.Length == 0)
{
throw new RpcMethodTransformException($"No RPC method found in type \"{typeName}\".");
}

if (methodInfos.Length > 1)

Check failure on line 51 in src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs

View workflow job for this annotation

GitHub Actions / CI - TcHaxx.Snappy.CLI

'if' statement can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046)

Check failure on line 51 in src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs

View workflow job for this annotation

GitHub Actions / CI - TcHaxx.Snappy.CLI

'if' statement can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046)

Check failure on line 51 in src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs

View workflow job for this annotation

GitHub Actions / CI - TcHaxx.Snappy.CLI

'if' statement can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046)

Check failure on line 51 in src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs

View workflow job for this annotation

GitHub Actions / CI - TcHaxx.Snappy.CLI

'if' statement can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046)
{
throw new RpcMethodTransformException($"Only one RPC method supported per type ({typeName}).");
}

return methodInfos[0];
}

private static string? GetAliasAttriubte(IRpcMethodMarker rpcMethod)
{
var aliasAttribute = rpcMethod.GetType()
.GetMethods()
.Select(x => x.GetCustomAttribute<AliasAttribute>())
.FirstOrDefault(x => x is not null);
return aliasAttribute?.AliasName;
}

private static MethodInfo[]? GetMethodInfos(IRpcMethodMarker rpcMethod)
{
var interfaces = rpcMethod.GetType().GetInterfaces();
var rpcInterfaces = interfaces
.FirstOrDefault(i => typeof(IRpcMethodMarker).IsAssignableFrom(i) && i.GetInterfaces().Length == 1 && i.GetInterfaces().Contains(typeof(IRpcMethodMarker)));

return rpcInterfaces?.GetMethods();
}
}
5 changes: 3 additions & 2 deletions src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using TcHaxx.Snappy.Common.RPC.Attributes;
using TcHaxx.Snappy.Common.RPC;
using TcHaxx.Snappy.Common.RPC.Attributes;

namespace TcHaxx.Snappy.Common.Verify;

public interface IVerifyMethod
public interface IVerifyMethod : IRpcMethodMarker
{
public VerificationResult Verify(
[String(Constants.DEFAULT_TEST_NAMES_LENGTH)] string testSuiteName,
Expand Down
33 changes: 32 additions & 1 deletion src/TcHaxx.Snappy.TcADS/SymbolicServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ protected override void OnConnected()
}

protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, IRpcMethod method, object[] values, out object? returnValue)
{
returnValue = null;
var retVal = OnRpcInvokeProxy(structInstance, method, values, out returnValue);

if (retVal != AdsErrorCode.NoError)
{
// Note:
// Due to a bug (?) in ADS.Net, all parameter values and the returnValue must not be NULL!
// Otherwise, it will throw an exception during marshalling, hence the client will only receive error 0x1
returnValue = 0;
PresetParameterValues(ref values);
}

return retVal;
}

private static void PresetParameterValues(ref object[] parameterValues)
{
for (var i = 0; i < parameterValues.Length; i++)
{
if (parameterValues[i] is not null)
{
continue;
}

parameterValues[i] = 0;
}
}

private AdsErrorCode OnRpcInvokeProxy(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue)
{
var iDataType = structInstance.DataType;
if (iDataType is null)
Expand All @@ -68,7 +98,8 @@ protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, I
_logger?.LogError("{OnRpcInvoke}: {IDataType} is null", nameof(OnRpcInvoke), nameof(IDataType));
return AdsErrorCode.DeviceInvalidContext;
}

_logger?.LogInformation("{OnRpcInvoke}: Invoking method {IRpcMethod} of {IDataTypeFullName}", nameof(OnRpcInvoke), method, iDataType.FullName);
return _symbolFactory.InvokeRpcMethod(iDataType, values, out returnValue);
return _symbolFactory.InvokeRpcMethod(structInstance, method, parameterValues, out returnValue);
}
}
2 changes: 1 addition & 1 deletion src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ namespace TcHaxx.Snappy.TcADS.Symbols;
internal interface ISymbolFactory
{
void AddSymbols(ServerSymbolFactory? serverSymbolFactory);
AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue);
AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue);
}
37 changes: 23 additions & 14 deletions src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.Extensions.Logging;
using TcHaxx.Snappy.Common.RPC;
using TcHaxx.Snappy.Common.RPC.Attributes;
using TcHaxx.Snappy.Common.Verify;
using TwinCAT.Ads;
using TwinCAT.Ads.Server.TypeSystem;
using TwinCAT.Ads.TypeSystem;
Expand All @@ -17,7 +16,7 @@ internal class SymbolFactory : ISymbolFactory
private readonly IRpcMethodDescriptor _rpcMethodDescriptor;
private readonly ILogger? _logger;

private readonly Dictionary<IDataType, IVerifyMethod> _mappedStructTypeToRpcMethod = [];
private readonly Dictionary<IDataType, IRpcMethodMarker> _mappedStructTypeToRpcMethod = [];

internal SymbolFactory(IRpcMethodDescriptor rpcMethodDescriptor, ILogger? logger)
{
Expand All @@ -42,47 +41,57 @@ public void AddSymbols(ServerSymbolFactory? serverSymbolFactory)
var retValKvp = GetMethodReturnValue(rpcMethodDescription.ReturnValue);
AddToServerSymbolFactory(serverSymbolFactory, [retValKvp]);

var fullName = rpcMethodDescription.Method.ReflectedType?.FullName ?? rpcMethodDescription.Method.Name;
var fullName = rpcMethodDescription.InstanceName;
var dataArea = new DataArea($"DATA::{fullName}", idxGrp, idxOffset++, 0x10000);

_ = serverSymbolFactory.AddDataArea(dataArea);

var rpc = BuildRpcMethod(rpcMethodDescription.Method, paramsKvp, retValKvp);
var rpc = BuildRpcMethod(rpcMethodDescription, paramsKvp, retValKvp);

var dtStructRpc = new StructType($"STRUCT::{fullName}");
_ = dtStructRpc.AddMethod(rpc);
_ = serverSymbolFactory.AddType(dtStructRpc);
_ = serverSymbolFactory.AddSymbol(fullName, dtStructRpc, dataArea);

_logger?.LogInformation("Adding RPC method {MappedTypeFullName}#{MethodName}", fullName, rpc.Name);
_mappedStructTypeToRpcMethod.Add(dtStructRpc, rpcMethodDescription.RpcInvocableMethod);
}
}

public AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue)
public AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue)
{
returnValue = null;
if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var value))
returnValue = Activator.CreateInstance(Type.GetType(method.ReturnType, false) ?? typeof(int));
var mappedType = structInstance.DataType!;

if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var rpcMethodType))
{
_logger?.LogError("No matching type found ({MappedTypeFullName})", mappedType.FullName);
return AdsErrorCode.DeviceServiceNotSupported;
}

var rpcMethodToInvoke = value;
var rpcMethodToInvoke = rpcMethodType.GetType()
.GetMethods()
.FirstOrDefault(x => string.Equals(x.Name, method.Name, StringComparison.OrdinalIgnoreCase));
if (rpcMethodToInvoke is null)
{
_logger?.LogError("Method \"{MethodName}\" not found in type \"{MappedTypeFullName}\"", method.Name, mappedType.FullName);
return AdsErrorCode.DeviceServiceNotSupported;
}

if (values.Length != rpcMethodToInvoke.GetType().GetMethod(nameof(IVerifyMethod.Verify))!.GetParameters().Length)
if (parameterValues.Length != rpcMethodToInvoke.GetParameters().Length)
{
_logger?.LogError("Different method parameter length: {ParameterValuesLength} != {RpcMethodParameterLength}", parameterValues.Length, rpcMethodToInvoke.GetParameters().Length);
return AdsErrorCode.DeviceInvalidParam;
}

// FIXME: This is ugly...
// Find a better solution, e.g. reflection.
returnValue = rpcMethodToInvoke.Verify((string)values[0], (string)values[1], (string)values[2]);
returnValue = rpcMethodToInvoke.Invoke(rpcMethodType, parameterValues);

return AdsErrorCode.NoError;
}

private static RpcMethod BuildRpcMethod(MethodInfo methodInfo, IEnumerable<KeyValuePair<ParameterInfo, IDataType>> paramsKvp, KeyValuePair<ParameterInfo, IDataType> retValKvp)
private static RpcMethod BuildRpcMethod(RpcMethodDescription rpcMethodDescription, IEnumerable<KeyValuePair<ParameterInfo, IDataType>> paramsKvp, KeyValuePair<ParameterInfo, IDataType> retValKvp)
{
var nameOrAlias = methodInfo.GetCustomAttribute<AliasAttribute>()?.AliasName ?? methodInfo.Name;
var nameOrAlias = rpcMethodDescription.Alias ?? rpcMethodDescription.Method.Name;
var rpc = new RpcMethod(nameOrAlias);
foreach (var (k, v) in paramsKvp)
{
Expand Down
14 changes: 14 additions & 0 deletions src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.13">
<GVL Name="Global_Version" Id="{e8c1b537-7c98-47f8-8420-3bfa56237b19}">
<Declaration><![CDATA[{attribute 'TcGenerated'}
{attribute 'no-analysis'}
{attribute 'linkalways'}
// This function has been automatically generated from the project information.
VAR_GLOBAL CONSTANT
{attribute 'const_non_replaced'}
stLibVersion_snappy : ST_LibVersion := (iMajor := 0, iMinor := 2, iBuild := 0, iRevision := 0, nFlags := 0, sVersion := '0.2.0.0');
END_VAR
]]></Declaration>
</GVL>
</TcPlcObject>
6 changes: 5 additions & 1 deletion src/TcHaxx.Snappy/snappy/snappy.plcproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<Company>TcHaxx</Company>
<Released>false</Released>
<Title>snappy</Title>
<ProjectVersion>0.1.0.0</ProjectVersion>
<ProjectVersion>0.2.0.0</ProjectVersion>
<LibraryCategories>
<LibraryCategory xmlns="">
<Id>{9c7e50a7-dead-beef-897b-4cdbc169222d}</Id>
Expand Down Expand Up @@ -142,6 +142,9 @@
<Compile Include="TESTs\snappy\FB_Snappy_Tests.TcPOU">
<SubType>Code</SubType>
</Compile>
<Compile Include="Version\Global_Version.TcGVL">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="DUTs" />
Expand All @@ -156,6 +159,7 @@
<Folder Include="TESTs" />
<Folder Include="TESTs\snappy" />
<Folder Include="TESTs\Serializer" />
<Folder Include="Version" />
<Folder Include="VISUs" />
<Folder Include="POUs" />
</ItemGroup>
Expand Down

0 comments on commit 1e73252

Please sign in to comment.