Bläddra i källkod

Refactor to move indication whether EndExecute has been invoked for a given IAsyncResult to the CommandAsyncResult class.
Revert unsubscribe from session events when finalizing SshCommand.
Do not create reply message for ExitStatusRequestInfo unless requested.
Modify PipeStream member to throw ObjectDisposedException when stream has been disposed.

drieseng 9 år sedan
förälder
incheckning
d48bb74aef

+ 22 - 1
src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

@@ -1091,6 +1091,27 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest.cs">
       <Link>Classes\SshCommandTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_BeginExecute_EndExecuteInvokedOnAsyncResultFromPreviousInvocation.cs">
+      <Link>Classes\SshCommandTest_BeginExecute_EndExecuteInvokedOnAsyncResultFromPreviousInvocation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_BeginExecute_EndExecuteNotInvokedOnAsyncResultFromPreviousInvocation.cs">
+      <Link>Classes\SshCommandTest_BeginExecute_EndExecuteNotInvokedOnAsyncResultFromPreviousInvocation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_Dispose.cs">
+      <Link>Classes\SshCommandTest_Dispose.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_EndExecute.cs">
+      <Link>Classes\SshCommandTest_EndExecute.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_EndExecute_AsyncResultFromOtherInstance.cs">
+      <Link>Classes\SshCommandTest_EndExecute_AsyncResultFromOtherInstance.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_EndExecute_AsyncResultIsNull.cs">
+      <Link>Classes\SshCommandTest_EndExecute_AsyncResultIsNull.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\SshCommandTest_EndExecute_ChannelOpen.cs">
+      <Link>Classes\SshCommandTest_EndExecute_ChannelOpen.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\SubsystemSessionStub.cs">
       <Link>Classes\SubsystemSessionStub.cs</Link>
     </Compile>
@@ -1250,7 +1271,7 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
+      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 211 - 0
src/Renci.SshNet.Tests/Classes/PipeStreamTest_Dispose.cs

