From d4281175d80b7b5dc1dcb6ce7431ce7df5029f25 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 14 May 2024 14:58:29 -0600 Subject: [PATCH] fix: Copies SSH updates from 1.3.3-hotfix-390 --- src/Pepperdash Core/Comm/GenericSshClient.cs | 660 ++++++++++--------- 1 file changed, 354 insertions(+), 306 deletions(-) diff --git a/src/Pepperdash Core/Comm/GenericSshClient.cs b/src/Pepperdash Core/Comm/GenericSshClient.cs index 51ba8f4..277b34c 100644 --- a/src/Pepperdash Core/Comm/GenericSshClient.cs +++ b/src/Pepperdash Core/Comm/GenericSshClient.cs @@ -8,167 +8,149 @@ using Crestron.SimplSharp.Ssh.Common; namespace PepperDash.Core { - /// - /// - /// + /// + /// + /// public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect - { - private const string SPlusKey = "Uninitialized SshClient"; + { + private const string SPlusKey = "Uninitialized SshClient"; /// /// Object to enable stream debugging /// public CommunicationStreamDebugging StreamDebugging { get; private set; } - /// - /// Event that fires when data is received. Delivers args with byte array - /// - public event EventHandler BytesReceived; + /// + /// Event that fires when data is received. Delivers args with byte array + /// + public event EventHandler BytesReceived; - /// - /// Event that fires when data is received. Delivered as text. - /// - public event EventHandler TextReceived; + /// + /// Event that fires when data is received. Delivered as text. + /// + public event EventHandler TextReceived; - /// - /// Event when the connection status changes. - /// - public event EventHandler ConnectionChange; + /// + /// Event when the connection status changes. + /// + public event EventHandler ConnectionChange; ///// ///// ///// //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - /// - /// Address of server - /// - public string Hostname { get; set; } + /// + /// Address of server + /// + public string Hostname { get; set; } - /// - /// Port on server - /// - public int Port { get; set; } + /// + /// Port on server + /// + public int Port { get; set; } - /// - /// Username for server - /// - public string Username { get; set; } + /// + /// Username for server + /// + public string Username { get; set; } - /// - /// And... Password for server. That was worth documenting! - /// - public string Password { get; set; } + /// + /// And... Password for server. That was worth documenting! + /// + public string Password { get; set; } - /// - /// True when the server is connected - when status == 2. - /// - public bool IsConnected - { - // returns false if no client or not connected + /// + /// True when the server is connected - when status == 2. + /// + public bool IsConnected + { + // returns false if no client or not connected get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } + } - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected - { - get { return (ushort)(IsConnected ? 1 : 0); } - } + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } - /// - /// - /// - public SocketStatus ClientStatus - { - get { return _ClientStatus; } - private set - { - if (_ClientStatus == value) - return; - _ClientStatus = value; - OnConnectionChange(); - } - } - SocketStatus _ClientStatus; + /// + /// + /// + public SocketStatus ClientStatus + { + get { return _ClientStatus; } + private set + { + if (_ClientStatus == value) + return; + _ClientStatus = value; + OnConnectionChange(); + } + } + SocketStatus _ClientStatus; - /// - /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected with be true when this == 2. - /// - public ushort UStatus - { - get { return (ushort)_ClientStatus; } - } + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected with be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)_ClientStatus; } + } - /// - /// Determines whether client will attempt reconnection on failure. Default is true - /// - public bool AutoReconnect { get; set; } + /// + /// Determines whether client will attempt reconnection on failure. Default is true + /// + public bool AutoReconnect { get; set; } - /// - /// Will be set and unset by connect and disconnect only - /// - public bool ConnectEnabled { get; private set; } + /// + /// Will be set and unset by connect and disconnect only + /// + public bool ConnectEnabled { get; private set; } - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } - /// - /// Millisecond value, determines the timeout period in between reconnect attempts. - /// Set to 5000 by default - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Millisecond value, determines the timeout period in between reconnect attempts. + /// Set to 5000 by default + /// + public int AutoReconnectIntervalMs { get; set; } - SshClient Client; + SshClient Client; - ShellStream TheStream; + ShellStream TheStream; - CTimer ReconnectTimer; + CTimer ReconnectTimer; //Lock object to prevent simulatneous connect/disconnect operations private CCriticalSection connectLock = new CCriticalSection(); private bool DisconnectLogged = false; - /// - /// Typical constructor. - /// - public GenericSshClient(string key, string hostname, int port, string username, string password) : - base(key) - { + /// + /// Typical constructor. + /// + public GenericSshClient(string key, string hostname, int port, string username, string password) : + base(key) + { StreamDebugging = new CommunicationStreamDebugging(key); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - Key = key; - Hostname = hostname; - Port = port; - Username = username; - Password = password; - AutoReconnectIntervalMs = 5000; - - ReconnectTimer = new CTimer(o => - { - if (ConnectEnabled) - { - Connect(); - } - }, Timeout.Infinite); - } - - /// - /// S+ Constructor - Must set all properties before calling Connect - /// - public GenericSshClient() - : base(SPlusKey) - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Key = key; + Hostname = hostname; + Port = port; + Username = username; + Password = password; + AutoReconnectIntervalMs = 5000; ReconnectTimer = new CTimer(o => { @@ -177,35 +159,53 @@ namespace PepperDash.Core Connect(); } }, Timeout.Infinite); - } + } - /// - /// Just to help S+ set the key - /// - public void Initialize(string key) - { - Key = key; - } + /// + /// S+ Constructor - Must set all properties before calling Connect + /// + public GenericSshClient() + : base(SPlusKey) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; - /// - /// Handles closing this up when the program shuts down - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - { - if (Client != null) - { - Debug.Console(1, this, "Program stopping. Closing connection"); + ReconnectTimer = new CTimer(o => + { + if (ConnectEnabled) + { + Connect(); + } + }, Timeout.Infinite); + } + + /// + /// Just to help S+ set the key + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + if (Client != null) + { + Debug.Console(1, this, "Program stopping. Closing connection"); Disconnect(); - } - } - } + } + } + } - /// - /// Connect to the server, using the provided properties. - /// - public void Connect() + /// + /// Connect to the server, using the provided properties. + /// + public void Connect() { // Don't go unless everything is here if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535 @@ -229,7 +229,10 @@ namespace PepperDash.Core Debug.Console(1, this, "Attempting connect"); // Cancel reconnect if running. - ReconnectTimer.Stop(); + if (ReconnectTimer != null) + { + ReconnectTimer.Stop(); + } // Cleanup the old client if it already exists if (Client != null) @@ -246,8 +249,6 @@ namespace PepperDash.Core 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 @@ -256,6 +257,11 @@ namespace PepperDash.Core { Client.Connect(); TheStream = Client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534); + if (TheStream.DataAvailable) + { + // empty the buffer if there is data + string str = TheStream.Read(); + } TheStream.DataReceived += Stream_DataReceived; Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connected"); ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; @@ -307,21 +313,21 @@ namespace PepperDash.Core } } - /// - /// Disconnect the clients and put away it's resources. - /// - public void Disconnect() - { - ConnectEnabled = false; - // Stop trying reconnects, if we are - if (ReconnectTimer != null) - { - ReconnectTimer.Stop(); - ReconnectTimer = null; - } + /// + /// Disconnect the clients and put away it's resources. + /// + public void Disconnect() + { + ConnectEnabled = false; + // Stop trying reconnects, if we are + if (ReconnectTimer != null) + { + ReconnectTimer.Stop(); + // ReconnectTimer = null; + } KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); - } + } /// /// Kills the stream, cleans up the client and sets it to null @@ -330,105 +336,125 @@ namespace PepperDash.Core { KillStream(); - if (Client != null) - { - Client.Disconnect(); - Client = null; - ClientStatus = status; - Debug.Console(1, this, "Disconnected"); + try + { + if (Client != null) + { + Client.ErrorOccurred -= Client_ErrorOccurred; + Client.Disconnect(); + Client.Dispose(); + Client = null; + ClientStatus = status; + Debug.Console(1, this, "Disconnected"); + } + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception in Kill Client:{0}", ex); } } - /// - /// Anything to do with reestablishing connection on failures - /// - void HandleConnectionFailure() - { + /// + /// Anything to do with reestablishing connection on failures + /// + void HandleConnectionFailure() + { KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); Debug.Console(1, this, "Client nulled due to connection failure. AutoReconnect: {0}, ConnectEnabled: {1}", AutoReconnect, ConnectEnabled); - if (AutoReconnect && ConnectEnabled) - { - Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - if (ReconnectTimer == null) - { - ReconnectTimer = new CTimer(o => - { - Connect(); - }, AutoReconnectIntervalMs); - Debug.Console(1, this, "Attempting connection in {0} seconds", - (float) (AutoReconnectIntervalMs/1000)); - } - else - { - Debug.Console(1, this, "{0} second reconnect cycle running", - (float) (AutoReconnectIntervalMs/1000)); - } - } - } + if (AutoReconnect && ConnectEnabled) + { + Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + if (ReconnectTimer == null) + { + ReconnectTimer = new CTimer(o => + { + Connect(); + }, AutoReconnectIntervalMs); + Debug.Console(1, this, "Attempting connection in {0} seconds", + (float)(AutoReconnectIntervalMs / 1000)); + } + else + { + Debug.Console(1, this, "{0} second reconnect cycle running", + (float)(AutoReconnectIntervalMs / 1000)); + } + } + } /// /// Kills the stream /// void KillStream() - { - if (TheStream != null) - { - TheStream.DataReceived -= Stream_DataReceived; - TheStream.Close(); - TheStream.Dispose(); - TheStream = null; - Debug.Console(1, this, "Disconnected stream"); - } - } - - /// - /// 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) - { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - } - - var textHandler = TextReceived; - if (textHandler != null) - { - var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Received: '{0}'", ComTextHelper.GetDebugText(str)); - - textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + { + try + { + if (TheStream != null) + { + TheStream.DataReceived -= Stream_DataReceived; + TheStream.Close(); + TheStream.Dispose(); + TheStream = null; + Debug.Console(1, this, "Disconnected stream"); } - } - } + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception in Kill Stream:{0}", ex); + } + } + + /// + /// 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) + { + if (((ShellStream)sender).Length <= 0L) + { + return; + } + var response = ((ShellStream)sender).Read(); + + var bytesHandler = BytesReceived; + + if (bytesHandler != null) + { + var bytes = Encoding.UTF8.GetBytes(response); + if (StreamDebugging.RxStreamDebuggingIsEnabled) + { + Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); + } + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + } + + var textHandler = TextReceived; + if (textHandler != null) + { + if (StreamDebugging.RxStreamDebuggingIsEnabled) + Debug.Console(0, this, "Received: '{0}'", ComTextHelper.GetDebugText(response)); + + textHandler(this, new GenericCommMethodReceiveTextArgs(response)); + } + + } - /// - /// 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) - { + /// + /// 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) + { CrestronInvoke.BeginInvoke(o => { if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) @@ -451,58 +477,69 @@ namespace PepperDash.Core ReconnectTimer.Reset(AutoReconnectIntervalMs); } }); - } + } - /// - /// Helper for ConnectionChange event - /// - void OnConnectionChange() - { - if (ConnectionChange != null) - ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); - } + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + if (ConnectionChange != null) + ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + } - #region IBasicCommunication Members + #region IBasicCommunication Members - /// - /// Sends text to the server - /// - /// - public void SendText(string text) - { - try - { + /// + /// Sends text to the server + /// + /// + public void SendText(string text) + { + try + { if (Client != null && TheStream != null && IsConnected) { if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + Debug.Console(0, + this, + "Sending {0} characters of text: '{1}'", + text.Length, + ComTextHelper.GetDebugText(text)); TheStream.Write(text); TheStream.Flush(); - } else { Debug.Console(1, this, "Client is null or disconnected. Cannot Send Text"); } - } - catch (Exception ex) - { - Debug.Console(0, "Exception: {0}", ex.Message); - Debug.Console(0, "Stack Trace: {0}", ex.StackTrace); + } + catch (ObjectDisposedException ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace); - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed. Disconnected, closing"); - } - } + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + ReconnectTimer.Reset(); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace); + + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed"); + } + } /// /// Sends Bytes to the server /// /// public void SendBytes(byte[] bytes) - { - try - { + { + try + { if (Client != null && TheStream != null && IsConnected) { if (StreamDebugging.TxStreamDebuggingIsEnabled) @@ -515,23 +552,34 @@ namespace PepperDash.Core { Debug.Console(1, this, "Client is null or disconnected. Cannot Send Bytes"); } - } - catch - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed. Disconnected, closing"); - } - } + } + catch (ObjectDisposedException ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace); - #endregion - } + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + ReconnectTimer.Reset(); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace); - //***************************************************************************************************** - //***************************************************************************************************** - /// - /// Fired when connection changes - /// - public class SshConnectionChangeEventArgs : EventArgs - { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed"); + } + } + + #endregion + } + + //***************************************************************************************************** + //***************************************************************************************************** + /// + /// Fired when connection changes + /// + public class SshConnectionChangeEventArgs : EventArgs + { /// /// Connection State /// @@ -552,10 +600,10 @@ namespace PepperDash.Core /// public ushort Status { get { return Client.UStatus; } } - /// + /// /// S+ Constructor - /// - public SshConnectionChangeEventArgs() { } + /// + public SshConnectionChangeEventArgs() { } /// /// EventArgs class @@ -563,9 +611,9 @@ namespace PepperDash.Core /// Connection State /// The Client public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client) - { - IsConnected = isConnected; - Client = client; - } - } -} + { + IsConnected = isConnected; + Client = client; + } + } +} \ No newline at end of file