Compare commits

..

2 Commits

Author SHA1 Message Date
Neil Dorin
ac393c4885 refactor(force-patch): Remove obsolete GenericHttpClient class in favor of built-in HttpClient 2026-03-10 17:35:23 -06:00
Neil Dorin
346a5e9e57 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.
2026-03-10 17:30:59 -06:00
24 changed files with 998 additions and 1091 deletions

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using System.Timers;
using PepperDash.Core;
namespace PepperDash.Core;
@@ -20,7 +20,7 @@ public class CommunicationStreamDebugging
/// <summary>
/// Timer to disable automatically if not manually disabled
/// </summary>
private CTimer DebugExpiryPeriod;
private Timer DebugExpiryPeriod;
/// <summary>
/// The current debug setting
@@ -93,7 +93,9 @@ public class CommunicationStreamDebugging
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)
RxStreamDebuggingIsEnabled = true;

View File

@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -277,7 +279,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
private readonly object _dequeueLock = new();
/// <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
@@ -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.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
if (Monitor.TryEnter(_dequeueLock))
Task.Run(() => DequeueEvent());
}
}
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>
/// 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.
/// </summary>
void DequeueEvent()
@@ -730,11 +731,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
{
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.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
Monitor.Exit(_dequeueLock);
}
void HeartbeatStart()

View File

@@ -15,6 +15,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -271,7 +273,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
private readonly object _dequeueLock = new();
/// <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
@@ -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.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
if (Monitor.TryEnter(_dequeueLock))
Task.Run(() => DequeueEvent());
}
}
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>
/// 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.
/// </summary>
void DequeueEvent()
@@ -689,11 +690,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{
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.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
Monitor.Exit(_dequeueLock);
}
void HeartbeatStart()

View File