@@ -0,0 +1,211 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Renci.SshNet.Common;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class PipeStreamTest_Dispose : TestBase
+    {
+        private PipeStream _pipeStream;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            _pipeStream = new PipeStream();
+        }
+
+        private void Act()
+        {
+            _pipeStream.Dispose();
+        }
+
+        [TestMethod]
+        public void BlockLastReadBuffer_Getter_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var value = _pipeStream.BlockLastReadBuffer;
+                Assert.Fail("" + value);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void BlockLastReadBuffer_Setter_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _pipeStream.BlockLastReadBuffer = true;
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void CanRead_ShouldReturnTrue()
+        {
+            Assert.IsFalse(_pipeStream.CanRead);
+        }
+
+        [TestMethod]
+        public void Flush_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _pipeStream.Flush();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void MaxBufferLength_Getter_ShouldReturnTwoHundredMegabyte()
+        {
+            Assert.AreEqual(200 * 1024 * 1024, _pipeStream.MaxBufferLength);
+        }
+
+        [TestMethod]
+        public void MaxBufferLength_Setter_ShouldModifyMaxBufferLength()
+        {
+            var newValue = new Random().Next(1, int.MaxValue);
+            _pipeStream.MaxBufferLength = newValue;
+            Assert.AreEqual(newValue, _pipeStream.MaxBufferLength);
+        }
+
+        [TestMethod]
+        public void Length_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var value = _pipeStream.Length;
+                Assert.Fail("" + value);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Position_Getter_ShouldReturnZero()
+        {
+            Assert.AreEqual(0, _pipeStream.Position);
+        }
+
+        [TestMethod]
+        public void Position_Setter_ShouldThrowNotSupportedException()
+        {
+            try
+            {
+                _pipeStream.Position = 0;
+                Assert.Fail();
+            }
+            catch (NotSupportedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Read_ByteArrayAndOffsetAndCount_ShouldThrowObjectDisposedException()
+        {
+            var buffer = new byte[0];
+            const int offset = 0;
+            const int count = 0;
+
+            try
+            {
+                _pipeStream.Read(buffer, offset, count);
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void ReadByte_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _pipeStream.ReadByte();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Seek_ShouldThrowNotSupportedException()
+        {
+            try
+            {
+                _pipeStream.Seek(0, SeekOrigin.Begin);
+                Assert.Fail();
+            }
+            catch (NotSupportedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void SetLength_ShouldThrowNotSupportedException()
+        {
+            try
+            {
+                _pipeStream.SetLength(0);
+                Assert.Fail();
+            }
+            catch (NotSupportedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Write_ByteArrayAndOffsetAndCount_ShouldThrowObjectDisposedException()
+        {
+            var buffer = new byte[0];
+            const int offset = 0;
+            const int count = 0;
+
+            try
+            {
+                _pipeStream.Write(buffer, offset, count);
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void WriteByte_ShouldThrowObjectDisposedException()
+        {
+            const byte b = 0x0a;
+
+            try
+            {
+                _pipeStream.WriteByte(b);
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+    }
+}

+ 0 - 33
src/Renci.SshNet.Tests/Classes/SshCommandTest.cs

@@ -533,39 +533,6 @@ namespace Renci.SshNet.Tests.Classes
             Assert.Inconclusive("A method that does not return a value cannot be verified.");
         }
 
-        /// <summary>
-        ///A test for Dispose
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void DisposeTest()
-        {
-            Session session = null; // TODO: Initialize to an appropriate value
-            string commandText = string.Empty; // TODO: Initialize to an appropriate value
-            var encoding = Encoding.UTF8;
-            SshCommand target = new SshCommand(session, commandText, encoding); // TODO: Initialize to an appropriate value
-            target.Dispose();
-            Assert.Inconclusive("A method that does not return a value cannot be verified.");
-        }
-
-        /// <summary>
-        ///A test for EndExecute
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void EndExecuteTest()
-        {
-            Session session = null; // TODO: Initialize to an appropriate value
-            string commandText = string.Empty; // TODO: Initialize to an appropriate value
-            var encoding = Encoding.UTF8;
-            SshCommand target = new SshCommand(session, commandText, encoding); // TODO: Initialize to an appropriate value
-            IAsyncResult asyncResult = null; // TODO: Initialize to an appropriate value
-            string expected = string.Empty; // TODO: Initialize to an appropriate value
-            string actual;
-            actual = target.EndExecute(asyncResult);
-            Assert.AreEqual(expected, actual);
-            Assert.Inconclusive("Verify the correctness of this test method.");
-        }
 
         /// <summary>
         ///A test for Execute

+ 75 - 0
src/Renci.SshNet.Tests/Classes/SshCommandTest_BeginExecute_EndExecuteInvokedOnAsyncResultFromPreviousInvocation.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Globalization;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Common;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class SshCommandTest_BeginExecute_EndExecuteInvokedOnAsyncResultFromPreviousInvocation : TestBase
+    {
+        private Mock<ISession> _sessionMock;
+        private Mock<IChannelSession> _channelSessionAMock;
+        private Mock<IChannelSession> _channelSessionBMock;
+        private string _commandText;
+        private Encoding _encoding;
+        private SshCommand _sshCommand;
+        private IAsyncResult _asyncResultA;
+        private IAsyncResult _asyncResultB;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            var random = new Random();
+
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _channelSessionAMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _channelSessionBMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _commandText = random.Next().ToString(CultureInfo.InvariantCulture);
+            _encoding = Encoding.UTF8;
+            _asyncResultA = null;
+            _asyncResultB = null;
+
+            var seq = new MockSequence();
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionAMock.Object);
+            _channelSessionAMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionAMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText))
+                .Returns(true)
+                .Raises(c => c.Closed += null, new ChannelEventArgs(5));
+            _channelSessionAMock.InSequence(seq).Setup(p => p.IsOpen).Returns(true);
+            _channelSessionAMock.InSequence(seq).Setup(p => p.Close());
+            _channelSessionAMock.InSequence(seq).Setup(p => p.Dispose());
+
+            _sshCommand = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+            _asyncResultA = _sshCommand.BeginExecute();
+            _sshCommand.EndExecute(_asyncResultA);
+
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionBMock.Object);
+            _channelSessionBMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionBMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText)).Returns(true);
+        }
+
+        private void Act()
+        {
+            _asyncResultB = _sshCommand.BeginExecute();
+        }
+
+        [TestMethod]
+        public void BeginExecuteShouldReturnNewAsyncResult()
+        {
+            Assert.IsNotNull(_asyncResultB);
+            Assert.AreNotSame(_asyncResultA, _asyncResultB);
+        }
+    }
+}

+ 70 - 0
src/Renci.SshNet.Tests/Classes/SshCommandTest_BeginExecute_EndExecuteNotInvokedOnAsyncResultFromPreviousInvocation.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Globalization;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class SshCommandTest_BeginExecute_EndExecuteNotInvokedOnAsyncResultFromPreviousInvocation : TestBase
+    {
+        private Mock<ISession> _sessionMock;
+        private Mock<IChannelSession> _channelSessionMock;
+        private string _commandText;
+        private Encoding _encoding;
+        private SshCommand _sshCommand;
+        private IAsyncResult _asyncResult;
+        private InvalidOperationException _actualException;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            var random = new Random();
+
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _commandText = random.Next().ToString(CultureInfo.InvariantCulture);
+            _encoding = Encoding.UTF8;
+            _asyncResult = null;
+
+            var seq = new MockSequence();
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
+            _channelSessionMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText));
+
+            _sshCommand = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+            _sshCommand.BeginExecute();
+        }
+
+        private void Act()
+        {
+            try
+            {
+                _sshCommand.BeginExecute();
+                Assert.Fail();
+            }
+            catch (InvalidOperationException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void BeginExecuteShouldThrowInvalidOperationException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Asynchronous operation is already in progress.", _actualException.Message);
+        }
+    }
+}

+ 120 - 0
src/Renci.SshNet.Tests/Classes/SshCommandTest_Dispose.cs

