LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
174 lines
5.9 KiB
C#
174 lines
5.9 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Serilog;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Host.Sessions
|
|
{
|
|
/// <summary>
|
|
/// Tracks active client sessions in memory.
|
|
/// Thread-safe via ConcurrentDictionary.
|
|
/// </summary>
|
|
public sealed class SessionManager : IDisposable
|
|
{
|
|
private static readonly ILogger Log = Serilog.Log.ForContext<SessionManager>();
|
|
|
|
private readonly ConcurrentDictionary<string, SessionInfo> _sessions
|
|
= new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private readonly Timer? _scavengingTimer;
|
|
private readonly TimeSpan _inactivityTimeout;
|
|
private Action<string>? _onSessionScavenged;
|
|
|
|
/// <summary>
|
|
/// Creates a SessionManager with optional inactivity scavenging.
|
|
/// </summary>
|
|
/// <param name="inactivityTimeoutMinutes">
|
|
/// Sessions inactive for this many minutes are automatically terminated.
|
|
/// Set to 0 to disable scavenging.
|
|
/// </param>
|
|
public SessionManager(int inactivityTimeoutMinutes = 5)
|
|
{
|
|
_inactivityTimeout = TimeSpan.FromMinutes(inactivityTimeoutMinutes);
|
|
|
|
if (inactivityTimeoutMinutes > 0)
|
|
{
|
|
// Check every 60 seconds
|
|
_scavengingTimer = new Timer(ScavengeInactiveSessions, null,
|
|
TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a callback invoked when a session is scavenged due to inactivity.
|
|
/// The callback receives the session ID.
|
|
/// </summary>
|
|
public void OnSessionScavenged(Action<string> callback)
|
|
{
|
|
_onSessionScavenged = callback;
|
|
}
|
|
|
|
/// <summary>Gets the count of active sessions.</summary>
|
|
public int ActiveSessionCount => _sessions.Count;
|
|
|
|
/// <summary>
|
|
/// Creates a new session.
|
|
/// Returns the 32-character hex GUID session ID.
|
|
/// </summary>
|
|
public string CreateSession(string clientId, string apiKey)
|
|
{
|
|
var sessionId = Guid.NewGuid().ToString("N"); // 32-char lowercase hex, no hyphens
|
|
var sessionInfo = new SessionInfo(sessionId, clientId, apiKey);
|
|
_sessions[sessionId] = sessionInfo;
|
|
|
|
Log.Information("Session created: {SessionId} for client {ClientId}", sessionId, clientId);
|
|
return sessionId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a session ID. Updates LastActivity on success.
|
|
/// Returns true if the session exists.
|
|
/// </summary>
|
|
public bool ValidateSession(string sessionId)
|
|
{
|
|
if (_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
session.TouchLastActivity();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Terminates a session. Returns true if the session existed.
|
|
/// </summary>
|
|
public bool TerminateSession(string sessionId)
|
|
{
|
|
if (_sessions.TryRemove(sessionId, out _))
|
|
{
|
|
Log.Information("Session terminated: {SessionId}", sessionId);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Gets session info by ID, or null if not found.</summary>
|
|
public SessionInfo? GetSession(string sessionId)
|
|
{
|
|
_sessions.TryGetValue(sessionId, out var session);
|
|
return session;
|
|
}
|
|
|
|
/// <summary>Gets a snapshot of all active sessions.</summary>
|
|
public IReadOnlyList<SessionInfo> GetAllSessions()
|
|
{
|
|
return _sessions.Values.ToList().AsReadOnly();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scavenges sessions that have been inactive for longer than the timeout.
|
|
/// </summary>
|
|
private void ScavengeInactiveSessions(object? state)
|
|
{
|
|
if (_inactivityTimeout <= TimeSpan.Zero) return;
|
|
|
|
var cutoff = DateTime.UtcNow - _inactivityTimeout;
|
|
var expired = _sessions.Where(kvp => kvp.Value.LastActivity < cutoff).ToList();
|
|
|
|
foreach (var kvp in expired)
|
|
{
|
|
if (_sessions.TryRemove(kvp.Key, out _))
|
|
{
|
|
Log.Information("Session {SessionId} scavenged (inactive since {LastActivity})",
|
|
kvp.Key, kvp.Value.LastActivity);
|
|
|
|
try
|
|
{
|
|
_onSessionScavenged?.Invoke(kvp.Key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Error in session scavenge callback for {SessionId}", kvp.Key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_scavengingTimer?.Dispose();
|
|
_sessions.Clear();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Information about an active client session.
|
|
/// </summary>
|
|
public class SessionInfo
|
|
{
|
|
public SessionInfo(string sessionId, string clientId, string apiKey)
|
|
{
|
|
SessionId = sessionId;
|
|
ClientId = clientId;
|
|
ApiKey = apiKey;
|
|
ConnectedAt = DateTime.UtcNow;
|
|
LastActivity = DateTime.UtcNow;
|
|
}
|
|
|
|
public string SessionId { get; }
|
|
public string ClientId { get; }
|
|
public string ApiKey { get; }
|
|
public DateTime ConnectedAt { get; }
|
|
public DateTime LastActivity { get; private set; }
|
|
public long ConnectedSinceUtcTicks => ConnectedAt.Ticks;
|
|
|
|
/// <summary>Updates the last activity timestamp to now.</summary>
|
|
public void TouchLastActivity()
|
|
{
|
|
LastActivity = DateTime.UtcNow;
|
|
}
|
|
}
|
|
}
|