feat: unique client IDs for direct server

When multiple UI applications were connecting using the same token, the actual websocket connection was getting lost, and could eventually be garbage-collected, leading to odd behavior from the UI. This is due to an existing client getting replaced and a reference to it lost. This has now been remedied, with each client getting a unique instance with a unique client ID.
This commit is contained in:
Andrew Welker
2025-07-10 10:36:47 -05:00
parent 5ff587a8c9
commit 8b098aac2c
12 changed files with 345 additions and 285 deletions

View File

@@ -0,0 +1,74 @@
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
{
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
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");
}
}
#endregion
}
}

View File

@@ -1,4 +1,10 @@
using Crestron.SimplSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.WebScripting;
@@ -30,12 +36,6 @@ using PepperDash.Essentials.RoomBridges;
using PepperDash.Essentials.Services;
using PepperDash.Essentials.WebApiHandlers;
using PepperDash.Essentials.WebSocketServer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WebSocketSharp;
namespace PepperDash.Essentials
@@ -1619,12 +1619,12 @@ Mobile Control Direct Server Information:
Tokens Defined: {0}
Clients Connected: {1}
",
_directServer.UiClients.Count,
_directServer.UiClientContexts.Count,
_directServer.ConnectedUiClientsCount
);
var clientNo = 1;
foreach (var clientContext in _directServer.UiClients)
foreach (var clientContext in _directServer.UiClientContexts)
{
var isAlive = false;
var duration = "Not Connected";

View File

@@ -1,13 +1,10 @@
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
@@ -65,66 +62,4 @@ namespace PepperDash.Essentials
#endregion
}
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
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");
}
}
#endregion
}
}

View File

