mbproxy: initial commit through Phase 9 (TxId multiplexing)
Adds the mbproxy service end-to-end. Phases 00-08 implement the production-ready single-listener / 1:1-backend transparent Modbus TCP proxy with bidirectional BCD rewriting for the ~54-PLC DL205/DL260 fleet. Phase 9 replaces the connection layer with a single backend socket per PLC plus MBAP TxId rewriting, lifting the H2-ECOM100's 4-concurrent-client cap as an operational ceiling. Phase 9 additions of note: - PlcMultiplexer + UpstreamPipe + TxIdAllocator + CorrelationMap - InFlightRequest with IReadOnlyList<InterestedParty> (load-bearing for Phase 10 read coalescing — do not collapse to a single field) - Per-request watchdog: surfaces Modbus exception 0x0B to upstream on BackendRequestTimeoutMs, defending against lost responses, dead-PLC paths, and pymodbus 3.13.0's concurrent-multiplexed- request bug (its ServerRequestHandler.last_pdu state race) - Status DTO + HTML gain inFlight / maxInFlight / txIdWraps / disconnectCascades / queueDepth (Tier 1.6 in docs/kpi.md) Tests: 263 unit + 38 E2E. Multiplexer correctness under truly concurrent backend traffic is proved against a stub backend in PlcMultiplexerTests; MultiplexerE2ETests paces requests so pymodbus 3.13's single-PDU framer stays in known-good mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Versioning;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Mbproxy.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Serilog sink that writes events at level Error and above to the Windows Event Log
|
||||
/// under source <c>mbproxy</c>.
|
||||
///
|
||||
/// <para>This sink is only active when the service is running as a Windows Service
|
||||
/// (<see cref="Microsoft.Extensions.Hosting.WindowsServices.WindowsServiceHelpers.IsWindowsService"/>
|
||||
/// returns <c>true</c>). Under <c>dotnet run</c> / test / interactive launch, the sink is
|
||||
/// a no-op so that the Event Log source registration (which requires admin rights) is not
|
||||
/// required in development.</para>
|
||||
///
|
||||
/// <para>The Event Log source <c>mbproxy</c> must be created by <c>install.ps1</c> before
|
||||
/// the service starts. The bridge does NOT attempt to create the source at runtime — the
|
||||
/// service account may not hold the required admin rights.</para>
|
||||
///
|
||||
/// <para>Messages are capped at 32 KB (the Windows Event Log single-entry limit).</para>
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal sealed class EventLogBridge : ILogEventSink
|
||||
{
|
||||
private const string Source = "mbproxy";
|
||||
private const string LogName = "Application";
|
||||
private const int MaxMessageBytes = 32 * 1024; // 32 KB Event Log limit
|
||||
|
||||
private readonly bool _enabled;
|
||||
|
||||
public EventLogBridge(bool enabled)
|
||||
{
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
if (!_enabled) return;
|
||||
if (logEvent.Level < LogEventLevel.Error) return;
|
||||
|
||||
// Check that the source exists; if not, silently swallow — the service
|
||||
// account may not be able to create it and we must not crash the logger.
|
||||
if (!EventLog.SourceExists(Source)) return;
|
||||
|
||||
string message = logEvent.RenderMessage();
|
||||
|
||||
// Append exception detail when present.
|
||||
if (logEvent.Exception is not null)
|
||||
{
|
||||
message += Environment.NewLine + logEvent.Exception;
|
||||
}
|
||||
|
||||
// Truncate to the Event Log single-entry limit.
|
||||
if (message.Length * 2 > MaxMessageBytes) // rough UTF-16 upper bound
|
||||
{
|
||||
int charLimit = MaxMessageBytes / 2 - 3;
|
||||
message = message[..charLimit] + "...";
|
||||
}
|
||||
|
||||
var type = logEvent.Level switch
|
||||
{
|
||||
LogEventLevel.Fatal => EventLogEntryType.Error,
|
||||
LogEventLevel.Error => EventLogEntryType.Error,
|
||||
LogEventLevel.Warning => EventLogEntryType.Warning,
|
||||
_ => EventLogEntryType.Information,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
EventLog.WriteEntry(Source, message, type);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow: if the Event Log write fails (e.g., source not registered,
|
||||
// quota exceeded) we must not crash the application or recurse.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user