698703744f
Task 9: DiscoverTreeAsync UntilStable settle loop (FOCAS) — re-capture on a 1s interval until
the node set is non-empty and stable across two passes, bounded by open-timeout; on
timeout with a prior non-empty capture, return it (best-effort) instead of failing.
Task 11: BrowserSessionService resolves bespoke-first, falls back to IUniversalDriverBrowser
when no bespoke browser matches and CanBrowse is true; new CanBrowse on the service.
16 unit tests green (4 settle + 12 service).
96 lines
4.1 KiB
C#
96 lines
4.1 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
|
|
|
/// <summary>
|
|
/// Default <see cref="IBrowserSessionService"/> implementation. Indexes injected
|
|
/// <see cref="IDriverBrowser"/>s by <see cref="IDriverBrowser.DriverType"/>
|
|
/// (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.
|
|
/// </summary>
|
|
public sealed class BrowserSessionService(
|
|
IEnumerable<IDriverBrowser> browsers,
|
|
BrowseSessionRegistry registry,
|
|
ILogger<BrowserSessionService> logger,
|
|
IUniversalDriverBrowser universalBrowser) : IBrowserSessionService
|
|
{
|
|
/// <summary>Upper bound on a single root/expand/attributes call.</summary>
|
|
public static readonly TimeSpan PerCallTimeout = TimeSpan.FromSeconds(20);
|
|
|
|
private readonly IReadOnlyDictionary<string, IDriverBrowser> _browsersByType =
|
|
browsers.ToDictionary(b => b.DriverType, StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <inheritdoc />
|
|
public async Task<BrowseOpenResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool CanBrowse(string driverType, string configJson) =>
|
|
_browsersByType.ContainsKey(driverType) || universalBrowser.CanBrowse(driverType, configJson);
|
|
|
|
private async Task<BrowseOpenResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<BrowseNode>> RootAsync(Guid token, CancellationToken ct) =>
|
|
InvokeAsync(token, ct, (s, c) => s.RootAsync(c));
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<BrowseNode>> ExpandAsync(Guid token, string nodeId, CancellationToken ct) =>
|
|
InvokeAsync(token, ct, (s, c) => s.ExpandAsync(nodeId, c));
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(Guid token, string nodeId, CancellationToken ct) =>
|
|
InvokeAsync<IReadOnlyList<AttributeInfo>>(token, ct, (s, c) => s.AttributesAsync(nodeId, c));
|
|
|
|
/// <inheritdoc />
|
|
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<T> InvokeAsync<T>(
|
|
Guid token, CancellationToken callerCt, Func<IBrowseSession, CancellationToken, Task<T>> 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);
|
|
}
|
|
}
|