mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-16 05:05:00 +00:00
Merge pull request #1376 from PepperDash/client-id-issues
fix: handle subsequent join calls and clientid/websocket client mismatches
This commit is contained in:
@@ -69,10 +69,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
|
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores queues of pending client IDs per token for legacy clients (FIFO)
|
/// Stores pending client registrations with timestamp for legacy clients
|
||||||
/// This ensures thread-safety when multiple legacy clients use the same token
|
/// Key is token, Value is list of (clientId, timestamp) tuples
|
||||||
|
/// Most recent registration is used to handle duplicate join requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> legacyClientIdQueues = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
|
private readonly ConcurrentDictionary<string, ConcurrentBag<(string clientId, DateTime timestamp)>> legacyClientRegistrations = new ConcurrentDictionary<string, ConcurrentBag<(string, DateTime)>>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of UI clients
|
/// Gets the collection of UI clients
|
||||||
@@ -736,13 +737,21 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
|
|
||||||
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
|
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
|
||||||
{
|
{
|
||||||
// Dequeue the next clientId for legacy client support (FIFO per token)
|
// Get the most recent unused clientId for this token (legacy support)
|
||||||
// New clients will override this ID in OnOpen with the validated query parameter value
|
// New clients will override this ID in OnOpen with the validated query parameter value
|
||||||
var clientId = "pending";
|
var clientId = "pending";
|
||||||
if (legacyClientIdQueues.TryGetValue(key, out var queue) && queue.TryDequeue(out var dequeuedId))
|
if (legacyClientRegistrations.TryGetValue(key, out var registrations))
|
||||||
{
|
{
|
||||||
clientId = dequeuedId;
|
// Get most recent registration
|
||||||
this.LogVerbose("Dequeued legacy clientId {clientId} for token {token}", clientId, key);
|
var sorted = registrations.OrderByDescending(r => r.timestamp).ToList();
|
||||||
|
if (sorted.Any())
|
||||||
|
{
|
||||||
|
clientId = sorted.First().clientId;
|
||||||
|
// Remove it from the bag
|
||||||
|
var newBag = new ConcurrentBag<(string, DateTime)>(sorted.Skip(1));
|
||||||
|
legacyClientRegistrations.TryUpdate(key, newBag, registrations);
|
||||||
|
this.LogVerbose("Assigned most recent legacy clientId {clientId} for token {token}", clientId, key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
|
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
|
||||||
@@ -766,10 +775,10 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
pendingClientRegistrations.TryRemove(k, out _);
|
pendingClientRegistrations.TryRemove(k, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up legacy queue if empty
|
// Clean up legacy registrations if empty
|
||||||
if (legacyClientIdQueues.TryGetValue(key, out var legacyQueue) && legacyQueue.IsEmpty)
|
if (legacyClientRegistrations.TryGetValue(key, out var legacyBag) && legacyBag.IsEmpty)
|
||||||
{
|
{
|
||||||
legacyClientIdQueues.TryRemove(key, out _);
|
legacyClientRegistrations.TryRemove(key, out _);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return c;
|
return c;
|
||||||
@@ -804,6 +813,58 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a client's ID when a mismatch is detected between stored ID and message ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oldClientId">The current/old client ID</param>
|
||||||
|
/// <param name="newClientId">The new client ID from the message</param>
|
||||||
|
/// <param name="tokenKey">The token key for validation</param>
|
||||||
|
/// <returns>True if update successful, false otherwise</returns>
|
||||||
|
public bool UpdateClientId(string oldClientId, string newClientId, string tokenKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(oldClientId) || string.IsNullOrEmpty(newClientId))
|
||||||
|
{
|
||||||
|
this.LogWarning("Cannot update client ID with null or empty values");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldClientId == newClientId)
|
||||||
|
{
|
||||||
|
return true; // No update needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the new clientId was registered for this token
|
||||||
|
var registrationKey = $"{tokenKey}-{newClientId}";
|
||||||
|
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
|
||||||
|
{
|
||||||
|
this.LogWarning("Cannot update to unregistered clientId {newClientId} for token {token}", newClientId, tokenKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the existing client
|
||||||
|
if (!uiClients.TryRemove(oldClientId, out var client))
|
||||||
|
{
|
||||||
|
this.LogWarning("Cannot find client with old ID {oldClientId}", oldClientId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the client's ID
|
||||||
|
client.UpdateId(newClientId);
|
||||||
|
|
||||||
|
// Re-add with new ID
|
||||||
|
if (!uiClients.TryAdd(newClientId, client))
|
||||||
|
{
|
||||||
|
// If add fails, try to restore old entry
|
||||||
|
uiClients.TryAdd(oldClientId, client);
|
||||||
|
client.UpdateId(oldClientId);
|
||||||
|
this.LogError("Failed to update client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.LogInformation("Successfully updated client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
|
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1133,16 +1194,17 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
|
|
||||||
// Generate a client ID for this join request
|
// Generate a client ID for this join request
|
||||||
var clientId = $"{Utilities.GetNextClientId()}";
|
var clientId = $"{Utilities.GetNextClientId()}";
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
// Store in pending registrations for new clients that send clientId via query param
|
// Store in pending registrations for new clients that send clientId via query param
|
||||||
var registrationKey = $"{token}-{clientId}";
|
var registrationKey = $"{token}-{clientId}";
|
||||||
pendingClientRegistrations.TryAdd(registrationKey, clientId);
|
pendingClientRegistrations.TryAdd(registrationKey, clientId);
|
||||||
|
|
||||||
// Also enqueue for legacy clients (thread-safe FIFO per token)
|
// For legacy clients, store with timestamp instead of FIFO queue
|
||||||
var queue = legacyClientIdQueues.GetOrAdd(token, _ => new ConcurrentQueue<string>());
|
var legacyBag = legacyClientRegistrations.GetOrAdd(token, _ => new ConcurrentBag<(string, DateTime)>());
|
||||||
queue.Enqueue(clientId);
|
legacyBag.Add((clientId, now));
|
||||||
|
|
||||||
this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token);
|
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
||||||
|
|
||||||
// Construct WebSocket URL with clientId query parameter
|
// Construct WebSocket URL with clientId query parameter
|
||||||
var wsProtocol = "ws";
|
var wsProtocol = "ws";
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Id { get; private set; }
|
public string Id { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the client ID - only accessible from within the assembly (e.g., by the server)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newId">The new client ID</param>
|
||||||
|
internal void UpdateId(string newId)
|
||||||
|
{
|
||||||
|
Id = newId;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Token associated with this client
|
/// Token associated with this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user