refactor: Refactor DeviceManager and related classes to improve thread safety and performance

- Replaced CCriticalSection with lock statements in DeviceManager for better thread management.
- Updated AddDevice and RemoveDevice methods to use Monitor for locking.
- Enhanced event handling for device activation and registration.
- Modified FileIO class to utilize Task for asynchronous file operations instead of CrestronInvoke.
- Improved feedback mechanisms in FeedbackBase and SystemMonitorController using Task.Run.
- Refactored GenericQueue to remove Crestron threading dependencies and utilize System.Threading.
- Updated BlueJeansPc and VideoCodecBase classes to use Task for asynchronous operations.
- Cleaned up unnecessary critical sections and improved code documentation across various files.
This commit is contained in:
Neil Dorin 2026-03-10 17:30:59 -06:00
parent 426ef4ad6b
commit 346a5e9e57
23 changed files with 998 additions and 912 deletions

View file

@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using System.Timers;
using PepperDash.Core; using PepperDash.Core;
namespace PepperDash.Core; namespace PepperDash.Core;
@ -20,7 +20,7 @@ public class CommunicationStreamDebugging
/// <summary> /// <summary>
/// Timer to disable automatically if not manually disabled /// Timer to disable automatically if not manually disabled
/// </summary> /// </summary>
private CTimer DebugExpiryPeriod; private Timer DebugExpiryPeriod;
/// <summary> /// <summary>
/// The current debug setting /// The current debug setting
@ -93,7 +93,9 @@ public class CommunicationStreamDebugging
StopDebugTimer(); StopDebugTimer();
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs); DebugExpiryPeriod = new Timer(_DebugTimeoutInMs) { AutoReset = false };
DebugExpiryPeriod.Elapsed += (s, e) => DisableDebugging();
DebugExpiryPeriod.Start();
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx) if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
RxStreamDebuggingIsEnabled = true; RxStreamDebuggingIsEnabled = true;

View file

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
@ -277,7 +279,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
/// <summary> /// <summary>
/// Queue lock /// Queue lock
/// </summary> /// </summary>
CCriticalSection DequeueLock = new CCriticalSection(); private readonly object _dequeueLock = new();
/// <summary> /// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
@ -696,9 +698,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null) if (handler != null)
{ {
var gotLock = DequeueLock.TryEnter(); if (Monitor.TryEnter(_dequeueLock))
if (gotLock) Task.Run(() => DequeueEvent());
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
} }
} }
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
@ -708,7 +709,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
} }
/// <summary> /// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically. /// It will dequeue items as they are enqueued automatically.
/// </summary> /// </summary>
void DequeueEvent() void DequeueEvent()
@ -730,11 +731,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
{ {
this.LogError(e, "DequeueEvent error: {0}", e.Message); this.LogError(e, "DequeueEvent error: {0}", e.Message);
} }
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null) Monitor.Exit(_dequeueLock);
{
DequeueLock.Leave();
}
} }
void HeartbeatStart() void HeartbeatStart()

View file

@ -15,6 +15,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
@ -271,7 +273,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
/// <summary> /// <summary>
/// Queue lock /// Queue lock
/// </summary> /// </summary>
CCriticalSection DequeueLock = new CCriticalSection(); private readonly object _dequeueLock = new();
/// <summary> /// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
@ -655,9 +657,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null) if (handler != null)
{ {
var gotLock = DequeueLock.TryEnter(); if (Monitor.TryEnter(_dequeueLock))
if (gotLock) Task.Run(() => DequeueEvent());
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
} }
} }
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
@ -667,7 +668,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
} }
/// <summary> /// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically. /// It will dequeue items as they are enqueued automatically.
/// </summary> /// </summary>
void DequeueEvent() void DequeueEvent()
@ -689,11 +690,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{ {
this.LogError("DequeueEvent error: {0}", e.Message, e); this.LogError("DequeueEvent error: {0}", e.Message, e);
} }
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null) Monitor.Exit(_dequeueLock);
{
DequeueLock.Leave();
}
} }
void HeartbeatStart() void HeartbeatStart()

View file

