using System.Reflection;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
///
/// PR-S7-E2 / #303 — narrow seam covering the "send a password to a hardened CPU"
/// wire path. calls
/// after OpenAsync succeeds and before the
/// pre-flight PUT/GET probe runs, so a hardened S7-1500 / ET 200SP CPU that gates
/// reads behind a connection-level password unlocks before the probe drops it.
///
///
///
/// The runtime implementation () discovers
/// the underlying S7.Net.Plc.SendPassword / SendPasswordAsync
/// methods reflectively because S7netplus 0.20 doesn't yet expose them in a
/// strongly-typed surface — the seam keeps this driver compiling against the
/// current pinned package version while still calling whatever the next minor
/// release ships. When neither method exists,
/// stays false and
/// is a no-op so a misconfigured "Password
/// set, library doesn't oblige" deployment surfaces as a one-line warning at
/// Init rather than a hard failure (failure shifts to first per-tag read on the
/// hardened CPU, which is the same shape as if the operator had forgotten to
/// enable PUT/GET).
///
///
/// Tests inject a fake to exercise both branches without touching the live
/// S7netplus stack.
///
///
internal interface IS7PlcAuthGate
{
///
/// true when the underlying S7netplus Plc exposes a public
/// SendPassword(string) or SendPasswordAsync(string, CancellationToken)
/// method. false on S7netplus 0.20 (which has no such surface).
///
bool SupportsSendPassword { get; }
///
/// Send to the connected PLC. No-op (and returns
/// false) when is false;
/// returns true after a successful send. Throws cleanly when the wire
/// reports auth-failed — wraps the
/// throw into a typed so the operator
/// sees a "password authentication failed" message rather than a generic
/// S7.Net.PlcException.
///
Task TrySendPasswordAsync(string password, CancellationToken cancellationToken);
}
///
/// Production backed by reflection over the S7netplus
/// S7.Net.Plc instance. S7netplus 0.20 does NOT expose a
/// SendPassword; the reflection probe survives that gracefully and a future
/// 0.21+ that adds the API gets called automatically without a code change here.
///
internal sealed class ReflectionS7PlcAuthGate : IS7PlcAuthGate
{
private readonly object _plc;
private readonly MethodInfo? _syncMethod;
private readonly MethodInfo? _asyncMethod;
public ReflectionS7PlcAuthGate(object plc)
{
_plc = plc ?? throw new ArgumentNullException(nameof(plc));
var type = plc.GetType();
// Probe both shapes: synchronous void SendPassword(string) and async
// Task SendPasswordAsync(string, CancellationToken). Either is acceptable;
// the async overload wins when both exist (no thread-block on init).
_asyncMethod = type.GetMethod(
"SendPasswordAsync",
BindingFlags.Instance | BindingFlags.Public,
binder: null,
types: [typeof(string), typeof(CancellationToken)],
modifiers: null);
_syncMethod = type.GetMethod(
"SendPassword",
BindingFlags.Instance | BindingFlags.Public,
binder: null,
types: [typeof(string)],
modifiers: null);
}
public bool SupportsSendPassword => _asyncMethod is not null || _syncMethod is not null;
public async Task TrySendPasswordAsync(string password, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(password);
if (_asyncMethod is not null)
{
// Unwrap TargetInvocationException so the caller sees the real S7.Net.PlcException
// (or whatever the library threw) rather than the reflection wrapper.
try
{
var result = _asyncMethod.Invoke(_plc, [password, cancellationToken]);
if (result is Task task)
{
await task.ConfigureAwait(false);
}
return true;
}
catch (TargetInvocationException tie) when (tie.InnerException is not null)
{
throw tie.InnerException;
}
}
if (_syncMethod is not null)
{
try
{
_syncMethod.Invoke(_plc, [password]);
return true;
}
catch (TargetInvocationException tie) when (tie.InnerException is not null)
{
throw tie.InnerException;
}
}
return false;
}
}