@@ -0,0 +1,120 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Common;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class SshCommandTest_Dispose : TestBase
+    {
+        private Mock<ISession> _sessionMock;
+        private Mock<IChannelSession> _channelSessionMock;
+        private string _commandText;
+        private Encoding _encoding;
+        private SshCommand _sshCommand;
+        private Stream _outputStream;
+        private Stream _extendedOutputStream;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _commandText = new Random().Next().ToString(CultureInfo.InvariantCulture);
+            _encoding = Encoding.UTF8;
+            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
+
+            var seq = new MockSequence();
+
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
+            _channelSessionMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText)).Returns(true);
+            _channelSessionMock.InSequence(seq).Setup(p => p.Dispose());
+
+            _sshCommand = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+            _sshCommand.BeginExecute();
+
+            _outputStream = _sshCommand.OutputStream;
+            _extendedOutputStream = _sshCommand.ExtendedOutputStream;
+        }
+
+        private void Act()
+        {
+            _sshCommand.Dispose();
+        }
+
+        [TestMethod]
+        public void ChannelSessionShouldBeDisposedOnce()
+        {
+            _channelSessionMock.Verify(p => p.Dispose(), Times.Once);
+        }
+
+        [TestMethod]
+        public void OutputStreamShouldReturnNull()
+        {
+            Assert.IsNull(_sshCommand.OutputStream);
+        }
+
+        [TestMethod]
+        public void OutputStreamShouldHaveBeenDisposed()
+        {
+            try
+            {
+                _outputStream.ReadByte();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void ExtendedOutputStreamShouldReturnNull()
+        {
+            Assert.IsNull(_sshCommand.ExtendedOutputStream);
+        }
+
+        [TestMethod]
+        public void ExtendedOutputStreamShouldHaveBeenDisposed()
+        {
+            try
+            {
+                _extendedOutputStream.ReadByte();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void RaisingDisconnectedOnSessionShouldDoNothing()
+        {
+            _sessionMock.Raise(s => s.Disconnected += null, new EventArgs());
+        }
+
+        [TestMethod]
+        public void RaisingErrorOccuredOnSessionShouldDoNothing()
+        {
+            _sessionMock.Raise(s => s.ErrorOccured += null, new ExceptionEventArgs(new Exception()));
+        }
+
+        [TestMethod]
+        public void InvokingDisposeAgainShouldNotRaiseAnException()
+        {
+            _sshCommand.Dispose();
+        }
+    }
+}

+ 78 - 0
src/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_AsyncResultFromOtherInstance.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Globalization;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class SshCommandTest_EndExecute_AsyncResultFromOtherInstance : TestBase
+    {
+        private Mock<ISession> _sessionMock;
+        private Mock<IChannelSession> _channelSessionAMock;
+        private Mock<IChannelSession> _channelSessionBMock;
+        private string _commandText;
+        private Encoding _encoding;
+        private SshCommand _sshCommandA;
+        private SshCommand _sshCommandB;
+        private ArgumentException _actualException;
+        private IAsyncResult _asyncResultB;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _channelSessionAMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _channelSessionBMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _commandText = new Random().Next().ToString(CultureInfo.InvariantCulture);
+            _encoding = Encoding.UTF8;
+            _asyncResultB = null;
+
+            var seq = new MockSequence();
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionAMock.Object);
+            _channelSessionAMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionAMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText)).Returns(true);
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionBMock.Object);
+            _channelSessionBMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionBMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText)).Returns(true);
+
+            _sshCommandA = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+            _sshCommandA.BeginExecute();
+
+            _sshCommandB = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+            _asyncResultB = _sshCommandB.BeginExecute();
+        }
+
+        private void Act()
+        {
+            try
+            {
+                _sshCommandA.EndExecute(_asyncResultB);
+                Assert.Fail();
+            }
+            catch (ArgumentException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void EndExecuteShouldHaveThrownArgumentException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", typeof(IAsyncResult).Name), _actualException.Message);
+            Assert.IsNull(_actualException.ParamName);
+        }
+    }
+}

+ 59 - 0
src/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_AsyncResultIsNull.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Globalization;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class SshCommandTest_EndExecute_AsyncResultIsNull : TestBase
+    {
+        private Mock<ISession> _sessionMock;
+        private string _commandText;
+        private Encoding _encoding;
+        private SshCommand _sshCommand;
+        private IAsyncResult _asyncResult;
+        private ArgumentNullException _actualException;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _commandText = new Random().Next().ToString(CultureInfo.InvariantCulture);
+            _encoding = Encoding.UTF8;
+            _asyncResult = null;
+
+            _sshCommand = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+        }
+
+        private void Act()
+        {
+            try
+            {
+                _sshCommand.EndExecute(_asyncResult);
+                Assert.Fail();
+            }
+            catch (ArgumentNullException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void EndExecuteShouldHaveThrownArgumentNullException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("asyncResult", _actualException.ParamName);
+        }
+    }
+}

+ 158 - 0
src/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_ChannelOpen.cs

