7b0b9c7365
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
201 lines
7.5 KiB
C#
201 lines
7.5 KiB
C#
using System.Collections;
|
|
using System.Data.Common;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
/// <summary>
|
|
/// Audit Log #23 — M4 Bundle A: <see cref="DbDataReader"/> decorator that
|
|
/// counts the number of rows read by the script and fires a single audit
|
|
/// emission callback when the reader closes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The wrapping reader counts each successful <see cref="Read"/> /
|
|
/// <see cref="ReadAsync(CancellationToken)"/> and invokes <c>onClose</c>
|
|
/// exactly once — on <see cref="Close"/>, <see cref="CloseAsync"/>, or
|
|
/// disposal — with the running tally. This lets
|
|
/// <see cref="AuditingDbCommand"/> emit one
|
|
/// <c>DbOutbound</c>/<c>DbWrite</c> row per <c>ExecuteReader</c> with
|
|
/// <c>Extra.rowsReturned</c> populated, matching the M4 vocabulary lock.
|
|
/// </para>
|
|
/// <para>
|
|
/// Multiple result sets via <see cref="NextResult"/> are folded into a single
|
|
/// <c>rowsReturned</c> tally — the script sees one audit row per
|
|
/// <c>ExecuteReader</c> call, not per result set.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal sealed class AuditingDbDataReader : DbDataReader
|
|
{
|
|
private readonly DbDataReader _inner;
|
|
private readonly Action<int> _onClose;
|
|
private int _rowsReturned;
|
|
private bool _closed;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AuditingDbDataReader"/> class, wrapping a data reader to count rows read.
|
|
/// </summary>
|
|
/// <param name="inner">The underlying DbDataReader to wrap and audit.</param>
|
|
/// <param name="onClose">Callback invoked once when the reader closes, receiving the total rows read.</param>
|
|
public AuditingDbDataReader(DbDataReader inner, Action<int> onClose)
|
|
{
|
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
|
_onClose = onClose ?? throw new ArgumentNullException(nameof(onClose));
|
|
}
|
|
|
|
// -- Row-count interception ------------------------------------------
|
|
|
|
/// <inheritdoc />
|
|
public override bool Read()
|
|
{
|
|
var more = _inner.Read();
|
|
if (more) _rowsReturned++;
|
|
return more;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override async Task<bool> ReadAsync(CancellationToken cancellationToken)
|
|
{
|
|
var more = await _inner.ReadAsync(cancellationToken).ConfigureAwait(false);
|
|
if (more) _rowsReturned++;
|
|
return more;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Close()
|
|
{
|
|
if (!_closed)
|
|
{
|
|
_closed = true;
|
|
try { _inner.Close(); }
|
|
finally { SafeFireOnClose(); }
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override async Task CloseAsync()
|
|
{
|
|
if (!_closed)
|
|
{
|
|
_closed = true;
|
|
try { await _inner.CloseAsync().ConfigureAwait(false); }
|
|
finally { SafeFireOnClose(); }
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
// DbDataReader.Dispose calls Close on most providers, but we
|
|
// guard with _closed to ensure onClose fires exactly once.
|
|
if (!_closed)
|
|
{
|
|
_closed = true;
|
|
try { _inner.Dispose(); }
|
|
finally { SafeFireOnClose(); }
|
|
}
|
|
else
|
|
{
|
|
_inner.Dispose();
|
|
}
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override async ValueTask DisposeAsync()
|
|
{
|
|
if (!_closed)
|
|
{
|
|
_closed = true;
|
|
try { await _inner.DisposeAsync().ConfigureAwait(false); }
|
|
finally { SafeFireOnClose(); }
|
|
}
|
|
else
|
|
{
|
|
await _inner.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private void SafeFireOnClose()
|
|
{
|
|
// The onClose callback runs the audit emission, which is itself
|
|
// best-effort and swallows internally — but defend the reader's own
|
|
// close path anyway so an audit fault never propagates out of
|
|
// Close/Dispose.
|
|
try { _onClose(_rowsReturned); }
|
|
catch { /* audit emission is best-effort by contract */ }
|
|
}
|
|
|
|
// -- Forwarded surface ------------------------------------------------
|
|
|
|
/// <inheritdoc />
|
|
public override object this[int ordinal] => _inner[ordinal];
|
|
/// <inheritdoc />
|
|
public override object this[string name] => _inner[name];
|
|
/// <inheritdoc />
|
|
public override int Depth => _inner.Depth;
|
|
/// <inheritdoc />
|
|
public override int FieldCount => _inner.FieldCount;
|
|
/// <inheritdoc />
|
|
public override bool HasRows => _inner.HasRows;
|
|
/// <inheritdoc />
|
|
public override bool IsClosed => _inner.IsClosed;
|
|
/// <inheritdoc />
|
|
public override int RecordsAffected => _inner.RecordsAffected;
|
|
/// <inheritdoc />
|
|
public override int VisibleFieldCount => _inner.VisibleFieldCount;
|
|
/// <inheritdoc />
|
|
public override bool GetBoolean(int ordinal) => _inner.GetBoolean(ordinal);
|
|
/// <inheritdoc />
|
|
public override byte GetByte(int ordinal) => _inner.GetByte(ordinal);
|
|
/// <inheritdoc />
|
|
public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length)
|
|
=> _inner.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length);
|
|
/// <inheritdoc />
|
|
public override char GetChar(int ordinal) => _inner.GetChar(ordinal);
|
|
/// <inheritdoc />
|
|
public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
|
|
=> _inner.GetChars(ordinal, dataOffset, buffer, bufferOffset, length);
|
|
/// <inheritdoc />
|
|
public override string GetDataTypeName(int ordinal) => _inner.GetDataTypeName(ordinal);
|
|
/// <inheritdoc />
|
|
public override DateTime GetDateTime(int ordinal) => _inner.GetDateTime(ordinal);
|
|
/// <inheritdoc />
|
|
public override decimal GetDecimal(int ordinal) => _inner.GetDecimal(ordinal);
|
|
/// <inheritdoc />
|
|
public override double GetDouble(int ordinal) => _inner.GetDouble(ordinal);
|
|
/// <inheritdoc />
|
|
public override IEnumerator GetEnumerator() => ((IEnumerable)_inner).GetEnumerator();
|
|
/// <inheritdoc />
|
|
public override Type GetFieldType(int ordinal) => _inner.GetFieldType(ordinal);
|
|
/// <inheritdoc />
|
|
public override float GetFloat(int ordinal) => _inner.GetFloat(ordinal);
|
|
/// <inheritdoc />
|
|
public override Guid GetGuid(int ordinal) => _inner.GetGuid(ordinal);
|
|
/// <inheritdoc />
|
|
public override short GetInt16(int ordinal) => _inner.GetInt16(ordinal);
|
|
/// <inheritdoc />
|
|
public override int GetInt32(int ordinal) => _inner.GetInt32(ordinal);
|
|
/// <inheritdoc />
|
|
public override long GetInt64(int ordinal) => _inner.GetInt64(ordinal);
|
|
/// <inheritdoc />
|
|
public override string GetName(int ordinal) => _inner.GetName(ordinal);
|
|
/// <inheritdoc />
|
|
public override int GetOrdinal(string name) => _inner.GetOrdinal(name);
|
|
/// <inheritdoc />
|
|
public override string GetString(int ordinal) => _inner.GetString(ordinal);
|
|
/// <inheritdoc />
|
|
public override object GetValue(int ordinal) => _inner.GetValue(ordinal);
|
|
/// <inheritdoc />
|
|
public override int GetValues(object[] values) => _inner.GetValues(values);
|
|
/// <inheritdoc />
|
|
public override bool IsDBNull(int ordinal) => _inner.IsDBNull(ordinal);
|
|
/// <inheritdoc />
|
|
public override bool NextResult() => _inner.NextResult();
|
|
/// <inheritdoc />
|
|
public override Task<bool> NextResultAsync(CancellationToken cancellationToken) => _inner.NextResultAsync(cancellationToken);
|
|
}
|