diff --git a/Pepperdash Core/Pepperdash Core/Comm/._GenericSshClient.cs b/Pepperdash Core/Pepperdash Core/Comm/._GenericSshClient.cs new file mode 100644 index 0000000..583c583 Binary files /dev/null and b/Pepperdash Core/Pepperdash Core/Comm/._GenericSshClient.cs differ diff --git a/Pepperdash Core/Pepperdash Core/Comm/._GenericTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/._GenericTcpIpClient.cs new file mode 100644 index 0000000..b65487e Binary files /dev/null and b/Pepperdash Core/Pepperdash Core/Comm/._GenericTcpIpClient.cs differ diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs index 742ea03..f9a6ef6 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs @@ -65,12 +65,9 @@ namespace PepperDash.Core public bool IsConnected { // returns false if no client or not connected - get { return ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - private bool IsConnecting = false; - private bool DisconnectLogged = false; - /// /// S+ helper for IsConnected /// @@ -135,10 +132,10 @@ namespace PepperDash.Core CTimer ReconnectTimer; - //string PreviousHostname; - //int PreviousPort; - //string PreviousUsername; - //string PreviousPassword; + //Lock object to prevent simulatneous connect/disconnect operations + private CCriticalSection connectLock = new CCriticalSection(); + + private bool DisconnectLogged = false; /// /// Typical constructor. @@ -154,6 +151,14 @@ namespace PepperDash.Core Username = username; Password = password; AutoReconnectIntervalMs = 5000; + + ReconnectTimer = new CTimer(o => + { + if (ConnectEnabled) + { + Connect(); + } + }, Timeout.Infinite); } /// @@ -162,9 +167,16 @@ namespace PepperDash.Core public GenericSshClient() : base(SPlusKey) { - StreamDebugging = new CommunicationStreamDebugging(SPlusKey); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; + + ReconnectTimer = new CTimer(o => + { + if (ConnectEnabled) + { + Connect(); + } + }, Timeout.Infinite); } /// @@ -195,104 +207,106 @@ namespace PepperDash.Core /// public void Connect() { - if (IsConnecting) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "Connection attempt in progress. Exiting Connect()"); - return; - } - - IsConnecting = true; - ConnectEnabled = true; - Debug.Console(1, this, "attempting connect"); - - // Cancel reconnect if running. - if (ReconnectTimer != null) - { - ReconnectTimer.Stop(); - ReconnectTimer = null; - } - - // Don't try to connect if already - if (IsConnected) - return; - // Don't go unless everything is here if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535 || Username == null || Password == null) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect failed. Check hostname, port, username and password are set or not null"); + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Connect failed. Check hostname, port, username and password are set or not null"); return; } - // Cleanup the old client if it already exists - if (Client != null) - { - Debug.Console(1, this, "Cleaning up disconnected client"); - Client.ErrorOccurred -= Client_ErrorOccurred; - KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); - } + ConnectEnabled = true; - // This handles both password and keyboard-interactive (like on OS-X, 'nixes) - KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username); - kauth.AuthenticationPrompt += new EventHandler(kauth_AuthenticationPrompt); - PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password); - - Debug.Console(1, this, "Creating new SshClient"); - ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); - Client = new SshClient(connectionInfo); - - Client.ErrorOccurred -= Client_ErrorOccurred; - Client.ErrorOccurred += Client_ErrorOccurred; - - //Attempt to connect - ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; try { - Client.Connect(); - TheStream = Client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534); - TheStream.DataReceived += Stream_DataReceived; - //TheStream.ErrorOccurred += TheStream_ErrorOccurred; - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connected"); - ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; - IsConnecting = false; - DisconnectLogged = false; - return; // Success will not pass here - } - catch (SshConnectionException e) - { - var ie = e.InnerException; // The details are inside!! - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; - - if (ie is SocketException) - Debug.Console(1, this, errorLogLevel, "'{0}' CONNECTION failure: Cannot reach host, ({1})", Key, ie.Message); - else if (ie is System.Net.Sockets.SocketException) - Debug.Console(1, this, errorLogLevel, "'{0}' Connection failure: Cannot reach host '{1}' on port {2}, ({3})", - Key, Hostname, Port, ie.GetType()); - else if (ie is SshAuthenticationException) + connectLock.Enter(); + if (IsConnected) { - Debug.Console(1, this, errorLogLevel, "Authentication failure for username '{0}', ({1})", - Username, ie.Message); + Debug.Console(1, this, "Connection already connected. Exiting Connect()"); } else - Debug.Console(1, this, errorLogLevel, "Error on connect:\r({0})", e); + { + Debug.Console(1, this, "Attempting connect"); - DisconnectLogged = true; - ClientStatus = SocketStatus.SOCKET_STATUS_CONNECT_FAILED; - HandleConnectionFailure(); + // Cancel reconnect if running. + ReconnectTimer.Stop(); + + // Cleanup the old client if it already exists + if (Client != null) + { + Debug.Console(1, this, "Cleaning up disconnected client"); + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); + } + + // This handles both password and keyboard-interactive (like on OS-X, 'nixes) + KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username); + kauth.AuthenticationPrompt += new EventHandler(kauth_AuthenticationPrompt); + PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password); + + Debug.Console(1, this, "Creating new SshClient"); + ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); + Client = new SshClient(connectionInfo); + + Client.ErrorOccurred -= Client_ErrorOccurred; + Client.ErrorOccurred += Client_ErrorOccurred; + + //Attempt to connect + ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; + try + { + Client.Connect(); + TheStream = Client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534); + TheStream.DataReceived += Stream_DataReceived; + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connected"); + ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; + DisconnectLogged = false; + } + catch (SshConnectionException e) + { + var ie = e.InnerException; // The details are inside!! + var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + + if (ie is SocketException) + Debug.Console(1, this, errorLogLevel, "'{0}' CONNECTION failure: Cannot reach host, ({1})", Key, ie.Message); + else if (ie is System.Net.Sockets.SocketException) + Debug.Console(1, this, errorLogLevel, "'{0}' Connection failure: Cannot reach host '{1}' on port {2}, ({3})", + Key, Hostname, Port, ie.GetType()); + else if (ie is SshAuthenticationException) + { + Debug.Console(1, this, errorLogLevel, "Authentication failure for username '{0}', ({1})", + Username, ie.Message); + } + else + Debug.Console(1, this, errorLogLevel, "Error on connect:\r({0})", ie.Message); + + DisconnectLogged = true; + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + if (AutoReconnect) + { + Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); + } + } + catch (Exception e) + { + var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + Debug.Console(1, this, errorLogLevel, "Unhandled exception on connect:\r({0})", e.Message); + DisconnectLogged = true; + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + if (AutoReconnect) + { + Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); + } + } + } } - catch (Exception e) + finally { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Unhandled exception on connect:\r({0})", e); - ClientStatus = SocketStatus.SOCKET_STATUS_CONNECT_FAILED; - HandleConnectionFailure(); + connectLock.Leave(); } - - ClientStatus = SocketStatus.SOCKET_STATUS_CONNECT_FAILED; - HandleConnectionFailure(); } - - /// /// Disconnect the clients and put away it's resources. /// @@ -317,8 +331,7 @@ namespace PepperDash.Core KillStream(); if (Client != null) - { - IsConnecting = false; + { Client.Disconnect(); Client = null; ClientStatus = status; @@ -365,6 +378,7 @@ namespace PepperDash.Core TheStream.Close(); TheStream.Dispose(); TheStream = null; + Debug.Console(1, this, "Disconnected stream"); } } @@ -415,13 +429,28 @@ namespace PepperDash.Core /// void Client_ErrorOccurred(object sender, Crestron.SimplSharp.Ssh.Common.ExceptionEventArgs e) { - if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Disconnected by remote"); - else - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Unhandled SSH client error: {0}", e.Exception); + CrestronInvoke.BeginInvoke(o => + { + if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Disconnected by remote"); + else + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Unhandled SSH client error: {0}", e.Exception); - ClientStatus = SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY; - HandleConnectionFailure(); + try + { + connectLock.Enter(); + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY); + } + finally + { + connectLock.Leave(); + } + if (AutoReconnect && ConnectEnabled) + { + Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); + } + }); } /// @@ -463,8 +492,6 @@ namespace PepperDash.Core Debug.Console(0, "Stack Trace: {0}", ex.StackTrace); Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed. Disconnected, closing"); - ClientStatus = SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY; - HandleConnectionFailure(); } } @@ -492,8 +519,6 @@ namespace PepperDash.Core catch { Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed. Disconnected, closing"); - ClientStatus = SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY; - HandleConnectionFailure(); } } diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs index 16013a3..8cfaad2 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs @@ -5,9 +5,7 @@ using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; - using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace PepperDash.Core { @@ -39,23 +37,24 @@ namespace PepperDash.Core public event EventHandler ConnectionChange; - private string _Hostname { get; set;} + private string _hostname; + /// /// Address of server /// - public string Hostname { + public string Hostname + { get { - return _Hostname; + return _hostname; } set { - _Hostname = value; - if (Client != null) + _hostname = value; + if (_client != null) { - - Client.AddressClientConnectedTo = _Hostname; + _client.AddressClientConnectedTo = _hostname; } } } @@ -83,14 +82,14 @@ namespace PepperDash.Core /// /// The actual client class /// - public TCPClient Client { get; private set; } + private TCPClient _client; /// - /// True if connected to the server + /// Bool showing if socket is connected /// public bool IsConnected { - get { return Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } /// @@ -102,21 +101,19 @@ namespace PepperDash.Core } /// - /// Status of the socket + /// _client socket status Read only /// public SocketStatus ClientStatus { get { - if (Client == null) - return SocketStatus.SOCKET_STATUS_NO_CONNECT; - return Client.ClientStatus; + return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; } } /// /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected with be true when this == 2. + /// and IsConnected would be true when this == 2. /// public ushort UStatus { @@ -124,7 +121,7 @@ namespace PepperDash.Core } /// - /// Status of the socket + /// Status text shows the message associated with socket status /// public string ClientStatusText { get { return ClientStatus.ToString(); } } @@ -140,7 +137,7 @@ namespace PepperDash.Core public string ConnectionFailure { get { return ClientStatus.ToString(); } } /// - /// If true, enables AutoConnect + /// bool to track if auto reconnect should be set on the socket /// public bool AutoReconnect { get; set; } @@ -152,13 +149,14 @@ namespace PepperDash.Core get { return (ushort)(AutoReconnect ? 1 : 0); } set { AutoReconnect = value == 1; } } + /// /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 /// public int AutoReconnectIntervalMs { get; set; } /// - /// Set only when the disconnect method is called. + /// Set only when the disconnect method is called /// bool DisconnectCalledByUser; @@ -167,10 +165,14 @@ namespace PepperDash.Core /// public bool Connected { - get { return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - CTimer RetryTimer; + //Lock object to prevent simulatneous connect/disconnect operations + private CCriticalSection connectLock = new CCriticalSection(); + + // private Timer for auto reconnect + private CTimer RetryTimer; /// /// Constructor @@ -183,13 +185,17 @@ namespace PepperDash.Core : base(key) { StreamDebugging = new CommunicationStreamDebugging(key); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; Hostname = address; Port = port; BufferSize = bufferSize; - AutoReconnectIntervalMs = 5000; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - } + RetryTimer = new CTimer(o => + { + Reconnect(); + }, Timeout.Infinite); + } /// /// Constructor @@ -202,6 +208,11 @@ namespace PepperDash.Core CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; BufferSize = 2000; + + RetryTimer = new CTimer(o => + { + Reconnect(); + }, Timeout.Infinite); } /// @@ -210,10 +221,14 @@ namespace PepperDash.Core public GenericTcpIpClient() : base(SplusKey) { - StreamDebugging = new CommunicationStreamDebugging(SplusKey); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; BufferSize = 2000; + + RetryTimer = new CTimer(o => + { + Reconnect(); + }, Timeout.Infinite); } /// @@ -232,7 +247,7 @@ namespace PepperDash.Core if (programEventType == eProgramStatusEventType.Stopping) { Debug.Console(1, this, "Program stopping. Closing connection"); - Disconnect(); + Deactivate(); } } @@ -242,9 +257,11 @@ namespace PepperDash.Core /// public override bool Deactivate() { - if (Client != null) + RetryTimer.Stop(); + RetryTimer.Dispose(); + if (_client != null) { - Client.SocketStatusChange -= this.Client_SocketStatusChange; + _client.SocketStatusChange -= this.Client_SocketStatusChange; DisconnectClient(); } return true; @@ -255,9 +272,6 @@ namespace PepperDash.Core /// public void Connect() { - if (IsConnected) - DisconnectClient(); - if (string.IsNullOrEmpty(Hostname)) { Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); @@ -271,36 +285,72 @@ namespace PepperDash.Core } } - if (Client == null) + try { - Client = new TCPClient(Hostname, Port, BufferSize); - Client.SocketStatusChange -= Client_SocketStatusChange; - Client.SocketStatusChange += Client_SocketStatusChange; + connectLock.Enter(); + if (IsConnected) + { + Debug.Console(1, this, "Connection already connected. Exiting Connect()"); + } + else + { + //Stop retry timer if running + RetryTimer.Stop(); + _client = new TCPClient(Hostname, Port, BufferSize); + _client.SocketStatusChange -= Client_SocketStatusChange; + _client.SocketStatusChange += Client_SocketStatusChange; + DisconnectCalledByUser = false; + _client.ConnectToServerAsync(ConnectToServerCallback); + } + } + finally + { + connectLock.Leave(); } - DisconnectCalledByUser = false; - - Client.ConnectToServerAsync(ConnectToServerCallback); // (null); } + private void Reconnect() + { + if (_client == null) + { + return; + } + try + { + connectLock.Enter(); + if (IsConnected || DisconnectCalledByUser == true) + { + Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()"); + } + else + { + Debug.Console(1, this, "Attempting reconnect now"); + _client.ConnectToServerAsync(ConnectToServerCallback); + } + } + finally + { + connectLock.Leave(); + } + } + /// /// Attempts to disconnect the client /// public void Disconnect() { - DisconnectCalledByUser = true; - - // Stop trying reconnects, if we are - if (RetryTimer != null) + try { + connectLock.Enter(); + DisconnectCalledByUser = true; + + // Stop trying reconnects, if we are RetryTimer.Stop(); - RetryTimer = null; - } - - if (Client != null) - { DisconnectClient(); - Client = null; - Debug.Console(1, this, "Disconnected"); + } + finally + { + connectLock.Leave(); } } @@ -309,11 +359,11 @@ namespace PepperDash.Core /// public void DisconnectClient() { - if (Client != null) + if (_client != null) { Debug.Console(1, this, "Disconnecting client"); - if(IsConnected) - Client.DisconnectFromServer(); + if (IsConnected) + _client.DisconnectFromServer(); } } @@ -323,9 +373,15 @@ namespace PepperDash.Core /// void ConnectToServerCallback(TCPClient c) { - Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); - if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED && AutoReconnect) - WaitAndTryReconnect(); + if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); + WaitAndTryReconnect(); + } + else + { + Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); + } } /// @@ -333,24 +389,23 @@ namespace PepperDash.Core /// void WaitAndTryReconnect() { - DisconnectClient(); - - if (Client != null) + CrestronInvoke.BeginInvoke(o => { - Debug.Console(1, this, "Attempting reconnect, status={0}", Client.ClientStatus); - - if (!DisconnectCalledByUser) - RetryTimer = new CTimer(o => + try + { + connectLock.Enter(); + if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null) { - if (Client == null) - { - return; - } - - Client.ConnectToServerAsync(ConnectToServerCallback); - }, AutoReconnectIntervalMs); - } - + DisconnectClient(); + Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus); + RetryTimer.Reset(AutoReconnectIntervalMs); + } + } + finally + { + connectLock.Leave(); + } + }); } /// @@ -380,15 +435,13 @@ namespace PepperDash.Core var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); if (StreamDebugging.RxStreamDebuggingIsEnabled) + { Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); + } textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - - } - - + } } - client.ReceiveDataAsync(Receive); } } @@ -402,10 +455,8 @@ namespace PepperDash.Core // 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)); - if(Client != null) - Client.SendData(bytes, bytes.Length); - - + if (_client != null) + _client.SendData(bytes, bytes.Length); } /// @@ -429,8 +480,8 @@ namespace PepperDash.Core { if (StreamDebugging.TxStreamDebuggingIsEnabled) Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); - if(Client != null) - Client.SendData(bytes, bytes.Length); + if (_client != null) + _client.SendData(bytes, bytes.Length); } /// @@ -440,27 +491,20 @@ namespace PepperDash.Core /// void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) { - Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - if (client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED && !DisconnectCalledByUser && AutoReconnect) - WaitAndTryReconnect(); - - // Probably doesn't need to be a switch since all other cases were eliminated - switch (clientSocketStatus) - { - case SocketStatus.SOCKET_STATUS_CONNECTED: - Client.ReceiveDataAsync(Receive); - DisconnectCalledByUser = false; - break; - } + if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + WaitAndTryReconnect(); + } + else + { + Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + _client.ReceiveDataAsync(Receive); + } var handler = ConnectionChange; if (handler != null) ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); - - // Relay the event - //var handler = SocketStatusChange; - //if (handler != null) - // SocketStatusChange(this); } } @@ -519,4 +563,4 @@ namespace PepperDash.Core } -} \ No newline at end of file +} diff --git a/Pepperdash Core/Pepperdash Core/Logging/Debug.cs b/Pepperdash Core/Pepperdash Core/Logging/Debug.cs index 23af35c..1a84968 100644 --- a/Pepperdash Core/Pepperdash Core/Logging/Debug.cs +++ b/Pepperdash Core/Pepperdash Core/Logging/Debug.cs @@ -572,6 +572,7 @@ namespace PepperDash.Core String.Format( @"Debug settings file migration not necessary. Using file at \user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber)); + return; } diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj index 86ae71d..ec00542 100644 --- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj @@ -7,7 +7,7 @@ {87E29B4C-569B-4368-A4ED-984AC1440C96} Library Properties - PepperDash_Core + PepperDash.Core PepperDash_Core {0B4745B0-194B-4BB6-8E21-E9057CA92500};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} WindowsCE @@ -51,6 +51,10 @@ False ..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpCustomAttributesInterface.dll + + False + ..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpCWSHelperInterface.dll + False ..\..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpHelperInterface.dll @@ -89,6 +93,9 @@ + + + diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj.DotSettings b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj.DotSettings new file mode 100644 index 0000000..8e644b9 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + False \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Web/RequestHandlers/DefaultRequestHandler.cs b/Pepperdash Core/Pepperdash Core/Web/RequestHandlers/DefaultRequestHandler.cs new file mode 100644 index 0000000..93cfae0 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Web/RequestHandlers/DefaultRequestHandler.cs @@ -0,0 +1,17 @@ +using Crestron.SimplSharp.WebScripting; + +namespace PepperDash.Core.Web.RequestHandlers +{ + /// + /// Web API default request handler + /// + public class DefaultRequestHandler : WebApiBaseRequestHandler + { + /// + /// Constructor + /// + public DefaultRequestHandler() + : base(true) + { } + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs b/Pepperdash Core/Pepperdash Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs new file mode 100644 index 0000000..a73abd1 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp.WebScripting; + +namespace PepperDash.Core.Web.RequestHandlers +{ + /// + /// CWS Base Handler, implements IHttpCwsHandler + /// + public abstract class WebApiBaseRequestHandler : IHttpCwsHandler + { + private readonly Dictionary> _handlers; + protected readonly bool EnableCors; + + /// + /// Constructor + /// + protected WebApiBaseRequestHandler(bool enableCors) + { + EnableCors = enableCors; + + _handlers = new Dictionary> + { + {"CONNECT", HandleConnect}, + {"DELETE", HandleDelete}, + {"GET", HandleGet}, + {"HEAD", HandleHead}, + {"OPTIONS", HandleOptions}, + {"PATCH", HandlePatch}, + {"POST", HandlePost}, + {"PUT", HandlePut}, + {"TRACE", HandleTrace} + }; + } + + /// + /// Constructor + /// + protected WebApiBaseRequestHandler() + : this(false) + { + } + + /// + /// Handles CONNECT method requests + /// + /// + protected virtual void HandleConnect(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles DELETE method requests + /// + /// + protected virtual void HandleDelete(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles GET method requests + /// + /// + protected virtual void HandleGet(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles HEAD method requests + /// + /// + protected virtual void HandleHead(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles OPTIONS method requests + /// + /// + protected virtual void HandleOptions(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PATCH method requests + /// + /// + protected virtual void HandlePatch(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles POST method requests + /// + /// + protected virtual void HandlePost(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PUT method requests + /// + /// + protected virtual void HandlePut(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles TRACE method requests + /// + /// + protected virtual void HandleTrace(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Process request + /// + /// + public void ProcessRequest(HttpCwsContext context) + { + Action handler; + + if (!_handlers.TryGetValue(context.Request.HttpMethod, out handler)) + { + return; + } + + if (EnableCors) + { + context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); + context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + } + + handler(context); + } + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Web/WebApiServer.cs b/Pepperdash Core/Pepperdash Core/Web/WebApiServer.cs new file mode 100644 index 0000000..f2f8464 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Web/WebApiServer.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Core.Web +{ + /// + /// Web API server + /// + 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 const uint DebugTrace = 0; + private const uint DebugInfo = 1; + private const uint DebugVerbose = 2; + + private readonly CCriticalSection _serverLock = new CCriticalSection(); + private HttpCwsServer _server; + + /// + /// Web API server key + /// + public string Key { get; private set; } + + /// + /// Web API server name + /// + public string Name { get; private set; } + + /// + /// CWS base path, will default to "/api" if not set via initialize method + /// + public string BasePath { get; private set; } + + /// + /// Indicates CWS is registered with base path + /// + public bool IsRegistered { get; private set; } + + /// + /// Http request handler + /// + //public IHttpCwsHandler HttpRequestHandler + //{ + // get { return _server.HttpRequestHandler; } + // set + // { + // if (_server == null) return; + // _server.HttpRequestHandler = value; + // } + //} + + /// + /// Received request event handler + /// + //public event EventHandler ReceivedRequestEvent + //{ + // add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); } + // remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); } + //} + + /// + /// Constructor for S+. Make sure to set necessary properties using init method + /// + public WebApiServer() + : this(SplusKey, DefaultName, null) + { + } + + /// + /// Constructor + /// + /// + /// + public WebApiServer(string key, string basePath) + : this(key, DefaultName, basePath) + { + } + + /// + /// Constructor + /// + /// + /// + /// + public WebApiServer(string key, string name, string basePath) + { + 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; + } + + /// + /// Program status event handler + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType != eProgramStatusEventType.Stopping) return; + + Debug.Console(DebugInfo, this, "Program stopping. stopping server"); + + Stop(); + } + + /// + /// Ethernet event handler + /// + /// + 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(); + } + + /// + /// Initializes CWS class + /// + public void Initialize(string key, string basePath) + { + Key = key; + BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; + } + + /// + /// Adds a route to CWS + /// + 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); + + } + + /// + /// Removes a route from CWS + /// + /// + 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); + } + + /// + /// Returns a list of the current routes + /// + public HttpCwsRouteCollection GetRouteCollection() + { + return _server.Routes; + } + + /// + /// Starts CWS instance + /// + public void Start() + { + try + { + _serverLock.Enter(); + + if (_server == null) + { + Debug.Console(DebugInfo, this, "Server is null, unable to start"); + return; + } + + if (IsRegistered) + { + Debug.Console(DebugInfo, this, "Server has already been started"); + return; + } + + IsRegistered = _server.Register(); + + Debug.Console(DebugInfo, this, "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); + } + finally + { + _serverLock.Leave(); + } + } + + /// + /// Stop CWS instance + /// + public void Stop() + { + try + { + _serverLock.Enter(); + + if (_server == null) + { + Debug.Console(DebugInfo, this, "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"); + + _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); + } + finally + { + _serverLock.Leave(); + } + } + + /// + /// Received request handler + /// + /// + /// This is here for development and testing + /// + /// + /// + public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args) + { + 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); + } + } + } +} \ No newline at end of file