@@ -0,0 +1,158 @@
+using System;
+using System.Globalization;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Common;
+using Renci.SshNet.Messages.Connection;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class SshCommandTest_EndExecute_ChannelOpen : TestBase
+    {
+        private Mock<ISession> _sessionMock;
+        private Mock<IChannelSession> _channelSessionMock;
+        private string _commandText;
+        private Encoding _encoding;
+        private SshCommand _sshCommand;
+        private IAsyncResult _asyncResult;
+        private string _actual;
+        private string _dataA;
+        private string _dataB;
+        private string _extendedDataA;
+        private string _extendedDataB;
+        private int _expectedExitStatus;
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            var random = new Random();
+
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _commandText = random.Next().ToString(CultureInfo.InvariantCulture);
+            _encoding = Encoding.UTF8;
+            _expectedExitStatus = random.Next();
+            _dataA = random.Next().ToString(CultureInfo.InvariantCulture);
+            _dataB = random.Next().ToString(CultureInfo.InvariantCulture);
+            _extendedDataA = random.Next().ToString(CultureInfo.InvariantCulture);
+            _extendedDataB = random.Next().ToString(CultureInfo.InvariantCulture);
+            _asyncResult = null;
+
+            var seq = new MockSequence();
+            _sessionMock.InSequence(seq).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
+            _channelSessionMock.InSequence(seq).Setup(p => p.Open());
+            _channelSessionMock.InSequence(seq).Setup(p => p.SendExecRequest(_commandText))
+                .Returns(true)
+                .Raises(c => c.Closed += null, new ChannelEventArgs(5));
+            _channelSessionMock.InSequence(seq).Setup(p => p.IsOpen).Returns(true);
+            _channelSessionMock.InSequence(seq).Setup(p => p.Close());
+            _channelSessionMock.InSequence(seq).Setup(p => p.Dispose());
+
+            _sshCommand = new SshCommand(_sessionMock.Object, _commandText, _encoding);
+            _asyncResult = _sshCommand.BeginExecute();
+
+            _channelSessionMock.Raise(c => c.DataReceived += null,
+                new ChannelDataEventArgs(0, _encoding.GetBytes(_dataA)));
+            _channelSessionMock.Raise(c => c.ExtendedDataReceived += null,
+                new ChannelExtendedDataEventArgs(0, _encoding.GetBytes(_extendedDataA), 0));
+            _channelSessionMock.Raise(c => c.DataReceived += null,
+                new ChannelDataEventArgs(0, _encoding.GetBytes(_dataB)));
+            _channelSessionMock.Raise(c => c.ExtendedDataReceived += null,
+                new ChannelExtendedDataEventArgs(0, _encoding.GetBytes(_extendedDataB), 0));
+            _channelSessionMock.Raise(c => c.RequestReceived += null,
+                new ChannelRequestEventArgs(new ExitStatusRequestInfo((uint) _expectedExitStatus)));
+        }
+
+        private void Act()
+        {
+            _actual = _sshCommand.EndExecute(_asyncResult);
+        }
+
+        [TestMethod]
+        public void ChannelSessionShouldBeClosedOnce()
+        {
+            _channelSessionMock.Verify(p => p.Close(), Times.Once);
+        }
+
+        [TestMethod]
+        public void ChannelSessionShouldBeDisposedOnce()
+        {
+            _channelSessionMock.Verify(p => p.Dispose(), Times.Once);
+        }
+
+        [TestMethod]
+        public void EndExecuteShouldReturnAllDataReceivedInSpecifiedEncoding()
+        {
+            Assert.AreEqual(string.Concat(_dataA, _dataB), _actual);
+        }
+
+        [TestMethod]
+        public void EndExecuteShouldThrowArgumentExceptionWhenInvokedAgainWithSameAsyncResult()
+        {
+            try
+            {
+                _sshCommand.EndExecute(_asyncResult);
+                Assert.Fail();
+            }
+            catch (ArgumentException ex)
+            {
+                Assert.AreEqual(typeof (ArgumentException), ex.GetType());
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("EndExecute can only be called once for each asynchronous operation.", ex.Message);
+                Assert.IsNull(ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void ErrorShouldReturnZeroLengthString()
+        {
+            Assert.AreEqual(string.Empty, _sshCommand.Error);
+        }
+
+        [TestMethod]
+        public void ExitStatusShouldReturnExitStatusFromExitStatusRequestInfo()
+        {
+            Assert.AreEqual(_expectedExitStatus, _sshCommand.ExitStatus);
+        }
+
+        [TestMethod]
+        public void ExtendedOutputStreamShouldContainAllExtendedDataReceived()
+        {
+            var extendedDataABytes = _encoding.GetBytes(_extendedDataA);
+            var extendedDataBBytes = _encoding.GetBytes(_extendedDataB);
+
+            var extendedOutputStream = _sshCommand.ExtendedOutputStream;
+            Assert.AreEqual(extendedDataABytes.Length + extendedDataBBytes.Length, extendedOutputStream.Length);
+
+            var buffer = new byte[extendedOutputStream.Length];
+            var bytesRead = extendedOutputStream.Read(buffer, 0, buffer.Length);
+
+            Assert.AreEqual(buffer.Length, bytesRead);
+            Assert.AreEqual(string.Concat(_extendedDataA, _extendedDataB), _encoding.GetString(buffer));
+            Assert.AreEqual(0, extendedOutputStream.Length);
+        }
+
+        [TestMethod]
+        public void OutputStreamShouldBeEmpty()
+        {
+            Assert.AreEqual(0, _sshCommand.OutputStream.Length);
+        }
+
+        [TestMethod]
+        public void ResultShouldReturnAllDataReceived()
+        {
+            Assert.AreEqual(string.Concat(_dataA, _dataB), _sshCommand.Result);
+        }
+    }
+}

+ 7 - 0
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -181,6 +181,7 @@
     <Compile Include="Classes\NetConfClientTest_Dispose_Disconnected.cs" />
     <Compile Include="Classes\NetConfClientTest_Dispose_Disposed.cs" />
     <Compile Include="Classes\NetConfClientTest_Finalize_Connected.cs" />
+    <Compile Include="Classes\PipeStreamTest_Dispose.cs" />
     <Compile Include="Classes\ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs" />
     <Compile Include="Classes\ScpClientTest_Upload_FileInfoAndPath_Success.cs" />
     <Compile Include="Classes\Security\AlgorithmTest.cs" />
@@ -394,7 +395,13 @@
     <Compile Include="Classes\ForwardedPortLocalTest.NET40.cs" />
     <Compile Include="Classes\SshCommandTest.NET40.cs" />
     <Compile Include="Classes\ScpClientTest.NET40.cs" />
+    <Compile Include="Classes\SshCommandTest_BeginExecute_EndExecuteInvokedOnAsyncResultFromPreviousInvocation.cs" />
+    <Compile Include="Classes\SshCommandTest_BeginExecute_EndExecuteNotInvokedOnAsyncResultFromPreviousInvocation.cs" />
+    <Compile Include="Classes\SshCommandTest_Dispose.cs" />
     <Compile Include="Classes\SshCommandTest_EndExecute.cs" />
+    <Compile Include="Classes\SshCommandTest_EndExecute_AsyncResultFromOtherInstance.cs" />
+    <Compile Include="Classes\SshCommandTest_EndExecute_AsyncResultIsNull.cs" />
+    <Compile Include="Classes\SshCommandTest_EndExecute_ChannelOpen.cs" />
     <Compile Include="Classes\SubsystemSessionStub.cs" />
     <Compile Include="Classes\SubsystemSession_Connect_Connected.cs" />
     <Compile Include="Classes\SubsystemSession_Connect_Disconnected.cs" />

+ 10 - 0
src/Renci.SshNet/CommandAsyncResult.cs

@@ -54,5 +54,15 @@ namespace Renci.SshNet
         public bool IsCompleted { get; internal set; }
 
         #endregion
+
+        /// <summary>
+        /// Gets a value indicating whether <see cref="SshCommand.EndExecute(IAsyncResult)"/> was already called for this
+        /// <see cref="CommandAsyncResult"/>.
+        /// </summary>
+        /// <returns>
+        /// <c>true</c> if <see cref="SshCommand.EndExecute(IAsyncResult)"/> was already called for this <see cref="CommandAsyncResult"/>;
+        /// otherwise, <c>false</c>.
+        /// </returns>
+        internal bool EndCalled { get; set; }
     }
 }

