mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-01-11 19:44:52 +00:00
fix: handle threading issues for concurrent clients joining
This commit is contained in:
@@ -1748,7 +1748,7 @@ namespace PepperDash.Essentials
|
|||||||
var clientNo = 1;
|
var clientNo = 1;
|
||||||
foreach (var clientContext in _directServer.UiClientContexts)
|
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(
|
CrestronConsole.ConsoleCommandResponse(
|
||||||
$"\r\nClient {clientNo}:\r\n" +
|
$"\r\nClient {clientNo}:\r\n" +
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Threading;
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Core.Logging;
|
using PepperDash.Core.Logging;
|
||||||
using WebSocketSharp;
|
using WebSocketSharp;
|
||||||
@@ -12,13 +13,12 @@ namespace PepperDash.Essentials
|
|||||||
private static int nextClientId = 0;
|
private static int nextClientId = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the next unique client ID
|
/// Get the next unique client ID (thread-safe)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Client ID</returns>
|
/// <returns>Client ID</returns>
|
||||||
public static int GetNextClientId()
|
public static int GetNextClientId()
|
||||||
{
|
{
|
||||||
nextClientId++;
|
return Interlocked.Increment(ref nextClientId);
|
||||||
return nextClientId;
|
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a WebSocketServer LogData object to Essentials logging calls.
|
/// Converts a WebSocketServer LogData object to Essentials logging calls.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -59,12 +60,18 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, UiClientContext> UiClientContexts { get; private set; }
|
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>
|
/// <summary>
|
||||||
/// Gets the collection of UI clients
|
/// Gets the collection of UI clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyDictionary<string, UiClient> UiClients => new ReadOnlyDictionary<string, UiClient>(uiClients);
|
public IReadOnlyDictionary<string, UiClient> UiClients => uiClients;
|
||||||
|
|
||||||
private readonly MobileControlSystemController _parent;
|
private readonly MobileControlSystemController _parent;
|
||||||
|
|
||||||
@@ -723,20 +730,46 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
|
|
||||||
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
|
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
|
||||||
{
|
{
|
||||||
var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey);
|
// Try to retrieve a pending client ID that was registered during the join request
|
||||||
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id);
|
// 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.Controller = _parent;
|
||||||
c.RoomKey = roomKey;
|
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);
|
this.LogWarning("replacing client with duplicate id {id}", id);
|
||||||
uiClients.Remove(token.Id);
|
return c;
|
||||||
}
|
});
|
||||||
uiClients.Add(token.Id, c);
|
|
||||||
// UiClients[key].SetClient(c);
|
// UiClients[key].SetClient(c);
|
||||||
c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId);
|
c.ConnectionClosed += (o, a) => uiClients.TryRemove(a.ClientId, out _);
|
||||||
token.Id = null;
|
|
||||||
return c;
|
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()}";
|
var clientId = $"{Utilities.GetNextClientId()}";
|
||||||
clientContext.Token.Id = clientId;
|
|
||||||
|
|
||||||
this.LogVerbose("Assigning ClientId: {clientId}", 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} for token: {token}", clientId, token);
|
||||||
|
|
||||||
// Construct the response object
|
// Construct the response object
|
||||||
JoinResponse jRes = new JoinResponse
|
JoinResponse jRes = new JoinResponse
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Token { get; private set; }
|
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>
|
/// <summary>
|
||||||
/// Touchpanel Key associated with this client
|
/// Touchpanel Key associated with this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -662,6 +662,7 @@ namespace PepperDash.Essentials
|
|||||||
|
|
||||||
if (jsonFiles.Length > 1)
|
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.");
|
throw new Exception("Multiple configuration files found. Cannot continue.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user