diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs new file mode 100644 index 0000000..ed14eba --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs @@ -0,0 +1,312 @@ +using System; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Ssh; +using Crestron.SimplSharp.Ssh.Common; + +namespace PepperDash.Core +{ + public class ConnectionChangeEventArgs : EventArgs + { + public bool IsConnected { get; private set; } + public GenericSshClient Client { get; private set; } + public ushort Status { get { return Client.UStatus; } } + + // S+ Constructor + public ConnectionChangeEventArgs() { } + + public ConnectionChangeEventArgs(bool isConnected, GenericSshClient client) + { + IsConnected = isConnected; + Client = client; + } + } + + //***************************************************************************************************** + //***************************************************************************************************** + + public class GenericSshClient : Device, IBasicCommunication + { + public event EventHandler BytesReceived; + public event EventHandler TextReceived; + + public event EventHandler ConnectionChange; + //public event EventHandler DataReceive; + + public string Hostname { get; set; } + /// + /// Port on server + /// + public int Port { get; set; } + public string Username { get; set; } + public string Password { get; set; } + + public bool IsConnected + { + // returns false if no client or not connected + get { return (Client != null ? Client.IsConnected : false); } + set + { + if (value) + UStatus = 2; + OnConnectionChange(); + } + } + /// + /// Contains the familiar Simpl analog status values + /// + public ushort UStatus { get; private set; } + + /// + /// Determines whether client will attempt reconnection on failure + /// + + public bool AutoReconnect { get; set; } + /// + /// S+ helper for bool value + /// + public ushort UAutoReconnect + { + set { AutoReconnect = value == 1; } + } + + /// + /// Millisecond value, determines the timeout period in between reconnect attempts + /// + public ushort AutoReconnectIntervalMs { get; set; } + + SshClient Client; + ShellStream TheStream; + CTimer ReconnectTimer; + bool ReconnectTimerRunning; + + public GenericSshClient(string key, string hostname, int port, string username, string password) : + base(key) + { + AutoReconnectIntervalMs = 5000; + + Hostname = hostname; + Port = port; + Username = username; + Password = password; + } + + /// + /// Connect to the server, using the provided properties. + /// + public void Connect() + { + ReconnectTimerRunning = false; + if (Hostname != null && Hostname != string.Empty && Port > 0 && + Username != null && Password != null) + { + Debug.Console(1, this, "attempting connect, IsConnected={0}", IsConnected); + if (!IsConnected) + { + UStatus = 1; + IsConnected = false; + + // 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); + ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); + Client = new SshClient(connectionInfo); + Client.ErrorOccurred += Client_ErrorOccurred; + try + { + Client.Connect(); + if (Client.IsConnected) + { + Client.KeepAliveInterval = TimeSpan.FromSeconds(2); + Client.SendKeepAlive(); + IsConnected = true; + Debug.Console(1, this, "Connected"); + TheStream = Client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534); + TheStream.DataReceived += Stream_DataReceived; + TheStream.ErrorOccurred += Stream_ErrorOccurred; + + } + return; + } + catch (SshConnectionException e) + { + var ie = e.InnerException; // The details are inside!! + string msg; + if (ie is SocketException) + msg = string.Format("'{0}' CONNECTION failure: Cannot reach host, ({1})", Key, ie.GetType()); + else if (ie is System.Net.Sockets.SocketException) + msg = string.Format("'{0}' Connection failure: Cannot reach host '{1}' on port {2}, ({3})", + Key, Hostname, Port, ie.GetType()); + else if (ie is SshAuthenticationException) + { + msg = string.Format("'{0}' Authentication failure for username '{1}', ({2})", + Username, Key, ie.GetType()); + Debug.Console(0, this, "Authentication failure for username '{0}', ({1})", + Username, ie.GetType()); + } + else + Debug.Console(0, this, "Error on connect:\r({0})", e); + } + } + } + else + { + Debug.Console(0, this, "Connect failed. Check hostname, port, username and password are set or not null"); + } + + // Sucess will not make it this far + UStatus = 3; + IsConnected = false; + HandleConnectionFailure(); + } + + /// + /// Disconnect the clients and put away it's resources. + /// + public void Disconnect() + { + // Stop trying reconnects, if we are + if(ReconnectTimer != null) ReconnectTimer.Stop(); + // Otherwise just close up + if (Client != null) // && Client.IsConnected) <-- Doesn't always report properly... + { + Debug.Console(1, this, "Disconnecting"); + Client.Disconnect(); + Cleanup(); + UStatus = 5; + IsConnected = false; + } + } + + /// + /// Anything to do with reestablishing connection on failures + /// + void HandleConnectionFailure() + { + Debug.Console(2, this, "Checking autoreconnect: {0}, {1}ms", + AutoReconnect, AutoReconnectIntervalMs); + if (AutoReconnect) + { + if (ReconnectTimer == null || !ReconnectTimerRunning) + { + ReconnectTimer = new CTimer(o => Connect(), AutoReconnectIntervalMs); + ReconnectTimerRunning = true; + Debug.Console(1, this, "Attempting connection in {0} seconds", + (float)(AutoReconnectIntervalMs / 1000)); + } + else + { + Debug.Console(2, this, "{0} second reconnect cycle running", + (float)(AutoReconnectIntervalMs / 1000)); + } + } + } + + void Cleanup() + { + Debug.Console(2, this, "cleaning up resources"); + Client = null; + } + + /// + /// Handles the keyboard interactive authentication, should it be required. + /// + void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) + { + foreach (AuthenticationPrompt prompt in e.Prompts) + if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) + prompt.Response = Password; + } + + /// + /// Handler for data receive on ShellStream. Passes data across to queue for line parsing. + /// + void Stream_DataReceived(object sender, Crestron.SimplSharp.Ssh.Common.ShellDataEventArgs e) + { + var bytes = e.Data; + if (bytes.Length > 0) + { + var bytesHandler = BytesReceived; + if (bytesHandler != null) + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + { + var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + } + } + } + + /// + /// Error event handler for stream events + /// + void Stream_ErrorOccurred(object sender, ExceptionEventArgs e) + { + Debug.Console(2, this, "CRITICAL: PLEASE REPORT - SSH client stream error:\r{0}", e.Exception); + } + + /// + /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange + /// event + /// + void Client_ErrorOccurred(object sender, Crestron.SimplSharp.Ssh.Common.ExceptionEventArgs e) + { + Debug.Console(0, this, "SSH client error: {0}", e.Exception); + if (e.Exception is SocketException) + { + // ****LOG SOMETHING + UStatus = 4; + } + IsConnected = false; + HandleConnectionFailure(); + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + if(ConnectionChange != null) + ConnectionChange(this, new ConnectionChangeEventArgs(IsConnected, this)); + } + + #region IBasicCommunication Members + + public void SendText(string text) + { + try + { + TheStream.Write(text); + TheStream.Flush(); + } + catch + { + Debug.Console(1, this, "Stream write failed. Disconnected, closing"); + UStatus = 4; + IsConnected = false; + HandleConnectionFailure(); + } + } + + public void SendBytes(byte[] bytes) + { + try + { + TheStream.Write(bytes, 0, bytes.Length); + TheStream.Flush(); + } + catch + { + Debug.Console(1, this, "Stream write failed. Disconnected, closing"); + UStatus = 4; + IsConnected = false; + HandleConnectionFailure(); + } + } + + #endregion + } +} diff --git a/Pepperdash Core/Pepperdash Core/GenericTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs similarity index 91% rename from Pepperdash Core/Pepperdash Core/GenericTcpIpClient.cs rename to Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs index b139a35..93dee26 100644 --- a/Pepperdash Core/Pepperdash Core/GenericTcpIpClient.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs @@ -16,6 +16,7 @@ namespace PepperDash.Core public event EventHandler BytesReceived; public event EventHandler TextReceived; + public TCPClient Client { get; private set; } public bool IsConnected { get { return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } public string Status { get { return Client.ClientStatus.ToString(); } } public string ConnectionFailure { get { return Client.ClientStatus.ToString(); } } @@ -25,7 +26,6 @@ namespace PepperDash.Core get { return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - public TCPClient Client { get; private set; } CTimer RetryTimer; public GenericTcpIpClient(string key, string address, int port, int bufferSize) @@ -74,9 +74,7 @@ namespace PepperDash.Core if (numBytes > 0) { var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); - //if (Debug.Level == 2) - // Debug.Console(2, this, "Received: {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); - var bytesHandler = BytesReceived; + var bytesHandler = BytesReceived; if (bytesHandler != null) bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); var textHandler = TextReceived; @@ -106,8 +104,6 @@ namespace PepperDash.Core /// public void SendEscapedText(string text) { - //if (Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - // Connect(); var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => { var hex = s.Groups[1].Value; @@ -115,9 +111,6 @@ namespace PepperDash.Core }); SendText(unescapedText); - //var bytes = Encoding.GetEncoding(28591).GetBytes(unescapedText); - //Debug.Console(2, this, "Sending {0} bytes: '{1}'", bytes.Length, text); - //Client.SendData(bytes, bytes.Length); } public void SendBytes(byte[] bytes) diff --git a/Pepperdash Core/Pepperdash Core/Comm/SshConfig.cs b/Pepperdash Core/Pepperdash Core/Comm/SshConfig.cs new file mode 100644 index 0000000..52e4028 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/SshConfig.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +using Newtonsoft.Json; + +namespace PepperDash.Core +{ + public class SshConfig : TcpIpConfig + { + public string Username { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/CommunicationExtras.cs b/Pepperdash Core/Pepperdash Core/CommunicationExtras.cs index 79235f7..a935d5e 100644 --- a/Pepperdash Core/Pepperdash Core/CommunicationExtras.cs +++ b/Pepperdash Core/Pepperdash Core/CommunicationExtras.cs @@ -16,13 +16,27 @@ namespace PepperDash.Core { event EventHandler BytesReceived; event EventHandler TextReceived; + bool IsConnected { get; } - bool Connected { get; } void SendText(string text); void SendBytes(byte[] bytes); void Connect(); } + /// + /// + /// + public enum eGenericCommMethodStatusChangeType + { + Connected, Disconnected + } + + /// + /// This delegate defines handler for IBasicCommunication status changes + /// + /// Device firing the status change + /// + public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); /// /// diff --git a/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs b/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs index 2155a5a..195d6e2 100644 --- a/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs +++ b/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs @@ -10,4 +10,9 @@ namespace PepperDash.Core { string Key { get; } } + + public interface IKeyName : IKeyed + { + string Name { get; } + } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Debug.cs b/Pepperdash Core/Pepperdash Core/Debug.cs index 0c26a14..d6567c6 100644 --- a/Pepperdash Core/Pepperdash Core/Debug.cs +++ b/Pepperdash Core/Pepperdash Core/Debug.cs @@ -8,8 +8,10 @@ using Crestron.SimplSharp.CrestronDataStore; namespace PepperDash.Core { - public class Debug + public static class Debug { + + public static uint Level { get; private set; } /// diff --git a/Pepperdash Core/Pepperdash Core/Device.cs b/Pepperdash Core/Pepperdash Core/Device.cs index 87bd106..66ca855 100644 --- a/Pepperdash Core/Pepperdash Core/Device.cs +++ b/Pepperdash Core/Pepperdash Core/Device.cs @@ -8,7 +8,7 @@ namespace PepperDash.Core /// /// The core event and status-bearing class that most if not all device and connectors can derive from. /// - public class Device : IKeyed + public class Device : IKeyName { public string Key { get; protected set; } public string Name { get; protected set; } diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj index c419411..4d73d1c 100644 --- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj @@ -63,11 +63,13 @@ + + - + @@ -80,7 +82,7 @@ C:\Users\hvolm\Desktop\working\pepperdash-simplsharp-core\Pepperdash Core\Pepperdash Core\bin\PepperDash_Core.clz 1.007.0017 - 6/17/2016 10:42:36 AM + 8/3/2016 3:25:15 PM False diff --git a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz index 1f05e19..5cc6640 100644 Binary files a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz and b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.clz differ diff --git a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config index c56cd9c..4b3a33e 100644 --- a/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config +++ b/Pepperdash Core/Pepperdash Core/bin/PepperDash_Core.config @@ -10,7 +10,7 @@ - 6/17/2016 10:42:36 AM - 1.0.0.17477 + 8/3/2016 3:25:15 PM + 1.0.0.25957 \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/bin/manifest.info b/Pepperdash Core/Pepperdash Core/bin/manifest.info index 88fce07..f7c94b9 100644 --- a/Pepperdash Core/Pepperdash Core/bin/manifest.info +++ b/Pepperdash Core/Pepperdash Core/bin/manifest.info @@ -1,4 +1,4 @@ -MainAssembly=PepperDash_Core.dll:2aa20b6ac2f133d96f5ebf4bd3388e0f +MainAssembly=PepperDash_Core.dll:2888e497caab5a6d6578b6174add3a96 MainAssemblyMinFirmwareVersion=1.007.0017 ü DependencySource=Newtonsoft.Json.Compact.dll:ea996aa2ec65aa1878e7c9d09e37a896 diff --git a/Pepperdash Core/Pepperdash Core/bin/manifest.ser b/Pepperdash Core/Pepperdash Core/bin/manifest.ser index 7c5f19d..d0d7667 100644 Binary files a/Pepperdash Core/Pepperdash Core/bin/manifest.ser and b/Pepperdash Core/Pepperdash Core/bin/manifest.ser differ