+ 114 - 81
src/Renci.SshNet/Common/PipeStream.cs

@@ -59,6 +59,11 @@
         /// </summary>
         private bool _canBlockLastRead;
 
+        /// <summary>
+        /// Indicates whether the current <see cref="PipeStream"/> is disposed.
+        /// </summary>
+        private bool _isDisposed;
+
         #endregion
 
         #region Public properties
@@ -84,11 +89,21 @@
         /// <value>
         /// 	<c>true</c> if block last read method before the buffer is empty; otherwise, <c>false</c>.
         /// </value>
+        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
         public bool BlockLastReadBuffer
         {
-            get { return this._canBlockLastRead; }
+            get
+            {
+                if (_isDisposed)
+                    throw CreateObjectDisposedException();
+
+                return this._canBlockLastRead;
+            }
             set
             {
+                if (_isDisposed)
+                    throw CreateObjectDisposedException();
+
                 this._canBlockLastRead = value;
 
                 // when turning off the block last read, signal Read() that it may now read the rest of the buffer.
@@ -102,41 +117,40 @@
 
         #region Stream overide methods
 
-        ///<summary>
-        ///When overridden in a derived class, clears all buffers for this stream and causes any buffered data to be written to the underlying device.
-        ///</summary>
-        ///
-        ///<exception cref="T:System.IO.IOException">An I/O error occurs. </exception><filterpriority>2</filterpriority>
+        /// <summary>
+        /// When overridden in a derived class, clears all buffers for this stream and causes any buffered data to be written to the underlying device.
+        /// </summary>
+        /// <exception cref="IOException">An I/O error occurs.</exception>
+        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
         public override void Flush()
         {
+            if (_isDisposed)
+                throw CreateObjectDisposedException();
+
             this._isFlushed = true;
             lock (this._buffer)
                 Monitor.Pulse(this._buffer);
         }
 
-        ///<summary>
-        ///When overridden in a derived class, sets the position within the current stream.
-        ///</summary>
-        ///<returns>
-        ///The new position within the current stream.
-        ///</returns>
-        ///<param name="offset">A byte offset relative to the origin parameter. </param>
-        ///<param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"></see> indicating the reference point used to obtain the new position. </param>
-        ///<exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
-        ///<exception cref="T:System.NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output. </exception>
-        ///<exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception><filterpriority>1</filterpriority>
+        /// <summary>
+        /// When overridden in a derived class, sets the position within the current stream.
+        /// </summary>
+        /// <returns>
+        /// The new position within the current stream.
+        /// </returns>
+        /// <param name="offset">A byte offset relative to the origin parameter.</param>
+        /// <param name="origin">A value of type <see cref="SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
+        /// <exception cref="NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output.</exception>
         public override long Seek(long offset, SeekOrigin origin)
         {
             throw new NotSupportedException();
         }
 
-        ///<summary>
-        ///When overridden in a derived class, sets the length of the current stream.
-        ///</summary>
-        ///<param name="value">The desired length of the current stream in bytes. </param>
-        ///<exception cref="T:System.NotSupportedException">The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. </exception>
-        ///<exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
-        ///<exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception><filterpriority>2</filterpriority>
+        /// <summary>
+        /// When overridden in a derived class, sets the length of the current stream.
+        /// </summary>
+        /// <param name="value">The desired length of the current stream in bytes.</param>
+        /// <exception cref="NotSupportedException">The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output.</exception>
         public override void SetLength(long value)
         {
             throw new NotSupportedException();
@@ -146,17 +160,17 @@
         ///When overridden in a derived class, reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
         ///</summary>
         ///<returns>
-        ///The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
+        ///The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero if the end of the stream has been reached.
         ///</returns>
-        ///<param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream. </param>
-        ///<param name="count">The maximum number of bytes to be read from the current stream. </param>
-        ///<param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. </param>
-        ///<exception cref="T:System.ArgumentException">The sum of offset and count is larger than the buffer length. </exception>
-        ///<exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        ///<exception cref="T:System.NotSupportedException">The stream does not support reading. </exception>
-        ///<exception cref="T:System.ArgumentNullException">buffer is null. </exception>
-        ///<exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
-        ///<exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception><filterpriority>1</filterpriority>
+        ///<param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
+        ///<param name="count">The maximum number of bytes to be read from the current stream.</param>
+        ///<param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
+        ///<exception cref="ArgumentException">The sum of offset and count is larger than the buffer length.</exception>
+        ///<exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
+        ///<exception cref="NotSupportedException">The stream does not support reading.</exception>
+        ///<exception cref="ArgumentNullException">buffer is null.</exception>
+        ///<exception cref="IOException">An I/O error occurs.</exception>
+        ///<exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception>
         public override int Read(byte[] buffer, int offset, int count)
         {
             if (offset != 0)
@@ -164,12 +178,13 @@
             if (buffer == null)
                 throw new ArgumentNullException("buffer");
             if (offset + count > buffer.Length)
-                throw new ArgumentException("The sum of offset and count is greater than the buffer length. ");
+                throw new ArgumentException("The sum of offset and count is greater than the buffer length.");
             if (offset < 0 || count < 0)
                 throw new ArgumentOutOfRangeException("offset", "offset or count is negative.");
             if (BlockLastReadBuffer && count >= _maxBufferLength)
                 throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "count({0}) > mMaxBufferLength({1})", count, _maxBufferLength));
-
+            if (_isDisposed)
+                throw CreateObjectDisposedException();
             if (count == 0)
                 return 0;
 
@@ -205,23 +220,25 @@
         ///<summary>
         ///When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
         ///</summary>
-        ///<param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream. </param>
-        ///<param name="count">The number of bytes to be written to the current stream. </param>
-        ///<param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream. </param>
-        ///<exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
-        ///<exception cref="T:System.NotSupportedException">The stream does not support writing. </exception>
-        ///<exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        ///<exception cref="T:System.ArgumentNullException">buffer is null. </exception>
-        ///<exception cref="T:System.ArgumentException">The sum of offset and count is greater than the buffer length. </exception>
-        ///<exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception><filterpriority>1</filterpriority>
+        ///<param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
+        ///<param name="count">The number of bytes to be written to the current stream.</param>
+        ///<param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
+        ///<exception cref="IOException">An I/O error occurs.</exception>
+        ///<exception cref="NotSupportedException">The stream does not support writing.</exception>
+        ///<exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
+        ///<exception cref="ArgumentNullException">buffer is null.</exception>
+        ///<exception cref="ArgumentException">The sum of offset and count is greater than the buffer length.</exception>
+        ///<exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception>
         public override void Write(byte[] buffer, int offset, int count)
         {
             if (buffer == null)
                 throw new ArgumentNullException("buffer");
             if (offset + count > buffer.Length)
-                throw new ArgumentException("The sum of offset and count is greater than the buffer length. ");
+                throw new ArgumentException("The sum of offset and count is greater than the buffer length.");
             if (offset < 0 || count < 0)
                 throw new ArgumentOutOfRangeException("offset", "offset or count is negative.");
+            if (_isDisposed)
+                throw CreateObjectDisposedException();
             if (count == 0)
                 return;
 
@@ -243,65 +260,76 @@
             }
         }
 
+        /// <summary>
+        /// Releases the unmanaged resources used by the Stream and optionally releases the managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected override void Dispose(bool disposing)
+        {
+            base.Dispose(disposing);
+
+            _isDisposed = true;
+        }
+
         ///<summary>
         ///When overridden in a derived class, gets a value indicating whether the current stream supports reading.
         ///</summary>
         ///<returns>
         ///true if the stream supports reading; otherwise, false.
         ///</returns>
-        ///<filterpriority>1</filterpriority>
         public override bool CanRead
         {
-            get { return true; }
+            get { return !_isDisposed; }
         }
 
-        ///<summary>
-        ///When overridden in a derived class, gets a value indicating whether the current stream supports seeking.
-        ///</summary>
-        ///<returns>
-        ///true if the stream supports seeking; otherwise, false.
+        /// <summary>
+        /// When overridden in a derived class, gets a value indicating whether the current stream supports seeking.
+        /// </summary>
+        /// <returns>
+        /// <c>true</c> if the stream supports seeking; otherwise, <c>false</c>.
         ///</returns>
-        ///<filterpriority>1</filterpriority>
         public override bool CanSeek
         {
             get { return false; }
         }
 
-        ///<summary>
-        ///When overridden in a derived class, gets a value indicating whether the current stream supports writing.
-        ///</summary>
-        ///<returns>
-        ///true if the stream supports writing; otherwise, false.
-        ///</returns>
-        ///<filterpriority>1</filterpriority>
+        /// <summary>
+        /// When overridden in a derived class, gets a value indicating whether the current stream supports writing.
+        /// </summary>
+        /// <returns>
+        /// <c>true</c> if the stream supports writing; otherwise, <c>false</c>.
+        /// </returns>
         public override bool CanWrite
         {
-            get { return true; }
+            get { return !_isDisposed; }
         }
 
-        ///<summary>
-        ///When overridden in a derived class, gets the length in bytes of the stream.
-        ///</summary>
-        ///<returns>
-        ///A long value representing the length of the stream in bytes.
-        ///</returns>
-        ///
-        ///<exception cref="T:System.NotSupportedException">A class derived from Stream does not support seeking. </exception>
-        ///<exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception><filterpriority>1</filterpriority>
+        /// <summary>
+        /// When overridden in a derived class, gets the length in bytes of the stream.
+        /// </summary>
+        /// <returns>
+        /// A long value representing the length of the stream in bytes.
+        /// </returns>
+        /// <exception cref="NotSupportedException">A class derived from Stream does not support seeking.</exception>
+        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
         public override long Length
         {
-            get { return this._buffer.Count; }
+            get
+            {
+                if (_isDisposed)
+                    throw CreateObjectDisposedException();
+
+                return this._buffer.Count;
+            }
         }
 
-        ///<summary>
-        ///When overridden in a derived class, gets or sets the position within the current stream.
-        ///</summary>
-        ///<returns>
-        ///The current position within the stream.
-        ///</returns>
-        ///<exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
-        ///<exception cref="T:System.NotSupportedException">The stream does not support seeking. </exception>
-        ///<exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception><filterpriority>1</filterpriority>
+        /// <summary>
+        /// When overridden in a derived class, gets or sets the position within the current stream.
+        /// </summary>
+        /// <returns>
+        /// The current position within the stream.
+        /// </returns>
+        /// <exception cref="NotSupportedException">The stream does not support seeking.</exception>
         public override long Position
         {
             get { return 0; }
@@ -309,5 +337,10 @@
         }
 
         #endregion
+
+        private ObjectDisposedException CreateObjectDisposedException()
+        {
+            return new ObjectDisposedException(GetType().FullName);
+        }
     }
 }

