Compare commits
1 Commits
phase-2-pr
...
phase-2-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ece6e22c |
@@ -54,6 +54,13 @@ public sealed class MxAccessClient : IDisposable
|
|||||||
public int SubscriptionCount => _subscriptions.Count;
|
public int SubscriptionCount => _subscriptions.Count;
|
||||||
public int ReconnectCount => _reconnectCount;
|
public int ReconnectCount => _reconnectCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wonderware client identity used when registering with the LMXProxyServer. Surfaced so
|
||||||
|
/// <see cref="Backend.MxAccessGalaxyBackend"/> can tag its <c>OnHostStatusChanged</c> IPC
|
||||||
|
/// pushes with a stable gateway name per PR 8.
|
||||||
|
/// </summary>
|
||||||
|
public string ClientName => _clientName;
|
||||||
|
|
||||||
/// <summary>Connects on the STA thread. Idempotent. Starts the reconnect monitor on first call.</summary>
|
/// <summary>Connects on the STA thread. Idempotent. Starts the reconnect monitor on first call.</summary>
|
||||||
public async Task<int> ConnectAsync()
|
public async Task<int> ConnectAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,16 +34,34 @@ public sealed class MxAccessGalaxyBackend : IGalaxyBackend, IDisposable
|
|||||||
_refToSubs = new(System.StringComparer.OrdinalIgnoreCase);
|
_refToSubs = new(System.StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public event System.EventHandler<OnDataChangeNotification>? OnDataChange;
|
public event System.EventHandler<OnDataChangeNotification>? OnDataChange;
|
||||||
#pragma warning disable CS0067 // event not yet raised — alarm + host-status wire-up in PR #4 follow-up
|
#pragma warning disable CS0067 // alarm wire-up deferred to PR 9
|
||||||
public event System.EventHandler<GalaxyAlarmEvent>? OnAlarmEvent;
|
public event System.EventHandler<GalaxyAlarmEvent>? OnAlarmEvent;
|
||||||
public event System.EventHandler<HostConnectivityStatus>? OnHostStatusChanged;
|
|
||||||
#pragma warning restore CS0067
|
#pragma warning restore CS0067
|
||||||
|
public event System.EventHandler<HostConnectivityStatus>? OnHostStatusChanged;
|
||||||
|
|
||||||
|
private readonly System.EventHandler<bool> _onConnectionStateChanged;
|
||||||
|
|
||||||
public MxAccessGalaxyBackend(GalaxyRepository repository, MxAccessClient mx, IHistorianDataSource? historian = null)
|
public MxAccessGalaxyBackend(GalaxyRepository repository, MxAccessClient mx, IHistorianDataSource? historian = null)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_mx = mx;
|
_mx = mx;
|
||||||
_historian = historian;
|
_historian = historian;
|
||||||
|
|
||||||
|
// PR 8: gateway-level host-status push. When the MXAccess COM proxy transitions
|
||||||
|
// connected↔disconnected, raise OnHostStatusChanged with a synthetic host entry named
|
||||||
|
// after the Wonderware client identity so the Admin UI surfaces top-level transport
|
||||||
|
// health even before per-platform/per-engine probing lands (deferred to a later PR that
|
||||||
|
// ports v1's GalaxyRuntimeProbeManager with ScanState subscriptions).
|
||||||
|
_onConnectionStateChanged = (_, connected) =>
|
||||||
|
{
|
||||||
|
OnHostStatusChanged?.Invoke(this, new HostConnectivityStatus
|
||||||
|
{
|
||||||
|
HostName = _mx.ClientName,
|
||||||
|
RuntimeStatus = connected ? "Running" : "Stopped",
|
||||||
|
LastObservedUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
_mx.ConnectionStateChanged += _onConnectionStateChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OpenSessionResponse> OpenSessionAsync(OpenSessionRequest req, CancellationToken ct)
|
public async Task<OpenSessionResponse> OpenSessionAsync(OpenSessionRequest req, CancellationToken ct)
|
||||||
@@ -267,7 +285,11 @@ public sealed class MxAccessGalaxyBackend : IGalaxyBackend, IDisposable
|
|||||||
public Task<RecycleStatusResponse> RecycleAsync(RecycleHostRequest req, CancellationToken ct)
|
public Task<RecycleStatusResponse> RecycleAsync(RecycleHostRequest req, CancellationToken ct)
|
||||||
=> Task.FromResult(new RecycleStatusResponse { Accepted = true, GraceSeconds = 15 });
|
=> Task.FromResult(new RecycleStatusResponse { Accepted = true, GraceSeconds = 15 });
|
||||||
|
|
||||||
public void Dispose() => _historian?.Dispose();
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_mx.ConnectionStateChanged -= _onConnectionStateChanged;
|
||||||
|
_historian?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private static GalaxyDataValue ToWire(string reference, Vtq vtq) => new()
|
private static GalaxyDataValue ToWire(string reference, Vtq vtq) => new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ArchestrA.MxAccess;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class HostStatusPushTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PR 8 — when MxAccessClient.ConnectionStateChanged fires false→true→false,
|
||||||
|
/// MxAccessGalaxyBackend raises OnHostStatusChanged once per transition with
|
||||||
|
/// HostName=ClientName, RuntimeStatus="Running"/"Stopped", and a timestamp.
|
||||||
|
/// This is the gateway-level signal; per-platform ScanState probes are deferred.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectionStateChanged_raises_OnHostStatusChanged_with_gateway_name()
|
||||||
|
{
|
||||||
|
using var pump = new StaPump("Test.Sta");
|
||||||
|
await pump.WaitForStartedAsync();
|
||||||
|
var proxy = new FakeProxy();
|
||||||
|
var mx = new MxAccessClient(pump, proxy, "GatewayClient", new MxAccessClientOptions { AutoReconnect = false });
|
||||||
|
using var backend = new MxAccessGalaxyBackend(
|
||||||
|
new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }),
|
||||||
|
mx,
|
||||||
|
historian: null);
|
||||||
|
|
||||||
|
var notifications = new ConcurrentQueue<HostConnectivityStatus>();
|
||||||
|
backend.OnHostStatusChanged += (_, s) => notifications.Enqueue(s);
|
||||||
|
|
||||||
|
await mx.ConnectAsync();
|
||||||
|
await mx.DisconnectAsync();
|
||||||
|
|
||||||
|
notifications.Count.ShouldBe(2);
|
||||||
|
notifications.TryDequeue(out var first).ShouldBeTrue();
|
||||||
|
first!.HostName.ShouldBe("GatewayClient");
|
||||||
|
first.RuntimeStatus.ShouldBe("Running");
|
||||||
|
first.LastObservedUtcUnixMs.ShouldBeGreaterThan(0);
|
||||||
|
|
||||||
|
notifications.TryDequeue(out var second).ShouldBeTrue();
|
||||||
|
second!.HostName.ShouldBe("GatewayClient");
|
||||||
|
second.RuntimeStatus.ShouldBe("Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Dispose_unsubscribes_so_post_dispose_state_changes_do_not_fire_events()
|
||||||
|
{
|
||||||
|
using var pump = new StaPump("Test.Sta");
|
||||||
|
await pump.WaitForStartedAsync();
|
||||||
|
var proxy = new FakeProxy();
|
||||||
|
var mx = new MxAccessClient(pump, proxy, "GatewayClient", new MxAccessClientOptions { AutoReconnect = false });
|
||||||
|
var backend = new MxAccessGalaxyBackend(
|
||||||
|
new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }),
|
||||||
|
mx,
|
||||||
|
historian: null);
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
backend.OnHostStatusChanged += (_, _) => Interlocked.Increment(ref count);
|
||||||
|
|
||||||
|
await mx.ConnectAsync();
|
||||||
|
count.ShouldBe(1);
|
||||||
|
|
||||||
|
backend.Dispose();
|
||||||
|
await mx.DisconnectAsync();
|
||||||
|
|
||||||
|
count.ShouldBe(1); // no second notification after Dispose
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeProxy : IMxProxy
|
||||||
|
{
|
||||||
|
private int _next = 1;
|
||||||
|
public int Register(string _) => 42;
|
||||||
|
public void Unregister(int _) { }
|
||||||
|
public int AddItem(int _, string __) => Interlocked.Increment(ref _next);
|
||||||
|
public void RemoveItem(int _, int __) { }
|
||||||
|
public void AdviseSupervisory(int _, int __) { }
|
||||||
|
public void UnAdviseSupervisory(int _, int __) { }
|
||||||
|
public void Write(int _, int __, object ___, int ____) { }
|
||||||
|
public event MxDataChangeHandler? OnDataChange { add { } remove { } }
|
||||||
|
public event MxWriteCompleteHandler? OnWriteComplete { add { } remove { } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
||||||
<Reference Include="System.ServiceProcess"/>
|
<Reference Include="System.ServiceProcess"/>
|
||||||
|
<!-- IMxProxy's delegate signatures mention ArchestrA.MxAccess.MXSTATUS_PROXY, so tests
|
||||||
|
implementing the interface must resolve that type at compile time. -->
|
||||||
|
<Reference Include="ArchestrA.MxAccess">
|
||||||
|
<HintPath>..\..\lib\ArchestrA.MxAccess.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user