@ -1,18 +1,9 @@
/*PepperDash Technology Corp. using System;
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
@ -69,12 +60,17 @@ public class GenericSecureTcpIpServer : Device
/// <summary> /// <summary>
/// Server listen lock /// Server listen lock
/// </summary> /// </summary>
CCriticalSection ServerCCSection = new CCriticalSection(); private readonly object _serverLock = new();
/// <summary> /// <summary>
/// Queue lock /// Queue lock
/// </summary> /// </summary>
CCriticalSection DequeueLock = new CCriticalSection(); private readonly object _dequeueLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <summary> /// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
@ -399,18 +395,19 @@ public class GenericSecureTcpIpServer : Device
/// </summary> /// </summary>
public void Listen() public void Listen()
{ {
ServerCCSection.Enter(); lock (_serverLock)
{
try try
{ {
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); this.LogError("Server '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); this.LogError("Server '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return; return;
} }
@ -434,22 +431,21 @@ public class GenericSecureTcpIpServer : Device
SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING) if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error starting WaitForConnectionAsync {0}", status); this.LogError("Error starting WaitForConnectionAsync {0}", status);
} }
else else
{ {
ServerStopped = false; ServerStopped = false;
} }
OnServerStateChange(SecureServer.State); OnServerStateChange(SecureServer.State);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
ServerCCSection.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
ServerCCSection.Leave(); this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
} }
} // end lock
} }
/// <summary> /// <summary>
@ -459,18 +455,18 @@ public class GenericSecureTcpIpServer : Device
{ {
try try
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); this.LogVerbose("Stopping Listener");
if (SecureServer != null) if (SecureServer != null)
{ {
SecureServer.Stop(); SecureServer.Stop();
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); this.LogVerbose("Server State: {0}", SecureServer.State);
OnServerStateChange(SecureServer.State); OnServerStateChange(SecureServer.State);
} }
ServerStopped = true; ServerStopped = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
} }
} }
@ -483,11 +479,11 @@ public class GenericSecureTcpIpServer : Device
try try
{ {
SecureServer.Disconnect(client); SecureServer.Disconnect(client);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); this.LogVerbose("Disconnected client index: {0}", client);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
} }
} }
/// <summary> /// <summary>
@ -495,7 +491,7 @@ public class GenericSecureTcpIpServer : Device
/// </summary> /// </summary>
public void DisconnectAllClientsForShutdown() public void DisconnectAllClientsForShutdown()
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); this.LogInformation("Disconnecting All Clients");
if (SecureServer != null) if (SecureServer != null)
{ {
SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
@ -507,17 +503,17 @@ public class GenericSecureTcpIpServer : Device
try try
{ {
SecureServer.Disconnect(i); SecureServer.Disconnect(i);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); this.LogInformation("Disconnected client index: {0}", i);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
} }
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); this.LogInformation("Server Status: {0}", SecureServer.ServerSocketStatus);
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); this.LogInformation("Disconnected All Clients");
ConnectedClientsIndexes.Clear(); ConnectedClientsIndexes.Clear();
if (!ProgramIsStopping) if (!ProgramIsStopping)
@ -535,8 +531,8 @@ public class GenericSecureTcpIpServer : Device
/// <param name="text"></param> /// <param name="text"></param>
public void BroadcastText(string text) public void BroadcastText(string text)
{ {
CCriticalSection CCBroadcast = new CCriticalSection(); lock (_broadcastLock)
CCBroadcast.Enter(); {
try try
{ {
if (ConnectedClientsIndexes.Count > 0) if (ConnectedClientsIndexes.Count > 0)
@ -552,13 +548,12 @@ public class GenericSecureTcpIpServer : Device
} }
} }
} }
CCBroadcast.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
CCBroadcast.Leave(); this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
} }
} // end lock
} }
/// <summary> /// <summary>
@ -579,7 +574,7 @@ public class GenericSecureTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
} }
} }
@ -603,7 +598,7 @@ public class GenericSecureTcpIpServer : Device
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat // Return Heartbeat
SendTextToClient(HeartbeatStringToMatch, clientIndex); SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText; return remainingText;
@ -618,13 +613,13 @@ public class GenericSecureTcpIpServer : Device
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
} }
return received; return received;
} }
@ -636,11 +631,11 @@ public class GenericSecureTcpIpServer : Device
/// <returns></returns> /// <returns></returns>
public string GetClientIPAddress(uint clientIndex) public string GetClientIPAddress(uint clientIndex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); this.LogInformation("GetClientIPAddress Index: {0}", clientIndex);
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
{ {
var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); this.LogInformation("GetClientIPAddress IPAddreess: {0}", ipa);
return ipa; return ipa;
} }
@ -663,7 +658,7 @@ public class GenericSecureTcpIpServer : Device
clientIndex = (uint)o; clientIndex = (uint)o;
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", this.LogInformation("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
@ -703,7 +698,7 @@ public class GenericSecureTcpIpServer : Device
// Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
if (ConnectedClientsIndexes.Contains(clientIndex)) if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex); ConnectedClientsIndexes.Remove(clientIndex);
@ -725,12 +720,12 @@ public class GenericSecureTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex.Message);
} }
//Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state
//after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag
//is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event.
CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null); Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
} }
#endregion #endregion
@ -745,7 +740,7 @@ public class GenericSecureTcpIpServer : Device
{ {
try try
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", this.LogInformation("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0) if (clientIndex != 0)
@ -765,7 +760,7 @@ public class GenericSecureTcpIpServer : Device
} }
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); this.LogInformation("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
} }
else else
{ {
@ -784,19 +779,19 @@ public class GenericSecureTcpIpServer : Device
} }
else else
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); this.LogError("Client attempt faulty.");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
} }
// Rearm the listner // Rearm the listner
SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING) if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Socket status connect callback status {0}", status); this.LogError("Socket status connect callback status {0}", status);
if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS) if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
{ {
// There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact. // There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
@ -833,7 +828,7 @@ public class GenericSecureTcpIpServer : Device
if (received != SharedKey) if (received != SharedKey)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
mySecureTCPServer.SendData(clientIndex, b, b.Length); mySecureTCPServer.SendData(clientIndex, b, b.Length);
mySecureTCPServer.Disconnect(clientIndex); mySecureTCPServer.Disconnect(clientIndex);
@ -844,7 +839,7 @@ public class GenericSecureTcpIpServer : Device
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
OnServerClientReadyForCommunications(clientIndex); OnServerClientReadyForCommunications(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); this.LogInformation("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
} }
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
{ {
@ -857,7 +852,7 @@ public class GenericSecureTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex.Message);
} }
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
@ -865,9 +860,8 @@ public class GenericSecureTcpIpServer : Device
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null) if (handler != null)
{ {
var gotLock = DequeueLock.TryEnter(); if (Monitor.TryEnter(_dequeueLock))
if (gotLock) Task.Run(() => DequeueEvent());
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
} }
} }
else else
@ -877,7 +871,7 @@ public class GenericSecureTcpIpServer : Device
} }
/// <summary> /// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically. /// It will dequeue items as they are enqueued automatically.
/// </summary> /// </summary>
void DequeueEvent() void DequeueEvent()
@ -899,11 +893,8 @@ public class GenericSecureTcpIpServer : Device
{ {
this.LogError(e, "DequeueEvent error"); this.LogError(e, "DequeueEvent error");
} }
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null) Monitor.Exit(_dequeueLock);
{
DequeueLock.Leave();
}
} }
#endregion #endregion
@ -974,7 +965,7 @@ public class GenericSecureTcpIpServer : Device
if (MonitorClient != null) if (MonitorClient != null)
MonitorClient.Disconnect(); MonitorClient.Disconnect();
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); this.LogInformation("Program stopping. Closing server");
KillServer(); KillServer();
} }
} }
@ -1015,7 +1006,7 @@ public class GenericSecureTcpIpServer : Device
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); this.LogInformation("Starting monitor check");
MonitorClient.Connect(); MonitorClient.Connect();
// From here MonitorCLient either connects or hangs, MonitorClient will call back // From here MonitorCLient either connects or hangs, MonitorClient will call back
@ -1042,7 +1033,7 @@ public class GenericSecureTcpIpServer : Device
{ {
if (args.IsReady) if (args.IsReady)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); this.LogInformation("Monitor client connection success. Disconnecting in 2s");
MonitorClientTimer.Stop(); MonitorClientTimer.Stop();
MonitorClientTimer = null; MonitorClientTimer = null;
MonitorClientFailureCount = 0; MonitorClientFailureCount = 0;
@ -1063,13 +1054,13 @@ public class GenericSecureTcpIpServer : Device
StopMonitorClient(); StopMonitorClient();
if (MonitorClientFailureCount < MonitorClientMaxFailureCount) if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
StartMonitorClient(); StartMonitorClient();
} }
else else
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, this.LogError(
"\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************",
MonitorClientMaxFailureCount); MonitorClientMaxFailureCount);

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities;
@ -135,8 +136,6 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
CTimer ReconnectTimer; CTimer ReconnectTimer;
//Lock object to prevent simulatneous connect/disconnect operations
//private CCriticalSection connectLock = new CCriticalSection();
private SemaphoreSlim connectLock = new SemaphoreSlim(1); private SemaphoreSlim connectLock = new SemaphoreSlim(1);
private bool DisconnectLogged = false; private bool DisconnectLogged = false;
@ -435,7 +434,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
/// </summary> /// </summary>
void Client_ErrorOccurred(object sender, ExceptionEventArgs e) void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{ {
CrestronInvoke.BeginInvoke(o => Task.Run(() =>
{ {
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
this.LogError("Disconnected by remote"); this.LogError("Disconnected by remote");

View file

@ -4,17 +4,21 @@ using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using Timer = System.Timers.Timer;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using Required = NewtonsoftJson::Newtonsoft.Json.Required; using Required = NewtonsoftJson::Newtonsoft.Json.Required;
using PepperDash.Core.Logging;
using System.Threading.Tasks;
namespace PepperDash.Core; namespace PepperDash.Core;
/// <summary> /// <summary>
/// A class to handle basic TCP/IP communications with a server /// A class to handle basic TCP/IP communications with a server
/// </summary> /// </summary>
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{ {
private const string SplusKey = "Uninitialized TcpIpClient"; private const string SplusKey = "Uninitialized TcpIpClient";
/// <summary> /// <summary>
@ -171,10 +175,10 @@ namespace PepperDash.Core;
} }
//Lock object to prevent simulatneous connect/disconnect operations //Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection(); private readonly object _connectLock = new();
// private Timer for auto reconnect // private Timer for auto reconnect
private CTimer RetryTimer; private Timer RetryTimer;
/// <summary> /// <summary>
/// Constructor /// Constructor
@ -193,10 +197,7 @@ namespace PepperDash.Core;
Port = port; Port = port;
BufferSize = bufferSize; BufferSize = bufferSize;
RetryTimer = new CTimer(o => SetupRetryTimer();
{
Reconnect();
}, Timeout.Infinite);
} }
/// <summary> /// <summary>
@ -211,10 +212,7 @@ namespace PepperDash.Core;
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
BufferSize = 2000; BufferSize = 2000;
RetryTimer = new CTimer(o => SetupRetryTimer();
{
Reconnect();
}, Timeout.Infinite);
} }
/// <summary> /// <summary>
@ -228,12 +226,17 @@ namespace PepperDash.Core;
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
BufferSize = 2000; BufferSize = 2000;
RetryTimer = new CTimer(o => SetupRetryTimer();
{
Reconnect();
}, Timeout.Infinite);
} }
private void SetupRetryTimer()
{
RetryTimer = new Timer { AutoReset = false, Enabled = false };
RetryTimer.Elapsed += (s, e) => Reconnect();
}
/// <summary> /// <summary>
/// Just to help S+ set the key /// Just to help S+ set the key
/// </summary> /// </summary>
@ -249,7 +252,7 @@ namespace PepperDash.Core;
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
Debug.Console(1, this, "Program stopping. Closing connection"); this.LogInformation("Program stopping. Closing connection");
Deactivate(); Deactivate();
} }
} }
@ -277,23 +280,22 @@ namespace PepperDash.Core;
{ {
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); this.LogWarning("GenericTcpIpClient '{0}': No address set", Key);
return; return;
} }
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key); this.LogWarning("GenericTcpIpClient '{0}': Invalid port", Key);
return; return;
} }
} }
try lock (_connectLock)
{ {
connectLock.Enter();
if (IsConnected) if (IsConnected)
{ {
Debug.Console(1, this, "Connection already connected. Exiting Connect()"); this.LogInformation("Connection already connected. Exiting Connect()");
} }
else else
{ {
@ -305,10 +307,6 @@ namespace PepperDash.Core;
DisconnectCalledByUser = false; DisconnectCalledByUser = false;
_client.ConnectToServerAsync(ConnectToServerCallback); _client.ConnectToServerAsync(ConnectToServerCallback);
} }
}
finally
{
connectLock.Leave();
} }
} }
@ -318,23 +316,18 @@ namespace PepperDash.Core;
{ {
return; return;
} }
try lock (_connectLock)
{ {
connectLock.Enter();
if (IsConnected || DisconnectCalledByUser == true) if (IsConnected || DisconnectCalledByUser == true)
{ {
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()"); this.LogInformation("Reconnect no longer needed. Exiting Reconnect()");
} }
else else
{ {
Debug.Console(1, this, "Attempting reconnect now"); this.LogInformation("Attempting reconnect now");
_client.ConnectToServerAsync(ConnectToServerCallback); _client.ConnectToServerAsync(ConnectToServerCallback);
} }
} }
finally
{
connectLock.Leave();
}
} }
/// <summary> /// <summary>
@ -342,18 +335,13 @@ namespace PepperDash.Core;
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
try lock (_connectLock)
{ {
connectLock.Enter();
DisconnectCalledByUser = true; DisconnectCalledByUser = true;
// Stop trying reconnects, if we are // Stop trying reconnects, if we are
RetryTimer.Stop(); RetryTimer.Stop();
DisconnectClient(); DisconnectClient();
}
finally
{
connectLock.Leave();
} }
} }
@ -364,7 +352,7 @@ namespace PepperDash.Core;
{ {
if (_client != null) if (_client != null)
{ {
Debug.Console(1, this, "Disconnecting client"); this.LogInformation("Disconnecting client");
if (IsConnected) if (IsConnected)
_client.DisconnectFromServer(); _client.DisconnectFromServer();
} }
@ -378,12 +366,12 @@ namespace PepperDash.Core;
{ {
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); this.LogInformation("Server connection result: {0}", c.ClientStatus);
WaitAndTryReconnect(); WaitAndTryReconnect();
} }
else else
{ {
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); this.LogInformation("Server connection result: {0}", c.ClientStatus);
} }
} }
@ -392,22 +380,19 @@ namespace PepperDash.Core;
/// </summary> /// </summary>
void WaitAndTryReconnect() void WaitAndTryReconnect()
{ {
CrestronInvoke.BeginInvoke(o => Task.Run(() =>
{ {
try lock (_connectLock)
{ {
connectLock.Enter();
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null) if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
{ {
DisconnectClient(); DisconnectClient();
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus); this.LogInformation("Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Reset(AutoReconnectIntervalMs); RetryTimer.Stop();
RetryTimer.Interval = AutoReconnectIntervalMs;
RetryTimer.Start();
} }
} }
finally
{
connectLock.Leave();
}
}); });
} }
@ -428,7 +413,7 @@ namespace PepperDash.Core;
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) if (StreamDebugging.RxStreamDebuggingIsEnabled)
{ {
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
} }
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
@ -439,7 +424,7 @@ namespace PepperDash.Core;
if (StreamDebugging.RxStreamDebuggingIsEnabled) if (StreamDebugging.RxStreamDebuggingIsEnabled)
{ {
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
} }
textHandler(this, new GenericCommMethodReceiveTextArgs(str)); textHandler(this, new GenericCommMethodReceiveTextArgs(str));
@ -457,7 +442,7 @@ namespace PepperDash.Core;
var bytes = Encoding.GetEncoding(28591).GetBytes(text); var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array // Check debug level before processing byte array
if (StreamDebugging.TxStreamDebuggingIsEnabled) if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); this.LogInformation("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
if (_client != null) if (_client != null)
_client.SendData(bytes, bytes.Length); _client.SendData(bytes, bytes.Length);
} }
@ -482,7 +467,7 @@ namespace PepperDash.Core;
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
if (StreamDebugging.TxStreamDebuggingIsEnabled) if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (_client != null) if (_client != null)
_client.SendData(bytes, bytes.Length); _client.SendData(bytes, bytes.Length);
} }
@ -496,12 +481,12 @@ namespace PepperDash.Core;
{ {
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
WaitAndTryReconnect(); WaitAndTryReconnect();
} }
else else
{ {
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive); _client.ReceiveDataAsync(Receive);
} }
@ -509,13 +494,13 @@ namespace PepperDash.Core;
if (handler != null) if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
} }
} }
/// <summary> /// <summary>
/// Configuration properties for TCP/SSH Connections /// Configuration properties for TCP/SSH Connections
/// </summary> /// </summary>
public class TcpSshPropertiesConfig public class TcpSshPropertiesConfig
{ {
/// <summary> /// <summary>
/// Address to connect to /// Address to connect to
/// </summary> /// </summary>
@ -570,4 +555,4 @@ namespace PepperDash.Core;
Password = ""; Password = "";
} }
} }

View file

@ -13,6 +13,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
@ -61,9 +62,14 @@ public class GenericTcpIpServer : Device
#region Properties/Variables #region Properties/Variables
/// <summary> /// <summary>
/// /// Server listen lock
/// </summary> /// </summary>
CCriticalSection ServerCCSection = new CCriticalSection(); object _serverLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <summary> /// <summary>
@ -365,12 +371,12 @@ public class GenericTcpIpServer : Device
} }
else else
{ {
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
} }
} }
catch catch
{ {
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
} }
} }
@ -379,19 +385,18 @@ public class GenericTcpIpServer : Device
/// </summary> /// </summary>
public void Listen() public void Listen()
{ {
ServerCCSection.Enter(); lock (_serverLock)
{
try try
{ {
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); this.LogError("Server '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); this.LogError("Server '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return; return;
} }
if (IsListening) if (IsListening)
@ -417,18 +422,15 @@ public class GenericTcpIpServer : Device
ServerStopped = false; ServerStopped = false;
myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
OnServerStateChange(myTcpServer.State); OnServerStateChange(myTcpServer.State);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus); this.LogInformation("TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
// StartMonitorClient(); // StartMonitorClient();
ServerCCSection.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
ServerCCSection.Leave(); this.LogException(ex, "Error with Dynamic Server: {0}", ex.Message);
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
} }
} // end lock
} }
/// <summary> /// <summary>
@ -438,18 +440,18 @@ public class GenericTcpIpServer : Device
{ {
try try
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); this.LogDebug("Stopping Listener");
if (myTcpServer != null) if (myTcpServer != null)
{ {
myTcpServer.Stop(); myTcpServer.Stop();
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); this.LogDebug("Server State: {0}", myTcpServer.State);
OnServerStateChange(myTcpServer.State); OnServerStateChange(myTcpServer.State);
} }
ServerStopped = true; ServerStopped = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
} }
} }
@ -462,11 +464,11 @@ public class GenericTcpIpServer : Device
try try
{ {
myTcpServer.Disconnect(client); myTcpServer.Disconnect(client);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); this.LogVerbose("Disconnected client index: {0}", client);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
} }
} }
/// <summary> /// <summary>
@ -474,7 +476,7 @@ public class GenericTcpIpServer : Device
/// </summary> /// </summary>
public void DisconnectAllClientsForShutdown() public void DisconnectAllClientsForShutdown()
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); this.LogInformation("Disconnecting All Clients");
if (myTcpServer != null) if (myTcpServer != null)
{ {
myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
@ -486,17 +488,17 @@ public class GenericTcpIpServer : Device
try try
{ {
myTcpServer.Disconnect(i); myTcpServer.Disconnect(i);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); this.LogVerbose("Disconnected client index: {0}", i);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
} }
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); this.LogVerbose("Server Status: {0}", myTcpServer.ServerSocketStatus);
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); this.LogInformation("Disconnected All Clients");
ConnectedClientsIndexes.Clear(); ConnectedClientsIndexes.Clear();
if (!ProgramIsStopping) if (!ProgramIsStopping)
@ -514,8 +516,8 @@ public class GenericTcpIpServer : Device
/// <param name="text"></param> /// <param name="text"></param>
public void BroadcastText(string text) public void BroadcastText(string text)
{ {
CCriticalSection CCBroadcast = new CCriticalSection(); lock (_broadcastLock)
CCBroadcast.Enter(); {
try try
{ {
if (ConnectedClientsIndexes.Count > 0) if (ConnectedClientsIndexes.Count > 0)
@ -531,13 +533,12 @@ public class GenericTcpIpServer : Device
} }
} }
} }
CCBroadcast.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
CCBroadcast.Leave(); this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
} }
} // end lock
} }
/// <summary> /// <summary>
@ -558,7 +559,7 @@ public class GenericTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
} }
} }
@ -582,7 +583,7 @@ public class GenericTcpIpServer : Device
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat // Return Heartbeat
SendTextToClient(HeartbeatStringToMatch, clientIndex); SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText; return remainingText;
@ -597,13 +598,13 @@ public class GenericTcpIpServer : Device
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
} }
return received; return received;
} }
@ -615,11 +616,11 @@ public class GenericTcpIpServer : Device
/// <returns>IP address of the client</returns> /// <returns>IP address of the client</returns>
public string GetClientIPAddress(uint clientIndex) public string GetClientIPAddress(uint clientIndex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); this.LogVerbose("GetClientIPAddress Index: {0}", clientIndex);
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
{ {
var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); this.LogVerbose("GetClientIPAddress IPAddreess: {0}", ipa);
return ipa; return ipa;
} }
@ -642,7 +643,7 @@ public class GenericTcpIpServer : Device
clientIndex = (uint)o; clientIndex = (uint)o;
address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", this.LogWarning("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
@ -678,7 +679,7 @@ public class GenericTcpIpServer : Device
try try
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); this.LogInformation("SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
if (ConnectedClientsIndexes.Contains(clientIndex)) if (ConnectedClientsIndexes.Contains(clientIndex))
@ -697,7 +698,7 @@ public class GenericTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex);
} }
onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
} }
@ -714,7 +715,7 @@ public class GenericTcpIpServer : Device
{ {
try try
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", this.LogDebug("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0) if (clientIndex != 0)
@ -734,10 +735,11 @@ public class GenericTcpIpServer : Device
} }
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); this.LogDebug("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
} }
else else
{ {
this.LogDebug("Client at index {0} is ready for communications", clientIndex);
OnServerClientReadyForCommunications(clientIndex); OnServerClientReadyForCommunications(clientIndex);
} }
if (HeartbeatRequired) if (HeartbeatRequired)
@ -753,7 +755,7 @@ public class GenericTcpIpServer : Device
} }
else else
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); this.LogError("Client attempt faulty.");
if (!ServerStopped) if (!ServerStopped)
{ {
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
@ -763,7 +765,7 @@ public class GenericTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex);
} }
//Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))",
// server.State, // server.State,
@ -771,7 +773,7 @@ public class GenericTcpIpServer : Device
// ServerStopped); // ServerStopped);
if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); this.LogDebug("Waiting for next connection");
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
} }
@ -802,7 +804,7 @@ public class GenericTcpIpServer : Device
if (received != SharedKey) if (received != SharedKey)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
myTCPServer.SendData(clientIndex, b, b.Length); myTCPServer.SendData(clientIndex, b, b.Length);
myTCPServer.Disconnect(clientIndex); myTCPServer.Disconnect(clientIndex);
return; return;
@ -812,7 +814,7 @@ public class GenericTcpIpServer : Device
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
myTCPServer.SendDataAsync(clientIndex, success, success.Length, null); myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
OnServerClientReadyForCommunications(clientIndex); OnServerClientReadyForCommunications(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); this.LogDebug("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
} }
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
@ -820,7 +822,7 @@ public class GenericTcpIpServer : Device
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex);
} }
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
@ -901,7 +903,7 @@ public class GenericTcpIpServer : Device
if (MonitorClient != null) if (MonitorClient != null)
MonitorClient.Disconnect(); MonitorClient.Disconnect();
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); this.LogInformation("Program stopping. Closing server");
KillServer(); KillServer();
} }
} }
@ -942,7 +944,7 @@ public class GenericTcpIpServer : Device
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); this.LogDebug("Starting monitor check");
MonitorClient.Connect(); MonitorClient.Connect();
// From here MonitorCLient either connects or hangs, MonitorClient will call back // From here MonitorCLient either connects or hangs, MonitorClient will call back
@ -969,7 +971,7 @@ public class GenericTcpIpServer : Device
{ {
if (args.IsReady) if (args.IsReady)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); this.LogInformation("Monitor client connection success. Disconnecting in 2s");
MonitorClientTimer.Stop(); MonitorClientTimer.Stop();
MonitorClientTimer = null; MonitorClientTimer = null;
MonitorClientFailureCount = 0; MonitorClientFailureCount = 0;
@ -990,13 +992,13 @@ public class GenericTcpIpServer : Device
StopMonitorClient(); StopMonitorClient();
if (MonitorClientFailureCount < MonitorClientMaxFailureCount) if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
StartMonitorClient(); StartMonitorClient();
} }
else else
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, this.LogError(
"\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************",
MonitorClientMaxFailureCount); MonitorClientMaxFailureCount);

View file

@ -1,6 +1,7 @@
extern alias NewtonsoftJson; extern alias NewtonsoftJson;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
@ -9,20 +10,21 @@ namespace PepperDash.Core.Logging;
/// <summary> /// <summary>
/// Class to persist current Debug settings across program restarts /// Class to persist current Debug settings across program restarts
/// </summary> /// </summary>
public class DebugContextCollection public class DebugContextCollection
{ {
/// <summary> /// <summary>
/// To prevent threading issues with the DeviceDebugSettings collection /// To prevent threading issues with the DeviceDebugSettings collection
/// </summary> /// </summary>
private readonly CCriticalSection _deviceDebugSettingsLock; private readonly object _deviceDebugSettingsLock = new();
[JsonProperty("items")] private readonly Dictionary<string, DebugContextItem> _items; [JsonProperty("items")]
private readonly Dictionary<string, DebugContextItem> _items = new Dictionary<string, DebugContextItem>();
/// <summary> /// <summary>
/// Collection of the debug settings for each device where the dictionary key is the device key /// Collection of the debug settings for each device where the dictionary key is the device key
/// </summary> /// </summary>
[JsonProperty("deviceDebugSettings")] [JsonProperty("deviceDebugSettings")]
private Dictionary<string, object> DeviceDebugSettings { get; set; } private Dictionary<string, object> DeviceDebugSettings { get; set; } = new Dictionary<string, object>();
/// <summary> /// <summary>
@ -30,9 +32,7 @@ namespace PepperDash.Core.Logging;
/// </summary> /// </summary>
public DebugContextCollection() public DebugContextCollection()
{ {
_deviceDebugSettingsLock = new CCriticalSection();
DeviceDebugSettings = new Dictionary<string, object>();
_items = new Dictionary<string, DebugContextItem>();
} }
/// <summary> /// <summary>
@ -75,10 +75,8 @@ namespace PepperDash.Core.Logging;
/// <returns></returns> /// <returns></returns>
public void SetDebugSettingsForKey(string deviceKey, object settings) public void SetDebugSettingsForKey(string deviceKey, object settings)
{ {
try lock (_deviceDebugSettingsLock)
{ {
_deviceDebugSettingsLock.Enter();
if (DeviceDebugSettings.ContainsKey(deviceKey)) if (DeviceDebugSettings.ContainsKey(deviceKey))
{ {
DeviceDebugSettings[deviceKey] = settings; DeviceDebugSettings[deviceKey] = settings;
@ -86,10 +84,6 @@ namespace PepperDash.Core.Logging;
else else
DeviceDebugSettings.Add(deviceKey, settings); DeviceDebugSettings.Add(deviceKey, settings);
} }
finally
{
_deviceDebugSettingsLock.Leave();
}
} }
/// <summary> /// <summary>
@ -101,13 +95,13 @@ namespace PepperDash.Core.Logging;
{ {
return DeviceDebugSettings[deviceKey]; return DeviceDebugSettings[deviceKey];
} }
} }
/// <summary> /// <summary>
/// Contains information about /// Contains information about
/// </summary> /// </summary>
public class DebugContextItem public class DebugContextItem
{ {
/// <summary> /// <summary>
/// The level of debug messages to print /// The level of debug messages to print
/// </summary> /// </summary>
@ -119,4 +113,4 @@ namespace PepperDash.Core.Logging;
/// </summary> /// </summary>
[JsonProperty("doNotLoadOnNextBoot")] [JsonProperty("doNotLoadOnNextBoot")]
public bool DoNotLoadOnNextBoot { get; set; } public bool DoNotLoadOnNextBoot { get; set; }
} }

View file

@ -3,29 +3,27 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using PepperDash.Core.Web.RequestHandlers; using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Core.Logging;
namespace PepperDash.Core.Web; namespace PepperDash.Core.Web;
/// <summary> /// <summary>
/// Web API server /// Web API server
/// </summary> /// </summary>
public class WebApiServer : IKeyName public class WebApiServer : IKeyName
{ {
private const string SplusKey = "Uninitialized Web API Server"; private const string SplusKey = "Uninitialized Web API Server";
private const string DefaultName = "Web API Server"; private const string DefaultName = "Web API Server";
private const string DefaultBasePath = "/api"; private const string DefaultBasePath = "/api";
private const uint DebugTrace = 0; private readonly object _serverLock = new();
private const uint DebugInfo = 1;
private const uint DebugVerbose = 2;
private readonly CCriticalSection _serverLock = new CCriticalSection();
private HttpCwsServer _server; private HttpCwsServer _server;
/// <summary> /// <summary>
@ -48,28 +46,6 @@ namespace PepperDash.Core.Web;
/// </summary> /// </summary>
public bool IsRegistered { get; private set; } public bool IsRegistered { get; private set; }
/// <summary>
/// Http request handler
/// </summary>
//public IHttpCwsHandler HttpRequestHandler
//{
// get { return _server.HttpRequestHandler; }
// set
// {
// if (_server == null) return;
// _server.HttpRequestHandler = value;
// }
//}
/// <summary>
/// Received request event handler
/// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
//}
/// <summary> /// <summary>
/// Constructor for S+. Make sure to set necessary properties using init method /// Constructor for S+. Make sure to set necessary properties using init method
/// </summary> /// </summary>
@ -117,7 +93,7 @@ namespace PepperDash.Core.Web;
{ {
if (programEventType != eProgramStatusEventType.Stopping) return; if (programEventType != eProgramStatusEventType.Stopping) return;
Debug.Console(DebugInfo, this, "Program stopping. stopping server"); this.LogInformation("Program stopping. stopping server");
Stop(); Stop();
} }
@ -131,11 +107,11 @@ namespace PepperDash.Core.Web;
// Re-enable the server if the link comes back up and the status should be connected // Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered) if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
{ {
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered."); this.LogInformation("Ethernet link up. Server is already registered.");
return; return;
} }
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server"); this.LogInformation("Ethernet link up. Starting server");
Start(); Start();
} }
@ -156,7 +132,7 @@ namespace PepperDash.Core.Web;
{ {
if (route == null) if (route == null)
{ {
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null"); this.LogWarning("Failed to add route, route parameter is null");
return; return;
} }
@ -175,7 +151,7 @@ namespace PepperDash.Core.Web;
{ {
if (route == null) if (route == null)
{ {
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null"); this.LogWarning("Failed to remove route, route parameter is null");
return; return;
} }
@ -194,73 +170,62 @@ namespace PepperDash.Core.Web;
/// Starts CWS instance /// Starts CWS instance
/// </summary> /// </summary>
public void Start() public void Start()
{
lock (_serverLock)
{ {
try try
{ {
_serverLock.Enter();
if (_server == null) if (_server == null)
{ {
Debug.Console(DebugInfo, this, "Server is null, unable to start"); this.LogDebug("Server is null, unable to start");
return; return;
} }
if (IsRegistered) if (IsRegistered)
{ {
Debug.Console(DebugInfo, this, "Server has already been started"); this.LogDebug("Server has already been started");
return; return;
} }
IsRegistered = _server.Register(); IsRegistered = _server.Register();
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed"); this.LogDebug("Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message); this.LogException(ex, "Start Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace); this.LogVerbose("Start Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
}
finally
{
_serverLock.Leave();
} }
} // end lock
} }
/// <summary> /// <summary>
/// Stop method /// Stop method
/// </summary> /// </summary>
public void Stop() public void Stop()
{
lock (_serverLock)
{ {
try try
{ {
_serverLock.Enter();
if (_server == null) if (_server == null)
{ {
Debug.Console(DebugInfo, this, "Server is null or has already been stopped"); this.LogDebug("Server is null or has already been stopped");
return; return;
} }
IsRegistered = _server.Unregister() == false; IsRegistered = _server.Unregister() == false;
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful"); this.LogDebug("Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
_server.Dispose(); _server.Dispose();
_server = null; _server = null;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message); this.LogException(ex, "Server Stop Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException);
}
finally
{
_serverLock.Leave();
} }
} // end lock
} }
/// <summary> /// <summary>
@ -276,14 +241,12 @@ namespace PepperDash.Core.Web;
try try
{ {
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented); var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j); this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message); this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace); this.LogVerbose("ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
}
} }
} }
}

View file

@ -1,10 +1,9 @@
 
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Threading;
using Crestron.SimplSharp; using Timer = System.Timers.Timer;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -18,12 +17,27 @@ namespace PepperDash.Essentials.Core.Config;
/// </summary> /// </summary>
public class ConfigWriter public class ConfigWriter
{ {
/// <summary>
/// The name of the subfolder where the config file will be written
/// </summary>
public const string LocalConfigFolder = "LocalConfig"; public const string LocalConfigFolder = "LocalConfig";
public const long WriteTimeout = 30000; /// <summary>
/// The amount of time in milliseconds to wait after the last config update before writing the config file. This is to prevent multiple rapid updates from causing multiple file writes.
/// Default is 30 seconds.
/// </summary>
public const long WriteTimeoutInMs = 30000;
public static CTimer WriteTimer; private static Timer WriteTimer;
static CCriticalSection fileLock = new CCriticalSection(); static readonly object _fileLock = new();
static ConfigWriter()
{
WriteTimer = new Timer(WriteTimeoutInMs);
WriteTimer.Elapsed += (s, e) => WriteConfigFile(null);
}
/// <summary> /// <summary>
/// Updates the config properties of a device /// Updates the config properties of a device
@ -53,6 +67,9 @@ public class ConfigWriter
return success; return success;
} }
/// <summary>
/// Updates the config properties of a device
/// </summary>
public static bool UpdateDeviceConfig(DeviceConfig config) public static bool UpdateDeviceConfig(DeviceConfig config)
{ {
bool success = false; bool success = false;
@ -73,6 +90,9 @@ public class ConfigWriter
return success; return success;
} }
/// <summary>
/// Updates the config properties of a room
/// </summary>
public static bool UpdateRoomConfig(DeviceConfig config) public static bool UpdateRoomConfig(DeviceConfig config)
{ {
bool success = false; bool success = false;
@ -83,7 +103,7 @@ public class ConfigWriter
{ {
ConfigReader.ConfigObject.Rooms[roomConfigIndex] = config; ConfigReader.ConfigObject.Rooms[roomConfigIndex] = config;
Debug.LogMessage(LogEventLevel.Debug, "Updated room of device: '{0}'", config.Key); Debug.LogMessage(LogEventLevel.Debug, "Updated config of room: '{0}'", config.Key);
success = true; success = true;
} }
@ -98,10 +118,9 @@ public class ConfigWriter
/// </summary> /// </summary>
static void ResetTimer() static void ResetTimer()
{ {
if (WriteTimer == null) WriteTimer.Stop();
WriteTimer = new CTimer(WriteConfigFile, WriteTimeout); WriteTimer.Interval = WriteTimeoutInMs;
WriteTimer.Start();
WriteTimer.Reset(WriteTimeout);
Debug.LogMessage(LogEventLevel.Debug, "Config File write timer has been reset."); Debug.LogMessage(LogEventLevel.Debug, "Config File write timer has been reset.");
} }
@ -120,10 +139,10 @@ public class ConfigWriter
} }
/// <summary> /// <summary>
/// Writes /// Writes the specified configuration data to a file.
/// </summary> /// </summary>
/// <param name="filepath"></param> /// <param name="filePath">The path of the file to write to.</param>
/// <param name="o"></param> /// <param name="configData">The configuration data to write.</param>
public static void WriteFile(string filePath, string configData) public static void WriteFile(string filePath, string configData)
{ {
if (WriteTimer != null) if (WriteTimer != null)
@ -133,9 +152,11 @@ public class ConfigWriter
Debug.LogMessage(LogEventLevel.Information, "Attempting to write config file: '{0}'", filePath); Debug.LogMessage(LogEventLevel.Information, "Attempting to write config file: '{0}'", filePath);
var lockAcquired = false;
try try
{ {
if (fileLock.TryEnter()) lockAcquired = Monitor.TryEnter(_fileLock);
if (lockAcquired)
{ {
using (StreamWriter sw = new StreamWriter(filePath)) using (StreamWriter sw = new StreamWriter(filePath))
{ {
@ -154,11 +175,8 @@ public class ConfigWriter
} }
finally finally
{ {
if (fileLock != null && !fileLock.Disposed) if (lockAcquired)
fileLock.Leave(); Monitor.Exit(_fileLock);
} }
} }
} }

View file

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using PepperDash.Core; using PepperDash.Core;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using PepperDash.Essentials.Core;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Essentials.Core.DeviceInfo; namespace PepperDash.Essentials.Core.DeviceInfo;
/// <summary>
/// Helper methods for network devices
/// </summary>
public static class NetworkDeviceHelpers public static class NetworkDeviceHelpers
{ {
/// <summary> /// <summary>
@ -24,7 +26,7 @@ public static class NetworkDeviceHelpers
private static readonly char NewLineSplitter = CrestronEnvironment.NewLine.ToCharArray().First(); private static readonly char NewLineSplitter = CrestronEnvironment.NewLine.ToCharArray().First();
private static readonly string NewLine = CrestronEnvironment.NewLine; private static readonly string NewLine = CrestronEnvironment.NewLine;
private static readonly CCriticalSection Lock = new CCriticalSection(); private static readonly object _lock = new();
/// <summary> /// <summary>
/// Last resolved ARP table - it is recommended to refresh the arp before using this. /// Last resolved ARP table - it is recommended to refresh the arp before using this.
@ -37,9 +39,10 @@ public static class NetworkDeviceHelpers
public static void RefreshArp() public static void RefreshArp()
{ {
var error = false; var error = false;
lock (_lock)
{
try try
{ {
Lock.Enter();
var consoleResponse = string.Empty; var consoleResponse = string.Empty;
if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return; if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return;
if (string.IsNullOrEmpty(consoleResponse)) if (string.IsNullOrEmpty(consoleResponse))
@ -72,12 +75,10 @@ public static class NetworkDeviceHelpers
Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message); Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message);
error = true; error = true;
} }
finally } // end lock
{
Lock.Leave();
OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error)); OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error));
} }
}
private static void OnArpTableUpdated(ArpTableEventArgs args) private static void OnArpTableUpdated(ArpTableEventArgs args)
@ -158,7 +159,14 @@ public static class NetworkDeviceHelpers
/// </summary> /// </summary>
public class ArpEntry public class ArpEntry
{ {
/// <summary>
/// IP Address of the ARP entry
/// </summary>
public readonly IPAddress IpAddress; public readonly IPAddress IpAddress;
/// <summary>
/// MAC Address of the ARP entry
/// </summary>
public readonly string MacAddress; public readonly string MacAddress;
/// <summary> /// <summary>

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharpPro; using Crestron.SimplSharpPro;
using PepperDash.Core; using PepperDash.Core;
@ -10,13 +11,27 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core; namespace PepperDash.Essentials.Core;
/// <summary>
/// Manages devices in the system, including activation and console commands to interact with devices
/// </summary>
public static class DeviceManager public static class DeviceManager
{ {
/// <summary>
/// Event raised when all devices have been activated
/// </summary>
public static event EventHandler<EventArgs> AllDevicesActivated; public static event EventHandler<EventArgs> AllDevicesActivated;
/// <summary>
/// Event raised when all devices have been registered
/// </summary>
public static event EventHandler<EventArgs> AllDevicesRegistered; public static event EventHandler<EventArgs> AllDevicesRegistered;
/// <summary>
/// Event raised when all devices have been initialized
/// </summary>
public static event EventHandler<EventArgs> AllDevicesInitialized; public static event EventHandler<EventArgs> AllDevicesInitialized;
private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); private static readonly object _deviceLock = new();
//public static List<Device> Devices { get { return _Devices; } } //public static List<Device> Devices { get { return _Devices; } }
//static List<Device> _Devices = new List<Device>(); //static List<Device> _Devices = new List<Device>();
@ -28,7 +43,10 @@ public static class DeviceManager
/// </summary> /// </summary>
public static List<IKeyed> AllDevices => [.. Devices.Values]; public static List<IKeyed> AllDevices => [.. Devices.Values];
public static bool AddDeviceEnabled; /// <summary>
/// Flag to indicate whether adding devices is currently allowed. This is set to false once ActivateAll is called to prevent changes to the device list after activation.
/// </summary>
public static bool AddDeviceEnabled { get; private set; }
/// <summary> /// <summary>
/// Initializes the control system by enabling device management and registering console commands. /// Initializes the control system by enabling device management and registering console commands.
@ -64,12 +82,11 @@ public static class DeviceManager
/// Calls activate steps on all Device class items /// Calls activate steps on all Device class items
/// </summary> /// </summary>
public static void ActivateAll() public static void ActivateAll()
{
try
{ {
OnAllDevicesRegistered(); OnAllDevicesRegistered();
DeviceCriticalSection.Enter(); lock (_deviceLock)
{
AddDeviceEnabled = false; AddDeviceEnabled = false;
// PreActivate all devices // PreActivate all devices
Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****"); Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****");
@ -125,11 +142,7 @@ public static class DeviceManager
Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****"); Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****");
OnAllDevicesActivated(); OnAllDevicesActivated();
} } // end lock
finally
{
DeviceCriticalSection.Leave();
}
} }
private static void DeviceManager_Initialized(object sender, EventArgs e) private static void DeviceManager_Initialized(object sender, EventArgs e)
@ -176,18 +189,13 @@ public static class DeviceManager
/// </summary> /// </summary>
public static void DeactivateAll() public static void DeactivateAll()
{ {
try lock (_deviceLock)
{ {
DeviceCriticalSection.Enter();
foreach (var d in Devices.Values.OfType<Device>()) foreach (var d in Devices.Values.OfType<Device>())
{ {
d.Deactivate(); d.Deactivate();
} }
} }
finally
{
DeviceCriticalSection.Leave();
}
} }
//static void ListMethods(string devKey) //static void ListMethods(string devKey)
@ -266,11 +274,16 @@ public static class DeviceManager
// Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned"); // Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned");
//} //}
/// <summary>
/// Adds a device to the manager
/// </summary>
public static void AddDevice(IKeyed newDev) public static void AddDevice(IKeyed newDev)
{ {
var lockAcquired = false;
try try
{ {
if (!DeviceCriticalSection.TryEnter()) lockAcquired = Monitor.TryEnter(_deviceLock);
if (!lockAcquired)
{ {
Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again");
return; return;
@ -300,15 +313,22 @@ public static class DeviceManager
} }
finally finally
{ {
DeviceCriticalSection.Leave(); if (lockAcquired)
Monitor.Exit(_deviceLock);
} }
} }
/// <summary>
/// Adds a list of devices to the manager
/// </summary>
/// <param name="devicesToAdd"></param>
public static void AddDevice(IEnumerable<IKeyed> devicesToAdd) public static void AddDevice(IEnumerable<IKeyed> devicesToAdd)
{ {
var lockAcquired = false;
try try
{ {
if (!DeviceCriticalSection.TryEnter()) lockAcquired = Monitor.TryEnter(_deviceLock);
if (!lockAcquired)
{ {
Debug.LogMessage(LogEventLevel.Information, Debug.LogMessage(LogEventLevel.Information,
"Currently unable to add devices to Device Manager. Please try again"); "Currently unable to add devices to Device Manager. Please try again");
@ -336,15 +356,19 @@ public static class DeviceManager
} }
finally finally
{ {
DeviceCriticalSection.Leave(); if (lockAcquired)
Monitor.Exit(_deviceLock);
} }
} }
/// <summary>
/// Removes a device from the manager
/// </summary>
/// <param name="newDev">The device to remove</param>
public static void RemoveDevice(IKeyed newDev) public static void RemoveDevice(IKeyed newDev)
{ {
try lock (_deviceLock)
{ {
DeviceCriticalSection.Enter();
if (newDev == null) if (newDev == null)
return; return;
if (Devices.ContainsKey(newDev.Key)) if (Devices.ContainsKey(newDev.Key))
@ -354,18 +378,22 @@ public static class DeviceManager
else else
Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key);
} }
finally
{
DeviceCriticalSection.Leave();
}
} }
/// <summary>
/// Returns a list of all device keys currently in the manager
/// </summary>
/// <returns>A list of device keys</returns>
/// <remarks>This method provides a way to retrieve a list of all device keys currently registered in the Device Manager. It returns an enumerable collection of strings representing the keys of the devices, allowing for easy access and manipulation of the device list as needed.</remarks>
public static IEnumerable<string> GetDeviceKeys() public static IEnumerable<string> GetDeviceKeys()
{ {
//return _Devices.Select(d => d.Key).ToList(); //return _Devices.Select(d => d.Key).ToList();
return Devices.Keys; return Devices.Keys;
} }
/// <summary>
/// Returns a list of all devices currently in the manager
/// </summary> <returns>A list of devices</returns>
public static IEnumerable<IKeyed> GetDevices() public static IEnumerable<IKeyed> GetDevices()
{ {
//return _Devices.Select(d => d.Key).ToList(); //return _Devices.Select(d => d.Key).ToList();

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using PepperDash.Core; using PepperDash.Core;
@ -77,11 +78,11 @@ namespace PepperDash.Essentials.Core
public abstract void FireUpdate(); public abstract void FireUpdate();
/// <summary> /// <summary>
/// Fires the update asynchronously within a CrestronInvoke /// Fires the update asynchronously within a Task
/// </summary> /// </summary>
public void InvokeFireUpdate() public void InvokeFireUpdate()
{ {
CrestronInvoke.BeginInvoke(o => FireUpdate()); Task.Run(() => FireUpdate());
} }
/// <summary> /// <summary>

View file

@ -1,11 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using PepperDash.Core; using PepperDash.Core;
using Crestron.SimplSharpPro.CrestronThread;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Essentials.Core namespace PepperDash.Essentials.Core
@ -16,7 +15,7 @@ namespace PepperDash.Essentials.Core
public static class FileIO public static class FileIO
{ {
static CCriticalSection fileLock = new CCriticalSection(); static readonly object _fileLock = new();
/// <summary> /// <summary>
/// Delegate for GotFileEventHandler /// Delegate for GotFileEventHandler
/// </summary> /// </summary>
@ -103,9 +102,11 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public static string ReadDataFromFile(FileInfo file) public static string ReadDataFromFile(FileInfo file)
{ {
var lockAcquired = false;
try try
{ {
if (fileLock.TryEnter()) lockAcquired = Monitor.TryEnter(_fileLock);
if (lockAcquired)
{ {
DirectoryInfo dirInfo = new DirectoryInfo(file.DirectoryName); DirectoryInfo dirInfo = new DirectoryInfo(file.DirectoryName);
Debug.LogMessage(LogEventLevel.Verbose, "FileIO Getting Data {0}", file.FullName); Debug.LogMessage(LogEventLevel.Verbose, "FileIO Getting Data {0}", file.FullName);
@ -128,7 +129,6 @@ namespace PepperDash.Essentials.Core
Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock"); Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock");
return ""; return "";
} }
} }
catch (Exception e) catch (Exception e)
{ {
@ -137,9 +137,8 @@ namespace PepperDash.Essentials.Core
} }
finally finally
{ {
if (fileLock != null && !fileLock.Disposed) if (lockAcquired)
fileLock.Leave(); Monitor.Exit(_fileLock);
} }
} }
@ -166,7 +165,7 @@ namespace PepperDash.Essentials.Core
{ {
try try
{ {
CrestronInvoke.BeginInvoke(o => _ReadDataFromFileASync(file)); Task.Run(() => _ReadDataFromFileASync(file));
} }
catch (Exception e) catch (Exception e)
{ {
@ -177,9 +176,11 @@ namespace PepperDash.Essentials.Core
private static void _ReadDataFromFileASync(FileInfo file) private static void _ReadDataFromFileASync(FileInfo file)
{ {
string data; string data;
var lockAcquired = false;
try try
{ {
if (fileLock.TryEnter()) lockAcquired = Monitor.TryEnter(_fileLock);
if (lockAcquired)
{ {
DirectoryInfo dirInfo = new DirectoryInfo(file.Name); DirectoryInfo dirInfo = new DirectoryInfo(file.Name);
Debug.LogMessage(LogEventLevel.Verbose, "FileIO Getting Data {0}", file.FullName); Debug.LogMessage(LogEventLevel.Verbose, "FileIO Getting Data {0}", file.FullName);
@ -212,13 +213,9 @@ namespace PepperDash.Essentials.Core
} }
finally finally
{ {
if (fileLock != null && !fileLock.Disposed) if (lockAcquired)
fileLock.Leave(); Monitor.Exit(_fileLock);
} }
} }
/// <summary> /// <summary>
@ -228,35 +225,35 @@ namespace PepperDash.Essentials.Core
/// <param name="filePath"></param> /// <param name="filePath"></param>
public static void WriteDataToFile(string data, string filePath) public static void WriteDataToFile(string data, string filePath)
{ {
Thread _WriteFileThread; var _WriteFileThread = new System.Threading.Thread(() => _WriteFileMethod(data, Global.FilePathPrefix + "/" + filePath))
_WriteFileThread = new Thread((O) => _WriteFileMethod(data, Global.FilePathPrefix + "/" + filePath), null, Thread.eThreadStartOptions.CreateSuspended); {
_WriteFileThread.Priority = Thread.eThreadPriority.LowestPriority; IsBackground = true,
Priority = ThreadPriority.Lowest
};
_WriteFileThread.Start(); _WriteFileThread.Start();
Debug.LogMessage(LogEventLevel.Information, "New WriteFile Thread"); Debug.LogMessage(LogEventLevel.Information, "New WriteFile Thread");
} }
static object _WriteFileMethod(string data, string filePath) static void _WriteFileMethod(string data, string filePath)
{ {
Debug.LogMessage(LogEventLevel.Information, "Attempting to write file: '{0}'", filePath); Debug.LogMessage(LogEventLevel.Information, "Attempting to write file: '{0}'", filePath);
var lockAcquired = false;
try try
{ {
if (fileLock.TryEnter()) lockAcquired = Monitor.TryEnter(_fileLock);
if (lockAcquired)
{ {
using (StreamWriter sw = new StreamWriter(filePath)) using (StreamWriter sw = new StreamWriter(filePath))
{ {
sw.Write(data); sw.Write(data);
sw.Flush(); sw.Flush();
} }
} }
else else
{ {
Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock"); Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock");
} }
} }
catch (Exception e) catch (Exception e)
{ {
@ -264,12 +261,9 @@ namespace PepperDash.Essentials.Core
} }
finally finally
{ {
if (fileLock != null && !fileLock.Disposed) if (lockAcquired)
fileLock.Leave(); Monitor.Exit(_fileLock);
} }
return null;
} }
/// <summary> /// <summary>

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using Timer = System.Timers.Timer;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.CrestronXml; using Crestron.SimplSharp.CrestronXml;
@ -78,6 +78,8 @@ namespace PepperDash.Essentials.Core.Fusion
private bool _helpRequestSent; private bool _helpRequestSent;
private readonly object _guidFileLock = new();
private eFusionHelpResponse _helpRequestStatus; private eFusionHelpResponse _helpRequestStatus;
/// <inheritdoc /> /// <inheritdoc />
@ -390,17 +392,10 @@ namespace PepperDash.Essentials.Core.Fusion
return; return;
} }
var fileLock = new CCriticalSection(); lock (_guidFileLock)
{
try try
{ {
if (fileLock.Disposed)
{
return;
}
fileLock.Enter();
Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file"); Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file");
_guids = FusionOccSensor == null _guids = FusionOccSensor == null
@ -421,13 +416,7 @@ namespace PepperDash.Essentials.Core.Fusion
{ {
Debug.LogMessage(LogEventLevel.Information, this, "Error writing guid file: {0}", e); Debug.LogMessage(LogEventLevel.Information, this, "Error writing guid file: {0}", e);
} }
finally } // end lock
{
if (!fileLock.Disposed)
{
fileLock.Leave();
}
}
} }
/// <summary> /// <summary>
@ -442,17 +431,10 @@ namespace PepperDash.Essentials.Core.Fusion
return; return;
} }
var fileLock = new CCriticalSection(); lock (_guidFileLock)
{
try try
{ {
if (fileLock.Disposed)
{
return;
}
fileLock.Enter();
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
var json = File.ReadToEnd(filePath, Encoding.ASCII); var json = File.ReadToEnd(filePath, Encoding.ASCII);
@ -479,13 +461,7 @@ namespace PepperDash.Essentials.Core.Fusion
{ {
Debug.LogMessage(LogEventLevel.Information, this, "Error reading guid file: {0}", e); Debug.LogMessage(LogEventLevel.Information, this, "Error reading guid file: {0}", e);
} }
finally } // end lock
{
if (!fileLock.Disposed)
{
fileLock.Leave();
}
}
} }
/// <summary> /// <summary>
@ -1950,12 +1926,13 @@ namespace PepperDash.Essentials.Core.Fusion
HelpRequestStatusFeedback.FireUpdate(); HelpRequestStatusFeedback.FireUpdate();
} }
private void OnTimedEvent(object source, ElapsedEventArgs e) private void OnTimedEvent(object sender, System.Timers.ElapsedEventArgs e)
{ {
this.LogInformation("Help request timeout reached for room '{0}'. Cancelling help request.", Room.Name); this.LogInformation("Help request timeout reached for room '{0}'. Cancelling help request.", Room.Name);
CancelHelpRequest(); CancelHelpRequest();
} }
/// <inheritdoc /> /// <inheritdoc />
public void CancelHelpRequest() public void CancelHelpRequest()
{ {

View file

@ -9,6 +9,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Bridges;
using Serilog.Events; using Serilog.Events;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.Monitoring; namespace PepperDash.Essentials.Core.Monitoring;
@ -272,7 +273,7 @@ public class SystemMonitorController : EssentialsBridgeableDevice
private void RefreshSystemMonitorData() private void RefreshSystemMonitorData()
{ {
// this takes a while, launch a new thread // this takes a while, launch a new thread
CrestronInvoke.BeginInvoke(UpdateFeedback); Task.Run(() => UpdateFeedback(null));
} }
private void UpdateFeedback(object o) private void UpdateFeedback(object o)
@ -744,7 +745,7 @@ public class ProgramStatusFeedbacks
/// </summary> /// </summary>
public void GetProgramInfo() public void GetProgramInfo()
{ {
CrestronInvoke.BeginInvoke(GetProgramInfo); Task.Run(() => GetProgramInfo(null));
} }
private void GetProgramInfo(object o) private void GetProgramInfo(object o)

View file

@ -3,13 +3,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json; using Newtonsoft.Json;
using PepperDash.Core; using PepperDash.Core;
//using SSMono.IO;
using PepperDash.Core.WebApi.Presets;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Essentials.Core.Presets; namespace PepperDash.Essentials.Core.Presets;
@ -19,11 +18,19 @@ namespace PepperDash.Essentials.Core.Presets;
/// </summary> /// </summary>
public class DevicePresetsModel : Device public class DevicePresetsModel : Device
{ {
/// <summary>
/// Delegate for PresetRecalled event, which is fired when a preset is recalled. Provides the device and channel that was recalled.
/// </summary>
/// <param name="device"></param>
/// <param name="channel"></param>
public delegate void PresetRecalledCallback(ISetTopBoxNumericKeypad device, string channel); public delegate void PresetRecalledCallback(ISetTopBoxNumericKeypad device, string channel);
/// <summary>
/// Delegate for PresetsSaved event, which is fired when presets are saved. Provides the list of presets that were saved.
/// </summary> <param name="presets"></param>
public delegate void PresetsSavedCallback(List<PresetChannel> presets); public delegate void PresetsSavedCallback(List<PresetChannel> presets);
private readonly CCriticalSection _fileOps = new CCriticalSection(); private readonly object _fileOps = new();
private readonly bool _initSuccess; private readonly bool _initSuccess;
private readonly ISetTopBoxNumericKeypad _setTopBox; private readonly ISetTopBoxNumericKeypad _setTopBox;
@ -37,6 +44,12 @@ public class DevicePresetsModel : Device
private Action<bool> _enterFunction; private Action<bool> _enterFunction;
private string _filePath; private string _filePath;
/// <summary>
/// Constructor for DevicePresetsModel when a set top box device is included. If the set top box does not implement the required INumericKeypad interface, the model will still be created but dialing functionality will be disabled and a message will be logged.
/// </summary>
/// <param name="key"></param>
/// <param name="setTopBox"></param>
/// <param name="fileName"></param>
public DevicePresetsModel(string key, ISetTopBoxNumericKeypad setTopBox, string fileName) public DevicePresetsModel(string key, ISetTopBoxNumericKeypad setTopBox, string fileName)
: this(key, fileName) : this(key, fileName)
{ {
@ -71,6 +84,11 @@ public class DevicePresetsModel : Device
_enterFunction = setTopBox.KeypadEnter; _enterFunction = setTopBox.KeypadEnter;
} }
/// <summary>
/// Constructor for DevicePresetsModel when only a file name is provided. Dialing functionality will be disabled.
/// </summary>
/// <param name="key"></param>
/// <param name="fileName"></param>
public DevicePresetsModel(string key, string fileName) : base(key) public DevicePresetsModel(string key, string fileName) : base(key)
{ {
PulseTime = 150; PulseTime = 150;
@ -88,27 +106,73 @@ public class DevicePresetsModel : Device
_initSuccess = true; _initSuccess = true;
} }
/// <summary>
/// Event fired when a preset is recalled, providing the device and channel that was recalled
/// </summary>
public event PresetRecalledCallback PresetRecalled; public event PresetRecalledCallback PresetRecalled;
/// <summary>
/// Event fired when presets are saved, providing the list of presets that were saved
/// </summary>
public event PresetsSavedCallback PresetsSaved; public event PresetsSavedCallback PresetsSaved;
public int PulseTime { get; set; } /// <summary>
public int DigitSpacingMs { get; set; } /// Time in milliseconds to pulse the digit for when dialing a channel
/// </summary>
public int PulseTime { get; private set; }
/// <summary>
/// Time in milliseconds to wait between pulsing digits when dialing a channel
/// </summary>
public int DigitSpacingMs { get; private set; }
/// <summary>
/// Whether the presets have finished loading from the file or not
/// </summary>
public bool PresetsAreLoaded { get; private set; } public bool PresetsAreLoaded { get; private set; }
/// <summary>
/// The list of presets to display
/// </summary>
public List<PresetChannel> PresetsList { get; private set; } public List<PresetChannel> PresetsList { get; private set; }
/// <summary>
/// The number of presets in the list
/// </summary>
public int Count public int Count
{ {
get { return PresetsList != null ? PresetsList.Count : 0; } get { return PresetsList != null ? PresetsList.Count : 0; }
} }
public bool UseLocalImageStorage { get; set; } /// <summary>
public string ImagesLocalHostPrefix { get; set; } /// Indicates whether to use local image storage for preset images, which allows for more and larger images than the SIMPL+ zip file method
public string ImagesPathPrefix { get; set; } /// </summary>
public string ListPathPrefix { get; set; } public bool UseLocalImageStorage { get; private set; }
/// <summary>
/// The prefix for the local host URL for preset images
/// </summary>
public string ImagesLocalHostPrefix { get; private set; }
/// <summary>
/// The path prefix for preset images
/// </summary>
public string ImagesPathPrefix { get; private set; }
/// <summary>
/// The path prefix for preset lists
/// </summary>
public string ListPathPrefix { get; private set; }
/// <summary>
/// Event fired when presets are loaded
/// </summary>
public event EventHandler PresetsLoaded; public event EventHandler PresetsLoaded;
/// <summary>
/// Sets the file name for the presets list and loads the presets from that file. The file should be a JSON file in the format of the PresetsList class. If the file cannot be read, an empty list will be created and a message will be logged. This method is thread safe.
/// </summary>
/// <param name="path">The path to the presets file.</param>
public void SetFileName(string path) public void SetFileName(string path)
{ {
_filePath = ListPathPrefix + path; _filePath = ListPathPrefix + path;
@ -117,12 +181,13 @@ public class DevicePresetsModel : Device
LoadChannels(); LoadChannels();
} }
/// <summary>
/// Loads the presets from the file specified by _filePath.
/// </summary>
public void LoadChannels() public void LoadChannels()
{ {
try lock (_fileOps)
{ {
_fileOps.Enter();
Debug.LogMessage(LogEventLevel.Verbose, this, "Loading presets from {0}", _filePath); Debug.LogMessage(LogEventLevel.Verbose, this, "Loading presets from {0}", _filePath);
PresetsAreLoaded = false; PresetsAreLoaded = false;
try try
@ -149,12 +214,12 @@ public class DevicePresetsModel : Device
handler(this, EventArgs.Empty); handler(this, EventArgs.Empty);
} }
} }
finally
{
_fileOps.Leave();
}
} }
/// <summary>
/// Dials a preset by its number in the list (starting at 1). If the preset number is out of range, nothing will happen.
/// </summary>
/// <param name="presetNum">The number of the preset to dial, starting at 1</param>
public void Dial(int presetNum) public void Dial(int presetNum)
{ {
if (presetNum <= PresetsList.Count) if (presetNum <= PresetsList.Count)
@ -163,6 +228,10 @@ public class DevicePresetsModel : Device
} }
} }
/// <summary>
/// Dials a preset by its channel number. If the channel number contains characters that are not 0-9 or '-', those characters will be ignored.
/// If the model was not initialized with a valid set top box device, dialing will be disabled and a message will be logged.
/// </summary> <param name="chanNum">The channel number to dial</param>
public void Dial(string chanNum) public void Dial(string chanNum)
{ {
if (_dialIsRunning || !_initSuccess) if (_dialIsRunning || !_initSuccess)
@ -176,7 +245,7 @@ public class DevicePresetsModel : Device
} }
_dialIsRunning = true; _dialIsRunning = true;
CrestronInvoke.BeginInvoke(o => Task.Run(() =>
{ {
foreach (var c in chanNum.ToCharArray()) foreach (var c in chanNum.ToCharArray())
{ {
@ -199,6 +268,11 @@ public class DevicePresetsModel : Device
OnPresetRecalled(_setTopBox, chanNum); OnPresetRecalled(_setTopBox, chanNum);
} }
/// <summary>
/// Dials a preset by its number in the list (starting at 1) using the provided set top box device. If the preset number is out of range, nothing will happen.
/// </summary>
/// <param name="presetNum"></param>
/// <param name="setTopBox"></param>
public void Dial(int presetNum, ISetTopBoxNumericKeypad setTopBox) public void Dial(int presetNum, ISetTopBoxNumericKeypad setTopBox)
{ {
if (presetNum <= PresetsList.Count) if (presetNum <= PresetsList.Count)
@ -207,6 +281,13 @@ public class DevicePresetsModel : Device
} }
} }
/// <summary>
/// Dials a preset by its channel number using the provided set top box device. If the channel number contains characters that are not 0-9 or '-', those characters will be ignored.
/// If the provided set top box device does not implement the required INumericKeypad interface, dialing will be disabled and a message will be logged.
/// If the model was not initialized with a valid set top box device, dialing will be disabled and a message will be logged.
/// </summary>
/// <param name="chanNum"></param>
/// <param name="setTopBox"></param>
public void Dial(string chanNum, ISetTopBoxNumericKeypad setTopBox) public void Dial(string chanNum, ISetTopBoxNumericKeypad setTopBox)
{ {
_dialFunctions = new Dictionary<char, Action<bool>>(10) _dialFunctions = new Dictionary<char, Action<bool>>(10)
@ -243,6 +324,11 @@ public class DevicePresetsModel : Device
handler(setTopBox, channel); handler(setTopBox, channel);
} }
/// <summary>
/// Updates the preset at the given index with the provided preset information, then saves the updated presets list to the file. If the index is out of range, nothing will happen.
/// </summary>
/// <param name="index">The index of the preset to update, starting at 0</param>
/// <param name="preset">The preset information to update</param>
public void UpdatePreset(int index, PresetChannel preset) public void UpdatePreset(int index, PresetChannel preset)
{ {
if (index >= PresetsList.Count) if (index >= PresetsList.Count)
@ -257,6 +343,10 @@ public class DevicePresetsModel : Device
OnPresetsSaved(); OnPresetsSaved();
} }
/// <summary>
/// Updates the entire presets list with the provided list, then saves the updated presets list to the file. If the provided list is null, nothing will happen.
/// </summary>
/// <param name="presets"></param>
public void UpdatePresets(List<PresetChannel> presets) public void UpdatePresets(List<PresetChannel> presets)
{ {
PresetsList = presets; PresetsList = presets;
@ -268,10 +358,9 @@ public class DevicePresetsModel : Device
private void SavePresets() private void SavePresets()
{ {
try lock (_fileOps)
{ {
_fileOps.Enter(); var pl = new PresetsList { Channels = PresetsList, Name = Name };
var pl = new PresetsList {Channels = PresetsList, Name = Name};
var json = JsonConvert.SerializeObject(pl, Formatting.Indented); var json = JsonConvert.SerializeObject(pl, Formatting.Indented);
using (var file = File.Open(_filePath, FileMode.Truncate)) using (var file = File.Open(_filePath, FileMode.Truncate))
@ -279,11 +368,6 @@ public class DevicePresetsModel : Device
file.Write(json, Encoding.UTF8); file.Write(json, Encoding.UTF8);
} }
} }
finally
{
_fileOps.Leave();
}
} }
private void OnPresetsSaved() private void OnPresetsSaved()

View file

@ -4,24 +4,35 @@ using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using PepperDash.Core; using PepperDash.Core;
using Serilog.Events; using Serilog.Events;
using Thread = Crestron.SimplSharpPro.CrestronThread.Thread;
namespace PepperDash.Essentials.Core.Queues; namespace PepperDash.Essentials.Core.Queues;
// TODO: The capacity argument in the constructors should be removed. Now that this class uses System.Threading rather than the Crestron library, there is no longer a thread capacity limit.
// If a capacity limit is needed, it should be implemented by the caller by checking the QueueCount property before enqueuing items and deciding how to handle the situation when the queue is too full (e.g. drop messages, log warnings, etc.)
/// <summary> /// <summary>
/// Threadsafe processing of queued items with pacing if required /// Threadsafe processing of queued items with pacing if required
/// </summary> /// </summary>
public class GenericQueue : IQueue<IQueueMessage> public class GenericQueue : IQueue<IQueueMessage>
{ {
private readonly string _key; private readonly string _key;
/// <summary>
/// Returns the number of items currently in the queue. This is not threadsafe, so it should only be used for informational purposes and not for processing logic.
/// </summary>
protected readonly ConcurrentQueue<IQueueMessage> _queue; protected readonly ConcurrentQueue<IQueueMessage> _queue;
/// <summary>
/// The thread that processes the queue items
/// </summary>
protected readonly Thread _worker; protected readonly Thread _worker;
protected readonly CEvent _waitHandle = new CEvent(); private readonly object _lock = new();
private bool _delayEnabled; private bool _delayEnabled;
private int _delayTime; private int _delayTime;
private const Thread.eThreadPriority _defaultPriority = Thread.eThreadPriority.MediumPriority; private const ThreadPriority _defaultPriority = ThreadPriority.Normal;
/// <summary> /// <summary>
/// If the instance has been disposed. /// If the instance has been disposed.
@ -96,7 +107,7 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="pacing"></param> /// <param name="pacing"></param>
/// <param name="priority"></param> /// <param name="priority"></param>
public GenericQueue(string key, int pacing, Thread.eThreadPriority priority) public GenericQueue(string key, int pacing, ThreadPriority priority)
: this(key, priority, 0, pacing) : this(key, priority, 0, pacing)
{ {
} }
@ -107,7 +118,7 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="priority"></param> /// <param name="priority"></param>
/// <param name="capacity"></param> /// <param name="capacity"></param>
public GenericQueue(string key, Thread.eThreadPriority priority, int capacity) public GenericQueue(string key, ThreadPriority priority, int capacity)
: this(key, priority, capacity, 0) : this(key, priority, capacity, 0)
{ {
} }
@ -119,7 +130,7 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="pacing"></param> /// <param name="pacing"></param>
/// <param name="priority"></param> /// <param name="priority"></param>
/// <param name="capacity"></param> /// <param name="capacity"></param>
public GenericQueue(string key, int pacing, Thread.eThreadPriority priority, int capacity) public GenericQueue(string key, int pacing, ThreadPriority priority, int capacity)
: this(key, priority, capacity, pacing) : this(key, priority, capacity, pacing)
{ {
} }
@ -131,21 +142,18 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="priority"></param> /// <param name="priority"></param>
/// <param name="capacity"></param> /// <param name="capacity"></param>
/// <param name="pacing"></param> /// <param name="pacing"></param>
protected GenericQueue(string key, Thread.eThreadPriority priority, int capacity, int pacing) protected GenericQueue(string key, ThreadPriority priority, int capacity, int pacing)
{ {
_key = key; _key = key;
int cap = 25; // sets default
if (capacity > 0)
{
cap = capacity; // overrides default
}
_queue = new ConcurrentQueue<IQueueMessage>(); _queue = new ConcurrentQueue<IQueueMessage>();
_worker = new Thread(ProcessQueue, null, Thread.eThreadStartOptions.Running) _worker = new Thread(ProcessQueue)
{ {
Priority = priority, Priority = priority,
Name = _key Name = _key,
IsBackground = true
}; };
_worker.Start();
SetDelayValues(pacing); SetDelayValues(pacing);
} }
@ -167,9 +175,8 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <summary> /// <summary>
/// Thread callback /// Thread callback
/// </summary> /// </summary>
/// <param name="obj">The action used to process dequeued items</param>
/// <returns>Null when the thread is exited</returns> /// <returns>Null when the thread is exited</returns>
private object ProcessQueue(object obj) private void ProcessQueue()
{ {
while (true) while (true)
{ {
@ -186,7 +193,7 @@ public class GenericQueue : IQueue<IQueueMessage>
if (_delayEnabled) if (_delayEnabled)
Thread.Sleep(_delayTime); Thread.Sleep(_delayTime);
} }
catch (ThreadAbortException) catch (ThreadInterruptedException)
{ {
//swallowing this exception, as it should only happen on shut down //swallowing this exception, as it should only happen on shut down
} }
@ -202,12 +209,21 @@ public class GenericQueue : IQueue<IQueueMessage>
} }
} }
} }
else _waitHandle.Wait(); else
} {
lock (_lock)
return null; {
if (_queue.IsEmpty)
Monitor.Wait(_lock);
}
}
}
} }
/// <summary>
/// Enqueues an item to be processed by the queue thread. If the queue has been disposed, the item will not be enqueued and a message will be logged.
/// </summary>
/// <param name="item"></param>
public void Enqueue(IQueueMessage item) public void Enqueue(IQueueMessage item)
{ {
if (Disposed) if (Disposed)
@ -217,7 +233,8 @@ public class GenericQueue : IQueue<IQueueMessage>
} }
_queue.Enqueue(item); _queue.Enqueue(item);
_waitHandle.Set(); lock (_lock)
Monitor.Pulse(_lock);
} }
/// <summary> /// <summary>
@ -241,19 +258,18 @@ public class GenericQueue : IQueue<IQueueMessage>
return; return;
if (disposing) if (disposing)
{
using (_waitHandle)
{ {
Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing..."); Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing...");
_queue.Enqueue(null); _queue.Enqueue(null);
_waitHandle.Set(); lock (_lock)
Monitor.Pulse(_lock);
_worker.Join(); _worker.Join();
} }
}
Disposed = true; Disposed = true;
} }
/// Finalizer in case Dispose is not called. This will clean up the thread, but any items still in the queue will not be processed and could potentially be lost.
~GenericQueue() ~GenericQueue()
{ {
Dispose(true); Dispose(true);

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Threading.Tasks;
using Crestron.SimplSharp;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Essentials.Core; using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
@ -10,19 +9,37 @@ using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common.SoftCodec; namespace PepperDash.Essentials.Devices.Common.SoftCodec;
/// <summary>
/// Class representing a BlueJeans soft codec running on an in-room PC.
/// </summary>
public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink
{ {
/// <summary>
/// The input port for any video source.
/// </summary>
public RoutingInputPort AnyVideoIn { get; private set; } public RoutingInputPort AnyVideoIn { get; private set; }
/// <summary>
/// The currently active input port, which for this device is always AnyVideoIn
/// This is used by the routing system to determine where to route video sources when this device is a destination
/// </summary>
public RoutingInputPort CurrentInputPort => AnyVideoIn; public RoutingInputPort CurrentInputPort => AnyVideoIn;
#region IRoutingInputs Members #region IRoutingInputs Members
/// <summary>
/// Collection of the input ports for this device
/// </summary>
public RoutingPortCollection<RoutingInputPort> InputPorts { get; private set; } public RoutingPortCollection<RoutingInputPort> InputPorts { get; private set; }
#endregion #endregion
/// <summary>
/// Initializes a new instance of the <see cref="BlueJeansPc"/> class.
/// </summary>
/// <param name="key">The key for the device.</param>
/// <param name="name">The name of the device.</param>
public BlueJeansPc(string key, string name) public BlueJeansPc(string key, string name)
: base(key, name) : base(key, name)
{ {
@ -34,14 +51,25 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink
#region IRunRouteAction Members #region IRunRouteAction Members
/// <summary>
/// Runs a route action for the specified route key and source list key. Optionally, a callback can be provided to be executed upon successful completion.
/// </summary>
/// <param name="routeKey"></param>
/// <param name="sourceListKey"></param>
public void RunRouteAction(string routeKey, string sourceListKey) public void RunRouteAction(string routeKey, string sourceListKey)
{ {
RunRouteAction(routeKey, sourceListKey, null); RunRouteAction(routeKey, sourceListKey, null);
} }
/// <summary>
/// Runs a route action for the specified route key and source list key. Optionally, a callback can be provided to be executed upon successful completion.
/// </summary>
/// <param name="routeKey"></param>
/// <param name="sourceListKey"></param>
/// <param name="successCallback"></param>
public void RunRouteAction(string routeKey, string sourceListKey, Action successCallback) public void RunRouteAction(string routeKey, string sourceListKey, Action successCallback)
{ {
CrestronInvoke.BeginInvoke(o => Task.Run(() =>
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "Run route action '{0}' on SourceList: {1}", routeKey, sourceListKey); Debug.LogMessage(LogEventLevel.Debug, this, "Run route action '{0}' on SourceList: {1}", routeKey, sourceListKey);
@ -127,6 +155,7 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink
#region IHasCurrentSourceInfoChange Members #region IHasCurrentSourceInfoChange Members
/// <inheritdoc />
public string CurrentSourceInfoKey { get; set; } public string CurrentSourceInfoKey { get; set; }
/// <summary> /// <summary>
@ -158,6 +187,7 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink
} }
SourceListItem _CurrentSourceInfo; SourceListItem _CurrentSourceInfo;
/// <inheritdoc />
public event SourceInfoChangeHandler CurrentSourceChange; public event SourceInfoChangeHandler CurrentSourceChange;
#endregion #endregion

View file

@ -20,6 +20,7 @@ using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces;
using Serilog.Events; using Serilog.Events;
using Feedback = PepperDash.Essentials.Core.Feedback; using Feedback = PepperDash.Essentials.Core.Feedback;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.VideoCodec; namespace PepperDash.Essentials.Devices.Common.VideoCodec;
@ -382,7 +383,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
/// </summary> /// </summary>
protected void SetIsReady() protected void SetIsReady()
{ {
CrestronInvoke.BeginInvoke((o) => Task.Run(() =>
{ {
try try
{ {

View file

@ -26,8 +26,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// </summary> /// </summary>
protected VideoCodecBase Codec { get; private set; } protected VideoCodecBase Codec { get; private set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -126,7 +124,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
//state.CurrentDirectory = PrefixDirectoryFolderItems(directory); //state.CurrentDirectory = PrefixDirectoryFolderItems(directory);
state.CurrentDirectory = directory; state.CurrentDirectory = directory;
CrestronInvoke.BeginInvoke((o) => PostStatusMessage(state)); Task.Run(() => PostStatusMessage(state));
} }
} }
catch (Exception ex) catch (Exception ex)
@ -162,7 +160,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary> /// <summary>
/// Called from base's RegisterWithAppServer method /// Called from base's RegisterWithAppServer method
/// </summary> /// </summary>
/// <param name="appServerController"></param>
protected override void RegisterActions() protected override void RegisterActions()
{ {
try try
@ -786,6 +783,10 @@ namespace PepperDash.Essentials.AppServer.Messengers
} }
} }
/// <summary>
/// Sends the full status of the codec, including active calls, camera status, and directory info if applicable
/// </summary>
/// <param name="id"></param>
protected virtual void SendFullStatus(string id = null) protected virtual void SendFullStatus(string id = null)
{ {
if (!Codec.IsReady) if (!Codec.IsReady)

View file

@ -103,7 +103,7 @@ namespace PepperDash.Essentials
/// </summary> /// </summary>
public MobileControlWebsocketServer DirectServer => _directServer; public MobileControlWebsocketServer DirectServer => _directServer;
private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); private readonly object _wsCriticalSection = new();
/// <summary> /// <summary>
/// Gets or sets the SystemUrl /// Gets or sets the SystemUrl
@ -206,14 +206,14 @@ namespace PepperDash.Essentials
//_receiveQueue = new ReceiveQueue(key, ParseStreamRx); //_receiveQueue = new ReceiveQueue(key, ParseStreamRx);
_receiveQueue = new GenericQueue( _receiveQueue = new GenericQueue(
key + "-rxqueue", key + "-rxqueue",
Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, System.Threading.ThreadPriority.Highest,
25 25
); );
// The queue that will collect the outgoing messages in the order they are received // The queue that will collect the outgoing messages in the order they are received
_transmitToServerQueue = new GenericQueue( _transmitToServerQueue = new GenericQueue(
key + "-txqueue", key + "-txqueue",
Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, System.Threading.ThreadPriority.Highest,
25 25
); );
@ -228,7 +228,7 @@ namespace PepperDash.Essentials
_transmitToClientsQueue = new GenericQueue( _transmitToClientsQueue = new GenericQueue(
key + "-clienttxqueue", key + "-clienttxqueue",
Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, System.Threading.ThreadPriority.Highest,
25 25
); );
} }
@ -1811,10 +1811,8 @@ namespace PepperDash.Essentials
private void ConnectWebsocketClient() private void ConnectWebsocketClient()
{ {
try lock (_wsCriticalSection)
{ {
_wsCriticalSection.Enter();
// set to 99999 to let things work on 4-Series // set to 99999 to let things work on 4-Series
if ( if (
(CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series4) (CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series4)
@ -1843,10 +1841,6 @@ namespace PepperDash.Essentials
TryConnect(); TryConnect();
} }
finally
{
_wsCriticalSection.Leave();
}
} }
private void TryConnect() private void TryConnect()

View file

@ -15,6 +15,7 @@ using PepperDash.Essentials.Core.Routing;
using System.Threading; using System.Threading;
using Timeout = Crestron.SimplSharp.Timeout; using Timeout = Crestron.SimplSharp.Timeout;
using Serilog.Events; using Serilog.Events;
using System.Threading.Tasks;
namespace PepperDash.Essentials; namespace PepperDash.Essentials;
@ -108,7 +109,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
if (Debug.DoNotLoadConfigOnNextBoot) if (Debug.DoNotLoadConfigOnNextBoot)
{ {
CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", CrestronConsole.AddNewConsoleCommand(s => Task.Run(() => GoWithLoad()), "go", "Loads configuration file",
ConsoleAccessLevelEnum.AccessOperator); ConsoleAccessLevelEnum.AccessOperator);
} }