+ 88 - 89
src/Renci.SshNet/SshCommand.cs

@@ -4,7 +4,6 @@ using System.Text;
 using System.Threading;
 using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
-using Renci.SshNet.Messages;
 using Renci.SshNet.Messages.Connection;
 using Renci.SshNet.Messages.Transport;
 using System.Globalization;
@@ -92,10 +91,9 @@ namespace Renci.SshNet
 
                 if (OutputStream != null && OutputStream.Length > 0)
                 {
-                    using (var sr = new StreamReader(OutputStream, _encoding))
-                    {
-                        _result.Append(sr.ReadToEnd());
-                    }
+                    // do not dispose the StreamReader, as it would also dispose the stream
+                    var sr = new StreamReader(OutputStream, _encoding);
+                    _result.Append(sr.ReadToEnd());
                 }
 
                 return _result.ToString();
@@ -122,10 +120,9 @@ namespace Renci.SshNet
 
                     if (ExtendedOutputStream != null && ExtendedOutputStream.Length > 0)
                     {
-                        using (var sr = new StreamReader(ExtendedOutputStream, _encoding))
-                        {
-                            _error.Append(sr.ReadToEnd());
-                        }
+                        // do not dispose the StreamReader, as it would also dispose the stream
+                        var sr = new StreamReader(ExtendedOutputStream, _encoding);
+                        _error.Append(sr.ReadToEnd());
                     }
 
                     return _error.ToString();
