Merge #568/#569/#580/#585: shared AB status map + orphan release, confinement guard tokeniser, adapter + DefaultTransport tests
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -3,9 +3,8 @@ using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// Maps transport status codes + PCCC STS/EXT_STS bytes to OPC UA StatusCodes. Mirrors the
|
||||
/// AbCip mapper — PCCC errors roughly align with CIP general-status in shape but with a
|
||||
/// different byte vocabulary (PCCC STS nibble-low + EXT_STS on code 0x0F).
|
||||
/// Maps transport status codes to OPC UA StatusCodes via the table shared with AbCip
|
||||
/// (<see cref="AbOpcUaStatusMap"/>).
|
||||
/// </summary>
|
||||
public static class AbLegacyStatusMapper
|
||||
{
|
||||
@@ -32,45 +31,5 @@ public static class AbLegacyStatusMapper
|
||||
/// <summary>Map a transport-neutral <see cref="AbStatus"/> to an OPC UA StatusCode.</summary>
|
||||
/// <param name="status">The transport status.</param>
|
||||
/// <returns>The corresponding OPC UA status code.</returns>
|
||||
public static uint MapStatus(AbStatus status) => status switch
|
||||
{
|
||||
AbStatus.Ok => Good,
|
||||
AbStatus.Pending => GoodMoreData,
|
||||
AbStatus.Timeout => BadTimeout,
|
||||
AbStatus.NotFound => BadNodeIdUnknown,
|
||||
AbStatus.NotAllowed => BadNotWritable,
|
||||
AbStatus.OutOfBounds or AbStatus.TooLarge or AbStatus.TooSmall => BadOutOfRange,
|
||||
AbStatus.Unsupported => BadNotSupported,
|
||||
AbStatus.TypeMismatch => BadTypeMismatch,
|
||||
AbStatus.DeviceStateConflict => BadDeviceFailure,
|
||||
_ => BadCommunicationError, // BadConnection, BadReply, Partial, Abort
|
||||
};
|
||||
|
||||
/// <summary>Map an exception thrown by a transport to an OPC UA StatusCode.</summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <returns>The mapped status for an <see cref="AbTransportException"/>, else BadCommunicationError.</returns>
|
||||
public static uint MapException(Exception ex) =>
|
||||
ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError;
|
||||
|
||||
/// <summary>
|
||||
/// Map a PCCC STS (status) byte. Common codes per AB PCCC reference:
|
||||
/// 0x00 = success, 0x10 = illegal command, 0x20 = bad address, 0x30 = protected,
|
||||
/// 0x40 = programmer busy, 0x50 = file locked, 0xF0 = extended status follows.
|
||||
/// the transports surface only an <see cref="AbStatus"/> rather than exposing
|
||||
/// the raw STS byte, so this method is not wired into the current read/write path.
|
||||
/// It is retained as the reference mapping for future PCCC-STS inspection.
|
||||
/// </summary>
|
||||
/// <param name="sts">The PCCC STS byte.</param>
|
||||
/// <returns>The corresponding OPC UA status code.</returns>
|
||||
public static uint MapPcccStatus(byte sts) => sts switch
|
||||
{
|
||||
0x00 => Good,
|
||||
0x10 => BadNotSupported,
|
||||
0x20 => BadNodeIdUnknown,
|
||||
0x30 => BadNotWritable,
|
||||
0x40 => BadDeviceFailure,
|
||||
0x50 => BadDeviceFailure,
|
||||
0xF0 => BadInternalError, // extended status not inspected at this layer
|
||||
_ => BadCommunicationError,
|
||||
};
|
||||
public static uint MapStatus(AbStatus status) => AbOpcUaStatusMap.MapStatus(status);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using libplctag;
|
||||
using libplctag.NativeImport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the native tag a FAILED libplctag initialisation leaves behind (T21 press pass 1 crash).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>libplctag.NET 1.5.2's <c>NativeTagWrapper.InitializeAsync</c> calls <c>plc_tag_create_ex</c>, which returns a
|
||||
/// native handle with the wrapper's <c>callback_func_ex</c> delegate registered, and only then waits for the
|
||||
/// <c>Created</c> event. When that event carries an error (a tag the controller does not have, a refused path, ...)
|
||||
/// it throws WITHOUT setting <c>_isInitialized</c>, and <c>Dispose()</c> unregisters and destroys only
|
||||
/// <c>if (_isInitialized)</c>. So the native tag outlives its wrapper with the callback still registered; once the
|
||||
/// wrapper is collected the delegate's thunk is freed, and the next event libplctag raises on that tag — at the
|
||||
/// latest the <c>DESTROYED</c> event from <c>plc_tag_shutdown</c>, which the native library runs from
|
||||
/// <c>atexit</c> — kills the process with "A callback was made on a garbage collected delegate of type
|
||||
/// 'libplctag.NativeImport!libplctag.NativeImport.plctag+callback_func_ex::Invoke'". A timed-out initialisation
|
||||
/// unregisters the callback but never destroys the handle either, so that native tag keeps reconnecting in the
|
||||
/// background for the life of the process.</para>
|
||||
/// <para>The wrapper exposes no handle, so it is read by reflection from the two private fields of the pinned
|
||||
/// 1.5.2 wrapper (<c>Tag._tag</c>, <c>NativeTagWrapper.nativeTagHandle</c>). If a future wrapper renames them the
|
||||
/// release cannot run; the tag is then rooted for the life of the process instead, which keeps its delegate
|
||||
/// alive. That is no crash but an UNBOUNDED leak — one native tag (holding its libplctag session open) per failed
|
||||
/// create, and the data path re-creates a NotFound tag on every read — so <c>LibplctagLegacyOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagOrphanRelease
|
||||
{
|
||||
private const BindingFlags Private = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
private static readonly FieldInfo? WrapperField = typeof(Tag).GetField("_tag", Private);
|
||||
// Final review M2: the field must also still be an int. A wrapper that retyped it (IntPtr, long) would make
|
||||
// the cast in HandleOf throw; treating it as unreadable instead degrades to the rooting fallback ("keep
|
||||
// alive"), which the pin test reports.
|
||||
private static readonly FieldInfo? HandleField =
|
||||
WrapperField?.FieldType.GetField("nativeTagHandle", Private) is { } f && f.FieldType == typeof(int) ? f : null;
|
||||
|
||||
/// <summary>Tags whose handle could not be read: kept reachable so their callback delegate is never collected.</summary>
|
||||
private static readonly ConcurrentBag<Tag> Rooted = [];
|
||||
|
||||
private static int _released;
|
||||
|
||||
/// <summary>True when the pinned wrapper's private fields were found, so a failed tag can be destroyed.</summary>
|
||||
internal static bool CanReadHandles => HandleField is not null && !ForceRootingForTest;
|
||||
|
||||
/// <summary>Test seam (review r1 M1): take the rooting fallback as if the wrapper's fields had been renamed.</summary>
|
||||
internal static bool ForceRootingForTest { get; set; }
|
||||
|
||||
/// <summary>Orphaned native tags destroyed so far (process-wide; diagnostics and tests).</summary>
|
||||
internal static int Released => Volatile.Read(ref _released);
|
||||
|
||||
/// <summary>Orphaned tags rooted for the process lifetime because their handle could not be read.</summary>
|
||||
internal static int RootedCount => Rooted.Count;
|
||||
|
||||
/// <summary>The native handle the wrapper holds (0 when none was ever created, or unreadable). Never throws:
|
||||
/// callers run it inside a catch block, where an exception would replace the failure being handled.</summary>
|
||||
internal static int HandleOf(Tag tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
return HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper
|
||||
&& HandleField.GetValue(wrapper) is int handle ? handle : 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clears the wrapper's handle after a destroy, so a repeat release is a no-op (review r1 M2).</summary>
|
||||
private static void ClearHandle(Tag tag)
|
||||
{
|
||||
if (HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper) HandleField.SetValue(wrapper, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation that may initialise <paramref name="tag"/> (InitializeAsync, or a first
|
||||
/// Read/Write, which initialise on demand). If it fails and leaves the tag uninitialised, the native tag it
|
||||
/// created is unregistered and destroyed before the failure propagates.
|
||||
/// </summary>
|
||||
internal static async Task RunAsync(Tag tag, Func<Task> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
await op().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseIfOrphaned(tag);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="RunAsync(Tag, Func{Task})"/>
|
||||
internal static async Task<T> RunAsync<T>(Tag tag, Func<Task<T>> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await op().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseIfOrphaned(tag);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the native tag behind an UNINITIALISED wrapper, unregistering its callback first. No-op for an
|
||||
/// initialised tag (its own <c>Dispose</c> handles it) or one that never got a handle. Never throws.
|
||||
/// </summary>
|
||||
/// <returns>The status the native tag held just before it was destroyed (what <c>GetStatus()</c> would have
|
||||
/// reported), or null when nothing was destroyed.</returns>
|
||||
internal static Status? ReleaseIfOrphaned(Tag tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tag.IsInitialized) return null;
|
||||
if (!CanReadHandles)
|
||||
{
|
||||
Rooted.Add(tag);
|
||||
return null;
|
||||
}
|
||||
var handle = HandleOf(tag);
|
||||
if (handle <= 0) return null; // plc_tag_create_ex refused synchronously: nothing native exists
|
||||
var status = (Status)plctag.plc_tag_status(handle);
|
||||
plctag.plc_tag_unregister_callback(handle);
|
||||
plctag.plc_tag_destroy(handle);
|
||||
ClearHandle(tag);
|
||||
Interlocked.Increment(ref _released);
|
||||
return status;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// A release must never mask the initialisation failure it follows. Root the tag so that, whatever
|
||||
// state the native side is in, its callback delegate stays alive.
|
||||
Rooted.Add(tag);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@
|
||||
Decision #41 — AbLegacy split from AbCip since PCCC addressing (file-based N7:0) and
|
||||
Logix addressing (symbolic Motor1.Speed) pull the abstraction in incompatible directions. -->
|
||||
<PackageReference Include="libplctag"/>
|
||||
<!-- #580: orphan-release helper shared with the sibling AB driver (source-linked; keeps a Libplctag*.cs name). -->
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared\LibplctagOrphanRelease.cs" Link="LibplctagOrphanRelease.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user