Skip to content

Commit 4fa7a42

Browse files
authored
Merge pull request #1376 from PepperDash/client-id-issues
fix: handle subsequent join calls and clientid/websocket client mismatches
2 parents 3fb30d5 + 9bad3ae commit 4fa7a42

2 files changed

Lines changed: 94 additions & 23 deletions

File tree

src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ public class MobileControlWebsocketServer : EssentialsDevice
6969
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
7070

7171
/// <summary>
72-
/// Stores queues of pending client IDs per token for legacy clients (FIFO)
73-
/// This ensures thread-safety when multiple legacy clients use the same token
72+
/// Stores pending client registrations with timestamp for legacy clients
73+
/// Key is token, Value is list of (clientId, timestamp) tuples
74+
/// Most recent registration is used to handle duplicate join requests
7475
/// </summary>
75-
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> legacyClientIdQueues = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
76+
private readonly ConcurrentDictionary<string, ConcurrentBag<(string clientId, DateTime timestamp)>> legacyClientRegistrations = new ConcurrentDictionary<string, ConcurrentBag<(string, DateTime)>>();
7677

7778
/// <summary>
7879
/// Gets the collection of UI clients
@@ -736,15 +737,23 @@ private void GenerateClientTokenFromConsole(string s)
736737

737738
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
738739
{
739-
// Dequeue the next clientId for legacy client support (FIFO per token)
740+
// Get the most recent unused clientId for this token (legacy support)
740741
// New clients will override this ID in OnOpen with the validated query parameter value
741742
var clientId = "pending";
742-
if (legacyClientIdQueues.TryGetValue(key, out var queue) && queue.TryDequeue(out var dequeuedId))
743+
if (legacyClientRegistrations.TryGetValue(key, out var registrations))
743744
{
744-
clientId = dequeuedId;
745-
this.LogVerbose("Dequeued legacy clientId {clientId} for token {token}", clientId, key);
745+
// Get most recent registration
746+
var sorted = registrations.OrderByDescending(r => r.timestamp).ToList();
747+
if (sorted.Any())
748+
{
749+
clientId = sorted.First().clientId;
750+
// Remove it from the bag
751+
var newBag = new ConcurrentBag<(string, DateTime)>(sorted.Skip(1));
752+
legacyClientRegistrations.TryUpdate(key, newBag, registrations);
753+
this.LogVerbose("Assigned most recent legacy clientId {clientId} for token {token}", clientId, key);
754+
}
746755
}
747-
756+
748757
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
749758
this.LogInformation("Constructing UiClient with key {key} and temporary ID (will be set from query param)", key);
750759
c.Controller = _parent;
@@ -753,8 +762,8 @@ private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
753762
c.Server = this; // Give UiClient access to server for ID registration
754763

755764
// Don't add to uiClients yet - will be added in OnOpen after ID is set from query param
756-
757-
c.ConnectionClosed += (o, a) =>
765+
766+
c.ConnectionClosed += (o, a) =>
758767
{
759768
uiClients.TryRemove(a.ClientId, out _);
760769
// Clean up any pending registrations for this token
@@ -765,11 +774,11 @@ private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
765774
{
766775
pendingClientRegistrations.TryRemove(k, out _);
767776
}
768-
769-
// Clean up legacy queue if empty
770-
if (legacyClientIdQueues.TryGetValue(key, out var legacyQueue) && legacyQueue.IsEmpty)
777+
778+
// Clean up legacy registrations if empty
779+
if (legacyClientRegistrations.TryGetValue(key, out var legacyBag) && legacyBag.IsEmpty)
771780
{
772-
legacyClientIdQueues.TryRemove(key, out _);
781+
legacyClientRegistrations.TryRemove(key, out _);
773782
}
774783
};
775784
return c;
@@ -785,7 +794,7 @@ private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
785794
public bool RegisterUiClient(UiClient client, string clientId, string tokenKey)
786795
{
787796
var registrationKey = $"{tokenKey}-{clientId}";
788-
797+
789798
// Verify this clientId was generated during a join request for this token
790799
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
791800
{
@@ -799,11 +808,63 @@ public bool RegisterUiClient(UiClient client, string clientId, string tokenKey)
799808
this.LogWarning("Replacing existing client with duplicate id {id}", id);
800809
return client;
801810
});
802-
811+
803812
this.LogInformation("Successfully registered UiClient with ID {clientId} for token {token}", clientId, tokenKey);
804813
return true;
805814
}
806815