@@ -217,7 +214,7 @@ namespace Renci.SshNet
         public IAsyncResult BeginExecute(AsyncCallback callback, object state)
         {
             //  Prevent from executing BeginExecute before calling EndExecute
-            if (_asyncResult != null)
+            if (_asyncResult != null && !_asyncResult.EndCalled)
             {
                 throw new InvalidOperationException("Asynchronous operation is already in progress.");
             }
@@ -236,16 +233,33 @@ namespace Renci.SshNet
                 throw new SshException("Invalid operation.");
             }
 
-            CreateChannel();
-
             if (string.IsNullOrEmpty(CommandText))
                 throw new ArgumentException("CommandText property is empty.");
 
+            var outputStream = OutputStream;
+            if (outputStream != null)
+            {
+                outputStream.Dispose();
+                OutputStream = null;
+            }
+
+            var extendedOutputStream = ExtendedOutputStream;
+            if (extendedOutputStream != null)
+            {
+                extendedOutputStream.Dispose();
+                ExtendedOutputStream = null;
+            }
+
+            //  Initialize output streams
+            OutputStream = new PipeStream();
+            ExtendedOutputStream = new PipeStream();
+
+            _result = null;
+            _error = null;
             _callback = callback;
 
+            _channel = CreateChannel();
             _channel.Open();
-
-            //  Send channel command request
             _channel.SendExecRequest(CommandText);
 
             return _asyncResult;
@@ -278,33 +292,42 @@ namespace Renci.SshNet
         ///     <code source="..\..\Renci.SshNet.Tests\Classes\SshCommandTest.cs" region="Example SshCommand CreateCommand BeginExecute IsCompleted EndExecute" language="C#" title="Asynchronous Command Execution" />
         /// </example>
         /// <exception cref="ArgumentException">Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
         public string EndExecute(IAsyncResult asyncResult)
         {
-            if (_asyncResult == asyncResult && _asyncResult != null)
+            if (asyncResult == null)
             {
-                lock (_endExecuteLock)
-                {
-                    if (_asyncResult != null)
-                    {
-                        //  Make sure that operation completed if not wait for it to finish
-                        WaitOnHandle(_asyncResult.AsyncWaitHandle);
+                throw new ArgumentNullException("asyncResult");
+            }
 
-                        if (_channel.IsOpen)
-                        {
-                            _channel.Close();
-                        }
+            var commandAsyncResult = asyncResult as CommandAsyncResult;
+            if (commandAsyncResult == null)
+            {
+                throw new ArgumentException(string.Format("The {0} object was not returned from the corresponding asynchronous method on this class.", typeof(IAsyncResult).Name));
+            }
 
-                        UnsubscribeFromEventsAndDisposeChannel(_channel);
-                        _channel = null;
+            lock (_endExecuteLock)
+            {
+                if (commandAsyncResult.EndCalled)
+                {
+                    throw new ArgumentException("EndExecute can only be called once for each asynchronous operation.");
+                }
 
-                        _asyncResult = null;
+                //  wait for operation to complete (or time out)
+                WaitOnHandle(_asyncResult.AsyncWaitHandle);
 
-                        return Result;
-                    }
+                if (_channel.IsOpen)
+                {
+                    _channel.Close();
                 }
-            }
 
-            throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.");
+                UnsubscribeFromEventsAndDisposeChannel(_channel);
+                _channel = null;
+
+                commandAsyncResult.EndCalled = true;
+
+                return Result;
+            }
         }
 
         /// <summary>
