using System; using System.Net.Sockets; using System.Threading; using Renci.SshNet.Common; namespace Renci.SshNet { /// /// Serves as base class for client implementations, provides common client functionality. /// public abstract class BaseClient : IDisposable { /// /// Holds value indicating whether the connection info is owned by this client. /// private readonly bool _ownsConnectionInfo; private TimeSpan _keepAliveInterval; private Timer _keepAliveTimer; private ConnectionInfo _connectionInfo; /// /// Gets current session. /// protected Session Session { get; private set; } /// /// Gets the connection info. /// /// /// The connection info. /// /// The method was called after the client was disposed. public ConnectionInfo ConnectionInfo { get { CheckDisposed(); return _connectionInfo; } private set { _connectionInfo = value; } } /// /// Gets a value indicating whether this client is connected to the server. /// /// /// true if this client is connected; otherwise, false. /// /// The method was called after the client was disposed. public bool IsConnected { get { CheckDisposed(); return this.Session != null && this.Session.IsConnected; } } /// /// Gets or sets the keep-alive interval. /// /// /// The keep-alive interval. Specify negative one (-1) milliseconds to disable the /// keep-alive. This is the default value. /// /// The method was called after the client was disposed. public TimeSpan KeepAliveInterval { get { CheckDisposed(); return this._keepAliveInterval; } set { CheckDisposed(); if (value == _keepAliveInterval) return; if (value == Session.InfiniteTimeSpan) { // stop the timer when the value is -1 milliseconds StopKeepAliveTimer(); } else { // change the due time and interval of the timer if has already // been created (which means the client is connected) // // if the client is not yet connected, then the timer will be // created with the new interval when Connect() is invoked if (_keepAliveTimer != null) _keepAliveTimer.Change(value, value); } this._keepAliveInterval = value; } } /// /// Occurs when an error occurred. /// /// /// /// public event EventHandler ErrorOccurred; /// /// Occurs when host key received. /// /// /// /// public event EventHandler HostKeyReceived; /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// is null. /// /// If is true, then the /// connection info will be disposed when this instance is disposed. /// protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo) { if (connectionInfo == null) throw new ArgumentNullException("connectionInfo"); ConnectionInfo = connectionInfo; _ownsConnectionInfo = ownsConnectionInfo; _keepAliveInterval = Session.InfiniteTimeSpan; } /// /// Connects client to the server. /// /// The client is already connected. /// The method was called after the client was disposed. /// Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname. /// SSH session could not be established. /// Authentication of SSH session failed. /// Failed to establish proxy connection. public void Connect() { CheckDisposed(); // TODO (see issue #1758): // we're not stopping the keep-alive timer and disposing the session here // // we could do this but there would still be side effects as concrete // implementations may still hang on to the original session // // therefore it would be better to actually invoke the Disconnect method // (and then the Dispose on the session) but even that would have side effects // eg. it would remove all forwarded ports from SshClient // // I think we should modify our concrete clients to better deal with a // disconnect. In case of SshClient this would mean not removing the // forwarded ports on disconnect (but only on dispose ?) and link a // forwarded port with a client instead of with a session // // To be discussed with Oleg (or whoever is interested) if (Session != null && Session.IsConnected) throw new InvalidOperationException("The client is already connected."); OnConnecting(); Session = new Session(ConnectionInfo); Session.HostKeyReceived += Session_HostKeyReceived; Session.ErrorOccured += Session_ErrorOccured; Session.Connect(); StartKeepAliveTimer(); OnConnected(); } /// /// Disconnects client from the server. /// /// The method was called after the client was disposed. public void Disconnect() { CheckDisposed(); OnDisconnecting(); StopKeepAliveTimer(); if (Session != null) Session.Disconnect(); OnDisconnected(); } /// /// Sends keep-alive message to the server. /// /// The method was called after the client was disposed. public void SendKeepAlive() { CheckDisposed(); if (Session != null && Session.IsConnected) Session.SendKeepAlive(); } /// /// Called when client is connecting to the server. /// protected virtual void OnConnecting() { } /// /// Called when client is connected to the server. /// protected virtual void OnConnected() { } /// /// Called when client is disconnecting from the server. /// protected virtual void OnDisconnecting() { if (Session != null) Session.OnDisconnecting(); } /// /// Called when client is disconnected from the server. /// protected virtual void OnDisconnected() { } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { var handler = this.ErrorOccurred; if (handler != null) { handler(this, e); } } private void Session_HostKeyReceived(object sender, HostKeyEventArgs e) { var handler = this.HostKeyReceived; if (handler != null) { handler(this, e); } } #region IDisposable Members private bool _isDisposed; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged ResourceMessages. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged ResourceMessages. protected virtual void Dispose(bool disposing) { // Check to see if Dispose has already been called. if (!this._isDisposed) { // If disposing equals true, dispose all managed // and unmanaged ResourceMessages. if (disposing) { // stop sending keep-alive messages before we close the // session StopKeepAliveTimer(); if (this.Session != null) { this.Session.ErrorOccured -= Session_ErrorOccured; this.Session.HostKeyReceived -= Session_HostKeyReceived; this.Session.Dispose(); this.Session = null; } if (_ownsConnectionInfo && _connectionInfo != null) { var connectionInfoDisposable = _connectionInfo as IDisposable; if (connectionInfoDisposable != null) connectionInfoDisposable.Dispose(); _connectionInfo = null; } } // Note disposing has been done. _isDisposed = true; } } /// /// Check if the current instance is disposed. /// /// THe current instance is disposed. protected void CheckDisposed() { if (_isDisposed) throw new ObjectDisposedException(GetType().FullName); } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~BaseClient() { // Do not re-create Dispose clean-up code here. // Calling Dispose(false) is optimal in terms of // readability and maintainability. Dispose(false); } #endregion /// /// Stops the keep-alive timer, and waits until all timer callbacks have been /// executed. /// private void StopKeepAliveTimer() { if (_keepAliveTimer == null) return; var timerDisposed = new ManualResetEvent(false); _keepAliveTimer.Dispose(timerDisposed); timerDisposed.WaitOne(); timerDisposed.Dispose(); _keepAliveTimer = null; } /// /// Starts the keep-alive timer. /// /// /// When is negative one (-1) milliseconds, then /// the timer will not be started. /// private void StartKeepAliveTimer() { if (_keepAliveInterval == Session.InfiniteTimeSpan) return; if (_keepAliveTimer == null) _keepAliveTimer = new Timer(state => this.SendKeepAlive()); _keepAliveTimer.Change(_keepAliveInterval, _keepAliveInterval); } } }