816+
/// <summary>
817+
/// Updates a client's ID when a mismatch is detected between stored ID and message ID
818+
/// </summary>
819+
/// <param name="oldClientId">The current/old client ID</param>
820+
/// <param name="newClientId">The new client ID from the message</param>
821+
/// <param name="tokenKey">The token key for validation</param>
822+
/// <returns>True if update successful, false otherwise</returns>
823+
public bool UpdateClientId(string oldClientId, string newClientId, string tokenKey)
824+
{
825+
if (string.IsNullOrEmpty(oldClientId) || string.IsNullOrEmpty(newClientId))
826+
{
827+
this.LogWarning("Cannot update client ID with null or empty values");
828+
return false;
829+
}
830+
831+
if (oldClientId == newClientId)
832+
{
833+
return true; // No update needed
834+
}
835+
836+
// Verify the new clientId was registered for this token
837+
var registrationKey = $"{tokenKey}-{newClientId}";
838+
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
839+
{
840+
this.LogWarning("Cannot update to unregistered clientId {newClientId} for token {token}", newClientId, tokenKey);
841+
return false;
842+
}
843+
844+
// Get the existing client
845+
if (!uiClients.TryRemove(oldClientId, out var client))
846+
{
847+
this.LogWarning("Cannot find client with old ID {oldClientId}", oldClientId);
848+
return false;
849+
}
850+
851+
// Update the client's ID
852+
client.UpdateId(newClientId);
853+
854+
// Re-add with new ID
855+
if (!uiClients.TryAdd(newClientId, client))
856+
{
857+
// If add fails, try to restore old entry
858+
uiClients.TryAdd(oldClientId, client);
859+
client.UpdateId(oldClientId);
860+
this.LogError("Failed to update client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
861+
return false;
862+
}
863+
864+
this.LogInformation("Successfully updated client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
865+
return true;
866+
}
867+
807868
/// <summary>
808869
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
809870
/// </summary>
@@ -821,7 +882,7 @@ public void RegisterLegacyUiClient(UiClient client)
821882
this.LogWarning("Replacing existing client with duplicate id {id} (legacy flow)", id);
822883
return client;
823884
});
824-
885+
825886
this.LogInformation("Successfully registered UiClient with ID {clientId} using legacy flow", client.Id);
826887
}
827888

@@ -1133,16 +1194,17 @@ private void HandleJoinRequest(HttpListenerRequest req, HttpListenerResponse res
11331194

11341195
// Generate a client ID for this join request
11351196
var clientId = $"{Utilities.GetNextClientId()}";
1136-
1197+
var now = DateTime.UtcNow;
1198+
11371199
// Store in pending registrations for new clients that send clientId via query param
11381200
var registrationKey = $"{token}-{clientId}";
11391201
pendingClientRegistrations.TryAdd(registrationKey, clientId);
1140-
1141-
// Also enqueue for legacy clients (thread-safe FIFO per token)
1142-
var queue = legacyClientIdQueues.GetOrAdd(token, _ => new ConcurrentQueue<string>());
1143-
queue.Enqueue(clientId);
11441202

1145-
this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token);
1203+
// For legacy clients, store with timestamp instead of FIFO queue
1204+
var legacyBag = legacyClientRegistrations.GetOrAdd(token, _ => new ConcurrentBag<(string, DateTime)>());
1205+
legacyBag.Add((clientId, now));
1206+
1207+
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
11461208

11471209
// Construct WebSocket URL with clientId query parameter
11481210
var wsProtocol = "ws";

src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ public class UiClient : WebSocketBehavior, IKeyed
2626
/// </summary>
2727
public string Id { get; private set; }
2828

29+
/// <summary>
30+
/// Updates the client ID - only accessible from within the assembly (e.g., by the server)
31+
/// </summary>
32+
/// <param name="newId">The new client ID</param>
33+
internal void UpdateId(string newId)
34+
{
35+
Id = newId;
36+
}
37+
2938
/// <summary>
3039
/// Token associated with this client
3140
/// </summary>

0 commit comments

Comments
 (0)