@@ -349,33 +372,14 @@ namespace Renci.SshNet
             return Execute();
         }
 
-        private void CreateChannel()
+        private IChannelSession CreateChannel()
         {
-            _channel = _session.CreateChannelSession();
-            _channel.DataReceived += Channel_DataReceived;
-            _channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
-            _channel.RequestReceived += Channel_RequestReceived;
-            _channel.Closed += Channel_Closed;
-
-            //  Dispose of streams if already exists
-            if (OutputStream != null)
-            {
-                OutputStream.Dispose();
-                OutputStream = null;
-            }
-
-            if (ExtendedOutputStream != null)
-            {
-                ExtendedOutputStream.Dispose();
-                ExtendedOutputStream = null;
-            }
-
-            //  Initialize output streams and StringBuilders
-            OutputStream = new PipeStream();
-            ExtendedOutputStream = new PipeStream();
-
-            _result = null;
-            _error = null;
+            var channel = _session.CreateChannelSession();
+            channel.DataReceived += Channel_DataReceived;
+            channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
+            channel.RequestReceived += Channel_RequestReceived;
+            channel.Closed += Channel_Closed;
+            return channel;
         }
 
         private void Session_Disconnected(object sender, EventArgs e)
@@ -402,14 +406,16 @@ namespace Renci.SshNet
 
         private void Channel_Closed(object sender, ChannelEventArgs e)
         {
-            if (OutputStream != null)
+            var outputStream = OutputStream;
+            if (outputStream != null)
             {
-                OutputStream.Flush();
+                outputStream.Flush();
             }
 
-            if (ExtendedOutputStream != null)
+            var extendedOutputStream = ExtendedOutputStream;
+            if (extendedOutputStream != null)
             {
-                ExtendedOutputStream.Flush();
+                extendedOutputStream.Flush();
             }
 
             _asyncResult.IsCompleted = true;
@@ -419,29 +425,29 @@ namespace Renci.SshNet
                 //  Execute callback on different thread
                 ThreadAbstraction.ExecuteThread(() => _callback(_asyncResult));
             }
-            ((EventWaitHandle)_asyncResult.AsyncWaitHandle).Set();
+            ((EventWaitHandle) _asyncResult.AsyncWaitHandle).Set();
         }
 
         private void Channel_RequestReceived(object sender, ChannelRequestEventArgs e)
         {
-            Message replyMessage;
-
-            if (e.Info is ExitStatusRequestInfo)
+            var exitStatusInfo = e.Info as ExitStatusRequestInfo;
+            if (exitStatusInfo != null)
             {
-                var exitStatusInfo = e.Info as ExitStatusRequestInfo;
-
                 ExitStatus = (int) exitStatusInfo.ExitStatus;
 
-                replyMessage = new ChannelSuccessMessage(_channel.LocalChannelNumber);
+                if (exitStatusInfo.WantReply)
+                {
+                    var replyMessage = new ChannelSuccessMessage(_channel.LocalChannelNumber);
+                    _session.SendMessage(replyMessage);
+                }
             }
             else
             {
-                replyMessage = new ChannelFailureMessage(_channel.LocalChannelNumber);
-            }
-
-            if (e.Info.WantReply)
-            {
-                _session.SendMessage(replyMessage);
+                if (e.Info.WantReply)
+                {
+                    var replyMessage = new ChannelFailureMessage(_channel.LocalChannelNumber);
+                    _session.SendMessage(replyMessage);
+                }
             }
         }
 
@@ -495,7 +501,7 @@ namespace Renci.SshNet
             }
         }
 
-        private void UnsubscribeFromEventsAndDisposeChannel(IChannelSession channel)
+        private void UnsubscribeFromEventsAndDisposeChannel(IChannel channel)
         {
             // unsubscribe from events as we do not want to be signaled should these get fired
             // during the dispose of the channel
@@ -530,22 +536,15 @@ namespace Renci.SshNet
             if (_isDisposed)
                 return;
 
-            // unsubscribe from session events to ensure other objects that we're going to dispose
-            // are not used while disposing
-            //
-            // we do this regardless of the value of the 'disposing' argument to avoid leaks
-            // when clients are not disposing the SshCommand instance
-            //
-            // note that you argue that we have the same problem for the IChannel events but
-            // the channel itself does not have a long lifetime and will be disposed or GC'd
-            // together with the SshCommand
-            _session.Disconnected -= Session_Disconnected;
-            _session.ErrorOccured -= Session_ErrorOccured;
-
             if (disposing)
             {
+                // unsubscribe from session events to ensure other objects that we're going to dispose
+                // are not accessed while disposing
+                _session.Disconnected -= Session_Disconnected;
+                _session.ErrorOccured -= Session_ErrorOccured;
+
                 // unsubscribe from channel events to ensure other objects that we're going to dispose
-                // are not used while disposing
+                // are not accessed while disposing
                 var channel = _channel;
                 if (channel != null)
                 {