From 8525134ae70d508336181cca8f86088003945b65 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 09:50:12 -0500 Subject: [PATCH] fix: direct server clients now have unique client IDs Using the generated security token as an ID was presenting problems with duplicate connections using the same ID and trying to figure out where to send the messages. Now, the clients have a unique ID that's an increasing integer that restarts at 1 when the program restarts. --- .../MessageToClients.cs | 90 ++++++ .../MobileControlSystemController.cs | 28 +- .../TransmitMessage.cs | 97 ++---- .../Utilities.cs | 97 ++++++ .../WebApiHandlers/MobileInfoHandler.cs | 12 +- .../WebApiHandlers/UiClientHandler.cs | 4 +- .../ConnectionClosedEventArgs.cs | 24 ++ .../WebSocketServer/JoinToken.cs | 4 + .../MobileControlWebsocketServer.cs | 286 ++++++++++-------- .../WebSocketServer/UiClient.cs | 57 +++- 10 files changed, 444 insertions(+), 255 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl/MessageToClients.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Utilities.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs diff --git a/src/PepperDash.Essentials.MobileControl/MessageToClients.cs b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs new file mode 100644 index 00000000..e1aa5eb5 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.Queues; +using PepperDash.Essentials.WebSocketServer; +using Serilog.Events; + +namespace PepperDash.Essentials +{ + /// + /// Represents a MessageToClients + /// + public class MessageToClients : IQueueMessage + { + private readonly MobileControlWebsocketServer _server; + private readonly object msgToSend; + + /// + /// Message to send to Direct Server Clients + /// + /// message object to send + /// WebSocket server instance + public MessageToClients(object msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + /// + /// Message to send to Direct Server Clients + /// + /// message object to send + /// WebSocket server instance + public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + #region Implementation of IQueueMessage + + /// + /// Dispatch method + /// + public void Dispatch() + { + try + { + if (_server == null) + { + Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); + return; + } + + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + var clientSpecificMessage = msgToSend as MobileControlMessage; + if (clientSpecificMessage.ClientId != null) + { + var clientId = clientSpecificMessage.ClientId; + + _server.LogVerbose("Message TX To client {clientId}: {message}", clientId, message); + + _server.SendMessageToClient(clientId, message); + + return; + } + + _server.SendMessageToAllClients(message); + + _server.LogVerbose("Message TX To all clients: {message}", message); + } + catch (ThreadAbortException) + { + //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + } + } + #endregion + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index d6a40ede..0b320185 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1382,33 +1382,13 @@ namespace PepperDash.Essentials { Log = { - Output = (data, message) => - { - switch (data.Level) - { - case LogLevel.Trace: - this.LogVerbose(data.Message); - break; - case LogLevel.Debug: - this.LogDebug(data.Message); - break; - case LogLevel.Info: - this.LogInformation(data.Message); - break; - case LogLevel.Warn: - this.LogWarning(data.Message); - break; - case LogLevel.Error: - this.LogError(data.Message); - break; - case LogLevel.Fatal: - this.LogFatal(data.Message); - break; - } - } + Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this) } }; + // setting to trace to let level be controlled by appdebug + _wsClient2.Log.Level = LogLevel.Trace; + _wsClient2.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12; diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 38f7944f..06595a9d 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -1,13 +1,9 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Core; -using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.Queues; -using PepperDash.Essentials.WebSocketServer; -using Serilog.Events; -using System; -using System.Threading; using WebSocketSharp; namespace PepperDash.Essentials @@ -20,12 +16,22 @@ namespace PepperDash.Essentials private readonly WebSocket _ws; private readonly object msgToSend; + /// + /// Initialize a message to send + /// + /// message object to send + /// WebSocket instance public TransmitMessage(object msg, WebSocket ws) { _ws = ws; msgToSend = msg; } + /// + /// Initialize a message to send + /// + /// message object to send + /// WebSocket instance public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws) { _ws = ws; @@ -43,13 +49,13 @@ namespace PepperDash.Essentials { if (_ws == null) { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null"); + Debug.LogWarning("Cannot send message. Websocket client is null"); return; } if (!_ws.IsAlive) { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is not connected"); + Debug.LogWarning("Cannot send message. Websocket client is not connected"); return; } @@ -57,83 +63,14 @@ namespace PepperDash.Essentials var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message); + Debug.LogVerbose("Message TX: {0}", message); _ws.Send(message); - - } catch (Exception ex) { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); - } - } - #endregion - } - - - - /// - /// Represents a MessageToClients - /// - public class MessageToClients : IQueueMessage - { - private readonly MobileControlWebsocketServer _server; - private readonly object msgToSend; - - public MessageToClients(object msg, MobileControlWebsocketServer server) - { - _server = server; - msgToSend = msg; - } - - public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) - { - _server = server; - msgToSend = msg; - } - - #region Implementation of IQueueMessage - - /// - /// Dispatch method - /// - public void Dispatch() - { - try - { - if (_server == null) - { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); - return; - } - - var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - - var clientSpecificMessage = msgToSend as MobileControlMessage; - if (clientSpecificMessage.ClientId != null) - { - var clientId = clientSpecificMessage.ClientId; - - _server.LogVerbose("Message TX To client {clientId} Message: {message}", clientId, message); - - _server.SendMessageToClient(clientId, message); - - return; - } - - _server.SendMessageToAllClients(message); - - _server.LogVerbose("Message TX To all clients: {message}", message); - } - catch (ThreadAbortException) - { - //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + Debug.LogError("Caught an exception in the Transmit Processor: {message}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); } } #endregion diff --git a/src/PepperDash.Essentials.MobileControl/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs new file mode 100644 index 00000000..83ebc5bc --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Utilities.cs @@ -0,0 +1,97 @@ +using PepperDash.Core; +using PepperDash.Core.Logging; +using WebSocketSharp; + +namespace PepperDash.Essentials +{ + /// + /// Utility functions for logging and other common tasks. + /// + public static class Utilities + { + private static int nextClientId = 0; + + /// + /// Get + /// + /// + public static int GetNextClientId() + { + nextClientId++; + return nextClientId; + } + /// + /// Converts a WebSocketServer LogData object to Essentials logging calls. + /// + /// The LogData object to convert. + /// The log message. + /// The device associated with the log message. + public static void ConvertWebsocketLog(LogData data, string message, IKeyed device = null) + { + + switch (data.Level) + { + case LogLevel.Trace: + if (device == null) + { + Debug.LogVerbose(message); + } + else + { + device.LogVerbose(message); + } + break; + case LogLevel.Debug: + if (device == null) + { + Debug.LogDebug(message); + } + else + { + device.LogDebug(message); + } + break; + case LogLevel.Info: + if (device == null) + { + Debug.LogInformation(message); + } + else + { + device.LogInformation(message); + } + break; + case LogLevel.Warn: + if (device == null) + { + Debug.LogWarning(message); + } + else + { + device.LogWarning(message); + } + break; + case LogLevel.Error: + if (device == null) + { + Debug.LogError(message); + } + else + { + device.LogError(message); + } + break; + case LogLevel.Fatal: + if (device == null) + { + Debug.LogFatal(message); + } + else + { + device.LogFatal(message); + } + break; + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index d123dbcd..588d3861 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -1,12 +1,12 @@ -using Crestron.SimplSharp.WebScripting; +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.WebSocketServer; -using System; -using System.Collections.Generic; -using System.Linq; namespace PepperDash.Essentials.WebApiHandlers { @@ -111,13 +111,13 @@ namespace PepperDash.Essentials.WebApiHandlers public int ServerPort => directServer.Port; [JsonProperty("tokensDefined")] - public int TokensDefined => directServer.UiClients.Count; + public int TokensDefined => directServer.UiClientContexts.Count; [JsonProperty("clientsConnected")] public int ClientsConnected => directServer.ConnectedUiClientsCount; [JsonProperty("clients")] - public List Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); + public List Clients => directServer.UiClientContexts.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); public MobileControlDirectServer(MobileControlWebsocketServer server) { diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index e45fcc39..23b79d93 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -93,7 +93,7 @@ namespace PepperDash.Essentials.WebApiHandlers - if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext)) + if (!server.UiClientContexts.TryGetValue(request.Token, out UiClientContext clientContext)) { var response = new ClientResponse { @@ -134,7 +134,7 @@ namespace PepperDash.Essentials.WebApiHandlers return; } - server.UiClients.Remove(request.Token); + server.UiClientContexts.Remove(request.Token); server.UpdateSecret(); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs new file mode 100644 index 00000000..6d877d07 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace PepperDash.Essentials.WebSocketServer +{ + /// + /// Event Args for ConnectionClosed event + /// + public class ConnectionClosedEventArgs : EventArgs + { + /// + /// Client ID that is being closed + /// + public string ClientId { get; private set; } + + /// + /// Initalize an instance of the class. + /// + /// client that's closing + public ConnectionClosedEventArgs(string clientId) + { + ClientId = clientId; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs index b3ea3c7c..7352dc6f 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs @@ -5,6 +5,10 @@ namespace PepperDash.Essentials.WebSocketServer /// public class JoinToken { + /// + /// Unique client ID for a client that is joining + /// + public string Id { get; set; } /// /// Gets or sets the Code /// diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index d15185b0..0b0390aa 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -10,6 +10,7 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Prng; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.Core; @@ -56,7 +57,9 @@ namespace PepperDash.Essentials.WebSocketServer /// /// Gets the collection of UI client contexts /// - public Dictionary UiClients { get; private set; } + public Dictionary UiClientContexts { get; private set; } + + private readonly Dictionary uiClients = new Dictionary(); private readonly MobileControlSystemController _parent; @@ -129,7 +132,7 @@ namespace PepperDash.Essentials.WebSocketServer { var count = 0; - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) { @@ -202,7 +205,7 @@ namespace PepperDash.Essentials.WebSocketServer } - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); //_joinTokens = new Dictionary(); @@ -278,29 +281,20 @@ namespace PepperDash.Essentials.WebSocketServer } _server.Log.Output = (data, message) => + { + switch (data.Level) { - switch (data.Level) - { - case LogLevel.Trace: - this.LogVerbose(data.Message); - break; - case LogLevel.Debug: - this.LogDebug(data.Message); - break; - case LogLevel.Info: - this.LogInformation(data.Message); - break; - case LogLevel.Warn: - this.LogWarning(data.Message); - break; - case LogLevel.Error: - this.LogError(data.Message); - break; - case LogLevel.Fatal: - this.LogFatal(data.Message); - break; - } - }; + case LogLevel.Trace: this.LogVerbose(message); break; + case LogLevel.Debug: this.LogDebug(message); break; + case LogLevel.Info: this.LogInformation(message); break; + case LogLevel.Warn: this.LogWarning(message); break; + case LogLevel.Error: this.LogError(message); break; + case LogLevel.Fatal: this.LogFatal(message); break; + } + }; + + // setting to trace to allow logging level to be controlled by appdebug + _server.Log.Level = LogLevel.Trace; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; @@ -554,20 +548,20 @@ namespace PepperDash.Essentials.WebSocketServer this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey); - if (UiClients == null) + if (UiClientContexts == null) { - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); } - UiClients.Add(token.Key, new UiClientContext(token.Value)); + UiClientContexts.Add(token.Key, new UiClientContext(token.Value)); } } - if (UiClients.Count > 0) + if (UiClientContexts.Count > 0) { - this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClients.Count); + this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClientContexts.Count); - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { var key = client.Key; var path = _wsPath + key; @@ -575,13 +569,8 @@ namespace PepperDash.Essentials.WebSocketServer _server.AddWebSocketService(path, () => { - var c = new UiClient($"uiclient-{key}-{roomKey}"); - this.LogDebug("Constructing UiClient with id: {key}", key); - - c.Controller = _parent; - c.RoomKey = roomKey; - UiClients[key].SetClient(c); - return c; + this.LogInformation("Building a UiClient with ID {id}", client.Value.Token.Id); + return BuildUiClient(roomKey, client.Value.Token, key); }); } } @@ -591,7 +580,7 @@ namespace PepperDash.Essentials.WebSocketServer this.LogWarning("No secret found"); } - this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClients.Count); + this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClientContexts.Count); } catch (Exception ex) { @@ -616,7 +605,7 @@ namespace PepperDash.Essentials.WebSocketServer _secret.Tokens.Clear(); - foreach (var uiClientContext in UiClients) + foreach (var uiClientContext in UiClientContexts) { _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token); } @@ -725,21 +714,17 @@ namespace PepperDash.Essentials.WebSocketServer var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey }; - UiClients.Add(key, new UiClientContext(token)); + UiClientContexts.Add(key, new UiClientContext(token)); var path = _wsPath + key; _server.AddWebSocketService(path, () => { - var c = new UiClient($"uiclient-{key}-{bridge.RoomKey}"); - this.LogVerbose("Constructing UiClient with id: {key}", key); - c.Controller = _parent; - c.RoomKey = bridge.RoomKey; - UiClients[key].SetClient(c); - return c; + this.LogInformation("Building a UiClient with ID {id}", token.Id); + return BuildUiClient(bridge.RoomKey, token, key); }); - this.LogInformation("Added new WebSocket UiClient service at path: {path}", path); + this.LogInformation("Added new WebSocket UiClient for path: {path}", path); this.LogInformation("Token: {@token}", token); this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count); @@ -749,6 +734,44 @@ namespace PepperDash.Essentials.WebSocketServer return (key, path); } + private UiClient BuildUiClient(string roomKey, JoinToken token, string key) + { + var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id); + this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); + c.Controller = _parent; + c.RoomKey = roomKey; + + if (uiClients.ContainsKey(token.Id)) + { + this.LogWarning("removing client with duplicate id {id}", token.Id); + uiClients.Remove(token.Id); + } + uiClients.Add(token.Id, c); + // UiClients[key].SetClient(c); + c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); + token.Id = null; + return c; + } + + /// + /// Prints out the session data for each path + /// + public void PrintSessionData() + { + foreach (var path in _server.WebSocketServices.Paths) + { + this.LogInformation("Path: {path}", path); + this.LogInformation(" Session Count: {sessionCount}", _server.WebSocketServices[path].Sessions.Count); + this.LogInformation(" Active Session Count: {activeSessionCount}", _server.WebSocketServices[path].Sessions.ActiveIDs.Count()); + this.LogInformation(" Inactive Session Count: {inactiveSessionCount}", _server.WebSocketServices[path].Sessions.InactiveIDs.Count()); + this.LogInformation(" Active Clients:"); + foreach (var session in _server.WebSocketServices[path].Sessions.IDs) + { + this.LogInformation(" Client ID: {id}", (_server.WebSocketServices[path].Sessions[session] as UiClient)?.Id); + } + } + } + /// /// Removes all clients from the server /// @@ -766,7 +789,7 @@ namespace PepperDash.Essentials.WebSocketServer return; } - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) { @@ -784,7 +807,7 @@ namespace PepperDash.Essentials.WebSocketServer } } - UiClients.Clear(); + UiClientContexts.Clear(); UpdateSecret(); } @@ -803,9 +826,9 @@ namespace PepperDash.Essentials.WebSocketServer var key = s; - if (UiClients.ContainsKey(key)) + if (UiClientContexts.ContainsKey(key)) { - var uiClientContext = UiClients[key]; + var uiClientContext = UiClientContexts[key]; if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive) { @@ -815,7 +838,7 @@ namespace PepperDash.Essentials.WebSocketServer var path = _wsPath + key; if (_server.RemoveWebSocketService(path)) { - UiClients.Remove(key); + UiClientContexts.Remove(key); UpdateSecret(); @@ -839,9 +862,9 @@ namespace PepperDash.Essentials.WebSocketServer { CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r"); - CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count)); + CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClientContexts.Count)); - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key)); } @@ -851,7 +874,7 @@ namespace PepperDash.Essentials.WebSocketServer { if (programEventType == eProgramStatusEventType.Stopping) { - foreach (var client in UiClients.Values) + foreach (var client in UiClientContexts.Values) { if (client.Client != null && client.Client.Context.WebSocket.IsAlive) { @@ -990,77 +1013,81 @@ namespace PepperDash.Essentials.WebSocketServer this.LogVerbose("Join Room Request with token: {token}", token); + byte[] body; - if (UiClients.TryGetValue(token, out UiClientContext clientContext)) - { - var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); - - if (bridge != null) - { - res.StatusCode = 200; - res.ContentType = "application/json"; - - var devices = DeviceManager.GetDevices(); - Dictionary deviceInterfaces = new Dictionary(); - - foreach (var device in devices) - { - var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); - deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo - { - Key = device.Key, - Name = device is IKeyName ? (device as IKeyName).Name : "", - Interfaces = interfaces - }); - } - - // Construct the response object - JoinResponse jRes = new JoinResponse - { - ClientId = token, - RoomKey = bridge.RoomKey, - SystemUuid = _parent.SystemUuid, - RoomUuid = _parent.SystemUuid, - Config = _parent.GetConfigWithPluginVersion(), - CodeExpires = new DateTime().AddYears(1), - UserCode = bridge.UserCode, - UserAppUrl = string.Format("http://{0}:{1}/mc/app", - CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), - Port), - EnableDebug = false, - DeviceInterfaceSupport = deviceInterfaces - }; - - // Serialize to JSON and convert to Byte[] - var json = JsonConvert.SerializeObject(jRes); - var body = Encoding.UTF8.GetBytes(json); - res.ContentLength64 = body.LongLength; - - // Send the response - res.Close(body, true); - } - else - { - var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); - res.StatusCode = 404; - res.ContentType = "application/json"; - this.LogVerbose("{message}", message); - var body = Encoding.UTF8.GetBytes(message); - res.ContentLength64 = body.LongLength; - res.Close(body, true); - - } - } - else + if (!UiClientContexts.TryGetValue(token, out UiClientContext clientContext)) { var message = "Token invalid or has expired"; res.StatusCode = 401; res.ContentType = "application/json"; this.LogVerbose("{message}", message); - var body = Encoding.UTF8.GetBytes(message); + body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); + return; } + + var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); + + if (bridge == null) + { + var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); + res.StatusCode = 404; + res.ContentType = "application/json"; + this.LogVerbose("{message}", message); + body = Encoding.UTF8.GetBytes(message); + res.ContentLength64 = body.LongLength; + res.Close(body, true); + return; + } + + res.StatusCode = 200; + res.ContentType = "application/json"; + + var devices = DeviceManager.GetDevices(); + Dictionary deviceInterfaces = new Dictionary(); + + foreach (var device in devices) + { + var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); + + deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? "", + Interfaces = interfaces + }); + } + + var clientId = $"{Utilities.GetNextClientId()}"; + clientContext.Token.Id = clientId; + + this.LogVerbose("Assigning ClientId: {clientId}", clientId); + + // Construct the response object + JoinResponse jRes = new JoinResponse + { + ClientId = clientId, + RoomKey = bridge.RoomKey, + SystemUuid = _parent.SystemUuid, + RoomUuid = _parent.SystemUuid, + Config = _parent.GetConfigWithPluginVersion(), + CodeExpires = new DateTime().AddYears(1), + UserCode = bridge.UserCode, + UserAppUrl = string.Format("http://{0}:{1}/mc/app", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), + Port), + EnableDebug = false, + DeviceInterfaceSupport = deviceInterfaces + }; + + // Serialize to JSON and convert to Byte[] + var json = JsonConvert.SerializeObject(jRes); + body = Encoding.UTF8.GetBytes(json); + res.ContentLength64 = body.LongLength; + + // Send the response + res.Close(body, true); } /// @@ -1242,12 +1269,14 @@ namespace PepperDash.Essentials.WebSocketServer /// public void SendMessageToAllClients(string message) { - foreach (var clientContext in UiClients.Values) + foreach (var client in uiClients.Values) { - if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive) + if (!client.Context.WebSocket.IsAlive) { - clientContext.Client.Context.WebSocket.Send(message); + continue; } + + client.Context.WebSocket.Send(message); } } @@ -1266,17 +1295,16 @@ namespace PepperDash.Essentials.WebSocketServer return; } - if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext)) + if (uiClients.TryGetValue((string)clientId, out var client)) { - if (clientContext.Client != null) - { - var socket = clientContext.Client.Context.WebSocket; + var socket = client.Context.WebSocket; - if (socket.IsAlive) - { - socket.Send(message); - } + if (!socket.IsAlive) + { + this.LogError("Unable to send message to client {id}. Client is disconnected: {message}", clientId, message); + return; } + socket.Send(message); } else { diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 3cdd183b..79becd33 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -64,9 +64,11 @@ namespace PepperDash.Essentials.WebSocketServer /// Initializes a new instance of the UiClient class with the specified key /// /// The unique key to identify this client - public UiClient(string key) + /// The client ID used by the client for this connection + public UiClient(string key, string id) { Key = key; + Id = id; } /// @@ -74,19 +76,33 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnOpen(); - var url = Context.WebSocket.Url; - this.LogInformation("New WebSocket Connection from: {url}", url); + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); + Log.Level = LogLevel.Trace; - var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); - - if (!match.Success) + try { - _connectionTime = DateTime.Now; - return; + this.LogDebug("Current session count on open {count}", Sessions.Count); + this.LogDebug("Current WebsocketServiceCount on open: {count}", Controller.DirectServer.WebsocketServiceCount); + } + catch (Exception ex) + { + this.LogError("Error getting service count: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); } - var clientId = match.Groups[1].Value; - _clientId = clientId; + // var url = Context.WebSocket.Url; + // this.LogInformation("New WebSocket Connection from: {url}", url); + + // var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); + + // if (!match.Success) + // { + // _connectionTime = DateTime.Now; + // return; + // } + + // var clientId = match.Groups[1].Value; + // _clientId = clientId; if (Controller == null) { @@ -99,7 +115,7 @@ namespace PepperDash.Essentials.WebSocketServer Type = "/system/clientJoined", Content = JToken.FromObject(new { - clientId, + clientId = Id, roomKey = RoomKey, }) }; @@ -110,7 +126,7 @@ namespace PepperDash.Essentials.WebSocketServer if (bridge == null) return; - SendUserCodeToClient(bridge, clientId); + SendUserCodeToClient(bridge, Id); bridge.UserCodeChanged -= Bridge_UserCodeChanged; bridge.UserCodeChanged += Bridge_UserCodeChanged; @@ -168,17 +184,30 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnClose(e); + try + { + this.LogDebug("Current session count on close {count}", Sessions.Count); + this.LogDebug("Current WebsocketServiceCount on close: {count}", Controller.DirectServer.WebsocketServiceCount); + } + catch (Exception ex) + { + this.LogError("Error getting service count: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); + } + this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); foreach (var messenger in Controller.Messengers) { - messenger.Value.UnsubscribeClient(_clientId); + messenger.Value.UnsubscribeClient(Id); } foreach (var messenger in Controller.DefaultMessengers) { - messenger.Value.UnsubscribeClient(_clientId); + messenger.Value.UnsubscribeClient(Id); } + + ConnectionClosed?.Invoke(this, new ConnectionClosedEventArgs(Id)); } ///