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:
+6
-2
@@ -3,7 +3,7 @@ using System.Reflection;
|
||||
using libplctag;
|
||||
using libplctag.NativeImport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the native tag a FAILED libplctag initialisation leaves behind (T21 press pass 1 crash).
|
||||
@@ -24,7 +24,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// 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>LibplctagOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// create, and the data path re-creates a NotFound tag on every read — so <c>LibplctagOrphanReleaseTests</c> / <c>LibplctagLegacyOrphanReleaseTests</c> fail, so the bump is noticed.</para>
|
||||
/// <para>SHARED SOURCE (#580): this directory has no project. <c>Driver.AbCip</c> and <c>Driver.AbLegacy</c> each
|
||||
/// compile this file in (<c><Compile Include></c>), so each assembly has its own internal copy and its own
|
||||
/// counters. It must keep a <c>Libplctag*.cs</c> name — <c>LibplctagConfinementGuardTests</c> allows libplctag
|
||||
/// references only in such files.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagOrphanRelease
|
||||
{
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// The single <see cref="AbStatus"/> to OPC UA StatusCode table shared by <c>AbCipStatusMapper</c> and
|
||||
/// <c>AbLegacyStatusMapper</c> (#580) — the two drivers used to carry identical copies.
|
||||
/// </summary>
|
||||
public static class AbOpcUaStatusMap
|
||||
{
|
||||
/// <summary>OPC UA <c>Good</c>.</summary>
|
||||
public const uint Good = 0u;
|
||||
/// <summary>OPC UA <c>GoodMoreData</c>.</summary>
|
||||
public const uint GoodMoreData = 0x00A60000u;
|
||||
/// <summary>OPC UA <c>BadNodeIdUnknown</c>.</summary>
|
||||
public const uint BadNodeIdUnknown = 0x80340000u;
|
||||
/// <summary>OPC UA <c>BadNotWritable</c>.</summary>
|
||||
public const uint BadNotWritable = 0x803B0000u;
|
||||
/// <summary>OPC UA <c>BadOutOfRange</c>.</summary>
|
||||
public const uint BadOutOfRange = 0x803C0000u;
|
||||
/// <summary>OPC UA <c>BadNotSupported</c>.</summary>
|
||||
public const uint BadNotSupported = 0x803D0000u;
|
||||
/// <summary>OPC UA <c>BadDeviceFailure</c>.</summary>
|
||||
public const uint BadDeviceFailure = 0x808B0000u;
|
||||
/// <summary>OPC UA <c>BadCommunicationError</c>.</summary>
|
||||
public const uint BadCommunicationError = 0x80050000u;
|
||||
/// <summary>OPC UA <c>BadTimeout</c>.</summary>
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
/// <summary>OPC UA <c>BadTypeMismatch</c>.</summary>
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
|
||||
/// <summary>Maps 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>
|
||||
/// Status for an exception thrown while creating / reading a tag: a transport exception maps through its
|
||||
/// <see cref="AbStatus"/>; anything else is <see cref="BadCommunicationError"/>.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <returns>The OPC UA status code.</returns>
|
||||
public static uint MapException(Exception ex) =>
|
||||
ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError;
|
||||
}
|
||||
@@ -3,35 +3,12 @@ using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// Maps libplctag / CIP General Status codes to OPC UA StatusCodes. Mirrors the shape of
|
||||
/// <c>ModbusDriver.MapModbusExceptionToStatus</c> so Admin UI status displays stay
|
||||
/// uniform across drivers.
|
||||
/// Maps transport <see cref="AbStatus"/> values to OPC UA StatusCodes through the table shared with AbLegacy
|
||||
/// (<see cref="AbOpcUaStatusMap"/>): timeout, not-found, not-allowed, out-of-bounds, unsupported, type-mismatch
|
||||
/// and device-state-conflict get their specific codes; the remaining transport errors fold into
|
||||
/// <c>BadCommunicationError</c>. (The raw CIP general-status mapper that used to live here had no callers — the
|
||||
/// transports surface an <see cref="AbStatus"/>, not the status byte — and was removed, #580.)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Coverage: the CIP general-status values an AB PLC actually returns during normal
|
||||
/// driver operation. Full CIP Volume 1 Appendix B lists 50+ codes; the ones here are the
|
||||
/// ones that move the driver's status needle:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>0x00 success — OPC UA <c>Good (0)</c>.</item>
|
||||
/// <item>0x04 path segment error / 0x05 path destination unknown — <c>BadNodeIdUnknown</c>
|
||||
/// (tag doesn't exist).</item>
|
||||
/// <item>0x06 partial data transfer — <c>GoodMoreData</c> (fragmented read underway).</item>
|
||||
/// <item>0x08 service not supported — <c>BadNotSupported</c> (e.g. write on a safety
|
||||
/// partition tag from a non-safety task).</item>
|
||||
/// <item>0x0A / 0x13 attribute-list error / insufficient data — <c>BadOutOfRange</c>
|
||||
/// (type mismatch or truncated buffer).</item>
|
||||
/// <item>0x0B already in requested mode — benign, treated as <c>Good</c>.</item>
|
||||
/// <item>0x0E attribute not settable — <c>BadNotWritable</c>.</item>
|
||||
/// <item>0x10 device state conflict — <c>BadDeviceFailure</c> (program-mode protected
|
||||
/// writes during download / test-mode transitions).</item>
|
||||
/// <item>0x16 object does not exist — <c>BadNodeIdUnknown</c>.</item>
|
||||
/// <item>0x1E embedded service error — unwrap to the extended status when possible.</item>
|
||||
/// <item>Transport <see cref="AbStatus"/> errors — mapped per-member by
|
||||
/// <see cref="MapStatus(AbStatus)"/>: timeout, not-found, not-allowed, and
|
||||
/// out-of-bounds get their specific OPC UA codes; the remaining transport errors
|
||||
/// fold into <c>BadCommunicationError</c>.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static class AbCipStatusMapper
|
||||
{
|
||||
public const uint Good = 0u;
|
||||
@@ -46,23 +23,6 @@ public static class AbCipStatusMapper
|
||||
public const uint BadTimeout = 0x800A0000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
|
||||
/// <summary>Map a CIP general-status byte to an OPC UA StatusCode.</summary>
|
||||
/// <param name="status">The CIP general-status byte value.</param>
|
||||
/// <returns>The mapped OPC UA StatusCode value.</returns>
|
||||
public static uint MapCipGeneralStatus(byte status) => status switch
|
||||
{
|
||||
0x00 => Good,
|
||||
0x04 or 0x05 => BadNodeIdUnknown,
|
||||
0x06 => GoodMoreData,
|
||||
0x08 => BadNotSupported,
|
||||
0x0A or 0x13 => BadOutOfRange,
|
||||
0x0B => Good,
|
||||
0x0E => BadNotWritable,
|
||||
0x10 => BadDeviceFailure,
|
||||
0x16 => BadNodeIdUnknown,
|
||||
_ => BadInternalError,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Map a transport status code to an OPC UA StatusCode. The integer is
|
||||
/// <see cref="IAbCipTagRuntime.GetStatus"/>, i.e. the underlying value of <see cref="AbStatus"/>.
|
||||
@@ -78,19 +38,7 @@ public static class AbCipStatusMapper
|
||||
/// </summary>
|
||||
/// <param name="status">The transport status.</param>
|
||||
/// <returns>The mapped OPC UA StatusCode value.</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
|
||||
};
|
||||
public static uint MapStatus(AbStatus status) => AbOpcUaStatusMap.MapStatus(status);
|
||||
|
||||
/// <summary>
|
||||
/// Status for an exception thrown while creating / reading a tag: a transport exception maps
|
||||
@@ -101,5 +49,5 @@ public static class AbCipStatusMapper
|
||||
/// <param name="ex">The exception.</param>
|
||||
/// <returns>The OPC UA status code.</returns>
|
||||
public static uint MapException(Exception ex) =>
|
||||
ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError;
|
||||
AbOpcUaStatusMap.MapException(ex);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared;
|
||||
using libplctag;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
Decision #11 — EtherNet/IP + CIP + Logix symbolic against ControlLogix / CompactLogix /
|
||||
Micro800 / SLC500 / PLC-5. -->
|
||||
<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>
|
||||
|
||||
@@ -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