a25593a9c6
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
7.0 KiB
C#
163 lines
7.0 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using MxGateway.Client;
|
|
using MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
/// <summary>
|
|
/// Production <see cref="IGalaxyDataWriter"/> over <see cref="GalaxyMxSession"/>.
|
|
/// For each batch entry: lazy-AddItem to obtain the MXAccess item handle, encode
|
|
/// the value via <see cref="MxValueEncoder"/>, route through Write or WriteSecured
|
|
/// based on the per-tag <see cref="SecurityClassification"/>, and translate the
|
|
/// reply's <c>MxStatusProxy</c> into an OPC UA <see cref="WriteResult"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Item handle cache survives across writes — repeated writes to the same tag avoid
|
|
/// re-AddItem. Per-tag failures are isolated: one bad write doesn't fail the batch.
|
|
/// PR 4.4 will share this cache with the subscription registry; for now it lives
|
|
/// here so the writer is independently testable.
|
|
/// </remarks>
|
|
public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
|
{
|
|
private readonly GalaxyMxSession _session;
|
|
private readonly int _writeUserId;
|
|
private readonly ILogger _logger;
|
|
private readonly ConcurrentDictionary<string, int> _itemHandles =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public GatewayGalaxyDataWriter(GalaxyMxSession session, int writeUserId, ILogger? logger = null)
|
|
{
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
_writeUserId = writeUserId;
|
|
_logger = logger ?? NullLogger.Instance;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
|
|
IReadOnlyList<WriteRequest> writes,
|
|
Func<string, SecurityClassification> securityResolver,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(writes);
|
|
ArgumentNullException.ThrowIfNull(securityResolver);
|
|
|
|
var session = _session.Session
|
|
?? throw new InvalidOperationException(
|
|
"GalaxyMxSession is not connected. Call ConnectAsync before issuing writes.");
|
|
var serverHandle = _session.ServerHandle;
|
|
|
|
var results = new WriteResult[writes.Count];
|
|
for (var i = 0; i < writes.Count; i++)
|
|
{
|
|
results[i] = await WriteOneAsync(session, serverHandle, writes[i],
|
|
securityResolver(writes[i].FullReference), cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
private async Task<WriteResult> WriteOneAsync(
|
|
MxGatewaySession session, int serverHandle, WriteRequest request,
|
|
SecurityClassification classification, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var itemHandle = await EnsureItemHandleAsync(session, serverHandle, request.FullReference, ct)
|
|
.ConfigureAwait(false);
|
|
var mxValue = MxValueEncoder.Encode(request.Value);
|
|
|
|
var reply = NeedsSecuredWrite(classification)
|
|
? await InvokeWriteSecuredAsync(session, serverHandle, itemHandle, mxValue, ct).ConfigureAwait(false)
|
|
: await session.WriteRawAsync(serverHandle, itemHandle, mxValue, _writeUserId, ct).ConfigureAwait(false);
|
|
|
|
return TranslateReply(reply, request.FullReference);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
// Bad value type — caller passed a CLR type the encoder can't render.
|
|
_logger.LogWarning(ex,
|
|
"GalaxyDriver write rejected — unsupported value type for {FullRef}", request.FullReference);
|
|
return new WriteResult(StatusCodeMap.BadInternalError);
|
|
}
|
|
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "GalaxyDriver write failed for {FullRef}", request.FullReference);
|
|
return new WriteResult(StatusCodeMap.BadCommunicationError);
|
|
}
|
|
}
|
|
|
|
private static bool NeedsSecuredWrite(SecurityClassification classification) =>
|
|
classification is SecurityClassification.SecuredWrite or SecurityClassification.VerifiedWrite;
|
|
|
|
private async Task<int> EnsureItemHandleAsync(
|
|
MxGatewaySession session, int serverHandle, string fullRef, CancellationToken ct)
|
|
{
|
|
if (_itemHandles.TryGetValue(fullRef, out var existing)) return existing;
|
|
var handle = await session.AddItemAsync(serverHandle, fullRef, ct).ConfigureAwait(false);
|
|
_itemHandles[fullRef] = handle;
|
|
return handle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Issue a WriteSecured command. The high-level session client doesn't expose
|
|
/// <c>WriteSecuredAsync</c> as a typed method — we build the <see cref="MxCommand"/>
|
|
/// directly and route through <c>InvokeAsync</c>. Verifier user is left at zero
|
|
/// for SecuredWrite; VerifiedWrite uses the same path because the gw's worker
|
|
/// interprets the underlying MXAccess command kind.
|
|
/// </summary>
|
|
private static Task<MxCommandReply> InvokeWriteSecuredAsync(
|
|
MxGatewaySession session, int serverHandle, int itemHandle, MxValue value, CancellationToken ct)
|
|
{
|
|
var command = new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured,
|
|
WriteSecured = new WriteSecuredCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = value,
|
|
CurrentUserId = 0,
|
|
VerifierUserId = 0,
|
|
},
|
|
};
|
|
var request = new MxCommandRequest
|
|
{
|
|
SessionId = session.SessionId,
|
|
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
Command = command,
|
|
};
|
|
return session.InvokeAsync(request, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translate a gateway <see cref="MxCommandReply"/> into an OPC UA
|
|
/// <see cref="WriteResult"/>. Honours the protocol-level Status field first
|
|
/// (transport / dispatch failures), then the first MXAccess status row.
|
|
/// </summary>
|
|
private WriteResult TranslateReply(MxCommandReply reply, string fullRef)
|
|
{
|
|
// Protocol status — wraps transport / worker-side failures that happen before
|
|
// MXAccess saw the command.
|
|
if (reply.ProtocolStatus is { } proto && proto.Code != ProtocolStatusCode.Ok)
|
|
{
|
|
_logger.LogWarning(
|
|
"GalaxyDriver write protocol failure {Code} for {FullRef}: {Message}",
|
|
proto.Code, fullRef, proto.Message);
|
|
return new WriteResult(StatusCodeMap.BadCommunicationError);
|
|
}
|
|
|
|
// MX-side status — the worker's WriteCompleteEvent rolls into the reply's
|
|
// statuses array. Use the first row (single-write contract).
|
|
if (reply.Statuses.Count > 0)
|
|
{
|
|
var status = reply.Statuses[0];
|
|
return new WriteResult(StatusCodeMap.FromMxStatus(status, _logger));
|
|
}
|
|
|
|
return new WriteResult(StatusCodeMap.Good);
|
|
}
|
|
}
|