fix: handle threading issues for concurrent clients joining

This commit is contained in:
Andrew Welker
2025-12-26 12:34:31 -06:00
parent 39c1f60a4d
commit 53e7a30224
5 changed files with 60 additions and 17 deletions

View File

@@ -1748,7 +1748,7 @@ namespace PepperDash.Essentials
var clientNo = 1;
foreach (var clientContext in _directServer.UiClientContexts)
{
var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token);
var clients = _directServer.UiClients.Values.Where(c => c.TokenKey == clientContext.Key);
CrestronConsole.ConsoleCommandResponse(
$"\r\nClient {clientNo}:\r\n" +

View File

@@ -1,3 +1,4 @@
using System.Threading;
using PepperDash.Core;
using PepperDash.Core.Logging;
using WebSocketSharp;
@@ -12,13 +13,12 @@ namespace PepperDash.Essentials
private static int nextClientId = 0;
/// <summary>
/// Get the next unique client ID
/// Get the next unique client ID (thread-safe)
/// </summary>
/// <returns>Client ID</returns>
public static int GetNextClientId()
{
nextClientId++;
return nextClientId;
return Interlocked.Increment(ref nextClientId);
}
/// <summary>
/// Converts a WebSocketServer LogData object to Essentials logging calls.

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@@ -59,12 +60,18 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public Dictionary<string, UiClientContext> UiClientContexts { get; private set; }
private readonly Dictionary<string, UiClient> uiClients = new Dictionary<string, UiClient>();
private readonly ConcurrentDictionary<string, UiClient> uiClients = new ConcurrentDictionary<string, UiClient>();
/// <summary>
/// Stores pending client registrations using composite key: token-clientId
/// This ensures the correct client ID is matched even when connections establish out of order
/// </summary>
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
/// <summary>
/// Gets the collection of UI clients
/// </summary>
public ReadOnlyDictionary<string, UiClient> UiClients => new ReadOnlyDictionary<string, UiClient>(uiClients);
public IReadOnlyDictionary<string, UiClient> UiClients => uiClients;
private readonly MobileControlSystemController _parent;
@@ -723,20 +730,46 @@ namespace PepperDash.Essentials.WebSocketServer
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{
var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id);
// Try to retrieve a pending client ID that was registered during the join request
// Use the composite key: token-clientId
string clientId = null;
string registrationKey = null;
// Find a registration for this token
var matchingRegistrations = pendingClientRegistrations.Keys
.Where(k => k.StartsWith($"{key}-"))
.OrderBy(k => k) // Process in order for FIFO behavior
.ToList();
if (matchingRegistrations.Any())
{
registrationKey = matchingRegistrations.First();
if (pendingClientRegistrations.TryRemove(registrationKey, out clientId))
{
this.LogVerbose("Retrieved pending clientId {clientId} for token {key}", clientId, key);
}
}
if (clientId == null)
{
// Fallback: generate a new client ID if none was pending
clientId = $"{Utilities.GetNextClientId()}";
this.LogWarning("No pending clientId found for token {key}, generated new ID {clientId}", key, clientId);
}
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, clientId);
c.Controller = _parent;
c.RoomKey = roomKey;
c.TokenKey = key; // Store the URL token key for filtering
if (uiClients.ContainsKey(token.Id))
uiClients.AddOrUpdate(clientId, c, (id, existingClient) =>
{
this.LogWarning("removing client with duplicate id {id}", token.Id);
uiClients.Remove(token.Id);
}
uiClients.Add(token.Id, c);
this.LogWarning("replacing client with duplicate id {id}", id);
return c;
});
// UiClients[key].SetClient(c);
c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId);
token.Id = null;
c.ConnectionClosed += (o, a) => uiClients.TryRemove(a.ClientId, out _);
return c;
}
@@ -1046,10 +1079,14 @@ namespace PepperDash.Essentials.WebSocketServer
});
}
// Generate a client ID for this join request and store it with a composite key
var clientId = $"{Utilities.GetNextClientId()}";
clientContext.Token.Id = clientId;
// Store registration with composite key: token-clientId so BuildUiClient can verify it
var registrationKey = $"{token}-{clientId}";
pendingClientRegistrations.TryAdd(registrationKey, clientId);
this.LogVerbose("Assigning ClientId: {clientId}", clientId);
this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token);
// Construct the response object
JoinResponse jRes = new JoinResponse

View File

@@ -31,6 +31,11 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public string Token { get; private set; }
/// <summary>
/// The URL token key used to connect (from UiClientContexts dictionary key)
/// </summary>
public string TokenKey { get; set; }
/// <summary>
/// Touchpanel Key associated with this client
/// </summary>

View File

@@ -662,6 +662,7 @@ namespace PepperDash.Essentials
if (jsonFiles.Length > 1)
{
Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray());
throw new Exception("Multiple configuration files found. Cannot continue.");
}