< Summary

Information
Class: Renci.SshNet.BaseClient
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\BaseClient.cs
Line coverage
88%
Covered lines: 202
Uncovered lines: 26
Coverable lines: 228
Total lines: 613
Line coverage: 88.5%
Branch coverage
84%
Covered branches: 42
Total branches: 50
Branch coverage: 84%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

\home\appveyor\projects\ssh-net\src\Renci.SshNet\BaseClient.cs

#LineLine coverage
 1using System;
 2using System.Net.Sockets;
 3using System.Threading;
 4using System.Threading.Tasks;
 5
 6using Renci.SshNet.Abstractions;
 7using Renci.SshNet.Common;
 8using Renci.SshNet.Messages.Transport;
 9
 10namespace Renci.SshNet
 11{
 12    /// <summary>
 13    /// Serves as base class for client implementations, provides common client functionality.
 14    /// </summary>
 15    public abstract class BaseClient : IBaseClient
 16    {
 17        /// <summary>
 18        /// Holds value indicating whether the connection info is owned by this client.
 19        /// </summary>
 20        private readonly bool _ownsConnectionInfo;
 21
 22        private readonly IServiceFactory _serviceFactory;
 155623        private readonly object _keepAliveLock = new object();
 24        private TimeSpan _keepAliveInterval;
 25        private Timer _keepAliveTimer;
 26        private ConnectionInfo _connectionInfo;
 27        private bool _isDisposed;
 28
 29        /// <summary>
 30        /// Gets the current session.
 31        /// </summary>
 32        /// <value>
 33        /// The current session.
 34        /// </value>
 1187235        internal ISession Session { get; private set; }
 36
 37        /// <summary>
 38        /// Gets the factory for creating new services.
 39        /// </summary>
 40        /// <value>
 41        /// The factory for creating new services.
 42        /// </value>
 43        internal IServiceFactory ServiceFactory
 44        {
 628545            get { return _serviceFactory; }
 46        }
 47
 48        /// <summary>
 49        /// Gets the connection info.
 50        /// </summary>
 51        /// <value>
 52        /// The connection info.
 53        /// </value>
 54        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 55        public ConnectionInfo ConnectionInfo
 56        {
 57            get
 445258            {
 445259                CheckDisposed();
 445260                return _connectionInfo;
 445261            }
 62            private set
 155363            {
 155364                _connectionInfo = value;
 155365            }
 66        }
 67
 68        /// <summary>
 69        /// Gets a value indicating whether this client is connected to the server.
 70        /// </summary>
 71        /// <value>
 72        /// <see langword="true"/> if this client is connected; otherwise, <see langword="false"/>.
 73        /// </value>
 74        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 75        public bool IsConnected
 76        {
 77            get
 2778            {
 2779                CheckDisposed();
 80
 2481                return IsSessionConnected();
 2482            }
 83        }
 84
 85        /// <summary>
 86        /// Gets or sets the keep-alive interval.
 87        /// </summary>
 88        /// <value>
 89        /// The keep-alive interval. Specify negative one (-1) milliseconds to disable the
 90        /// keep-alive. This is the default value.
 91        /// </value>
 92        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 93        public TimeSpan KeepAliveInterval
 94        {
 95            get
 2796            {
 2797                CheckDisposed();
 2798                return _keepAliveInterval;
 2799            }
 100            set
 86101            {
 86102                CheckDisposed();
 103
 86104                if (value == _keepAliveInterval)
 0105                {
 0106                    return;
 107                }
 108
 86109                if (value == SshNet.Session.InfiniteTimeSpan)
 21110                {
 111                    // stop the timer when the value is -1 milliseconds
 21112                    StopKeepAliveTimer();
 21113                }
 114                else
 65115                {
 65116                    if (_keepAliveTimer != null)
 0117                    {
 118                        // change the due time and interval of the timer if has already
 119                        // been created (which means the client is connected)
 0120                        _ = _keepAliveTimer.Change(value, value);
 0121                    }
 65122                    else if (IsSessionConnected())
 36123                    {
 124                        // if timer has not yet been created and the client is already connected,
 125                        // then we need to create the timer now
 126                        //
 127                        // this means that - before connecting - the keep-alive interval was set to
 128                        // negative one (-1) and as such we did not create the timer
 36129                        _keepAliveTimer = CreateKeepAliveTimer(value, value);
 36130                    }
 131
 132                    // note that if the client is not yet connected, then the timer will be created with the
 133                    // new interval when Connect() is invoked
 65134                }
 135
 86136                _keepAliveInterval = value;
 86137            }
 138        }
 139
 140        /// <summary>
 141        /// Occurs when an error occurred.
 142        /// </summary>
 143        /// <example>
 144        ///   <code source="..\..\src\Renci.SshNet.Tests\Classes\SshClientTest.cs" region="Example SshClient Connect Err
 145        /// </example>
 146        public event EventHandler<ExceptionEventArgs> ErrorOccurred;
 147
 148        /// <summary>
 149        /// Occurs when host key received.
 150        /// </summary>
 151        /// <example>
 152        ///   <code source="..\..\src\Renci.SshNet.Tests\Classes\SshClientTest.cs" region="Example SshClient Connect Hos
 153        /// </example>
 154        public event EventHandler<HostKeyEventArgs> HostKeyReceived;
 155
 156        /// <summary>
 157        /// Occurs when server identification received.
 158        /// </summary>
 159        public event EventHandler<SshIdentificationEventArgs> ServerIdentificationReceived;
 160
 161        /// <summary>
 162        /// Initializes a new instance of the <see cref="BaseClient"/> class.
 163        /// </summary>
 164        /// <param name="connectionInfo">The connection info.</param>
 165        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
 166        /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</except
 167        /// <remarks>
 168        /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
 169        /// connection info will be disposed when this instance is disposed.
 170        /// </remarks>
 171        protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
 0172            : this(connectionInfo, ownsConnectionInfo, new ServiceFactory())
 0173        {
 0174        }
 175
 176        /// <summary>
 177        /// Initializes a new instance of the <see cref="BaseClient"/> class.
 178        /// </summary>
 179        /// <param name="connectionInfo">The connection info.</param>
 180        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
 181        /// <param name="serviceFactory">The factory to use for creating new services.</param>
 182        /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</except
 183        /// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <see langword="null"/>.</except
 184        /// <remarks>
 185        /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
 186        /// connection info will be disposed when this instance is disposed.
 187        /// </remarks>
 1556188        private protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFact
 1556189        {
 1556190            if (connectionInfo is null)
 3191            {
 3192                throw new ArgumentNullException(nameof(connectionInfo));
 193            }
 194
 1553195            if (serviceFactory is null)
 0196            {
 0197                throw new ArgumentNullException(nameof(serviceFactory));
 198            }
 199
 1553200            ConnectionInfo = connectionInfo;
 1553201            _ownsConnectionInfo = ownsConnectionInfo;
 1553202            _serviceFactory = serviceFactory;
 1553203            _keepAliveInterval = SshNet.Session.InfiniteTimeSpan;
 1553204        }
 205
 206        /// <summary>
 207        /// Connects client to the server.
 208        /// </summary>
 209        /// <exception cref="InvalidOperationException">The client is already connected.</exception>
 210        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 211        /// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be establish
 212        /// <exception cref="SshConnectionException">SSH session could not be established.</exception>
 213        /// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
 214        /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
 215        public void Connect()
 1719216        {
 1719217            CheckDisposed();
 218
 219            // TODO (see issue #1758):
 220            // we're not stopping the keep-alive timer and disposing the session here
 221            //
 222            // we could do this but there would still be side effects as concrete
 223            // implementations may still hang on to the original session
 224            //
 225            // therefore it would be better to actually invoke the Disconnect method
 226            // (and then the Dispose on the session) but even that would have side effects
 227            // eg. it would remove all forwarded ports from SshClient
 228            //
 229            // I think we should modify our concrete clients to better deal with a
 230            // disconnect. In case of SshClient this would mean not removing the
 231            // forwarded ports on disconnect (but only on dispose ?) and link a
 232            // forwarded port with a client instead of with a session
 233            //
 234            // To be discussed with Oleg (or whoever is interested)
 1719235            if (IsSessionConnected())
 0236            {
 0237                throw new InvalidOperationException("The client is already connected.");
 238            }
 239
 1719240            OnConnecting();
 241
 1719242            Session = CreateAndConnectSession();
 243
 244            try
 1708245            {
 246                // Even though the method we invoke makes you believe otherwise, at this point only
 247                // the SSH session itself is connected.
 1708248                OnConnected();
 1650249            }
 58250            catch
 58251            {
 252                // Only dispose the session as Disconnect() would have side-effects (such as remove forwarded
 253                // ports in SshClient).
 58254                DisposeSession();
 58255                throw;
 256            }
 257
 1650258            StartKeepAliveTimer();
 1650259        }
 260
 261        /// <summary>
 262        /// Asynchronously connects client to the server.
 263        /// </summary>
 264        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
 265        /// <returns>A <see cref="Task"/> that represents the asynchronous connect operation.
 266        /// </returns>
 267        /// <exception cref="InvalidOperationException">The client is already connected.</exception>
 268        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 269        /// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be establish
 270        /// <exception cref="SshConnectionException">SSH session could not be established.</exception>
 271        /// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
 272        /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
 273        public async Task ConnectAsync(CancellationToken cancellationToken)
 8274        {
 8275            CheckDisposed();
 8276            cancellationToken.ThrowIfCancellationRequested();
 277
 278            // TODO (see issue #1758):
 279            // we're not stopping the keep-alive timer and disposing the session here
 280            //
 281            // we could do this but there would still be side effects as concrete
 282            // implementations may still hang on to the original session
 283            //
 284            // therefore it would be better to actually invoke the Disconnect method
 285            // (and then the Dispose on the session) but even that would have side effects
 286            // eg. it would remove all forwarded ports from SshClient
 287            //
 288            // I think we should modify our concrete clients to better deal with a
 289            // disconnect. In case of SshClient this would mean not removing the
 290            // forwarded ports on disconnect (but only on dispose ?) and link a
 291            // forwarded port with a client instead of with a session
 292            //
 293            // To be discussed with Oleg (or whoever is interested)
 8294            if (IsSessionConnected())
 0295            {
 0296                throw new InvalidOperationException("The client is already connected.");
 297            }
 298
 8299            OnConnecting();
 300
 8301            Session = await CreateAndConnectSessionAsync(cancellationToken).ConfigureAwait(false);
 302
 303            try
 2304            {
 305                // Even though the method we invoke makes you believe otherwise, at this point only
 306                // the SSH session itself is connected.
 2307                OnConnected();
 2308            }
 0309            catch
 0310            {
 311                // Only dispose the session as Disconnect() would have side-effects (such as remove forwarded
 312                // ports in SshClient).
 0313                DisposeSession();
 0314                throw;
 315            }
 316
 2317            StartKeepAliveTimer();
 2318        }
 319
 320        /// <summary>
 321        /// Disconnects client from the server.
 322        /// </summary>
 323        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 324        public void Disconnect()
 1835325        {
 1835326            DiagnosticAbstraction.Log("Disconnecting client.");
 327
 1835328            CheckDisposed();
 329
 1835330            OnDisconnecting();
 331
 332            // stop sending keep-alive messages before we close the session
 1835333            StopKeepAliveTimer();
 334
 335            // dispose the SSH session
 1835336            DisposeSession();
 337
 1835338            OnDisconnected();
 1835339        }
 340
 341        /// <summary>
 342        /// Sends a keep-alive message to the server.
 343        /// </summary>
 344        /// <remarks>
 345        /// Use <see cref="KeepAliveInterval"/> to configure the client to send a keep-alive at regular
 346        /// intervals.
 347        /// </remarks>
 348        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
 349#pragma warning disable S1133 // Deprecated code should be removed
 350        [Obsolete("Use KeepAliveInterval to send a keep-alive message at regular intervals.")]
 351#pragma warning restore S1133 // Deprecated code should be removed
 352        public void SendKeepAlive()
 0353        {
 0354            CheckDisposed();
 355
 0356            SendKeepAliveMessage();
 0357        }
 358
 359        /// <summary>
 360        /// Called when client is connecting to the server.
 361        /// </summary>
 362        protected virtual void OnConnecting()
 1727363        {
 1727364        }
 365
 366        /// <summary>
 367        /// Called when client is connected to the server.
 368        /// </summary>
 369        protected virtual void OnConnected()
 1710370        {
 1710371        }
 372
 373        /// <summary>
 374        /// Called when client is disconnecting from the server.
 375        /// </summary>
 376        protected virtual void OnDisconnecting()
 1835377        {
 1835378            Session?.OnDisconnecting();
 1835379        }
 380
 381        /// <summary>
 382        /// Called when client is disconnected from the server.
 383        /// </summary>
 384        protected virtual void OnDisconnected()
 1835385        {
 1835386        }
 387
 388        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
 7389        {
 7390            ErrorOccurred?.Invoke(this, e);
 7391        }
 392
 393        private void Session_HostKeyReceived(object sender, HostKeyEventArgs e)
 1199394        {
 1199395            HostKeyReceived?.Invoke(this, e);
 1199396        }
 397
 398        private void Session_ServerIdentificationReceived(object sender, SshIdentificationEventArgs e)
 1199399        {
 1199400            ServerIdentificationReceived?.Invoke(this, e);
 1199401        }
 402
 403        /// <summary>
 404        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 405        /// </summary>
 406        public void Dispose()
 1377407        {
 1377408            Dispose(disposing: true);
 1377409            GC.SuppressFinalize(this);
 1377410        }
 411
 412        /// <summary>
 413        /// Releases unmanaged and - optionally - managed resources.
 414        /// </summary>
 415        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor
 416        protected virtual void Dispose(bool disposing)
 1623417        {
 1623418            if (_isDisposed)
 67419            {
 67420                return;
 421            }
 422
 1556423            if (disposing)
 1310424            {
 1310425                DiagnosticAbstraction.Log("Disposing client.");
 426
 1310427                Disconnect();
 428
 1310429                if (_ownsConnectionInfo && _connectionInfo is not null)
 174430                {
 174431                    if (_connectionInfo is IDisposable connectionInfoDisposable)
 174432                    {
 174433                        connectionInfoDisposable.Dispose();
 174434                    }
 435
 174436                    _connectionInfo = null;
 174437                }
 438
 1310439                _isDisposed = true;
 1310440            }
 1623441        }
 442
 443        /// <summary>
 444        /// Check if the current instance is disposed.
 445        /// </summary>
 446        /// <exception cref="ObjectDisposedException">THe current instance is disposed.</exception>
 447        protected void CheckDisposed()
 19631448        {
 449#if NET7_0_OR_GREATER
 18966450            ObjectDisposedException.ThrowIf(_isDisposed, this);
 451#else
 665452            if (_isDisposed)
 3453            {
 3454                throw new ObjectDisposedException(GetType().FullName);
 455            }
 456#endif // NET7_0_OR_GREATER
 19622457        }
 458
 459        /// <summary>
 460        /// Finalizes an instance of the <see cref="BaseClient"/> class.
 461        /// </summary>
 462        ~BaseClient()
 492463        {
 246464            Dispose(disposing: false);
 492465        }
 466
 467        /// <summary>
 468        /// Stops the keep-alive timer, and waits until all timer callbacks have been
 469        /// executed.
 470        /// </summary>
 471        private void StopKeepAliveTimer()
 1856472        {
 1856473            if (_keepAliveTimer is null)
 1812474            {
 1812475                return;
 476            }
 477
 44478            _keepAliveTimer.Dispose();
 44479            _keepAliveTimer = null;
 1856480        }
 481
 482        private void SendKeepAliveMessage()
 88483        {
 88484            var session = Session;
 485
 486            // do nothing if we have disposed or disconnected
 88487            if (session is null)
 0488            {
 0489                return;
 490            }
 491
 492            // do not send multiple keep-alive messages concurrently
 88493            if (Monitor.TryEnter(_keepAliveLock))
 88494            {
 495                try
 88496                {
 88497                    _ = session.TrySendMessage(new IgnoreMessage());
 88498                }
 499                finally
 88500                {
 88501                    Monitor.Exit(_keepAliveLock);
 88502                }
 88503            }
 88504        }
 505
 506        /// <summary>
 507        /// Starts the keep-alive timer.
 508        /// </summary>
 509        /// <remarks>
 510        /// When <see cref="KeepAliveInterval"/> is negative one (-1) milliseconds, then
 511        /// the timer will not be started.
 512        /// </remarks>
 513        private void StartKeepAliveTimer()
 1652514        {
 1652515            if (_keepAliveInterval == SshNet.Session.InfiniteTimeSpan)
 1644516            {
 1644517                return;
 518            }
 519
 8520            if (_keepAliveTimer != null)
 0521            {
 522                // timer is already started
 0523                return;
 524            }
 525
 8526            _keepAliveTimer = CreateKeepAliveTimer(_keepAliveInterval, _keepAliveInterval);
 1652527        }
 528
 529        /// <summary>
 530        /// Creates a <see cref="Timer"/> with the specified due time and interval.
 531        /// </summary>
 532        /// <param name="dueTime">The amount of time to delay before the keep-alive message is first sent. Specify negat
 533        /// <param name="period">The time interval between attempts to send a keep-alive message. Specify negative one (
 534        /// <returns>
 535        /// A <see cref="Timer"/> with the specified due time and interval.
 536        /// </returns>
 537        private Timer CreateKeepAliveTimer(TimeSpan dueTime, TimeSpan period)
 44538        {
 132539            return new Timer(state => SendKeepAliveMessage(), Session, dueTime, period);
 44540        }
 541
 542        private ISession CreateAndConnectSession()
 1719543        {
 1719544            var session = _serviceFactory.CreateSession(ConnectionInfo, _serviceFactory.CreateSocketFactory());
 1719545            session.ServerIdentificationReceived += Session_ServerIdentificationReceived;
 1719546            session.HostKeyReceived += Session_HostKeyReceived;
 1719547            session.ErrorOccured += Session_ErrorOccured;
 548
 549            try
 1719550            {
 1719551                session.Connect();
 1708552                return session;
 553            }
 11554            catch
 11555            {
 11556                DisposeSession(session);
 11557                throw;
 558            }
 1708559        }
 560
 561        private async Task<ISession> CreateAndConnectSessionAsync(CancellationToken cancellationToken)
 8562        {
 8563            var session = _serviceFactory.CreateSession(ConnectionInfo, _serviceFactory.CreateSocketFactory());
 8564            session.ServerIdentificationReceived += Session_ServerIdentificationReceived;
 8565            session.HostKeyReceived += Session_HostKeyReceived;
 8566            session.ErrorOccured += Session_ErrorOccured;
 567
 568            try
 8569            {
 8570                await session.ConnectAsync(cancellationToken).ConfigureAwait(false);
 2571                return session;
 572            }
 6573            catch
 6574            {
 6575                DisposeSession(session);
 6576                throw;
 577            }
 2578        }
 579
 580        private void DisposeSession(ISession session)
 1595581        {
 1595582            session.ErrorOccured -= Session_ErrorOccured;
 1595583            session.HostKeyReceived -= Session_HostKeyReceived;
 1595584            session.ServerIdentificationReceived -= Session_ServerIdentificationReceived;
 1595585            session.Dispose();
 1595586        }
 587
 588        /// <summary>
 589        /// Disposes the SSH session, and assigns <see langword="null"/> to <see cref="Session"/>.
 590        /// </summary>
 591        private void DisposeSession()
 1893592        {
 1893593            var session = Session;
 1893594            if (session != null)
 1578595            {
 1578596                Session = null;
 1578597                DisposeSession(session);
 1578598            }
 1893599        }
 600
 601        /// <summary>
 602        /// Returns a value indicating whether the SSH session is established.
 603        /// </summary>
 604        /// <returns>
 605        /// <see langword="true"/> if the SSH session is established; otherwise, <see langword="false"/>.
 606        /// </returns>
 607        private bool IsSessionConnected()
 1816608        {
 1816609            var session = Session;
 1816610            return session != null && session.IsConnected;
 1816611        }
 612    }
 613}