@@ -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
{
@@ -99,13 +99,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<MobileControlDirectClient> Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList();
public List<MobileControlDirectClient> Clients => directServer.UiClientContexts.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList();
public MobileControlDirectServer(MobileControlWebsocketServer server)
{

View File

@@ -90,7 +90,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
{
@@ -131,7 +131,7 @@ namespace PepperDash.Essentials.WebApiHandlers
return;
}
server.UiClients.Remove(request.Token);
server.UiClientContexts.Remove(request.Token);
server.UpdateSecret();

View File

@@ -0,0 +1,39 @@
using System;
using Newtonsoft.Json;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Represents the structure of the join response
/// </summary>
public class JoinResponse
{
[JsonProperty("clientId")]
public string ClientId { get; set; }
[JsonProperty("roomKey")]
public string RoomKey { get; set; }
[JsonProperty("systemUUid")]
public string SystemUuid { get; set; }
[JsonProperty("roomUUid")]
public string RoomUuid { get; set; }
[JsonProperty("config")]
public object Config { get; set; }
[JsonProperty("codeExpires")]
public DateTime CodeExpires { get; set; }
[JsonProperty("userCode")]
public string UserCode { get; set; }
[JsonProperty("userAppUrl")]
public string UserAppUrl { get; set; }
[JsonProperty("enableDebug")]
public bool EnableDebug { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using Independentsoft.Exchange;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Represents a join token with the associated properties
/// </summary>
public class JoinToken
{
public string Code { get; set; }
public string RoomKey { get; set; }
public string Uuid { get; set; }
public string TouchpanelKey { get; set; } = "";
public string Token { get; set; } = null;
public string Id { get; set; }
}
}

View File

@@ -1,4 +1,10 @@
using Crestron.SimplSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
@@ -9,12 +15,6 @@ using PepperDash.Essentials.Core.Web;
using PepperDash.Essentials.RoomBridges;
using PepperDash.Essentials.WebApiHandlers;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using WebSocketSharp;
using WebSocketSharp.Net;
using WebSocketSharp.Server;
@@ -24,6 +24,7 @@ namespace PepperDash.Essentials.WebSocketServer
{
public class MobileControlWebsocketServer : EssentialsDevice
{
private int nextClientId = 0;
private readonly string userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator;
private readonly string localConfigFolderName = "_local-config";
@@ -40,7 +41,9 @@ namespace PepperDash.Essentials.WebSocketServer
public HttpServer Server => _server;
public Dictionary<string, UiClientContext> UiClients { get; private set; }
public Dictionary<string, UiClientContext> UiClientContexts { get; private set; }
public Dictionary<string, UiClient> UiClients { get; private set; } = new Dictionary<string, UiClient>();
private readonly MobileControlSystemController _parent;
@@ -104,7 +107,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)
{
@@ -173,7 +176,7 @@ namespace PepperDash.Essentials.WebSocketServer
}
UiClients = new Dictionary<string, UiClientContext>();
UiClientContexts = new Dictionary<string, UiClientContext>();
//_joinTokens = new Dictionary<string, JoinToken>();
@@ -378,7 +381,7 @@ namespace PepperDash.Essentials.WebSocketServer
return;
}
if(csAdapterId == -1)
if (csAdapterId == -1)
{
this.LogDebug("CS LAN Adapter not found");
return;
@@ -472,45 +475,39 @@ namespace PepperDash.Essentials.WebSocketServer
Debug.LogMessage(LogEventLevel.Information, "Adding token: {0} for room: {1}", this, token.Key, token.Value.RoomKey);
if (UiClients == null)
if (UiClientContexts == null)
{
Debug.LogMessage(LogEventLevel.Warning, "UiClients is null", this);
UiClients = new Dictionary<string, UiClientContext>();
UiClientContexts = new Dictionary<string, UiClientContext>();
}
UiClients.Add(token.Key, new UiClientContext(token.Value));
UiClientContexts.Add(token.Key, new UiClientContext(token.Value));
}
}
if (UiClients.Count > 0)
if (UiClientContexts.Count > 0)
{
Debug.LogMessage(LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClients.Count);
Debug.LogMessage(LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClientContexts.Count);
foreach (var client in UiClients)
foreach (var client in UiClientContexts)
{
var key = client.Key;
var path = _wsPath + key;
var roomKey = client.Value.Token.RoomKey;
var token = client.Value.Token;
var bridge = _parent.GetRoomBridge(roomKey);
if (bridge == null)
{
this.LogWarning("No bridge found for room key: {0}", this, roomKey);
continue;
}
_server.AddWebSocketService(path, () =>
{
var c = new UiClient();
Debug.LogMessage(LogEventLevel.Debug, "Constructing UiClient with id: {key}", this, key);
c.Controller = _parent;
c.RoomKey = roomKey;
UiClients[key].SetClient(c);
return c;
return InitializeUiClient(token, bridge);
});
//_server.WebSocketServices.AddService<UiClient>(path, (c) =>
//{
// Debug.Console(2, this, "Constructing UiClient with id: {0}", key);
// c.Controller = _parent;
// c.RoomKey = roomKey;
// UiClients[key].SetClient(c);
//});
}
}
}
@@ -519,7 +516,7 @@ namespace PepperDash.Essentials.WebSocketServer
Debug.LogMessage(LogEventLevel.Warning, "No secret found");
}
Debug.LogMessage(LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClients.Count);
Debug.LogMessage(LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClientContexts.Count);
}
catch (Exception ex)
{
@@ -543,7 +540,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);
}
@@ -572,7 +569,7 @@ namespace PepperDash.Essentials.WebSocketServer
var values = s.Split(' ');
if(values.Length < 2)
if (values.Length < 2)
{
CrestronConsole.ConsoleCommandResponse("Invalid number of arguments. Please provide a room key and a grant code");
return;
@@ -636,24 +633,50 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
private UiClient InitializeUiClient(JoinToken token, MobileControlBridgeBase bridge)
{
Debug.LogMessage(LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, token.Id);
var c = new UiClient(token.Id)
{
Controller = _parent,
RoomKey = bridge.RoomKey
};
UiClients.Add(c.ClientId, c);
// reset client ID in token for next use
token.Id = null;
c.Context.WebSocket.OnClose += (sender, e) =>
{
if (UiClients.ContainsKey(c.ClientId))
{
UiClients.Remove(c.ClientId);
}
};
return c;
}
public (string, string) GenerateClientToken(MobileControlBridgeBase bridge, string touchPanelKey = "")
{
var key = Guid.NewGuid().ToString();
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();
Debug.LogMessage(LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, key);
c.Controller = _parent;
c.RoomKey = bridge.RoomKey;
UiClients[key].SetClient(c);
return c;
if (string.IsNullOrEmpty(token.Id))
{
token.Id = nextClientId++.ToString();
}
return InitializeUiClient(token, bridge);
});
Debug.LogMessage(LogEventLevel.Information, "Added new WebSocket UiClient service at path: {path}", this, path);
@@ -683,7 +706,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)
{
@@ -701,7 +724,7 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
UiClients.Clear();
UiClientContexts.Clear();
UpdateSecret();
}
@@ -720,9 +743,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)
{
@@ -732,7 +755,7 @@ namespace PepperDash.Essentials.WebSocketServer
var path = _wsPath + key;
if (_server.RemoveWebSocketService(path))
{
UiClients.Remove(key);
UiClientContexts.Remove(key);
UpdateSecret();
@@ -756,9 +779,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));
}
@@ -768,7 +791,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)
{
@@ -907,7 +930,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogVerbose("Join Room Request with token: {token}", token);
if (UiClients.TryGetValue(token, out UiClientContext clientContext))
if (UiClientContexts.TryGetValue(token, out UiClientContext clientContext))
{
var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey);
@@ -916,10 +939,12 @@ namespace PepperDash.Essentials.WebSocketServer
res.StatusCode = 200;
res.ContentType = "application/json";
clientContext.Token.Id = nextClientId++.ToString();
// Construct the response object
JoinResponse jRes = new JoinResponse
{
ClientId = token,
ClientId = clientContext.Token.Id,
RoomKey = bridge.RoomKey,
SystemUuid = _parent.SystemUuid,
RoomUuid = _parent.SystemUuid,
@@ -1137,12 +1162,14 @@ namespace PepperDash.Essentials.WebSocketServer
/// <param name="message"></param>
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);
}
}
@@ -1153,129 +1180,25 @@ namespace PepperDash.Essentials.WebSocketServer
/// <param name="message"></param>
public void SendMessageToClient(object clientId, string message)
{
if (clientId == null)
if (clientId == null || !(clientId is string))
{
return;
}
if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext))
{
if (clientContext.Client != null)
{
var socket = clientContext.Client.Context.WebSocket;
if (socket.IsAlive)
{
socket.Send(message);
}
}
}
else
if (!UiClients.TryGetValue((string)clientId, out UiClient client))
{
this.LogWarning("Unable to find client with ID: {clientId}", clientId);
return;
}
var socket = client.Context.WebSocket;
if (!socket.IsAlive)
{
return;
}
socket.Send(message);
}
}
/// <summary>
/// Class to describe the server version info
/// </summary>
public class Version
{
[JsonProperty("serverVersion")]
public string ServerVersion { get; set; }
[JsonProperty("serverIsRunningOnProcessorHardware")]
public bool ServerIsRunningOnProcessorHardware { get; private set; }
public Version()
{
ServerIsRunningOnProcessorHardware = true;
}
}
/// <summary>
/// Represents an instance of a UiClient and the associated Token
/// </summary>
public class UiClientContext
{
public UiClient Client { get; private set; }
public JoinToken Token { get; private set; }
public UiClientContext(JoinToken token)
{
Token = token;
}
public void SetClient(UiClient client)
{
Client = client;
}
}
/// <summary>
/// Represents the data structure for the grant code and UiClient tokens to be stored in the secrets manager
/// </summary>
public class ServerTokenSecrets
{
public string GrantCode { get; set; }
public Dictionary<string, JoinToken> Tokens { get; set; }
public ServerTokenSecrets(string grantCode)
{
GrantCode = grantCode;
Tokens = new Dictionary<string, JoinToken>();
}
}
/// <summary>
/// Represents a join token with the associated properties
/// </summary>
public class JoinToken
{
public string Code { get; set; }
public string RoomKey { get; set; }
public string Uuid { get; set; }
public string TouchpanelKey { get; set; } = "";
public string Token { get; set; } = null;
}
/// <summary>
/// Represents the structure of the join response
/// </summary>
public class JoinResponse
{
[JsonProperty("clientId")]
public string ClientId { get; set; }
[JsonProperty("roomKey")]
public string RoomKey { get; set; }
[JsonProperty("systemUUid")]
public string SystemUuid { get; set; }
[JsonProperty("roomUUid")]
public string RoomUuid { get; set; }
[JsonProperty("config")]
public object Config { get; set; }
[JsonProperty("codeExpires")]
public DateTime CodeExpires { get; set; }
[JsonProperty("userCode")]
public string UserCode { get; set; }
[JsonProperty("userAppUrl")]
public string UserAppUrl { get; set; }
[JsonProperty("enableDebug")]
public bool EnableDebug { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Represents the data structure for the grant code and UiClient tokens to be stored in the secrets manager
/// </summary>
public class ServerTokenSecrets
{
public string GrantCode { get; set; }
public Dictionary<string, JoinToken> Tokens { get; set; }
public ServerTokenSecrets(string grantCode)
{
GrantCode = grantCode;
Tokens = new Dictionary<string, JoinToken>();
}
}
}

View File

@@ -1,11 +1,11 @@
using Newtonsoft.Json;
using System;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.RoomBridges;
using Serilog.Events;
using System;
using System.Text.RegularExpressions;
using WebSocketSharp;
using WebSocketSharp.Server;
using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
@@ -22,7 +22,10 @@ namespace PepperDash.Essentials.WebSocketServer
public string RoomKey { get; set; }
private string _clientId;
public string ClientId
{
get; private set;
}
private DateTime _connectionTime;
@@ -41,9 +44,9 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
public UiClient()
public UiClient(string clientId)
{
ClientId = clientId;
}
protected override void OnOpen()
@@ -61,8 +64,7 @@ namespace PepperDash.Essentials.WebSocketServer
return;
}
var clientId = match.Groups[1].Value;
_clientId = clientId;
var clientId = ClientId;
if (Controller == null)
{
@@ -96,7 +98,7 @@ namespace PepperDash.Essentials.WebSocketServer
private void Bridge_UserCodeChanged(object sender, EventArgs e)
{
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, ClientId);
}
private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId)

View File

@@ -0,0 +1,22 @@
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Represents an instance of a UiClient and the associated Token
/// </summary>
public class UiClientContext
{
public UiClient Client { get; private set; }
public JoinToken Token { get; private set; }
public UiClientContext(JoinToken token)
{
Token = token;
}
public void SetClient(UiClient client)
{
Client = client;
}
}
}

View File

@@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Class to describe the server version info
/// </summary>
public class Version
{
[JsonProperty("serverVersion")]
public string ServerVersion { get; set; }
[JsonProperty("serverIsRunningOnProcessorHardware")]
public bool ServerIsRunningOnProcessorHardware { get; private set; }
public Version()
{
ServerIsRunningOnProcessorHardware = true;
}
}
}