@@ -1,18 +1,9 @@
/*PepperDash Technology Corp.
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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -69,12 +60,17 @@ public class GenericSecureTcpIpServer : Device
/// <summary>
/// Server listen lock
/// </summary>
CCriticalSection ServerCCSection = new CCriticalSection();
private readonly object _serverLock = new();
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
private readonly object _dequeueLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <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
@@ -399,18 +395,19 @@ public class GenericSecureTcpIpServer : Device
/// </summary>
public void Listen()
{
ServerCCSection.Enter();
lock (_serverLock)
{
try
{
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;
}
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;
}
@@ -434,22 +431,21 @@ public class GenericSecureTcpIpServer : Device
SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
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
{
ServerStopped = false;
}
OnServerStateChange(SecureServer.State);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
ServerCCSection.Leave();
this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
}
catch (Exception ex)
{
ServerCCSection.Leave();
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
}
} // end lock
}
/// <summary>
@@ -459,18 +455,18 @@ public class GenericSecureTcpIpServer : Device
{
try
{
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
this.LogVerbose("Stopping Listener");
if (SecureServer != null)
{
SecureServer.Stop();
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State);
this.LogVerbose("Server State: {0}", SecureServer.State);
OnServerStateChange(SecureServer.State);
}
ServerStopped = true;
}
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
{
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)
{
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>
@@ -495,7 +491,7 @@ public class GenericSecureTcpIpServer : Device
/// </summary>
public void DisconnectAllClientsForShutdown()
{
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
this.LogInformation("Disconnecting All Clients");
if (SecureServer != null)
{
SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
@@ -507,17 +503,17 @@ public class GenericSecureTcpIpServer : Device
try
{
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)
{
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();
if (!ProgramIsStopping)
@@ -535,8 +531,8 @@ public class GenericSecureTcpIpServer : Device
/// <param name="text"></param>
public void BroadcastText(string text)
{
CCriticalSection CCBroadcast = new CCriticalSection();
CCBroadcast.Enter();
lock (_broadcastLock)
{
try
{
if (ConnectedClientsIndexes.Count > 0)
@@ -552,13 +548,12 @@ public class GenericSecureTcpIpServer : Device
}
}
}
CCBroadcast.Leave();
}
catch (Exception ex)
{
CCBroadcast.Leave();
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
}
} // end lock
}
/// <summary>
@@ -579,7 +574,7 @@ public class GenericSecureTcpIpServer : Device
}
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);
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
SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText;
@@ -618,13 +613,13 @@ public class GenericSecureTcpIpServer : Device
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
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)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
@@ -636,11 +631,11 @@ public class GenericSecureTcpIpServer : Device
/// <returns></returns>
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)))
{
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;
}
@@ -663,7 +658,7 @@ public class GenericSecureTcpIpServer : Device
clientIndex = (uint)o;
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);
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));
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))
ConnectedClientsIndexes.Remove(clientIndex);
@@ -725,12 +720,12 @@ public class GenericSecureTcpIpServer : Device
}
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
//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.
CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null);
Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
}
#endregion
@@ -745,7 +740,7 @@ public class GenericSecureTcpIpServer : Device
{
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),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0)
@@ -765,7 +760,7 @@ public class GenericSecureTcpIpServer : Device
}
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
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
{
@@ -784,19 +779,19 @@ public class GenericSecureTcpIpServer : Device
}
else
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
this.LogError("Client attempt faulty.");
}
}
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
SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
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)
{
// 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)
{
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.Disconnect(clientIndex);
@@ -844,7 +839,7 @@ public class GenericSecureTcpIpServer : Device
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
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)))
{
@@ -857,7 +852,7 @@ public class GenericSecureTcpIpServer : Device
}
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)
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.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
if (Monitor.TryEnter(_dequeueLock))
Task.Run(() => DequeueEvent());
}
}
else
@@ -877,7 +871,7 @@ public class GenericSecureTcpIpServer : Device
}
/// <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.
/// </summary>
void DequeueEvent()
@@ -899,11 +893,8 @@ public class GenericSecureTcpIpServer : Device
{
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.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
Monitor.Exit(_dequeueLock);
}
#endregion
@@ -974,7 +965,7 @@ public class GenericSecureTcpIpServer : Device
if (MonitorClient != null)
MonitorClient.Disconnect();
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
this.LogInformation("Program stopping. Closing server");
KillServer();
}
}
@@ -1015,7 +1006,7 @@ public class GenericSecureTcpIpServer : Device
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
this.LogInformation("Starting monitor check");
MonitorClient.Connect();
// From here MonitorCLient either connects or hangs, MonitorClient will call back
@@ -1042,7 +1033,7 @@ public class GenericSecureTcpIpServer : Device
{
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 = null;
MonitorClientFailureCount = 0;
@@ -1063,13 +1054,13 @@ public class GenericSecureTcpIpServer : Device
StopMonitorClient();
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);
StartMonitorClient();
}
else
{
Debug.Console(2, this, Debug.ErrorLogLevel.Error,
this.LogError(
"\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************",
MonitorClientMaxFailureCount);

View File

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

View File

@@ -4,17 +4,21 @@ using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Timer = System.Timers.Timer;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
using PepperDash.Core.Logging;
using System.Threading.Tasks;
namespace PepperDash.Core;
/// <summary>
/// A class to handle basic TCP/IP communications with a server
/// </summary>
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized TcpIpClient";
/// <summary>
@@ -22,44 +26,44 @@ namespace PepperDash.Core;
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
private string _hostname;
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get
{
return _hostname;
}
get
{
return _hostname;
}
set
set
{
_hostname = value;
if (_client != null)
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Port on server
@@ -81,19 +85,19 @@ namespace PepperDash.Core;
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The actual client class
/// </summary>
private TCPClient _client;
/// <summary>
/// The actual client class
/// </summary>
private TCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
@@ -102,15 +106,15 @@ namespace PepperDash.Core;
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
@@ -122,26 +126,26 @@ namespace PepperDash.Core;
get { return (ushort)ClientStatus; }
}
/// <summary>
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Ushort representation of client status
/// </summary>
/// <summary>
/// Ushort representation of client status
/// </summary>
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
@@ -152,29 +156,29 @@ namespace PepperDash.Core;
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Set only when the disconnect method is called
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// Set only when the disconnect method is called
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
//Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection();
private readonly object _connectLock = new();
// private Timer for auto reconnect
private CTimer RetryTimer;
private Timer RetryTimer;
/// <summary>
/// Constructor
@@ -183,9 +187,9 @@ namespace PepperDash.Core;
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
@@ -193,10 +197,7 @@ namespace PepperDash.Core;
Port = port;
BufferSize = bufferSize;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
SetupRetryTimer();
}
/// <summary>
@@ -211,28 +212,30 @@ namespace PepperDash.Core;
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
SetupRetryTimer();
}
/// <summary>
/// Default constructor for S+
/// </summary>
public GenericTcpIpClient()
: base(SplusKey)
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
SetupRetryTimer();
}
private void SetupRetryTimer()
{
RetryTimer = new Timer { AutoReset = false, Enabled = false };
RetryTimer.Elapsed += (s, e) => Reconnect();
}
/// <summary>
/// Just to help S+ set the key
@@ -249,7 +252,7 @@ namespace PepperDash.Core;
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Debug.Console(1, this, "Program stopping. Closing connection");
this.LogInformation("Program stopping. Closing connection");
Deactivate();
}
}
@@ -258,42 +261,41 @@ namespace PepperDash.Core;
///
/// </summary>
/// <returns></returns>
public override bool Deactivate()
{
public override bool Deactivate()
{
RetryTimer.Stop();
RetryTimer.Dispose();
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
return true;
}
/// <summary>
/// Attempts to connect to the server
/// </summary>
public void Connect()
{
public void Connect()
{
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;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key);
this.LogWarning("GenericTcpIpClient '{0}': Invalid port", Key);
return;
}
}
try
lock (_connectLock)
{
connectLock.Enter();
if (IsConnected)
{
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
this.LogInformation("Connection already connected. Exiting Connect()");
}
else
{
@@ -306,11 +308,7 @@ namespace PepperDash.Core;
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
}
private void Reconnect()
{
@@ -318,44 +316,34 @@ namespace PepperDash.Core;
{
return;
}
try
lock (_connectLock)
{
connectLock.Enter();
if (IsConnected || DisconnectCalledByUser == true)
{
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()");
this.LogInformation("Reconnect no longer needed. Exiting Reconnect()");
}
else
{
Debug.Console(1, this, "Attempting reconnect now");
this.LogInformation("Attempting reconnect now");
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// Attempts to disconnect the client
/// </summary>
public void Disconnect()
{
try
public void Disconnect()
{
lock (_connectLock)
{
connectLock.Enter();
DisconnectCalledByUser = true;
// Stop trying reconnects, if we are
RetryTimer.Stop();
DisconnectClient();
}
finally
{
connectLock.Leave();
}
}
}
/// <summary>
/// Does the actual disconnect business
@@ -364,7 +352,7 @@ namespace PepperDash.Core;
{
if (_client != null)
{
Debug.Console(1, this, "Disconnecting client");
this.LogInformation("Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
}
@@ -374,50 +362,47 @@ namespace PepperDash.Core;
/// Callback method for connection attempt
/// </summary>
/// <param name="c"></param>
void ConnectToServerCallback(TCPClient c)
{
void ConnectToServerCallback(TCPClient c)
{
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();
}
else
{
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
this.LogInformation("Server connection result: {0}", c.ClientStatus);
}
}
}
/// <summary>
/// Disconnects, waits and attemtps to connect again
/// </summary>
void WaitAndTryReconnect()
{
CrestronInvoke.BeginInvoke(o =>
void WaitAndTryReconnect()
{
Task.Run(() =>
{
try
lock (_connectLock)
{
connectLock.Enter();
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
{
DisconnectClient();
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Reset(AutoReconnectIntervalMs);
this.LogInformation("Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Stop();
RetryTimer.Interval = AutoReconnectIntervalMs;
RetryTimer.Start();
}
}
finally
{
connectLock.Leave();
}
});
}
}
/// <summary>
/// Recieves incoming data
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes)
{
void Receive(TCPClient client, int numBytes)
{
if (client != null)
{
if (numBytes > 0)
@@ -428,7 +413,7 @@ namespace PepperDash.Core;
{
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));
}
@@ -439,135 +424,135 @@ namespace PepperDash.Core;
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));
}
}
}
client.ReceiveDataAsync(Receive);
}
}
}
/// <summary>
/// General send method
/// </summary>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array
/// <summary>
/// General send method
/// </summary>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array
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)
_client.SendData(bytes, bytes.Length);
}
_client.SendData(bytes, bytes.Length);
}
/// <summary>
/// SendEscapedText method
/// </summary>
public void SendEscapedText(string text)
{
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
{
var hex = s.Groups[1].Value;
return ((char)Convert.ToByte(hex, 16)).ToString();
});
SendText(unescapedText);
}
/// <summary>
/// SendEscapedText method
/// </summary>
public void SendEscapedText(string text)
{
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
{
var hex = s.Groups[1].Value;
return ((char)Convert.ToByte(hex, 16)).ToString();
});
SendText(unescapedText);
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes"></param>
public void SendBytes(byte[] bytes)
{
public void SendBytes(byte[] bytes)
{
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)
_client.SendData(bytes, bytes.Length);
}
_client.SendData(bytes, bytes.Length);
}
/// <summary>
/// Socket Status Change Handler
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
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();
}
else
{
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive);
this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive);
}
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
}
/// <summary>
/// Configuration properties for TCP/SSH Connections
/// </summary>
public class TcpSshPropertiesConfig
{
public class TcpSshPropertiesConfig
{
/// <summary>
/// Address to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
/// Port to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Username credential
/// </summary>
public string Username { get; set; }
public string Username { get; set; }
/// <summary>
/// Passord credential
/// </summary>
public string Password { get; set; }
public string Password { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Gets or sets the AutoReconnectIntervalMs
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Gets or sets the AutoReconnectIntervalMs
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
[JsonProperty("disableSshEcho")]
public bool DisableSshEcho { get; set; }
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
[JsonProperty("disableSshEcho")]
public bool DisableSshEcho { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public TcpSshPropertiesConfig()
{
BufferSize = 32768;
AutoReconnect = true;
AutoReconnectIntervalMs = 5000;
public TcpSshPropertiesConfig()
{
BufferSize = 32768;
AutoReconnect = true;
AutoReconnectIntervalMs = 5000;
Username = "";
Password = "";
}
}
}
}

View File

@@ -13,6 +13,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -61,9 +62,14 @@ public class GenericTcpIpServer : Device
#region Properties/Variables
/// <summary>
///
/// Server listen lock
/// </summary>
CCriticalSection ServerCCSection = new CCriticalSection();
object _serverLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <summary>
@@ -365,12 +371,12 @@ public class GenericTcpIpServer : Device
}
else
{
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
}
}
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>
public void Listen()
{
ServerCCSection.Enter();
lock (_serverLock)
{
try
{
if (Port < 1 || Port > 65535)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
this.LogError("Server '{0}': Invalid port", Key);
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
this.LogError("Server '{0}': No Shared Key set", Key);
return;
}
if (IsListening)
@@ -417,18 +422,15 @@ public class GenericTcpIpServer : Device
ServerStopped = false;
myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
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();
ServerCCSection.Leave();
}
catch (Exception ex)
{
ServerCCSection.Leave();
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
this.LogException(ex, "Error with Dynamic Server: {0}", ex.Message);
}
} // end lock
}
/// <summary>
@@ -438,18 +440,18 @@ public class GenericTcpIpServer : Device
{
try
{
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
this.LogDebug("Stopping Listener");
if (myTcpServer != null)
{
myTcpServer.Stop();
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State);
this.LogDebug("Server State: {0}", myTcpServer.State);
OnServerStateChange(myTcpServer.State);
}
ServerStopped = true;
}
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
{
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)
{
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>
@@ -474,7 +476,7 @@ public class GenericTcpIpServer : Device
/// </summary>
public void DisconnectAllClientsForShutdown()
{
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
this.LogInformation("Disconnecting All Clients");
if (myTcpServer != null)
{
myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
@@ -486,17 +488,17 @@ public class GenericTcpIpServer : Device
try
{
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)
{
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();
if (!ProgramIsStopping)
@@ -514,8 +516,8 @@ public class GenericTcpIpServer : Device
/// <param name="text"></param>
public void BroadcastText(string text)
{
CCriticalSection CCBroadcast = new CCriticalSection();
CCBroadcast.Enter();
lock (_broadcastLock)
{
try
{
if (ConnectedClientsIndexes.Count > 0)
@@ -531,13 +533,12 @@ public class GenericTcpIpServer : Device
}
}
}
CCBroadcast.Leave();
}
catch (Exception ex)
{
CCBroadcast.Leave();
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
}
} // end lock
}
/// <summary>
@@ -558,7 +559,7 @@ public class GenericTcpIpServer : Device
}
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);
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
SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText;
@@ -597,13 +598,13 @@ public class GenericTcpIpServer : Device
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
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)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
@@ -615,11 +616,11 @@ public class GenericTcpIpServer : Device
/// <returns>IP address of the client</returns>
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)))
{
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;
}
@@ -642,7 +643,7 @@ public class GenericTcpIpServer : Device
clientIndex = (uint)o;
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);
if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
@@ -678,7 +679,7 @@ public class GenericTcpIpServer : Device
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 (ConnectedClientsIndexes.Contains(clientIndex))
@@ -697,7 +698,7 @@ public class GenericTcpIpServer : Device
}
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));
}
@@ -714,7 +715,7 @@ public class GenericTcpIpServer : Device
{
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),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0)
@@ -734,10 +735,11 @@ public class GenericTcpIpServer : Device
}
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
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
{
this.LogDebug("Client at index {0} is ready for communications", clientIndex);
OnServerClientReadyForCommunications(clientIndex);
}
if (HeartbeatRequired)
@@ -753,7 +755,7 @@ public class GenericTcpIpServer : Device
}
else
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
this.LogError("Client attempt faulty.");
if (!ServerStopped)
{
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
@@ -763,7 +765,7 @@ public class GenericTcpIpServer : Device
}
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}))))))",
// server.State,
@@ -771,7 +773,7 @@ public class GenericTcpIpServer : Device
// 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);
}
@@ -802,7 +804,7 @@ public class GenericTcpIpServer : Device
if (received != SharedKey)
{
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.Disconnect(clientIndex);
return;
@@ -812,7 +814,7 @@ public class GenericTcpIpServer : Device
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
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)))
@@ -820,7 +822,7 @@ public class GenericTcpIpServer : Device
}
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)
myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
@@ -901,7 +903,7 @@ public class GenericTcpIpServer : Device
if (MonitorClient != null)
MonitorClient.Disconnect();
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
this.LogInformation("Program stopping. Closing server");
KillServer();
}
}
@@ -942,7 +944,7 @@ public class GenericTcpIpServer : Device
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
this.LogDebug("Starting monitor check");
MonitorClient.Connect();
// From here MonitorCLient either connects or hangs, MonitorClient will call back
@@ -969,7 +971,7 @@ public class GenericTcpIpServer : Device
{
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 = null;
MonitorClientFailureCount = 0;
@@ -990,13 +992,13 @@ public class GenericTcpIpServer : Device
StopMonitorClient();
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);
StartMonitorClient();
}
else
{
Debug.Console(2, this, Debug.ErrorLogLevel.Error,
this.LogError(
"\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************",
MonitorClientMaxFailureCount);

View File

@@ -1,6 +1,7 @@
extern alias NewtonsoftJson;
using System.Collections.Generic;
using System.Threading;
using Crestron.SimplSharp;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
@@ -9,62 +10,61 @@ namespace PepperDash.Core.Logging;
/// <summary>
/// Class to persist current Debug settings across program restarts
/// </summary>
public class DebugContextCollection
{
public class DebugContextCollection
{
/// <summary>
/// To prevent threading issues with the DeviceDebugSettings collection
/// </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>
/// Collection of the debug settings for each device where the dictionary key is the device key
/// </summary>
[JsonProperty("deviceDebugSettings")]
private Dictionary<string, object> DeviceDebugSettings { get; set; }
private Dictionary<string, object> DeviceDebugSettings { get; set; } = new Dictionary<string, object>();
/// <summary>
/// Default constructor
/// </summary>
public DebugContextCollection()
{
_deviceDebugSettingsLock = new CCriticalSection();
DeviceDebugSettings = new Dictionary<string, object>();
_items = new Dictionary<string, DebugContextItem>();
}
/// <summary>
/// Default constructor
/// </summary>
public DebugContextCollection()
{
/// <summary>
/// Sets the level of a given context item, and adds that item if it does not
/// exist
/// </summary>
/// <param name="contextKey"></param>
/// <param name="level"></param>
/// <summary>
/// SetLevel method
/// </summary>
public void SetLevel(string contextKey, int level)
{
if (level < 0 || level > 2)
return;
GetOrCreateItem(contextKey).Level = level;
}
}
/// <summary>
/// Gets a level or creates it if not existing
/// </summary>
/// <param name="contextKey"></param>
/// <returns></returns>
/// <summary>
/// GetOrCreateItem method
/// </summary>
public DebugContextItem GetOrCreateItem(string contextKey)
{
if (!_items.ContainsKey(contextKey))
_items[contextKey] = new DebugContextItem { Level = 0 };
return _items[contextKey];
}
/// <summary>
/// Sets the level of a given context item, and adds that item if it does not
/// exist
/// </summary>
/// <param name="contextKey"></param>
/// <param name="level"></param>
/// <summary>
/// SetLevel method
/// </summary>
public void SetLevel(string contextKey, int level)
{
if (level < 0 || level > 2)
return;
GetOrCreateItem(contextKey).Level = level;
}
/// <summary>
/// Gets a level or creates it if not existing
/// </summary>
/// <param name="contextKey"></param>
/// <returns></returns>
/// <summary>
/// GetOrCreateItem method
/// </summary>
public DebugContextItem GetOrCreateItem(string contextKey)
{
if (!_items.ContainsKey(contextKey))
_items[contextKey] = new DebugContextItem { Level = 0 };
return _items[contextKey];
}
/// <summary>
@@ -75,10 +75,8 @@ namespace PepperDash.Core.Logging;
/// <returns></returns>
public void SetDebugSettingsForKey(string deviceKey, object settings)
{
try
lock (_deviceDebugSettingsLock)
{
_deviceDebugSettingsLock.Enter();
if (DeviceDebugSettings.ContainsKey(deviceKey))
{
DeviceDebugSettings[deviceKey] = settings;
@@ -86,10 +84,6 @@ namespace PepperDash.Core.Logging;
else
DeviceDebugSettings.Add(deviceKey, settings);
}
finally
{
_deviceDebugSettingsLock.Leave();
}
}
/// <summary>
@@ -101,22 +95,22 @@ namespace PepperDash.Core.Logging;
{
return DeviceDebugSettings[deviceKey];
}
}
}
/// <summary>
/// Contains information about
/// </summary>
public class DebugContextItem
{
/// <summary>
/// Contains information about
/// </summary>
public class DebugContextItem
{
/// <summary>
/// The level of debug messages to print
/// </summary>
[JsonProperty("level")]
public int Level { get; set; }
[JsonProperty("level")]
public int Level { get; set; }
/// <summary>
/// Property to tell the program not to intitialize when it boots, if desired
/// </summary>
[JsonProperty("doNotLoadOnNextBoot")]
public bool DoNotLoadOnNextBoot { get; set; }
}
}

View File

@@ -3,287 +3,250 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Core.Logging;
namespace PepperDash.Core.Web;
/// <summary>
/// Web API server
/// </summary>
public class WebApiServer : IKeyName
{
private const string SplusKey = "Uninitialized Web API Server";
private const string DefaultName = "Web API Server";
private const string DefaultBasePath = "/api";
private readonly object _serverLock = new();
private HttpCwsServer _server;
/// <summary>
/// Web API server
/// Gets or sets the Key
/// </summary>
public class WebApiServer : IKeyName
public string Key { get; private set; }
/// <summary>
/// Gets or sets the Name
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Gets or sets the BasePath
/// </summary>
public string BasePath { get; private set; }
/// <summary>
/// Gets or sets the IsRegistered
/// </summary>
public bool IsRegistered { get; private set; }
/// <summary>
/// Constructor for S+. Make sure to set necessary properties using init method
/// </summary>
public WebApiServer()
: this(SplusKey, DefaultName, null)
{
private const string SplusKey = "Uninitialized Web API Server";
private const string DefaultName = "Web API Server";
private const string DefaultBasePath = "/api";
}
private const uint DebugTrace = 0;
private const uint DebugInfo = 1;
private const uint DebugVerbose = 2;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string basePath)
: this(key, DefaultName, basePath)
{
}
private readonly CCriticalSection _serverLock = new CCriticalSection();
private HttpCwsServer _server;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string name, string basePath)
{
Key = key;
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
/// <summary>
/// Gets or sets the Key
/// </summary>
public string Key { get; private set; }
if (_server == null) _server = new HttpCwsServer(BasePath);
/// <summary>
/// Gets or sets the Name
/// </summary>
public string Name { get; private set; }
_server.setProcessName(Key);
_server.HttpRequestHandler = new DefaultRequestHandler();
/// <summary>
/// Gets or sets the BasePath
/// </summary>
public string BasePath { get; private set; }
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
}
/// <summary>
/// Gets or sets the IsRegistered
/// </summary>
public bool IsRegistered { get; private set; }
/// <summary>
/// Program status event handler
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping) return;
/// <summary>
/// Http request handler
/// </summary>
//public IHttpCwsHandler HttpRequestHandler
//{
// get { return _server.HttpRequestHandler; }
// set
// {
// if (_server == null) return;
// _server.HttpRequestHandler = value;
// }
//}
this.LogInformation("Program stopping. stopping server");
/// <summary>
/// Received request event handler
/// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
//}
Stop();
}
/// <summary>
/// Constructor for S+. Make sure to set necessary properties using init method
/// </summary>
public WebApiServer()
: this(SplusKey, DefaultName, null)
/// <summary>
/// Ethernet event handler
/// </summary>
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
{
this.LogInformation("Ethernet link up. Server is already registered.");
return;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string basePath)
: this(key, DefaultName, basePath)
this.LogInformation("Ethernet link up. Starting server");
Start();
}
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key, string basePath)
{
Key = key;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
}
/// <summary>
/// Adds a route to CWS
/// </summary>
public void AddRoute(HttpCwsRoute route)
{
if (route == null)
{
this.LogWarning("Failed to add route, route parameter is null");
return;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string name, string basePath)
_server.Routes.Add(route);
}
/// <summary>
/// Removes a route from CWS
/// </summary>
/// <param name="route"></param>
/// <summary>
/// RemoveRoute method
/// </summary>
public void RemoveRoute(HttpCwsRoute route)
{
if (route == null)
{
Key = key;
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
if (_server == null) _server = new HttpCwsServer(BasePath);
_server.setProcessName(Key);
_server.HttpRequestHandler = new DefaultRequestHandler();
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
this.LogWarning("Failed to remove route, route parameter is null");
return;
}
/// <summary>
/// Program status event handler
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping) return;
_server.Routes.Remove(route);
}
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
/// <summary>
/// GetRouteCollection method
/// </summary>
public HttpCwsRouteCollection GetRouteCollection()
{
return _server.Routes;
}
Stop();
}
/// <summary>
/// Ethernet event handler
/// </summary>
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
{
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
return;
}
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server");
Start();
}
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key, string basePath)
{
Key = key;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
}
/// <summary>
/// Adds a route to CWS
/// </summary>
public void AddRoute(HttpCwsRoute route)
{
if (route == null)
{
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null");
return;
}
_server.Routes.Add(route);
}
/// <summary>
/// Removes a route from CWS
/// </summary>
/// <param name="route"></param>
/// <summary>
/// RemoveRoute method
/// </summary>
public void RemoveRoute(HttpCwsRoute route)
{
if (route == null)
{
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null");
return;
}
_server.Routes.Remove(route);
}
/// <summary>
/// GetRouteCollection method
/// </summary>
public HttpCwsRouteCollection GetRouteCollection()
{
return _server.Routes;
}
/// <summary>
/// Starts CWS instance
/// </summary>
public void Start()
/// <summary>
/// Starts CWS instance
/// </summary>
public void Start()
{
lock (_serverLock)
{
try
{
_serverLock.Enter();
if (_server == null)
{
Debug.Console(DebugInfo, this, "Server is null, unable to start");
this.LogDebug("Server is null, unable to start");
return;
}
if (IsRegistered)
{
Debug.Console(DebugInfo, this, "Server has already been started");
this.LogDebug("Server has already been started");
return;
}
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)
{
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
this.LogException(ex, "Start Exception Message: {0}", ex.Message);
this.LogVerbose("Start Exception StackTrace: {0}", ex.StackTrace);
}
finally
{
_serverLock.Leave();
}
}
} // end lock
}
/// <summary>
/// Stop method
/// </summary>
public void Stop()
/// <summary>
/// Stop method
/// </summary>
public void Stop()
{
lock (_serverLock)
{
try
{
_serverLock.Enter();
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;
}
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 = null;
}
catch (Exception ex)
{
Debug.Console(DebugInfo, this, "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);
this.LogException(ex, "Server Stop Exception Message: {0}", ex.Message);
}
finally
{
_serverLock.Leave();
}
}
} // end lock
}
/// <summary>
/// Received request handler
/// </summary>
/// <remarks>
/// This is here for development and testing
/// </remarks>
/// <param name="sender"></param>
/// <param name="args"></param>
public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args)
/// <summary>
/// Received request handler
/// </summary>
/// <remarks>
/// This is here for development and testing
/// </remarks>
/// <param name="sender"></param>
/// <param name="args"></param>
public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args)
{
try
{
try
{
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
}
catch (Exception ex)
{
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
}
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
}
}
catch (Exception ex)
{
this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
this.LogVerbose("ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
}
}
}

View File

@@ -1,179 +0,0 @@
using Crestron.SimplSharp.Net.Http;
using PepperDash.Core;
using System;
namespace PepperDash.Essentials.Core;
[Obsolete("Please use the builtin HttpClient class instead: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines")]
public class GenericHttpClient : Device, IBasicCommunication
{
private readonly HttpClient Client;
/// <summary>
/// Event raised when response is received
/// </summary>
public event EventHandler<GenericHttpClientEventArgs> ResponseRecived;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">key of the device</param>
/// <param name="name">name of the device</param>
/// <param name="hostname">hostname for the HTTP client</param>
public GenericHttpClient(string key, string name, string hostname)
: base(key, name)
{
Client = new HttpClient
{
HostName = hostname
};
}
/// <summary>
/// SendText method
/// </summary>
/// <param name="path">the path to send the request to</param>
public void SendText(string path)
{
HttpClientRequest request = new HttpClientRequest();
string url = string.Format("http://{0}/{1}", Client.HostName, path);
request.Url = new UrlParser(url);
HttpClient.DISPATCHASYNC_ERROR error = Client.DispatchAsyncEx(request, Response, request);
}
/// <summary>
/// SendText method
/// </summary>
/// <param name="format">format for the items</param>
/// <param name="items">items to format</param>
public void SendText(string format, params object[] items)
{
HttpClientRequest request = new HttpClientRequest();
string url = string.Format("http://{0}/{1}", Client.HostName, string.Format(format, items));
request.Url = new UrlParser(url);
HttpClient.DISPATCHASYNC_ERROR error = Client.DispatchAsyncEx(request, Response, request);
}
/// <summary>
/// SendTextNoResponse method
/// </summary>
/// <param name="format">format for the items</param>
/// <param name="items">items to format</param>
public void SendTextNoResponse(string format, params object[] items)
{
HttpClientRequest request = new HttpClientRequest();
string url = string.Format("http://{0}/{1}", Client.HostName, string.Format(format, items));
request.Url = new UrlParser(url);
Client.Dispatch(request);
}
/// <summary>
/// Response method
/// </summary>
/// <param name="response">response received from the HTTP client</param>
/// <param name="error">error status of the HTTP callback</param>
/// <param name="request">original HTTP client request</param>
private void Response(HttpClientResponse response, HTTP_CALLBACK_ERROR error, object request)
{
if (error == HTTP_CALLBACK_ERROR.COMPLETED)
{
var responseReceived = response;
if (responseReceived.ContentString.Length > 0)
{
ResponseRecived?.Invoke(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error));
}
}
}
#region IBasicCommunication Members
/// <summary>
/// SendBytes method
/// </summary>
/// <param name="bytes">bytes to send</param>
public void SendBytes(byte[] bytes)
{
throw new NotImplementedException();
}
#endregion
#region ICommunicationReceiver Members
/// <summary>
/// BytesReceived event
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
throw new NotImplementedException();
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
throw new NotImplementedException();
}
/// <summary>
/// IsConnected property
/// </summary>
public bool IsConnected
{
get { return true; }
}
/// <summary>
/// TextReceived event
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
#endregion
}
/// <summary>
/// Represents a GenericHttpClientEventArgs
/// </summary>
public class GenericHttpClientEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the ResponseText
/// </summary>
public string ResponseText { get; private set; }
/// <summary>
/// Gets or sets the RequestPath
/// </summary>
public string RequestPath { get; private set; }
/// <summary>
/// Gets or sets the Error
/// </summary>
public HTTP_CALLBACK_ERROR Error { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="response">response text</param>
/// <param name="request">request path</param>
/// <param name="error">error status</param>
public GenericHttpClientEventArgs(string response, string request, HTTP_CALLBACK_ERROR error)
{
ResponseText = response;
RequestPath = request;
Error = error;
}
}

View File

@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using System.Threading;
using Timer = System.Timers.Timer;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -18,12 +17,27 @@ namespace PepperDash.Essentials.Core.Config;
/// </summary>
public class ConfigWriter
{
/// <summary>
/// The name of the subfolder where the config file will be written
/// </summary>
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;
static CCriticalSection fileLock = new CCriticalSection();
private static Timer WriteTimer;
static readonly object _fileLock = new();
static ConfigWriter()
{
WriteTimer = new Timer(WriteTimeoutInMs);
WriteTimer.Elapsed += (s, e) => WriteConfigFile(null);
}
/// <summary>
/// Updates the config properties of a device
@@ -53,6 +67,9 @@ public class ConfigWriter
return success;
}
/// <summary>
/// Updates the config properties of a device
/// </summary>
public static bool UpdateDeviceConfig(DeviceConfig config)
{
bool success = false;
@@ -73,17 +90,20 @@ public class ConfigWriter
return success;
}
/// <summary>
/// Updates the config properties of a room
/// </summary>
public static bool UpdateRoomConfig(DeviceConfig config)
{
bool success = false;
var roomConfigIndex = ConfigReader.ConfigObject.Rooms.FindIndex(d => d.Key.Equals(config.Key));
var roomConfigIndex = ConfigReader.ConfigObject.Rooms.FindIndex(d => d.Key.Equals(config.Key));
if (roomConfigIndex >= 0)
if (roomConfigIndex >= 0)
{
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;
}
@@ -98,10 +118,9 @@ public class ConfigWriter
/// </summary>
static void ResetTimer()
{
if (WriteTimer == null)
WriteTimer = new CTimer(WriteConfigFile, WriteTimeout);
WriteTimer.Reset(WriteTimeout);
WriteTimer.Stop();
WriteTimer.Interval = WriteTimeoutInMs;
WriteTimer.Start();
Debug.LogMessage(LogEventLevel.Debug, "Config File write timer has been reset.");
}
@@ -120,10 +139,10 @@ public class ConfigWriter
}
/// <summary>
/// Writes
/// Writes the specified configuration data to a file.
/// </summary>
/// <param name="filepath"></param>
/// <param name="o"></param>
/// <param name="filePath">The path of the file to write to.</param>
/// <param name="configData">The configuration data to write.</param>
public static void WriteFile(string filePath, string configData)
{
if (WriteTimer != null)
@@ -133,9 +152,11 @@ public class ConfigWriter
Debug.LogMessage(LogEventLevel.Information, "Attempting to write config file: '{0}'", filePath);
var lockAcquired = false;
try
{
if (fileLock.TryEnter())
lockAcquired = Monitor.TryEnter(_fileLock);
if (lockAcquired)
{
using (StreamWriter sw = new StreamWriter(filePath))
{
@@ -154,11 +175,8 @@ public class ConfigWriter
}
finally
{
if (fileLock != null && !fileLock.Disposed)
fileLock.Leave();
if (lockAcquired)
Monitor.Exit(_fileLock);
}
}
}

View File

@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using PepperDash.Core;
using Crestron.SimplSharp;
using PepperDash.Essentials.Core;
using Serilog.Events;
namespace PepperDash.Essentials.Core.DeviceInfo;
/// <summary>
/// Helper methods for network devices
/// </summary>
public static class NetworkDeviceHelpers
{
/// <summary>
@@ -24,7 +26,7 @@ public static class NetworkDeviceHelpers
private static readonly char NewLineSplitter = CrestronEnvironment.NewLine.ToCharArray().First();
private static readonly string NewLine = CrestronEnvironment.NewLine;
private static readonly CCriticalSection Lock = new CCriticalSection();
private static readonly object _lock = new();
/// <summary>
/// Last resolved ARP table - it is recommended to refresh the arp before using this.
@@ -37,46 +39,45 @@ public static class NetworkDeviceHelpers
public static void RefreshArp()
{
var error = false;
try
lock (_lock)
{
Lock.Enter();
var consoleResponse = string.Empty;
if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return;
if (string.IsNullOrEmpty(consoleResponse))
try
{
var consoleResponse = string.Empty;
if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return;
if (string.IsNullOrEmpty(consoleResponse))
{
error = true;
return;
}
ArpTable.Clear();
Debug.LogMessage(LogEventLevel.Verbose, "ConsoleResponse of 'showarptable' : {0}{1}", NewLine, consoleResponse);
var myLines =
consoleResponse.Split(NewLineSplitter)
.ToList()
.Where(o => (o.Contains(':') && !o.Contains("Type", StringComparison.OrdinalIgnoreCase)))
.ToList();
foreach (var line in myLines)
{
var item = line;
var seperator = item.Contains('\t') ? '\t' : ' ';
var dataPoints = item.Split(seperator);
if (dataPoints == null || dataPoints.Length < 2) continue;
var ipAddress = SanitizeIpAddress(dataPoints.First().TrimAll());
var macAddress = dataPoints.Last();
ArpTable.Add(new ArpEntry(ipAddress, macAddress));
}
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message);
error = true;
return;
}
ArpTable.Clear();
} // end lock
Debug.LogMessage(LogEventLevel.Verbose, "ConsoleResponse of 'showarptable' : {0}{1}", NewLine, consoleResponse);
var myLines =
consoleResponse.Split(NewLineSplitter)
.ToList()
.Where(o => (o.Contains(':') && !o.Contains("Type", StringComparison.OrdinalIgnoreCase)))
.ToList();
foreach (var line in myLines)
{
var item = line;
var seperator = item.Contains('\t') ? '\t' : ' ';
var dataPoints = item.Split(seperator);
if (dataPoints == null || dataPoints.Length < 2) continue;
var ipAddress = SanitizeIpAddress(dataPoints.First().TrimAll());
var macAddress = dataPoints.Last();
ArpTable.Add(new ArpEntry(ipAddress, macAddress));
}
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message);
error = true;
}
finally
{
Lock.Leave();
OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error));
}
OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error));
}
@@ -158,7 +159,14 @@ public static class NetworkDeviceHelpers
/// </summary>
public class ArpEntry
{
/// <summary>
/// IP Address of the ARP entry
/// </summary>
public readonly IPAddress IpAddress;
/// <summary>
/// MAC Address of the ARP entry
/// </summary>
public readonly string MacAddress;
/// <summary>

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using PepperDash.Core;
@@ -10,13 +11,27 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core;
/// <summary>
/// Manages devices in the system, including activation and console commands to interact with devices
/// </summary>
public static class DeviceManager
{
/// <summary>
/// Event raised when all devices have been activated
/// </summary>
public static event EventHandler<EventArgs> AllDevicesActivated;
/// <summary>
/// Event raised when all devices have been registered
/// </summary>
public static event EventHandler<EventArgs> AllDevicesRegistered;
/// <summary>
/// Event raised when all devices have been initialized
/// </summary>
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; } }
//static List<Device> _Devices = new List<Device>();
@@ -28,7 +43,10 @@ public static class DeviceManager
/// </summary>
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>
/// Initializes the control system by enabling device management and registering console commands.
@@ -65,11 +83,10 @@ public static class DeviceManager
/// </summary>
public static void ActivateAll()
{
try
{
OnAllDevicesRegistered();
OnAllDevicesRegistered();
DeviceCriticalSection.Enter();
lock (_deviceLock)
{
AddDeviceEnabled = false;
// PreActivate all devices
Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****");
@@ -125,11 +142,7 @@ public static class DeviceManager
Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****");
OnAllDevicesActivated();
}
finally
{
DeviceCriticalSection.Leave();
}
} // end lock
}
private static void DeviceManager_Initialized(object sender, EventArgs e)
@@ -176,18 +189,13 @@ public static class DeviceManager
/// </summary>
public static void DeactivateAll()
{
try
lock (_deviceLock)
{
DeviceCriticalSection.Enter();
foreach (var d in Devices.Values.OfType<Device>())
{
d.Deactivate();
}
}
finally
{
DeviceCriticalSection.Leave();
}
}
//static void ListMethods(string devKey)
@@ -266,11 +274,16 @@ public static class DeviceManager
// Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned");
//}
/// <summary>
/// Adds a device to the manager
/// </summary>
public static void AddDevice(IKeyed newDev)
{
var lockAcquired = false;
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");
return;
@@ -300,15 +313,22 @@ public static class DeviceManager
}
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)
{
var lockAcquired = false;
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");
@@ -336,15 +356,19 @@ public static class DeviceManager
}
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)
{
try
lock (_deviceLock)
{
DeviceCriticalSection.Enter();
if (newDev == null)
return;
if (Devices.ContainsKey(newDev.Key))
@@ -354,18 +378,22 @@ public static class DeviceManager
else
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()
{
//return _Devices.Select(d => d.Key).ToList();
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()
{
//return _Devices.Select(d => d.Key).ToList();

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using Timer = System.Timers.Timer;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.CrestronXml;
@@ -78,6 +78,8 @@ namespace PepperDash.Essentials.Core.Fusion
private bool _helpRequestSent;
private readonly object _guidFileLock = new();
private eFusionHelpResponse _helpRequestStatus;
/// <inheritdoc />
@@ -390,44 +392,31 @@ namespace PepperDash.Essentials.Core.Fusion
return;
}
var fileLock = new CCriticalSection();
try
lock (_guidFileLock)
{
if (fileLock.Disposed)
try
{
return;
Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file");
_guids = FusionOccSensor == null
? new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets)
: new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets, FusionOccSensor);
var json = JsonConvert.SerializeObject(_guids, Newtonsoft.Json.Formatting.Indented);
using (var sw = new StreamWriter(filePath))
{
sw.Write(json);
sw.Flush();
}
Debug.LogMessage(LogEventLevel.Debug, this, "Guids successfully written to file '{0}'", filePath);
}
fileLock.Enter();
Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file");
_guids = FusionOccSensor == null
? new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets)
: new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets, FusionOccSensor);
var json = JsonConvert.SerializeObject(_guids, Newtonsoft.Json.Formatting.Indented);
using (var sw = new StreamWriter(filePath))
catch (Exception e)
{
sw.Write(json);
sw.Flush();
Debug.LogMessage(LogEventLevel.Information, this, "Error writing guid file: {0}", e);
}
Debug.LogMessage(LogEventLevel.Debug, this, "Guids successfully written to file '{0}'", filePath);
}
catch (Exception e)
{
Debug.LogMessage(LogEventLevel.Information, this, "Error writing guid file: {0}", e);
}
finally
{
if (!fileLock.Disposed)
{
fileLock.Leave();
}
}
} // end lock
}
/// <summary>
@@ -442,50 +431,37 @@ namespace PepperDash.Essentials.Core.Fusion
return;
}
var fileLock = new CCriticalSection();
try
lock (_guidFileLock)
{
if (fileLock.Disposed)
try
{
return;
if (File.Exists(filePath))
{
var json = File.ReadToEnd(filePath, Encoding.ASCII);
_guids = JsonConvert.DeserializeObject<FusionRoomGuids>(json);
// _config.IpId = _guids.IpId;
FusionStaticAssets = _guids.StaticAssets;
}
Debug.LogMessage(LogEventLevel.Information, this, "Fusion Guids successfully read from file: {0}",
filePath);
Debug.LogMessage(LogEventLevel.Debug, this, "\r\n********************\r\n\tRoom Name: {0}\r\n\tIPID: {1:X}\r\n\tRoomGuid: {2}\r\n*******************", Room.Name, _config.IpIdInt, RoomGuid);
foreach (var item in FusionStaticAssets)
{
Debug.LogMessage(LogEventLevel.Debug, this, "\nAsset Name: {0}\nAsset No: {1}\n Guid: {2}", item.Value.Name,
item.Value.SlotNumber, item.Value.InstanceId);
}
}
fileLock.Enter();
if (File.Exists(filePath))
catch (Exception e)
{
var json = File.ReadToEnd(filePath, Encoding.ASCII);
_guids = JsonConvert.DeserializeObject<FusionRoomGuids>(json);
// _config.IpId = _guids.IpId;
FusionStaticAssets = _guids.StaticAssets;
Debug.LogMessage(LogEventLevel.Information, this, "Error reading guid file: {0}", e);
}
Debug.LogMessage(LogEventLevel.Information, this, "Fusion Guids successfully read from file: {0}",
filePath);
Debug.LogMessage(LogEventLevel.Debug, this, "\r\n********************\r\n\tRoom Name: {0}\r\n\tIPID: {1:X}\r\n\tRoomGuid: {2}\r\n*******************", Room.Name, _config.IpIdInt, RoomGuid);
foreach (var item in FusionStaticAssets)
{
Debug.LogMessage(LogEventLevel.Debug, this, "\nAsset Name: {0}\nAsset No: {1}\n Guid: {2}", item.Value.Name,
item.Value.SlotNumber, item.Value.InstanceId);
}
}
catch (Exception e)
{
Debug.LogMessage(LogEventLevel.Information, this, "Error reading guid file: {0}", e);
}
finally
{
if (!fileLock.Disposed)
{
fileLock.Leave();
}
}
} // end lock
}
/// <summary>
@@ -1950,12 +1926,13 @@ namespace PepperDash.Essentials.Core.Fusion
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);
CancelHelpRequest();
}
/// <inheritdoc />
public void CancelHelpRequest()
{

View File

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

View File

@@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using PepperDash.Core;
//using SSMono.IO;
using PepperDash.Core.WebApi.Presets;
using Serilog.Events;
namespace PepperDash.Essentials.Core.Presets;
@@ -19,11 +18,19 @@ namespace PepperDash.Essentials.Core.Presets;
/// </summary>
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);
/// <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);
private readonly CCriticalSection _fileOps = new CCriticalSection();
private readonly object _fileOps = new();
private readonly bool _initSuccess;
private readonly ISetTopBoxNumericKeypad _setTopBox;
@@ -37,6 +44,12 @@ public class DevicePresetsModel : Device
private Action<bool> _enterFunction;
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)
: this(key, fileName)
{
@@ -71,6 +84,11 @@ public class DevicePresetsModel : Device
_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)
{
PulseTime = 150;
@@ -88,27 +106,73 @@ public class DevicePresetsModel : Device
_initSuccess = true;
}
/// <summary>
/// Event fired when a preset is recalled, providing the device and channel that was recalled
/// </summary>
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 int PulseTime { get; set; }
public int DigitSpacingMs { get; set; }
/// <summary>
/// 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; }
/// <summary>
/// The list of presets to display
/// </summary>
public List<PresetChannel> PresetsList { get; private set; }
/// <summary>
/// The number of presets in the list
/// </summary>
public int Count
{
get { return PresetsList != null ? PresetsList.Count : 0; }
}
public bool UseLocalImageStorage { get; set; }
public string ImagesLocalHostPrefix { get; set; }
public string ImagesPathPrefix { get; set; }
public string ListPathPrefix { get; set; }
/// <summary>
/// Indicates whether to use local image storage for preset images, which allows for more and larger images than the SIMPL+ zip file method
/// </summary>
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;
/// <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)
{
_filePath = ListPathPrefix + path;
@@ -117,12 +181,13 @@ public class DevicePresetsModel : Device
LoadChannels();
}
/// <summary>
/// Loads the presets from the file specified by _filePath.
/// </summary>
public void LoadChannels()
{
try
lock (_fileOps)
{
_fileOps.Enter();
Debug.LogMessage(LogEventLevel.Verbose, this, "Loading presets from {0}", _filePath);
PresetsAreLoaded = false;
try
@@ -149,12 +214,12 @@ public class DevicePresetsModel : Device
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)
{
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)
{
if (_dialIsRunning || !_initSuccess)
@@ -176,7 +245,7 @@ public class DevicePresetsModel : Device
}
_dialIsRunning = true;
CrestronInvoke.BeginInvoke(o =>
Task.Run(() =>
{
foreach (var c in chanNum.ToCharArray())
{
@@ -199,6 +268,11 @@ public class DevicePresetsModel : Device
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)
{
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)
{
_dialFunctions = new Dictionary<char, Action<bool>>(10)
@@ -243,6 +324,11 @@ public class DevicePresetsModel : Device
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)
{
if (index >= PresetsList.Count)
@@ -257,6 +343,10 @@ public class DevicePresetsModel : Device
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)
{
PresetsList = presets;
@@ -268,10 +358,9 @@ public class DevicePresetsModel : Device
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);
using (var file = File.Open(_filePath, FileMode.Truncate))
@@ -279,11 +368,6 @@ public class DevicePresetsModel : Device
file.Write(json, Encoding.UTF8);
}
}
finally
{
_fileOps.Leave();
}
}
private void OnPresetsSaved()

View File

@@ -4,24 +4,35 @@ using System.Threading;
using Crestron.SimplSharp;
using PepperDash.Core;
using Serilog.Events;
using Thread = Crestron.SimplSharpPro.CrestronThread.Thread;
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>
/// Threadsafe processing of queued items with pacing if required
/// </summary>
public class GenericQueue : IQueue<IQueueMessage>
{
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;
/// <summary>
/// The thread that processes the queue items
/// </summary>
protected readonly Thread _worker;
protected readonly CEvent _waitHandle = new CEvent();
private readonly object _lock = new();
private bool _delayEnabled;
private int _delayTime;
private const Thread.eThreadPriority _defaultPriority = Thread.eThreadPriority.MediumPriority;
private const ThreadPriority _defaultPriority = ThreadPriority.Normal;
/// <summary>
/// If the instance has been disposed.
@@ -96,7 +107,7 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="key"></param>
/// <param name="pacing"></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)
{
}
@@ -107,7 +118,7 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="key"></param>
/// <param name="priority"></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)
{
}
@@ -119,7 +130,7 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="pacing"></param>
/// <param name="priority"></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)
{
}
@@ -131,21 +142,18 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <param name="priority"></param>
/// <param name="capacity"></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;
int cap = 25; // sets default
if (capacity > 0)
{
cap = capacity; // overrides default
}
_queue = new ConcurrentQueue<IQueueMessage>();
_worker = new Thread(ProcessQueue, null, Thread.eThreadStartOptions.Running)
_worker = new Thread(ProcessQueue)
{
Priority = priority,
Name = _key
Name = _key,
IsBackground = true
};
_worker.Start();
SetDelayValues(pacing);
}
@@ -167,9 +175,8 @@ public class GenericQueue : IQueue<IQueueMessage>
/// <summary>
/// Thread callback
/// </summary>
/// <param name="obj">The action used to process dequeued items</param>
/// <returns>Null when the thread is exited</returns>
private object ProcessQueue(object obj)
private void ProcessQueue()
{
while (true)
{
@@ -186,7 +193,7 @@ public class GenericQueue : IQueue<IQueueMessage>
if (_delayEnabled)
Thread.Sleep(_delayTime);
}
catch (ThreadAbortException)
catch (ThreadInterruptedException)
{
//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)
{
if (_queue.IsEmpty)
Monitor.Wait(_lock);
}
}
}
return null;
}
/// <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)
{
if (Disposed)
@@ -217,7 +233,8 @@ public class GenericQueue : IQueue<IQueueMessage>
}
_queue.Enqueue(item);
_waitHandle.Set();
lock (_lock)
Monitor.Pulse(_lock);
}
/// <summary>
@@ -242,18 +259,17 @@ public class GenericQueue : IQueue<IQueueMessage>
if (disposing)
{
using (_waitHandle)
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing...");
_queue.Enqueue(null);
_waitHandle.Set();
_worker.Join();
}
Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing...");
_queue.Enqueue(null);
lock (_lock)
Monitor.Pulse(_lock);
_worker.Join();
}
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()
{
Dispose(true);

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using System.Threading.Tasks;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
@@ -10,19 +9,37 @@ using Serilog.Events;
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
{
/// <summary>
/// The input port for any video source.
/// </summary>
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;
#region IRoutingInputs Members
/// <summary>
/// Collection of the input ports for this device
/// </summary>
public RoutingPortCollection<RoutingInputPort> InputPorts { get; private set; }
#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)
: base(key, name)
{
@@ -34,14 +51,25 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink
#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)
{
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)
{
CrestronInvoke.BeginInvoke(o =>
Task.Run(() =>
{
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
/// <inheritdoc />
public string CurrentSourceInfoKey { get; set; }
/// <summary>
@@ -158,6 +187,7 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink
}
SourceListItem _CurrentSourceInfo;
/// <inheritdoc />
public event SourceInfoChangeHandler CurrentSourceChange;
#endregion

View File

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

View File

@@ -26,8 +26,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// </summary>
protected VideoCodecBase Codec { get; private set; }
/// <summary>
///
/// </summary>
@@ -126,7 +124,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
//state.CurrentDirectory = PrefixDirectoryFolderItems(directory);
state.CurrentDirectory = directory;
CrestronInvoke.BeginInvoke((o) => PostStatusMessage(state));
Task.Run(() => PostStatusMessage(state));
}
}
catch (Exception ex)
@@ -162,7 +160,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary>
/// Called from base's RegisterWithAppServer method
/// </summary>
/// <param name="appServerController"></param>
protected override void RegisterActions()
{
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)
{
if (!Codec.IsReady)

View File

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

View File

@@ -15,6 +15,7 @@ using PepperDash.Essentials.Core.Routing;
using System.Threading;
using Timeout = Crestron.SimplSharp.Timeout;
using Serilog.Events;
using System.Threading.Tasks;
namespace PepperDash.Essentials;
@@ -108,7 +109,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
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);
}