< Summary

Information
Class: Renci.SshNet.SshCommand
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\SshCommand.cs
Line coverage
81%
Covered lines: 230
Uncovered lines: 53
Coverable lines: 283
Total lines: 619
Line coverage: 81.2%
Branch coverage
71%
Covered branches: 66
Total branches: 92
Branch coverage: 71.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
.ctor(...)50%670%
get_CommandText()100%1100%
get_CommandTimeout()100%1100%
get_ExitStatus()100%1100%
get_OutputStream()100%1100%
get_ExtendedOutputStream()100%1100%
get_Result()100%6100%
get_Error()87.5%8100%
BeginExecute()100%1100%
BeginExecute(...)100%10%
BeginExecute(...)83.33%1289.47%
BeginExecute(...)100%10%
EndExecute(...)87.5%8100%
CancelAsync()0%60%
Execute()100%1100%
Execute(...)100%1100%
CreateChannel()100%1100%
Session_Disconnected(...)0%20%
Session_ErrorOccured(...)0%20%
Channel_Closed(...)66.66%6100%
Channel_RequestReceived(...)33.33%638.88%
Channel_ExtendedDataReceived(...)100%4100%
Channel_DataReceived(...)100%4100%
WaitOnHandle(...)66.66%678.57%
UnsubscribeFromEventsAndDisposeChannel(...)50%280%
Dispose()100%1100%
Dispose(...)100%14100%
Finalize()100%1100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.IO;
 4using System.Runtime.ExceptionServices;
 5using System.Text;
 6using System.Threading;
 7
 8using Renci.SshNet.Abstractions;
 9using Renci.SshNet.Channels;
 10using Renci.SshNet.Common;
 11using Renci.SshNet.Messages.Connection;
 12using Renci.SshNet.Messages.Transport;
 13
 14namespace Renci.SshNet
 15{
 16    /// <summary>
 17    /// Represents SSH command that can be executed.
 18    /// </summary>
 19    public class SshCommand : IDisposable
 20    {
 21        private readonly Encoding _encoding;
 78422        private readonly object _endExecuteLock = new object();
 23
 24        private ISession _session;
 25        private IChannelSession _channel;
 26        private CommandAsyncResult _asyncResult;
 27        private AsyncCallback _callback;
 28        private EventWaitHandle _sessionErrorOccuredWaitHandle;
 29        private Exception _exception;
 30        private StringBuilder _result;
 31        private StringBuilder _error;
 32        private bool _hasError;
 33        private bool _isDisposed;
 34
 35        /// <summary>
 36        /// Gets the command text.
 37        /// </summary>
 235638        public string CommandText { get; private set; }
 39
 40        /// <summary>
 41        /// Gets or sets the command timeout.
 42        /// </summary>
 43        /// <value>
 44        /// The command timeout.
 45        /// </value>
 46        /// <example>
 47        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 48        /// </example>
 153449        public TimeSpan CommandTimeout { get; set; }
 50
 51        /// <summary>
 52        /// Gets the command exit status.
 53        /// </summary>
 54        /// <example>
 55        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunComm
 56        /// </example>
 110257        public int ExitStatus { get; private set; }
 58
 59        /// <summary>
 60        /// Gets the output stream.
 61        /// </summary>
 62        /// <example>
 63        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 64        /// </example>
 65#pragma warning disable CA1859 // Use concrete types when possible for improved performance
 550766        public Stream OutputStream { get; private set; }
 67#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 68
 69        /// <summary>
 70        /// Gets the extended output stream.
 71        /// </summary>
 72        /// <example>
 73        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 74        /// </example>
 75#pragma warning disable CA1859 // Use concrete types when possible for improved performance
 286476        public Stream ExtendedOutputStream { get; private set; }
 77#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 78
 79        /// <summary>
 80        /// Gets the command execution result.
 81        /// </summary>
 82        /// <example>
 83        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand RunComm
 84        /// </example>
 85        public string Result
 86        {
 87            get
 75988            {
 75989                _result ??= new StringBuilder();
 90
 75991                if (OutputStream != null && OutputStream.Length > 0)
 34392                {
 34393                    using (var sr = new StreamReader(OutputStream,
 34394                                                     _encoding,
 34395                                                     detectEncodingFromByteOrderMarks: true,
 34396                                                     bufferSize: 1024,
 34397                                                     leaveOpen: true))
 34398                    {
 34399                        _ = _result.Append(sr.ReadToEnd());
 343100                    }
 343101                }
 102
 759103                return _result.ToString();
 759104            }
 105        }
 106
 107        /// <summary>
 108        /// Gets the command execution error.
 109        /// </summary>
 110        /// <example>
 111        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 112        /// </example>
 113        public string Error
 114        {
 115            get
 62116            {
 62117                if (_hasError)
 22118                {
 22119                    _error ??= new StringBuilder();
 120
 22121                    if (ExtendedOutputStream != null && ExtendedOutputStream.Length > 0)
 21122                    {
 21123                        using (var sr = new StreamReader(ExtendedOutputStream,
 21124                                                         _encoding,
 21125                                                         detectEncodingFromByteOrderMarks: true,
 21126                                                         bufferSize: 1024,
 21127                                                         leaveOpen: true))
 21128                        {
 21129                            _ = _error.Append(sr.ReadToEnd());
 21130                        }
 21131                    }
 132
 22133                    return _error.ToString();
 134                }
 135
 40136                return string.Empty;
 62137            }
 138        }
 139
 140        /// <summary>
 141        /// Initializes a new instance of the <see cref="SshCommand"/> class.
 142        /// </summary>
 143        /// <param name="session">The session.</param>
 144        /// <param name="commandText">The command text.</param>
 145        /// <param name="encoding">The encoding to use for the results.</param>
 146        /// <exception cref="ArgumentNullException">Either <paramref name="session"/>, <paramref name="commandText"/> is
 784147        internal SshCommand(ISession session, string commandText, Encoding encoding)
 784148        {
 784149            if (session is null)
 0150            {
 0151                throw new ArgumentNullException(nameof(session));
 152            }
 153
 784154            if (commandText is null)
 0155            {
 0156                throw new ArgumentNullException(nameof(commandText));
 157            }
 158
 784159            if (encoding is null)
 0160            {
 0161                throw new ArgumentNullException(nameof(encoding));
 162            }
 163
 784164            _session = session;
 784165            CommandText = commandText;
 784166            _encoding = encoding;
 784167            CommandTimeout = Session.InfiniteTimeSpan;
 784168            _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false);
 169
 784170            _session.Disconnected += Session_Disconnected;
 784171            _session.ErrorOccured += Session_ErrorOccured;
 784172        }
 173
 174        /// <summary>
 175        /// Begins an asynchronous command execution.
 176        /// </summary>
 177        /// <returns>
 178        /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pend
 179        /// </returns>
 180        /// <example>
 181        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 182        /// </example>
 183        /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
 184        /// <exception cref="SshException">Invalid operation.</exception>
 185        /// <exception cref="ArgumentException">CommandText property is empty.</exception>
 186        /// <exception cref="SshConnectionException">Client is not connected.</exception>
 187        /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
 188        public IAsyncResult BeginExecute()
 77189        {
 77190            return BeginExecute(callback: null, state: null);
 74191        }
 192
 193        /// <summary>
 194        /// Begins an asynchronous command execution.
 195        /// </summary>
 196        /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complet
 197        /// <returns>
 198        /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pend
 199        /// </returns>
 200        /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
 201        /// <exception cref="SshException">Invalid operation.</exception>
 202        /// <exception cref="ArgumentException">CommandText property is empty.</exception>
 203        /// <exception cref="SshConnectionException">Client is not connected.</exception>
 204        /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
 205        public IAsyncResult BeginExecute(AsyncCallback callback)
 0206        {
 0207            return BeginExecute(callback, state: null);
 0208        }
 209
 210        /// <summary>
 211        /// Begins an asynchronous command execution.
 212        /// </summary>
 213        /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complet
 214        /// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from
 215        /// <returns>
 216        /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pend
 217        /// </returns>
 218        /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
 219        /// <exception cref="SshException">Invalid operation.</exception>
 220        /// <exception cref="ArgumentException">CommandText property is empty.</exception>
 221        /// <exception cref="SshConnectionException">Client is not connected.</exception>
 222        /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
 223#pragma warning disable CA1859 // Use concrete types when possible for improved performance
 224        public IAsyncResult BeginExecute(AsyncCallback callback, object state)
 225#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 788226        {
 227            // Prevent from executing BeginExecute before calling EndExecute
 788228            if (_asyncResult != null && !_asyncResult.EndCalled)
 3229            {
 3230                throw new InvalidOperationException("Asynchronous operation is already in progress.");
 231            }
 232
 233            // Create new AsyncResult object
 785234            _asyncResult = new CommandAsyncResult
 785235                {
 785236                    AsyncWaitHandle = new ManualResetEvent(initialState: false),
 785237                    IsCompleted = false,
 785238                    AsyncState = state,
 785239                };
 240
 241            // When command re-executed again, create a new channel
 785242            if (_channel is not null)
 0243            {
 0244                throw new SshException("Invalid operation.");
 245            }
 246
 785247            if (string.IsNullOrEmpty(CommandText))
 0248            {
 0249                throw new ArgumentException("CommandText property is empty.");
 250            }
 251
 785252            var outputStream = OutputStream;
 785253            if (outputStream is not null)
 7254            {
 7255                outputStream.Dispose();
 7256                OutputStream = null;
 7257            }
 258
 785259            var extendedOutputStream = ExtendedOutputStream;
 785260            if (extendedOutputStream is not null)
 7261            {
 7262                extendedOutputStream.Dispose();
 7263                ExtendedOutputStream = null;
 7264            }
 265
 266            // Initialize output streams
 785267            OutputStream = new PipeStream();
 785268            ExtendedOutputStream = new PipeStream();
 269
 785270            _result = null;
 785271            _error = null;
 785272            _callback = callback;
 273
 785274            _channel = CreateChannel();
 785275            _channel.Open();
 785276            _ = _channel.SendExecRequest(CommandText);
 277
 785278            return _asyncResult;
 785279        }
 280
 281        /// <summary>
 282        /// Begins an asynchronous command execution.
 283        /// </summary>
 284        /// <param name="commandText">The command text.</param>
 285        /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complet
 286        /// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from
 287        /// <returns>
 288        /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pend
 289        /// </returns>
 290        /// <exception cref="SshConnectionException">Client is not connected.</exception>
 291        /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
 292        public IAsyncResult BeginExecute(string commandText, AsyncCallback callback, object state)
 0293        {
 0294            CommandText = commandText;
 295
 0296            return BeginExecute(callback, state);
 0297        }
 298
 299        /// <summary>
 300        /// Waits for the pending asynchronous command execution to complete.
 301        /// </summary>
 302        /// <param name="asyncResult">The reference to the pending asynchronous request to finish.</param>
 303        /// <returns>Command execution result.</returns>
 304        /// <example>
 305        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 306        /// </example>
 307        /// <exception cref="ArgumentException">Either the IAsyncResult object did not come from the corresponding async
 308        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception
 309        public string EndExecute(IAsyncResult asyncResult)
 759310        {
 759311            if (asyncResult is null)
 4312            {
 4313                throw new ArgumentNullException(nameof(asyncResult));
 314            }
 315
 755316            if (asyncResult is not CommandAsyncResult commandAsyncResult || _asyncResult != commandAsyncResult)
 3317            {
 3318                throw new ArgumentException(string.Format("The {0} object was not returned from the corresponding asynch
 319            }
 320
 752321            lock (_endExecuteLock)
 752322            {
 752323                if (commandAsyncResult.EndCalled)
 3324                {
 3325                    throw new ArgumentException("EndExecute can only be called once for each asynchronous operation.");
 326                }
 327
 328                // wait for operation to complete (or time out)
 749329                WaitOnHandle(_asyncResult.AsyncWaitHandle);
 330
 748331                UnsubscribeFromEventsAndDisposeChannel(_channel);
 748332                _channel = null;
 333
 748334                commandAsyncResult.EndCalled = true;
 335
 748336                return Result;
 337            }
 748338        }
 339
 340        /// <summary>
 341        /// Cancels command execution in asynchronous scenarios.
 342        /// </summary>
 343        public void CancelAsync()
 0344        {
 0345            if (_channel is not null && _channel.IsOpen && _asyncResult is not null)
 0346            {
 347                // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ?
 0348                _channel.Dispose();
 0349            }
 0350        }
 351
 352        /// <summary>
 353        /// Executes command specified by <see cref="CommandText"/> property.
 354        /// </summary>
 355        /// <returns>
 356        /// Command execution result.
 357        /// </returns>
 358        /// <example>
 359        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 360        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 361        ///     <code source="..\..\src\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateC
 362        /// </example>
 363        /// <exception cref="SshConnectionException">Client is not connected.</exception>
 364        /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
 365        public string Execute()
 707366        {
 707367            return EndExecute(BeginExecute(callback: null, state: null));
 706368        }
 369
 370        /// <summary>
 371        /// Executes the specified command text.
 372        /// </summary>
 373        /// <param name="commandText">The command text.</param>
 374        /// <returns>
 375        /// The result of the command execution.
 376        /// </returns>
 377        /// <exception cref="SshConnectionException">Client is not connected.</exception>
 378        /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
 379        public string Execute(string commandText)
 1380        {
 1381            CommandText = commandText;
 382
 1383            return Execute();
 1384        }
 385
 386        private IChannelSession CreateChannel()
 785387        {
 785388            var channel = _session.CreateChannelSession();
 785389            channel.DataReceived += Channel_DataReceived;
 785390            channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
 785391            channel.RequestReceived += Channel_RequestReceived;
 785392            channel.Closed += Channel_Closed;
 785393            return channel;
 785394        }
 395
 396        private void Session_Disconnected(object sender, EventArgs e)
 0397        {
 398            // If objected is disposed or being disposed don't handle this event
 0399            if (_isDisposed)
 0400            {
 0401                return;
 402            }
 403
 0404            _exception = new SshConnectionException("An established connection was aborted by the software in your host 
 405
 0406            _ = _sessionErrorOccuredWaitHandle.Set();
 0407        }
 408
 409        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
 0410        {
 411            // If objected is disposed or being disposed don't handle this event
 0412            if (_isDisposed)
 0413            {
 0414                return;
 415            }
 416
 0417            _exception = e.Exception;
 418
 0419            _ = _sessionErrorOccuredWaitHandle.Set();
 0420        }
 421
 422        private void Channel_Closed(object sender, ChannelEventArgs e)
 748423        {
 748424            OutputStream?.Flush();
 748425            ExtendedOutputStream?.Flush();
 426
 748427            _asyncResult.IsCompleted = true;
 428
 748429            if (_callback is not null)
 2430            {
 431                // Execute callback on different thread
 4432                ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult));
 2433            }
 434
 748435            _ = ((EventWaitHandle) _asyncResult.AsyncWaitHandle).Set();
 748436        }
 437
 438        private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e)
 739439        {
 739440            if (e.Info is ExitStatusRequestInfo exitStatusInfo)
 739441            {
 739442                ExitStatus = (int) exitStatusInfo.ExitStatus;
 443
 739444                if (exitStatusInfo.WantReply)
 0445                {
 0446                    var replyMessage = new ChannelSuccessMessage(_channel.LocalChannelNumber);
 0447                    _session.SendMessage(replyMessage);
 0448                }
 739449            }
 450            else
 0451            {
 0452                if (e.Info.WantReply)
 0453                {
 0454                    var replyMessage = new ChannelFailureMessage(_channel.LocalChannelNumber);
 0455                    _session.SendMessage(replyMessage);
 0456                }
 0457            }
 739458        }
 459
 460        private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e)
 88461        {
 88462            if (ExtendedOutputStream != null)
 88463            {
 88464                ExtendedOutputStream.Write(e.Data, 0, e.Data.Length);
 88465                ExtendedOutputStream.Flush();
 88466            }
 467
 88468            if (e.DataTypeCode == 1)
 40469            {
 40470                _hasError = true;
 40471            }
 88472        }
 473
 474        private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
 371475        {
 371476            if (OutputStream != null)
 371477            {
 371478                OutputStream.Write(e.Data, 0, e.Data.Length);
 371479                OutputStream.Flush();
 371480            }
 481
 371482            if (_asyncResult != null)
 371483            {
 371484                lock (_asyncResult)
 371485                {
 371486                    _asyncResult.BytesReceived += e.Data.Length;
 371487                }
 371488            }
 371489        }
 490
 491        /// <exception cref="SshOperationTimeoutException">Command '{0}' has timed out.</exception>
 492        /// <remarks>The actual command will be included in the exception message.</remarks>
 493        private void WaitOnHandle(WaitHandle waitHandle)
 749494        {
 749495            var waitHandles = new[]
 749496                {
 749497                    _sessionErrorOccuredWaitHandle,
 749498                    waitHandle
 749499                };
 500
 749501            var signaledElement = WaitHandle.WaitAny(waitHandles, CommandTimeout);
 749502            switch (signaledElement)
 503            {
 504                case 0:
 0505                    ExceptionDispatchInfo.Capture(_exception).Throw();
 0506                    break;
 507                case 1:
 508                    // Specified waithandle was signaled
 748509                    break;
 510                case WaitHandle.WaitTimeout:
 1511                    throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has 
 512                default:
 0513                    throw new SshException($"Unexpected element '{signaledElement.ToString(CultureInfo.InvariantCulture)
 514            }
 748515        }
 516
 517        /// <summary>
 518        /// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
 519        /// the <see cref="IChannel"/>.
 520        /// </summary>
 521        /// <param name="channel">The channel.</param>
 522        /// <remarks>
 523        /// Does nothing when <paramref name="channel"/> is <see langword="null"/>.
 524        /// </remarks>
 525        private void UnsubscribeFromEventsAndDisposeChannel(IChannel channel)
 772526        {
 772527            if (channel is null)
 0528            {
 0529                return;
 530            }
 531
 532            // unsubscribe from events as we do not want to be signaled should these get fired
 533            // during the dispose of the channel
 772534            channel.DataReceived -= Channel_DataReceived;
 772535            channel.ExtendedDataReceived -= Channel_ExtendedDataReceived;
 772536            channel.RequestReceived -= Channel_RequestReceived;
 772537            channel.Closed -= Channel_Closed;
 538
 539            // actually dispose the channel
 772540            channel.Dispose();
 772541        }
 542
 543        /// <summary>
 544        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 545        /// </summary>
 546        public void Dispose()
 92547        {
 92548            Dispose(disposing: true);
 92549            GC.SuppressFinalize(this);
 92550        }
 551
 552        /// <summary>
 553        /// Releases unmanaged and - optionally - managed resources.
 554        /// </summary>
 555        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langwor
 556        protected virtual void Dispose(bool disposing)
 677557        {
 677558            if (_isDisposed)
 3559            {
 3560                return;
 561            }
 562
 674563            if (disposing)
 89564            {
 565                // unsubscribe from session events to ensure other objects that we're going to dispose
 566                // are not accessed while disposing
 89567                var session = _session;
 89568                if (session != null)
 89569                {
 89570                    session.Disconnected -= Session_Disconnected;
 89571                    session.ErrorOccured -= Session_ErrorOccured;
 89572                    _session = null;
 89573                }
 574
 575                // unsubscribe from channel events to ensure other objects that we're going to dispose
 576                // are not accessed while disposing
 89577                var channel = _channel;
 89578                if (channel != null)
 24579                {
 24580                    UnsubscribeFromEventsAndDisposeChannel(channel);
 24581                    _channel = null;
 24582                }
 583
 89584                var outputStream = OutputStream;
 89585                if (outputStream != null)
 89586                {
 89587                    outputStream.Dispose();
 89588                    OutputStream = null;
 89589                }
 590
 89591                var extendedOutputStream = ExtendedOutputStream;
 89592                if (extendedOutputStream != null)
 89593                {
 89594                    extendedOutputStream.Dispose();
 89595                    ExtendedOutputStream = null;
 89596                }
 597
 89598                var sessionErrorOccuredWaitHandle = _sessionErrorOccuredWaitHandle;
 89599                if (sessionErrorOccuredWaitHandle != null)
 89600                {
 89601                    sessionErrorOccuredWaitHandle.Dispose();
 89602                    _sessionErrorOccuredWaitHandle = null;
 89603                }
 604
 89605                _isDisposed = true;
 89606            }
 677607        }
 608
 609        /// <summary>
 610        /// Finalizes an instance of the <see cref="SshCommand"/> class.
 611        /// Releases unmanaged resources and performs other cleanup operations before the
 612        /// <see cref="SshCommand"/> is reclaimed by garbage collection.
 613        /// </summary>
 614        ~SshCommand()
 1170615        {
 585616            Dispose(disposing: false);
 1170617        }
 618    }
 619}