diff --git a/Libraries/Opc.Ua.Server/Session/ISessionManager.cs b/Libraries/Opc.Ua.Server/Session/ISessionManager.cs index 1eb759c75..29ee3ac84 100644 --- a/Libraries/Opc.Ua.Server/Session/ISessionManager.cs +++ b/Libraries/Opc.Ua.Server/Session/ISessionManager.cs @@ -58,6 +58,11 @@ public interface ISessionManager : IDisposable /// event SessionEventHandler SessionClosing; + /// + /// Raised after diagnostics of an existing session were changed. + /// + event SessionEventHandler SessionDiagnosticsChanged; + /// /// Raised to signal a channel that the session is still alive. /// @@ -141,6 +146,11 @@ ValueTask CreateSessionAsync( /// and that the sequence number is not out of order (update requests only). /// OperationContext ValidateRequest(RequestHeader requestHeader, SecureChannelContext secureChannelContext, RequestType requestType); + + /// + /// Triggers the event so subscribers can react. + /// + void RaiseSessionDiagnosticsChangedEvent(ISession session); } /// @@ -194,6 +204,11 @@ public enum SessionEventReason /// Activated, + /// + /// The diagnostics of an existing session were changed. + /// + DiagnosticsChanged, + /// /// A session is about to be closed. /// diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index e6d0f6a94..fb348f74e 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -350,7 +350,6 @@ public virtual void ValidateRequest(RequestHeader requestHeader, SecureChannelCo lock (m_lock) { - if (secureChannelContext == null || !IsSecureChannelValid(secureChannelContext.SecureChannelId)) { UpdateDiagnosticCounters(requestType, true, true); @@ -1141,6 +1140,8 @@ private void UpdateDiagnosticCounters( bool error, bool authorizationError) { + ServiceCounterDataType counter = null; + lock (DiagnosticsLock) { if (!error) @@ -1160,8 +1161,6 @@ private void UpdateDiagnosticCounters( } } - ServiceCounterDataType counter = null; - switch (requestType) { case RequestType.Read: @@ -1271,6 +1270,11 @@ private void UpdateDiagnosticCounters( } } } + + if (counter != null) + { + m_server.SessionManager.RaiseSessionDiagnosticsChangedEvent(this); + } } private readonly Lock m_lock = new(); diff --git a/Libraries/Opc.Ua.Server/Session/SessionManager.cs b/Libraries/Opc.Ua.Server/Session/SessionManager.cs index 25b1898e6..a087b3bf8 100644 --- a/Libraries/Opc.Ua.Server/Session/SessionManager.cs +++ b/Libraries/Opc.Ua.Server/Session/SessionManager.cs @@ -571,6 +571,17 @@ protected virtual ISession CreateSession( m_maxHistoryContinuationPoints); } + /// + public virtual void RaiseSessionDiagnosticsChangedEvent(ISession session) + { + if (session == null) + { + throw new ArgumentNullException(nameof(session)); + } + + RaiseSessionEvent(session, SessionEventReason.DiagnosticsChanged); + } + /// /// Raises an event related to a session. /// @@ -592,6 +603,9 @@ protected virtual void RaiseSessionEvent(ISession session, SessionEventReason re case SessionEventReason.Closing: handler = m_SessionClosing; break; + case SessionEventReason.DiagnosticsChanged: + handler = m_SessionDiagnosticsChanged; + break; case SessionEventReason.ChannelKeepAlive: handler = m_SessionChannelKeepAlive; break; @@ -688,6 +702,7 @@ await m_server.CloseSessionAsync(null, session.Id, false) private event SessionEventHandler m_SessionCreated; private event SessionEventHandler m_SessionActivated; private event SessionEventHandler m_SessionClosing; + private event SessionEventHandler m_SessionDiagnosticsChanged; private event SessionEventHandler m_SessionChannelKeepAlive; private event ImpersonateEventHandler m_ImpersonateUser; private event EventHandler m_ValidateSessionLessRequest; @@ -730,6 +745,25 @@ public event SessionEventHandler SessionActivated } } + /// + public event SessionEventHandler SessionDiagnosticsChanged + { + add + { + lock (m_eventLock) + { + m_SessionDiagnosticsChanged += value; + } + } + remove + { + lock (m_eventLock) + { + m_SessionDiagnosticsChanged -= value; + } + } + } + /// public event SessionEventHandler SessionClosing { diff --git a/Tests/Opc.Ua.Server.Tests/SessionTests.cs b/Tests/Opc.Ua.Server.Tests/SessionTests.cs new file mode 100644 index 000000000..8812489bd --- /dev/null +++ b/Tests/Opc.Ua.Server.Tests/SessionTests.cs @@ -0,0 +1,95 @@ +using System.Threading.Tasks; +using NUnit.Framework; + +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Server.Tests +{ + [TestFixture] + [Category("Event")] + public class SessionTests + { + [Test] + public async Task UpdateDiagnosticCounters_RaisesEvent_WhenPerRequestCounterChanged() + { + var fixture = new ServerFixture(); + await fixture.StartAsync().ConfigureAwait(false); + + try + { + StandardServer server = fixture.Server; + + (RequestHeader requestHeader, SecureChannelContext secureChannelContext) = + await ServerFixtureUtils.CreateAndActivateSessionAsync(server, "UpdateDiagnosticCountersTest").ConfigureAwait(false); + + ISession session = server.CurrentInstance.SessionManager.GetSession(requestHeader.AuthenticationToken); + Assert.NotNull(session, "Session should exist after Create/Activate."); + + bool eventRaised = false; + + server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason) + => eventRaised = true; + + uint before = session.SessionDiagnostics.ReadCount.TotalCount; + + // Call ValidateRequest for a request type that maps to a counter (Read). + session.ValidateRequest(requestHeader, secureChannelContext, RequestType.Read); + + Assert.IsTrue(eventRaised, "SessionDiagnosticsChanged event should be raised when a per-request counter changes."); + Assert.Greater(session.SessionDiagnostics.ReadCount.TotalCount, before, "ReadCount.TotalCount should have incremented."); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + [Test] + [Category("Event")] + [TestCase(RequestType.Unknown)] + [TestCase(RequestType.FindServers)] + [TestCase(RequestType.GetEndpoints)] + [TestCase(RequestType.CreateSession)] + [TestCase(RequestType.ActivateSession)] + [TestCase(RequestType.CloseSession)] + [TestCase(RequestType.Cancel)] + public async Task UpdateDiagnosticCounters_DoesNotRaiseEvent_ForIgnoredRequestTypes(RequestType requestType) + { + var fixture = new ServerFixture(); + await fixture.StartAsync().ConfigureAwait(false); + + try + { + StandardServer server = fixture.Server; + + (RequestHeader requestHeader, SecureChannelContext secureChannelContext) = + await ServerFixtureUtils.CreateAndActivateSessionAsync(server, "UpdateDiagnosticCountersIgnoredTest").ConfigureAwait(false); + + ISession session = server.CurrentInstance.SessionManager.GetSession(requestHeader.AuthenticationToken); + Assert.NotNull(session, "Session should exist after Create/Activate."); + + bool eventRaised = false; + + server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason) + => eventRaised = true; + + // Capture total requests before; UpdateDiagnosticCounters always increments TotalRequestCount. + uint totalBefore = session.SessionDiagnostics.TotalRequestCount.TotalCount; + + // Call ValidateRequest with one of the ignored request types. + session.ValidateRequest(requestHeader, secureChannelContext, requestType); + + Assert.AreEqual( + totalBefore + 1, + session.SessionDiagnostics.TotalRequestCount.TotalCount, + "TotalRequestCount should increment for all request types."); + + Assert.IsFalse(eventRaised, $"SessionDiagnosticsChanged event must NOT be raised for request type {requestType}."); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + } +}