diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs
index 2e431e8..acecbd6 100644
--- a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs
+++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs
@@ -1,5 +1,5 @@
using System;
-using System.Linq;
+using System.Collections.Generic;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
@@ -8,262 +8,324 @@ using Crestron.SimplSharp.Ssh.Common;
namespace PepperDash.Core
{
- /*
- ///
- ///
- ///
- public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
- {
- 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. Delivered as text.
- ///
- public event EventHandler TextReceived;
-
- ///
- /// Event when the connection status changes.
- ///
- public event EventHandler ConnectionChange;
-
- /////
- /////
- /////
- //public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
-
- ///
- /// Address of server
- ///
- public string Hostname { get; set; }
-
- ///
- /// Port on server
- ///
- public int Port { get; set; }
-
- ///
- /// Username for server
- ///
- public string Username { 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
- get { return ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
- }
-
- private bool IsConnecting = false;
-
- ///
- /// 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;
-
- ///
- /// 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; }
-
- ///
- /// 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; }
- }
-
- ///
- /// Millisecond value, determines the timeout period in between reconnect attempts.
- /// Set to 5000 by default
- ///
- public int AutoReconnectIntervalMs { get; set; }
-
- SshClient Client;
-
- ShellStream TheStream;
-
- CTimer ReconnectTimer;
-
- //string PreviousHostname;
- //int PreviousPort;
- //string PreviousUsername;
- //string PreviousPassword;
-
- ///
- /// 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;
- }
-
- ///
- /// S+ Constructor - Must set all properties before calling Connect
- ///
- public GenericSshClient()
- : base(SPlusKey)
- {
- StreamDebugging = new CommunicationStreamDebugging(SPlusKey);
- CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
- AutoReconnectIntervalMs = 5000;
- }
-
- ///
- /// 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()
+ ///
+ /// Generic ssh client
+ ///
+ public class GenericSshClient : Device, IStreamDebugging, ISocketStatus, IAutoReconnect, IDisposable
+ {
+ private static class ConnectionProgress
{
- if (IsConnecting)
+ public const int Idle = 0;
+ public const int InProgress = 1;
+ }
+
+ private SshClient client;
+ private ShellStream stream;
+ private CTimer connectTimer;
+ private CTimer disconnectTimer;
+ private int connectionProgress = ConnectionProgress.Idle;
+ private KeyboardInteractiveAuthenticationMethod keyboardAuth;
+ private PasswordAuthenticationMethod passwordAuth;
+ private SocketStatus socketSatus;
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname { get; set; }
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Username for server
+ ///
+ public string Username { get; set; }
+
+ ///
+ /// And... Password for server. That was worth documenting!
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Client socket status
+ ///
+ public SocketStatus ClientStatus
+ {
+ get { return socketSatus; }
+ private set
{
- Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "Connection attempt in progress. Exiting Connect()");
- return;
+ if (socketSatus == value)
+ return;
+ socketSatus = value;
+ OnConnectionChange();
}
+ }
- IsConnecting = true;
- ConnectEnabled = true;
- Debug.Console(1, this, "attempting connect");
+ ///
+ /// Will be set and unset by connect and disconnect only
+ ///
+ public bool ConnectEnabled { get; private set; }
- // Cancel reconnect if running.
- if (ReconnectTimer != null)
+ private void OnConnectionChange()
+ {
+ var handler = ConnectionChange;
+ if (handler != null)
+ handler(this, new GenericSocketStatusChageEventArgs(this));
+ }
+
+ private const string SPlusKey = "Uninitialized SshClient";
+
+ ///
+ /// S+ Constructor - Must set all properties before calling Connect
+ ///
+ public GenericSshClient()
+ : base(SPlusKey)
+ {
+ StreamDebugging = new CommunicationStreamDebugging(SPlusKey);
+ AutoReconnectIntervalMs = 10000;
+
+ connectTimer = new CTimer(_ =>
{
- ReconnectTimer.Stop();
- ReconnectTimer = null;
- }
+ Debug.Console(1, this, "Attempting to reconnect");
+ Connect();
+ }, Timeout.Infinite);
- // 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)
+ disconnectTimer = new CTimer(_ =>
{
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect failed. Check hostname, port, username and password are set or not null");
- return;
- }
+ Debug.Console(1, this, "Attempting to disconnect");
+ Disconnect();
+ }, Timeout.Infinite);
- // Cleanup the old client if it already exists
- if (Client != null)
+ CrestronEnvironment.ProgramStatusEventHandler += OnProgramShutdown;
+ }
+
+ ///
+ /// Typical constructor.
+ ///
+ public GenericSshClient(string key, string hostname, int port, string username, string password)
+ : base(key)
+ {
+ Key = key;
+ Hostname = hostname;
+ Port = port;
+ Username = username;
+ Password = password;
+ AutoReconnectIntervalMs = 10000;
+
+ connectTimer = new CTimer(_ =>
{
- Debug.Console(1, this, "Cleaning up disconnected client");
- Client.ErrorOccurred -= Client_ErrorOccurred;
- KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
- }
+ Debug.Console(1, this, "Attempting to reconnect");
+ Connect();
+ }, Timeout.Infinite);
- // 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);
+ disconnectTimer = new CTimer(_ =>
+ {
+ Debug.Console(1, this, "Attempting to disconnect");
+ Disconnect();
+ }, Timeout.Infinite);
- 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;
+ StreamDebugging = new CommunicationStreamDebugging(key);
+ CrestronEnvironment.ProgramStatusEventHandler += OnProgramShutdown;
+ }
- //Attempt to connect
- ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
+ private void OnProgramShutdown(eProgramStatusEventType type)
+ {
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;
- return; // Success will not pass here
+ if (type == eProgramStatusEventType.Stopping)
+ Disconnect();
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Error at shutdown:{0}", ex);
+ }
+ }
+
+ ///
+ /// Just to help S+ set the key
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// True when the server is connected - when status == 2.
+ ///
+ public bool IsConnected
+ {
+ get { return client != null && client.IsConnected; }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// Event fired when bytes are received on the shell
+ ///
+ public event EventHandler BytesReceived;
+
+ ///
+ /// Event fired when test is received on the shell
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// Sends text to the server
+ ///
+ ///
+ public void SendText(string text)
+ {
+ if (CanSend())
+ {
+ if (StreamDebugging.TxStreamDebuggingIsEnabled)
+ Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text.Trim()));
+
+ stream.WriteLine(text);
+ }
+ else
+ {
+ Debug.Console(1, this, "SendText() called but not connected");
+ }
+ }
+
+ ///
+ /// Sends bytes to the server
+ ///
+ ///
+ public void SendBytes(byte[] bytes)
+ {
+ if (CanSend())
+ {
+ if (StreamDebugging.TxStreamDebuggingIsEnabled)
+ Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
+
+ stream.Write(bytes, 0, bytes.Length);
+ stream.Flush();
+ }
+ else
+ {
+ Debug.Console(1, this, "SendText() called but not connected");
+ }
+ }
+
+ private bool CanSend()
+ {
+ return client != null && stream != null && client.IsConnected;
+ }
+
+ ///
+ /// Connect to the server, using the provided properties.
+ ///
+ public void Connect()
+ {
+ ConnectEnabled = true;
+ DisposeOfTimer(disconnectTimer);
+
+ if (
+ Interlocked.CompareExchange(ref connectionProgress, ConnectionProgress.InProgress,
+ ConnectionProgress.Idle) == ConnectionProgress.Idle)
+ {
+ if (client != null && client.IsConnected)
+ {
+ Debug.Console(1, this, "Ignoring connection request... already connected");
+ Interlocked.Exchange(ref connectionProgress, ConnectionProgress.Idle);
+ return;
+ }
+
+ CrestronInvoke.BeginInvoke(_ =>
+ {
+ const int defaultPort = 22;
+ var p = Port == default(int) ? defaultPort : Port;
+
+ if (keyboardAuth != null)
+ {
+ keyboardAuth.AuthenticationPrompt -= AuthenticationPromptHandler;
+ keyboardAuth.Dispose();
+ }
+
+ if (passwordAuth != null)
+ {
+ passwordAuth.Dispose();
+ }
+
+ keyboardAuth = new KeyboardInteractiveAuthenticationMethod(Username);
+ passwordAuth = new PasswordAuthenticationMethod(Username, Password);
+ keyboardAuth.AuthenticationPrompt += AuthenticationPromptHandler;
+
+ var connectionInfo = new ConnectionInfo(Hostname, p, Username, passwordAuth, keyboardAuth);
+
+ if (connectTimer.Disposed && AutoReconnect)
+ {
+ connectTimer = new CTimer(obj =>
+ {
+ Debug.Console(1, this, "Attempting to reconnect");
+ Connect();
+ }, Timeout.Infinite);
+ }
+
+ ConnectInternal(connectionInfo);
+ });
+ }
+ else
+ {
+
+ Debug.Console(1, this, "Ignoring connection request while connect/disconnect in progress...");
+ if (!connectTimer.Disposed && AutoReconnect)
+ connectTimer.Reset(AutoReconnectIntervalMs);
+ }
+ }
+
+ private void ConnectInternal(ConnectionInfo connectionInfo)
+ {
+ Debug.Console(1, this, "Attempting connection to: " + Hostname);
+ try
+ {
+ // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Creating new client...");
+ client = new SshClient(connectionInfo);
+ client.ErrorOccurred += ClientErrorHandler;
+ client.HostKeyReceived += HostKeyReceivedHandler;
+ ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
+
+ // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connecting...");
+ client.Connect();
+
+ // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Creating shell...");
+ stream = client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534);
+ stream.DataReceived += StreamDataReceivedHandler;
+ stream.ErrorOccurred += StreamErrorOccurredHandler;
+ // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Shell created");
+
+ if (client.IsConnected)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connected");
+ connectTimer.Stop();
+ ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
+ Interlocked.Exchange(ref connectionProgress, ConnectionProgress.Idle);
+ }
+ else
+ {
+ throw new Exception("Unknown connection error");
+ }
}
catch (SshConnectionException e)
{
var ie = e.InnerException; // The details are inside!!
+
if (ie is SocketException)
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "'{0}' CONNECTION failure: Cannot reach host, ({1})", Key, ie.Message);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error,
+ "'{0}' CONNECTION failure: Cannot reach host, ({1})",
+ Key, ie.Message);
else if (ie is System.Net.Sockets.SocketException)
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "'{0}' Connection failure: Cannot reach host '{1}' on port {2}, ({3})",
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error,
+ "'{0}' Connection failure: Cannot reach host '{1}' on port {2}, ({3})",
Key, Hostname, Port, ie.GetType());
else if (ie is SshAuthenticationException)
{
@@ -271,273 +333,245 @@ namespace PepperDash.Core
Username, ie.Message);
}
else
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error on connect:\r({0})", e);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error on connect:({0})", ie.Message);
- ClientStatus = SocketStatus.SOCKET_STATUS_CONNECT_FAILED;
- HandleConnectionFailure();
+ DisconnectInternal(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
}
catch (Exception e)
{
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Unhandled exception on connect:\r({0})", e);
- ClientStatus = SocketStatus.SOCKET_STATUS_CONNECT_FAILED;
- HandleConnectionFailure();
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connection error: " + e.Message);
+ DisconnectInternal(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
}
-
- ClientStatus = SocketStatus.SOCKET_STATUS_CONNECT_FAILED;
- HandleConnectionFailure();
}
-
-
- ///
- /// 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
+ /// Disconnect the clients and put away it's resources.
///
- private void KillClient(SocketStatus status)
+ public void Disconnect()
{
- KillStream();
+ ConnectEnabled = false;
+ Disconnect(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
+ }
- if (Client != null)
+ private void Disconnect(SocketStatus status)
+ {
+ // kill the reconnect timer if we are disconnecting locally
+ if (status == SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY)
+ DisposeOfTimer(connectTimer);
+
+ if (
+ Interlocked.CompareExchange(ref connectionProgress, ConnectionProgress.InProgress,
+ ConnectionProgress.Idle) == ConnectionProgress.Idle)
{
- IsConnecting = false;
- Client.Disconnect();
- Client = null;
- ClientStatus = status;
- Debug.Console(1, this, "Disconnected");
+ Debug.Console(1, this, "Disconnecting...");
+ CrestronInvoke.BeginInvoke(_ => DisconnectInternal(status));
+ }
+ else
+ {
+ if (!disconnectTimer.Disposed)
+ DisposeOfTimer(disconnectTimer);
+
+ disconnectTimer = new CTimer(_ => Disconnect(status), 10000);
+ Debug.Console(1, this, "Ignoring disconnect request while connect/disconnect in progress... will try again soon");
}
}
- ///
- /// Anything to do with reestablishing connection on failures
- ///
- void HandleConnectionFailure()
- {
- KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
+ private void DisconnectInternal(SocketStatus status)
+ {
+ try
+ {
+ DisposeOfStream();
+ DisposeOfClient();
+ DisposeOfAuthMethods();
- 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, Debug.ErrorLogLevel.Notice, "Attempting connection in {0} seconds",
- (float) (AutoReconnectIntervalMs/1000));
- }
- else
- {
- Debug.Console(1, this, "{0} second reconnect cycle running",
- (float) (AutoReconnectIntervalMs/1000));
- }
- }
- }
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnect Complete");
+ ClientStatus = status;
- ///
- /// Kills the stream
- ///
- void KillStream()
- {
- if (TheStream != null)
- {
- TheStream.DataReceived -= Stream_DataReceived;
- TheStream.Close();
- TheStream.Dispose();
- TheStream = null;
- }
- }
+ DisposeOfTimer(disconnectTimer);
+ if (!connectTimer.Disposed && AutoReconnect)
+ {
+ Debug.Console(1, this, "Autoreconnect try again soon");
+ connectTimer.Reset(AutoReconnectIntervalMs);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Caught a general exception in disconnect:{0}", ex);
+ }
+ finally
+ {
+ Interlocked.Exchange(ref connectionProgress, ConnectionProgress.Idle);
+ }
+ }
- ///
- /// 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)
- {
+ private void DisposeOfAuthMethods()
+ {
+ try
+ {
+ if (keyboardAuth != null)
+ {
+ keyboardAuth.AuthenticationPrompt -= AuthenticationPromptHandler;
+ keyboardAuth.Dispose();
+ keyboardAuth = null;
+ }
+
+ if (passwordAuth != null)
+ {
+ passwordAuth.Dispose();
+ passwordAuth = null;
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice,
+ "Disconnect() Exception occured freeing auth: " + e.Message);
+ }
+ }
+
+ private void DisposeOfClient()
+ {
+ try
+ {
+ if (client != null)
+ {
+ if (client.IsConnected)
+ client.Disconnect();
+
+ client.ErrorOccurred -= ClientErrorHandler;
+ client.HostKeyReceived -= HostKeyReceivedHandler;
+ client.Dispose();
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice,
+ "Disconnect() Exception occured freeing client: " + e.Message);
+ }
+ }
+
+ private void DisposeOfStream()
+ {
+ try
+ {
+ if (stream != null)
+ {
+ stream.DataReceived -= StreamDataReceivedHandler;
+ stream.ErrorOccurred -= StreamErrorOccurredHandler;
+ stream.Close();
+ stream.Dispose();
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice,
+ "Disconnect() exception occured freeing stream: " + e.Message);
+ }
+ }
+
+ private static void DisposeOfTimer(CTimer timer)
+ {
+ if (timer == null || timer.Disposed) return;
+ timer.Stop();
+ timer.Dispose();
+ }
+
+ private void StreamDataReceivedHandler(object sender, 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);
+ }
- 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);
+ }
+
+ 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));
}
- }
- }
+ }
+ }
+ private void ClientErrorHandler(object sender, ExceptionEventArgs e)
+ {
+ Debug.Console(1, this, "SSH client error:{0}", e.Exception);
+ Disconnect(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
+ }
- ///
- /// 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)
- {
- 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);
+ private void StreamErrorOccurredHandler(object sender, EventArgs e)
+ {
+ Debug.Console(1, this, "SSH Shellstream error");
+ Disconnect(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
+ }
- ClientStatus = SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY;
- HandleConnectionFailure();
- }
+ private void AuthenticationPromptHandler(object sender, AuthenticationPromptEventArgs e)
+ {
+ foreach (var prompt in e.Prompts)
+ if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
+ prompt.Response = Password;
+ }
- ///
- /// Helper for ConnectionChange event
- ///
- void OnConnectionChange()
- {
- if (ConnectionChange != null)
- ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
- }
+ private void HostKeyReceivedHandler(object sender, HostKeyEventArgs e)
+ {
+ e.CanTrust = true;
+ }
- #region IBasicCommunication Members
-
- ///
- /// 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));
-
- 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);
-
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed. Disconnected, closing");
- ClientStatus = SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY;
- HandleConnectionFailure();
- }
- }
+ private static IEnumerable SplitDataReceived(string str, int maxChunkSize)
+ {
+ for (var i = 0; i < str.Length; i += maxChunkSize)
+ {
+ yield return str.Substring(i, Math.Min(maxChunkSize, str.Length - i));
+ }
+ }
///
- /// Sends Bytes to the server
+ /// Stream debugging properties
///
- ///
- public void SendBytes(byte[] bytes)
- {
- try
- {
- if (Client != null && TheStream != null && IsConnected)
- {
- if (StreamDebugging.TxStreamDebuggingIsEnabled)
- Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
-
- TheStream.Write(bytes, 0, bytes.Length);
- TheStream.Flush();
- }
- else
- {
- 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");
- ClientStatus = SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY;
- HandleConnectionFailure();
- }
- }
-
- #endregion
- }
-
- //*****************************************************************************************************
- //*****************************************************************************************************
- ///
- /// Fired when connection changes
- ///
- public class SshConnectionChangeEventArgs : EventArgs
- {
- ///
- /// Connection State
- ///
- public bool IsConnected { get; private set; }
+ public CommunicationStreamDebugging StreamDebugging { get; private set; }
///
- /// Connection Status represented as a ushort
+ /// Event fired on a connection status change
///
- public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
+ public event EventHandler ConnectionChange;
///
- /// The client
+ /// Determines if autoreconnect is enabled
///
- public GenericSshClient Client { get; private set; }
+ public bool AutoReconnect { get; set; }
///
- /// Socket Status as represented by
+ /// Millisecond value, determines the timeout period in between reconnect attempts.
+ /// Set to 10000 by default
///
- public ushort Status { get { return Client.UStatus; } }
+ public int AutoReconnectIntervalMs { get; set; }
- ///
- /// S+ Constructor
- ///
- public SshConnectionChangeEventArgs() { }
+ private bool disposed;
+ public void Dispose()
+ {
+ Dispose(true);
+ CrestronEnvironment.GC.SuppressFinalize(this);
+ }
- ///
- /// EventArgs class
- ///
- /// Connection State
- /// The Client
- public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
- {
- IsConnected = isConnected;
- Client = client;
- }
- }*/
-}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposed)
+ return;
+
+ if (disposing)
+ Disconnect();
+
+ disposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClientV2.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClientV2.cs
deleted file mode 100644
index 7a0c34c..0000000
--- a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClientV2.cs
+++ /dev/null
@@ -1,577 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Crestron.SimplSharp;
-using Crestron.SimplSharp.CrestronSockets;
-using Crestron.SimplSharp.Ssh;
-using Crestron.SimplSharp.Ssh.Common;
-
-namespace PepperDash.Core
-{
- ///
- /// Generic ssh client
- ///
- public class GenericSshClient : Device, IStreamDebugging, ISocketStatus, IAutoReconnect, IDisposable
- {
- private static class ConnectionProgress
- {
- public const int Idle = 0;
- public const int InProgress = 1;
- }
-
- private SshClient client;
- private ShellStream stream;
- private CTimer connectTimer;
- private CTimer disconnectTimer;
- private int connectionProgress = ConnectionProgress.Idle;
- private KeyboardInteractiveAuthenticationMethod keyboardAuth;
- private PasswordAuthenticationMethod passwordAuth;
- private SocketStatus socketSatus;
-
- ///
- /// Address of server
- ///
- public string Hostname { get; set; }
-
- ///
- /// Port on server
- ///
- public int Port { get; set; }
-
- ///
- /// Username for server
- ///
- public string Username { get; set; }
-
- ///
- /// And... Password for server. That was worth documenting!
- ///
- public string Password { get; set; }
-
- ///
- /// Client socket status
- ///
- public SocketStatus ClientStatus
- {
- get { return socketSatus; }
- private set
- {
- if (socketSatus == value)
- return;
- socketSatus = value;
- OnConnectionChange();
- }
- }
-
- ///
- /// Will be set and unset by connect and disconnect only
- ///
- public bool ConnectEnabled { get; private set; }
-
- private void OnConnectionChange()
- {
- var handler = ConnectionChange;
- if (handler != null)
- handler(this, new GenericSocketStatusChageEventArgs(this));
- }
-
- private const string SPlusKey = "Uninitialized SshClient";
-
- ///
- /// S+ Constructor - Must set all properties before calling Connect
- ///
- public GenericSshClient()
- : base(SPlusKey)
- {
- StreamDebugging = new CommunicationStreamDebugging(SPlusKey);
- AutoReconnectIntervalMs = 10000;
-
- connectTimer = new CTimer(_ =>
- {
- Debug.Console(1, this, "Attempting to reconnect");
- Connect();
- }, Timeout.Infinite);
-
- disconnectTimer = new CTimer(_ =>
- {
- Debug.Console(1, this, "Attempting to disconnect");
- Disconnect();
- }, Timeout.Infinite);
-
- CrestronEnvironment.ProgramStatusEventHandler += OnProgramShutdown;
- }
-
- ///
- /// Typical constructor.
- ///
- public GenericSshClient(string key, string hostname, int port, string username, string password) : base(key)
- {
- Key = key;
- Hostname = hostname;
- Port = port;
- Username = username;
- Password = password;
- AutoReconnectIntervalMs = 10000;
-
- connectTimer = new CTimer(_ =>
- {
- Debug.Console(1, this, "Attempting to reconnect");
- Connect();
- }, Timeout.Infinite);
-
- disconnectTimer = new CTimer(_ =>
- {
- Debug.Console(1, this, "Attempting to disconnect");
- Disconnect();
- }, Timeout.Infinite);
-
- StreamDebugging = new CommunicationStreamDebugging(key);
- CrestronEnvironment.ProgramStatusEventHandler += OnProgramShutdown;
- }
-
- private void OnProgramShutdown(eProgramStatusEventType type)
- {
- try
- {
- if (type == eProgramStatusEventType.Stopping)
- Disconnect();
- }
- catch (Exception ex)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Error at shutdown:{0}", ex);
- }
- }
-
- ///
- /// Just to help S+ set the key
- ///
- public void Initialize(string key)
- {
- Key = key;
- }
-
- ///
- /// True when the server is connected - when status == 2.
- ///
- public bool IsConnected
- {
- get { return client != null && client.IsConnected; }
- }
-
- ///
- /// S+ helper for IsConnected
- ///
- public ushort UIsConnected
- {
- get { return (ushort)(IsConnected ? 1 : 0); }
- }
-
- ///
- /// Event fired when bytes are received on the shell
- ///
- public event EventHandler BytesReceived;
-
- ///
- /// Event fired when test is received on the shell
- ///
- public event EventHandler TextReceived;
-
- ///
- /// Sends text to the server
- ///
- ///
- public void SendText(string text)
- {
- if (CanSend())
- {
- if (StreamDebugging.TxStreamDebuggingIsEnabled)
- Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text.Trim()));
-
- stream.WriteLine(text);
- }
- else
- {
- Debug.Console(1, this, "SendText() called but not connected");
- }
- }
-
- ///
- /// Sends bytes to the server
- ///
- ///
- public void SendBytes(byte[] bytes)
- {
- if (CanSend())
- {
- if (StreamDebugging.TxStreamDebuggingIsEnabled)
- Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
-
- stream.Write(bytes, 0, bytes.Length);
- stream.Flush();
- }
- else
- {
- Debug.Console(1, this, "SendText() called but not connected");
- }
- }
-
- private bool CanSend()
- {
- return client != null && stream != null && client.IsConnected;
- }
-
- ///
- /// Connect to the server, using the provided properties.
- ///
- public void Connect()
- {
- ConnectEnabled = true;
- DisposeOfTimer(disconnectTimer);
-
- if (
- Interlocked.CompareExchange(ref connectionProgress, ConnectionProgress.InProgress,
- ConnectionProgress.Idle) == ConnectionProgress.Idle)
- {
- if (client != null && client.IsConnected)
- {
- Debug.Console(1, this, "Ignoring connection request... already connected");
- Interlocked.Exchange(ref connectionProgress, ConnectionProgress.Idle);
- return;
- }
-
- CrestronInvoke.BeginInvoke(_ =>
- {
- const int defaultPort = 22;
- var p = Port == default(int) ? defaultPort : Port;
-
- if (keyboardAuth != null)
- {
- keyboardAuth.AuthenticationPrompt -= AuthenticationPromptHandler;
- keyboardAuth.Dispose();
- }
-
- if (passwordAuth != null)
- {
- passwordAuth.Dispose();
- }
-
- keyboardAuth = new KeyboardInteractiveAuthenticationMethod(Username);
- passwordAuth = new PasswordAuthenticationMethod(Username, Password);
- keyboardAuth.AuthenticationPrompt += AuthenticationPromptHandler;
-
- var connectionInfo = new ConnectionInfo(Hostname, p, Username, passwordAuth, keyboardAuth);
-
- if (connectTimer.Disposed && AutoReconnect)
- {
- connectTimer = new CTimer(obj =>
- {
- Debug.Console(1, this, "Attempting to reconnect");
- Connect();
- }, Timeout.Infinite);
- }
-
- ConnectInternal(connectionInfo);
- });
- }
- else
- {
-
- Debug.Console(1, this, "Ignoring connection request while connect/disconnect in progress...");
- if (!connectTimer.Disposed && AutoReconnect)
- connectTimer.Reset(AutoReconnectIntervalMs);
- }
- }
-
- private void ConnectInternal(ConnectionInfo connectionInfo)
- {
- Debug.Console(1, this, "Attempting connection to: " + Hostname);
- try
- {
- // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Creating new client...");
- client = new SshClient(connectionInfo);
- client.ErrorOccurred += ClientErrorHandler;
- client.HostKeyReceived += HostKeyReceivedHandler;
- ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
-
- // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connecting...");
- client.Connect();
-
- // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Creating shell...");
- stream = client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534);
- stream.DataReceived += StreamDataReceivedHandler;
- stream.ErrorOccurred += StreamErrorOccurredHandler;
- // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Shell created");
-
- if (client.IsConnected)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connected");
- connectTimer.Stop();
- ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
- Interlocked.Exchange(ref connectionProgress, ConnectionProgress.Idle);
- }
- else
- {
- throw new Exception("Unknown connection error");
- }
- }
- catch (SshConnectionException e)
- {
- var ie = e.InnerException; // The details are inside!!
-
- if (ie is SocketException)
- Debug.Console(1, this, Debug.ErrorLogLevel.Error,
- "'{0}' CONNECTION failure: Cannot reach host, ({1})",
- Key, ie.Message);
- else if (ie is System.Net.Sockets.SocketException)
- Debug.Console(1, this, Debug.ErrorLogLevel.Error,
- "'{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, Debug.ErrorLogLevel.Error, "Authentication failure for username '{0}', ({1})",
- Username, ie.Message);
- }
- else
- Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error on connect:({0})", ie.Message);
-
- DisconnectInternal(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- }
- catch (Exception e)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connection error: " + e.Message);
- DisconnectInternal(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- }
- }
-
- ///
- /// Disconnect the clients and put away it's resources.
- ///
- public void Disconnect()
- {
- ConnectEnabled = false;
- Disconnect(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
- }
-
- private void Disconnect(SocketStatus status)
- {
- // kill the reconnect timer if we are disconnecting locally
- if (status == SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY)
- DisposeOfTimer(connectTimer);
-
- if (
- Interlocked.CompareExchange(ref connectionProgress, ConnectionProgress.InProgress,
- ConnectionProgress.Idle) == ConnectionProgress.Idle)
- {
- Debug.Console(1, this, "Disconnecting...");
- CrestronInvoke.BeginInvoke(_ => DisconnectInternal(status));
- }
- else
- {
- if (!disconnectTimer.Disposed)
- DisposeOfTimer(disconnectTimer);
-
- disconnectTimer = new CTimer(_ => Disconnect(status), 10000);
- Debug.Console(1, this, "Ignoring disconnect request while connect/disconnect in progress... will try again soon");
- }
- }
-
- private void DisconnectInternal(SocketStatus status)
- {
- try
- {
- DisposeOfStream();
- DisposeOfClient();
- DisposeOfAuthMethods();
-
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnect Complete");
- ClientStatus = status;
-
- DisposeOfTimer(disconnectTimer);
- if (!connectTimer.Disposed && AutoReconnect)
- {
- Debug.Console(1, this, "Autoreconnect try again soon");
- connectTimer.Reset(AutoReconnectIntervalMs);
- }
- }
- catch (Exception ex)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Caught a general exception in disconnect:{0}", ex);
- }
- finally
- {
- Interlocked.Exchange(ref connectionProgress, ConnectionProgress.Idle);
- }
- }
-
- private void DisposeOfAuthMethods()
- {
- try
- {
- if (keyboardAuth != null)
- {
- keyboardAuth.AuthenticationPrompt -= AuthenticationPromptHandler;
- keyboardAuth.Dispose();
- keyboardAuth = null;
- }
-
- if (passwordAuth != null)
- {
- passwordAuth.Dispose();
- passwordAuth = null;
- }
- }
- catch (Exception e)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice,
- "Disconnect() Exception occured freeing auth: " + e.Message);
- }
- }
-
- private void DisposeOfClient()
- {
- try
- {
- if (client != null)
- {
- if (client.IsConnected)
- client.Disconnect();
-
- client.ErrorOccurred -= ClientErrorHandler;
- client.HostKeyReceived -= HostKeyReceivedHandler;
- client.Dispose();
- }
- }
- catch (Exception e)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice,
- "Disconnect() Exception occured freeing client: " + e.Message);
- }
- }
-
- private void DisposeOfStream()
- {
- try
- {
- if (stream != null)
- {
- stream.DataReceived -= StreamDataReceivedHandler;
- stream.ErrorOccurred -= StreamErrorOccurredHandler;
- stream.Close();
- stream.Dispose();
- }
- }
- catch (Exception e)
- {
- Debug.Console(1, this, Debug.ErrorLogLevel.Notice,
- "Disconnect() exception occured freeing stream: " + e.Message);
- }
- }
-
- private static void DisposeOfTimer(CTimer timer)
- {
- if (timer == null || timer.Disposed) return;
- timer.Stop();
- timer.Dispose();
- }
-
- private void StreamDataReceivedHandler(object sender, 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));
- }
- }
- }
-
- private void ClientErrorHandler(object sender, ExceptionEventArgs e)
- {
- Debug.Console(1, this, "SSH client error:{0}", e.Exception);
- Disconnect(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
- }
-
- private void StreamErrorOccurredHandler(object sender, EventArgs e)
- {
- Debug.Console(1, this, "SSH Shellstream error");
- Disconnect(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
- }
-
- private void AuthenticationPromptHandler(object sender, AuthenticationPromptEventArgs e)
- {
- foreach (var prompt in e.Prompts)
- if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
- prompt.Response = Password;
- }
-
- private void HostKeyReceivedHandler(object sender, HostKeyEventArgs e)
- {
- e.CanTrust = true;
- }
-
- private static IEnumerable SplitDataReceived(string str, int maxChunkSize)
- {
- for (var i = 0; i < str.Length; i += maxChunkSize)
- {
- yield return str.Substring(i, Math.Min(maxChunkSize, str.Length - i));
- }
- }
-
- ///
- /// Stream debugging properties
- ///
- public CommunicationStreamDebugging StreamDebugging { get; private set; }
-
- ///
- /// Event fired on a connection status change
- ///
- public event EventHandler ConnectionChange;
-
- ///
- /// Determines if autoreconnect is enabled
- ///
- public bool AutoReconnect { get; set; }
-
- ///
- /// Millisecond value, determines the timeout period in between reconnect attempts.
- /// Set to 10000 by default
- ///
- public int AutoReconnectIntervalMs { get; set; }
-
- private bool disposed;
- public void Dispose()
- {
- Dispose(true);
- CrestronEnvironment.GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (disposed)
- return;
-
- if (disposing)
- Disconnect();
-
- disposed = true;
- }
- }
-}
\ No newline at end of file
diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj
index 41f64ed..86ae71d 100644
--- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj
+++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj
@@ -72,7 +72,6 @@
-