using Mbproxy.Options; using Mbproxy.Proxy; using Mbproxy.Proxy.Multiplexing; using Mbproxy.Proxy.Supervision; using Microsoft.Extensions.Options; namespace Mbproxy.Admin; /// /// Pure orchestration: reads live state from injected singletons and builds a /// for GET / and GET /status.json. /// /// No I/O; no side effects. Constructed once via DI; is the /// only operation and may be called on any thread at any time. /// internal sealed class StatusSnapshotBuilder { private readonly IOptionsMonitor _options; private readonly ServiceCounters _serviceCounters; private readonly AssemblyVersionAccessor _version; private readonly ProxyWorker _proxyWorker; public StatusSnapshotBuilder( IOptionsMonitor options, ServiceCounters serviceCounters, AssemblyVersionAccessor version, ProxyWorker proxyWorker) { _options = options; _serviceCounters = serviceCounters; _version = version; _proxyWorker = proxyWorker; } /// /// Builds a point-in-time . /// Each counter is read atomically; no locks are held across the build. /// public StatusResponse Build() { var opts = _options.CurrentValue; var now = DateTimeOffset.UtcNow; var started = _serviceCounters.StartedAtUtc; var uptime = (long)(now - started).TotalSeconds; var supervisors = _proxyWorker.Supervisors; // ── Build per-PLC status rows ───────────────────────────────────────── var plcStatuses = new List(opts.Plcs.Count); int boundCount = 0; foreach (var plc in opts.Plcs) { supervisors.TryGetValue(plc.Name, out var supervisor); // Supervisor state SupervisorSnapshot? snap = supervisor?.Snapshot(); string stateStr = snap?.State switch { SupervisorState.Bound => "bound", SupervisorState.Recovering => "recovering", _ => "stopped", }; if (snap?.State == SupervisorState.Bound) boundCount++; // Per-client snapshots var activeUpstreams = supervisor?.ActiveUpstreams ?? Array.Empty(); var clientSnapshots = activeUpstreams .Select(p => new ClientSnapshot( Remote: p.RemoteEp?.ToString() ?? p.RemoteEp?.Address.ToString() ?? "?", ConnectedAtUtc: p.ConnectedAtUtc, PdusForwarded: p.PdusForwardedCount)) .ToList(); // Counter snapshot var counters = supervisor?.CurrentCounters.Snapshot() ?? new CounterSnapshot( PdusForwarded: 0, Fc03: 0, Fc04: 0, Fc06: 0, Fc16: 0, FcOther: 0, RewrittenSlots: 0, PartialBcdWarnings: 0, InvalidBcdWarnings: 0, BackendException01: 0, BackendException02: 0, BackendException03: 0, BackendException04: 0, BackendExceptionOther: 0, BytesUpstreamIn: 0, BytesUpstreamOut: 0, RecoveryAttempts: 0, LastBindError: null, LastRoundTripMs: 0.0, ConnectsSuccess: 0, ConnectsFailed: 0, InFlightCount: 0, MaxInFlight: 0, TxIdWraps: 0, BackendDisconnectCascades: 0, BackendQueueDepth: 0); // Phase 08: ConnectsSuccess / ConnectsFailed are now tracked in ProxyCounters. long connectsSuccess = counters.ConnectsSuccess; long connectsFailed = counters.ConnectsFailed; plcStatuses.Add(new PlcStatus( Name: plc.Name, Host: plc.Host, ListenPort: plc.ListenPort, Listener: new PlcListenerStatus( State: stateStr, LastBindError: snap?.LastBindError, RecoveryAttempts: snap?.RecoveryAttempts ?? 0), Clients: new PlcClientsStatus( Connected: clientSnapshots.Count, RemoteEndpoints: clientSnapshots), Pdus: new PlcPdusStatus( Forwarded: counters.PdusForwarded, ByFc: new FcCounts(counters.Fc03, counters.Fc04, counters.Fc06, counters.Fc16, counters.FcOther), RewrittenSlots: counters.RewrittenSlots, PartialBcdWarnings: counters.PartialBcdWarnings), Backend: new PlcBackendStatus( ConnectsSuccess: connectsSuccess, ConnectsFailed: connectsFailed, ExceptionsByCode: new ExceptionCounts( counters.BackendException01, counters.BackendException02, counters.BackendException03, counters.BackendException04), LastRoundTripMs: counters.LastRoundTripMs, InFlight: counters.InFlightCount, MaxInFlight: counters.MaxInFlight, TxIdWraps: counters.TxIdWraps, DisconnectCascades: counters.BackendDisconnectCascades, QueueDepth: counters.BackendQueueDepth), Bytes: new PlcBytesStatus( UpstreamIn: counters.BytesUpstreamIn, UpstreamOut: counters.BytesUpstreamOut))); } // ── Service-wide fields ─────────────────────────────────────────────── var service = new ServiceFields( UptimeSeconds: uptime, Version: _version.Version, ConfigLastReloadUtc: _serviceCounters.LastReloadUtc, ConfigReloadCount: _serviceCounters.ReloadAppliedCount, ConfigReloadRejectedCount: _serviceCounters.ReloadRejectedCount); var listeners = new ListenersAggregate( Bound: boundCount, Configured: opts.Plcs.Count); return new StatusResponse(service, listeners, plcStatuses); } }