@using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin @inject AdminProbeService Probe @implements IAsyncDisposable
@if (_result is not null) { @if (_result.Ok) { OK · @_result.LatencyMs?.ToString("F0") ms } else { Failed · @DisplayText.Abbreviate(_result.Message, 60) } }
@code { /// Driver type key — must match an installed IDriverProbe.DriverType. [Parameter, EditorRequired] public string DriverType { get; set; } = ""; /// Callback that returns the current form config as JSON. Called at click time. [Parameter, EditorRequired] public Func GetConfigJson { get; set; } = () => "{}"; /// /// Fallback probe budget, used only when the config JSON carries no positive /// ProbeTimeoutSeconds and the driver has no documented default (TestConnectBudget.DriverDefaults). The configured value wins (it is every driver's documented "Test Connect /// probe timeout"), so it can lengthen the budget as well as shorten it. Clamped to [1, 60] s. /// [Parameter] public int TimeoutSeconds { get; set; } = TestConnectBudget.DefaultSeconds; private const int ClearAfterMs = 30_000; private bool _busy; private int? _lastBudgetSeconds; private TestDriverConnectResult? _result; // System.Threading.Timer (it replaced an Elapsed-event timer with an async void lambda): DisposeAsync awaits an // in-flight callback, and the callback body never throws onto the thread pool (R04 L2). The generation // makes a superseded timer's late fire a no-op, so it cannot clear a newer result. private System.Threading.Timer? _clearTimer; private int _resultGeneration; private volatile bool _disposed; private async Task OnClickAsync() { _busy = true; _result = null; StateHasChanged(); try { var json = GetConfigJson() ?? "{}"; var budget = TestConnectBudget.Resolve(json, TimeoutSeconds, DriverType); _lastBudgetSeconds = budget; _result = await Probe.TestAsync(DriverType, json, budget); } catch (Exception ex) { _result = new TestDriverConnectResult(false, ex.Message, null, Guid.Empty); } finally { _busy = false; ScheduleClear(); if (!_disposed) StateHasChanged(); } } private void ScheduleClear() { if (_disposed) return; var generation = ++_resultGeneration; _clearTimer?.Dispose(); _clearTimer = new System.Threading.Timer(_ => _ = ClearAsync(generation), null, ClearAfterMs, Timeout.Infinite); } private async Task ClearAsync(int generation) { try { await InvokeAsync(() => { if (_disposed || generation != _resultGeneration) return; _result = null; StateHasChanged(); }); } catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) { // The circuit went away between the fire and the render — nothing to clear. } } /// public async ValueTask DisposeAsync() { _disposed = true; if (_clearTimer is not null) await _clearTimer.DisposeAsync(); } }