using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.Commons.Browsing; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing; /// /// Default implementation. Indexes injected /// s by /// (case-insensitive) at construction, registers opened sessions, and wraps each /// expand/attributes call in a 20-second linked CTS so a stuck driver cannot /// stall the UI indefinitely. /// public sealed class BrowserSessionService( IEnumerable browsers, BrowseSessionRegistry registry, ILogger logger, IUniversalDriverBrowser universalBrowser) : IBrowserSessionService { /// Upper bound on a single root/expand/attributes call. public static readonly TimeSpan PerCallTimeout = TimeSpan.FromSeconds(20); private readonly IReadOnlyDictionary _browsersByType = browsers.ToDictionary(b => b.DriverType, StringComparer.OrdinalIgnoreCase); /// public async Task OpenAsync(string driverType, string configJson, CancellationToken ct) { if (!_browsersByType.TryGetValue(driverType, out var browser)) return await OpenUniversalAsync(driverType, configJson, ct).ConfigureAwait(false); try { var session = await browser.OpenAsync(configJson, ct).ConfigureAwait(false); registry.Register(session); return new(true, null, session.Token); } catch (Exception ex) { logger.LogInformation(ex, "Browser open failed for driverType={DriverType}: {Message}", driverType, ex.Message); return new(false, ex.Message, Guid.Empty); } } /// public bool CanBrowse(string driverType, string configJson) => _browsersByType.ContainsKey(driverType) || universalBrowser.CanBrowse(driverType, configJson); private async Task OpenUniversalAsync(string driverType, string configJson, CancellationToken ct) { if (!universalBrowser.CanBrowse(driverType, configJson)) return new(false, $"No browser registered for driver type '{driverType}'.", Guid.Empty); try { var session = await universalBrowser.OpenAsync(driverType, configJson, ct).ConfigureAwait(false); registry.Register(session); return new(true, null, session.Token); } catch (Exception ex) { logger.LogInformation(ex, "Universal browse open failed for driverType={DriverType}: {Message}", driverType, ex.Message); return new(false, ex.Message, Guid.Empty); } } /// public Task> RootAsync(Guid token, CancellationToken ct) => InvokeAsync(token, ct, (s, c) => s.RootAsync(c)); /// public Task> ExpandAsync(Guid token, string nodeId, CancellationToken ct) => InvokeAsync(token, ct, (s, c) => s.ExpandAsync(nodeId, c)); /// public Task> AttributesAsync(Guid token, string nodeId, CancellationToken ct) => InvokeAsync>(token, ct, (s, c) => s.AttributesAsync(nodeId, c)); /// public async Task CloseAsync(Guid token) { if (!registry.TryRemove(token, out var session)) return; try { await session.DisposeAsync().ConfigureAwait(false); } catch { } logger.LogDebug("Browse session {Token} closed reason=user-close", token); } private async Task InvokeAsync( Guid token, CancellationToken callerCt, Func> op) { if (!registry.TryGet(token, out var session)) throw new BrowseSessionNotFoundException(token); using var cts = CancellationTokenSource.CreateLinkedTokenSource(callerCt); cts.CancelAfter(PerCallTimeout); return await op(session, cts.Token).ConfigureAwait(false); } }