diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs
index 0509e60..9878d51 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs
@@ -154,23 +154,57 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
}
///
- /// Writes the union of and
- /// to the OPC UA standard
- /// Server.ServerArray property (NodeId i=2254). Clients in a warm-redundancy
- /// deployment discover the partner endpoint by reading this property.
+ /// Publishes via the OPC UA
+ /// standard Server.ServerArray property (NodeId i=2254) so warm-redundancy clients
+ /// can discover the partner endpoint.
+ ///
+ /// The wire-served value of Server.ServerArray comes from
+ /// (an ) via the
+ /// SDK's OnReadServerArray callback — writes to
+ /// ServerObject.ServerArray.Value are NOT what clients read. The SDK auto-populates
+ /// slot 0 with the local ApplicationUri on ApplicationInstance.Start; we
+ /// append the configured peers at slots 1, 2, … here.
+ ///
+ /// The address-space property is also mirrored for in-process readers (the unit-test
+ /// observation seam) and as a defensive belt-and-braces measure.
///
private void PopulateServerArray()
{
- var serverObject = _server?.CurrentInstance?.ServerObject;
- if (serverObject is null) return;
+ var internalData = _server?.CurrentInstance;
+ if (internalData is null) return;
+
+ // Wire path: append peers to IServerInternal.ServerUris — this is what
+ // OnReadServerArray serves to remote clients reading VariableIds.Server_ServerArray.
+ var serverUris = internalData.ServerUris;
+ var existing = new HashSet(StringComparer.Ordinal);
+ for (uint i = 0; i < (uint)serverUris.Count; i++)
+ {
+ var existingUri = serverUris.GetString(i);
+ if (existingUri is not null) existing.Add(existingUri);
+ }
- var uris = new List { _options.ApplicationUri };
foreach (var peer in _options.PeerApplicationUris)
{
- if (!string.IsNullOrWhiteSpace(peer) && !uris.Contains(peer))
- uris.Add(peer);
+ if (string.IsNullOrWhiteSpace(peer)) continue;
+ if (existing.Contains(peer)) continue;
+ serverUris.Append(peer);
+ existing.Add(peer);
+ }
+
+ // In-process mirror: ServerObject.ServerArray.Value is consulted by some tests and
+ // tooling that read the SDK's address-space model directly rather than going through
+ // a session. Harmless on the wire (the SDK ignores it) but useful in-VM.
+ var serverObject = internalData.ServerObject;
+ if (serverObject is not null)
+ {
+ var uris = new List { _options.ApplicationUri };
+ foreach (var peer in _options.PeerApplicationUris)
+ {
+ if (!string.IsNullOrWhiteSpace(peer) && !uris.Contains(peer))
+ uris.Add(peer);
+ }
+ serverObject.ServerArray.Value = uris.ToArray();
}
- serverObject.ServerArray.Value = uris.ToArray();
}
private void OnImpersonateUser(Session session, ImpersonateEventArgs args) =>