Sfoglia il codice sorgente

Use CountDownEvent to wait for pending requests to finish instead of using a while/sleep approach.
Reuse SocketAsyncEventArgs for accepting connections in our EAP-based socket code.

drieseng 9 anni fa
parent
commit
18c1b79589

+ 4 - 1
src/Renci.SshNet.Silverlight5/Renci.SshNet.Silverlight5.csproj

@@ -195,6 +195,9 @@
     <Compile Include="..\Renci.SshNet\Common\ChannelRequestEventArgs.cs">
       <Link>Common\ChannelRequestEventArgs.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\Common\CountdownEvent.cs">
+      <Link>Common\CountdownEvent.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\Common\DerData.cs">
       <Link>Common\DerData.cs</Link>
     </Compile>
@@ -917,7 +920,7 @@
       <FlavorProperties GUID="{A1591282-1198-4647-A2B1-27E5FF5F6F3B}">
         <SilverlightProjectProperties />
       </FlavorProperties>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
+      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 204 - 0
src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest.cs

@@ -0,0 +1,204 @@
+using System;
+using System.Threading;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Renci.SshNet.Tests.Classes.Common
+{
+    [TestClass]
+    public class CountdownEventTest
+    {
+        private Random _random;
+
+        [TestInitialize]
+        public void Init()
+        {
+            _random = new Random();
+        }
+
+        [TestMethod]
+        public void Ctor_InitialCountGreatherThanZero()
+        {
+            var initialCount = _random.Next(1, 500);
+
+            var countdownEvent = CreateCountdownEvent(initialCount);
+            Assert.AreEqual(initialCount, countdownEvent.CurrentCount);
+            Assert.IsFalse(countdownEvent.IsSet);
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void Ctor_InitialCountZero()
+        {
+            const int initialCount = 0;
+
+            var countdownEvent = CreateCountdownEvent(0);
+            Assert.AreEqual(initialCount, countdownEvent.CurrentCount);
+            Assert.IsTrue(countdownEvent.IsSet);
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void Signal_CurrentCountGreatherThanOne()
+        {
+            var initialCount = _random.Next(2, 1000);
+
+            var countdownEvent = CreateCountdownEvent(initialCount);
+            Assert.IsFalse(countdownEvent.Signal());
+            Assert.AreEqual(--initialCount, countdownEvent.CurrentCount);
+            Assert.IsFalse(countdownEvent.IsSet);
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void Signal_CurrentCountOne()
+        {
+            var countdownEvent = CreateCountdownEvent(1);
+            Assert.IsTrue(countdownEvent.Signal());
+            Assert.AreEqual(0, countdownEvent.CurrentCount);
+            Assert.IsTrue(countdownEvent.IsSet);
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void Signal_CurrentCountZero()
+        {
+            var countdownEvent = CreateCountdownEvent(0);
+
+            try
+            {
+                countdownEvent.Signal();
+                Assert.Fail();
+            }
+            catch (InvalidOperationException)
+            {
+                // Invalid attempt made to decrement the event's count below zero
+            }
+            finally
+            {
+                countdownEvent.Dispose();
+            }
+        }
+
+        public void CurrentCountShouldReturnZeroAfterAttemptToDecrementCountBelowZero()
+        {
+        }
+
+        [TestMethod]
+        public void Wait_TimeoutInfinite_ShouldBlockUntilCountdownEventIsSet()
+        {
+            var sleep = TimeSpan.FromMilliseconds(100);
+            var timeout = Session.InfiniteTimeSpan;
+
+            var countdownEvent = CreateCountdownEvent(1);
+            var signalCount = 0;
+            var expectedSignalCount = _random.Next(5, 20);
+
+            for (var i = 0; i < (expectedSignalCount - 1); i++)
+                countdownEvent.AddCount();
+
+            var threads = new Thread[expectedSignalCount];
+            for (var i = 0; i < expectedSignalCount; i++)
+            {
+                threads[i] = new Thread(() =>
+                    {
+                        Thread.Sleep(sleep);
+                        countdownEvent.Signal();
+                        Interlocked.Increment(ref signalCount);
+                    });
+                threads[i].Start();
+            }
+
+            var start = DateTime.Now;
+            var actual = countdownEvent.Wait(timeout);
+            var elapsedTime = DateTime.Now - start;
+
+            Assert.IsTrue(actual);
+            Assert.AreEqual(expectedSignalCount, signalCount);
+            Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(elapsedTime >= sleep);
+            Assert.IsTrue(elapsedTime <= sleep.Add(TimeSpan.FromMilliseconds(100)));
+
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void Wait_ShouldReturnTrueWhenCountdownEventIsSetBeforeTimeoutExpires()
+        {
+            var sleep = TimeSpan.FromMilliseconds(100);
+            var timeout = sleep.Add(TimeSpan.FromSeconds(2));
+
+            var countdownEvent = CreateCountdownEvent(1);
+            var signalCount = 0;
+            var expectedSignalCount = _random.Next(5, 20);
+
+            for (var i = 0; i < (expectedSignalCount - 1); i++)
+                countdownEvent.AddCount();
+
+            var threads = new Thread[expectedSignalCount];
+            for (var i = 0; i < expectedSignalCount; i++)
+            {
+                threads[i] = new Thread(() =>
+                {
+                    Thread.Sleep(sleep);
+                    countdownEvent.Signal();
+                    Interlocked.Increment(ref signalCount);
+                });
+                threads[i].Start();
+            }
+
+            var start = DateTime.Now;
+            var actual = countdownEvent.Wait(timeout);
+            var elapsedTime = DateTime.Now - start;
+
+            Assert.IsTrue(actual);
+            Assert.AreEqual(expectedSignalCount, signalCount);
+            Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(elapsedTime >= sleep);
+            Assert.IsTrue(elapsedTime <= timeout);
+
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void Wait_ShouldReturnFalseTimeoutExpiresBeforeCountdownEventIsSet()
+        {
+            var sleep = TimeSpan.FromMilliseconds(100);
+            var timeout = TimeSpan.FromMilliseconds(30);
+
+            var countdownEvent = CreateCountdownEvent(1);
+            var signalCount = 0;
+            var expectedSignalCount = _random.Next(5, 20);
+
+            for (var i = 0; i < (expectedSignalCount - 1); i++)
+                countdownEvent.AddCount();
+
+            var threads = new Thread[expectedSignalCount];
+            for (var i = 0; i < expectedSignalCount; i++)
+            {
+                threads[i] = new Thread(() =>
+                {
+                    Thread.Sleep(sleep);
+                    countdownEvent.Signal();
+                    Interlocked.Increment(ref signalCount);
+                });
+                threads[i].Start();
+            }
+
+            var start = DateTime.Now;
+            var actual = countdownEvent.Wait(timeout);
+            var elapsedTime = DateTime.Now - start;
+
+            Assert.IsFalse(actual);
+            Assert.IsFalse(countdownEvent.IsSet);
+            Assert.IsTrue(elapsedTime >= timeout);
+
+            countdownEvent.Wait(Session.InfiniteTimeSpan);
+            countdownEvent.Dispose();
+        }
+
+        private static CountdownEvent CreateCountdownEvent(int initialCount)
+        {
+            return new CountdownEvent(initialCount);
+        }
+    }
+}

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

@@ -131,6 +131,7 @@
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs" />
+    <Compile Include="Classes\Common\CountdownEventTest.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Concat.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Take_Count.cs" />

+ 153 - 0
src/Renci.SshNet/Common/CountdownEvent.cs

@@ -0,0 +1,153 @@
+#if !FEATURE_THREAD_COUNTDOWNEVENT
+
+using System;
+using System.Threading;
+
+namespace Renci.SshNet.Common
+{
+    /// <summary>
+    /// Represents a synchronization primitive that is signaled when its count reaches zero.
+    /// </summary>
+    internal class CountdownEvent : IDisposable
+    {
+        private int _count;
+        private ManualResetEvent _event;
+        private bool _disposed;
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="CountdownEvent"/> class with the specified count.
+        /// </summary>
+        /// <param name="initialCount">The number of signals initially required to set the <see cref="CountdownEvent"/>.</param>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="initialCount"/> is less than zero.</exception>
+        /// <remarks>
+        /// If <paramref name="initialCount"/> is <c>zero</c>, the event is created in a signaled state.
+        /// </remarks>
+        public CountdownEvent(int initialCount)
+        {
+            if (initialCount < 0)
+            {
+                throw new ArgumentOutOfRangeException("initialCount");
+            }
+
+            _count = initialCount;
+
+            var initialState = _count == 0;
+            _event = new ManualResetEvent(initialState);
+        }
+
+        /// <summary>
+        /// Gets the number of remaining signals required to set the event.
+        /// </summary>
+        /// <value>
+        /// The number of remaining signals required to set the event.
+        /// </value>
+        public int CurrentCount
+        {
+            get { return _count; }
+        }
+
+        /// <summary>
+        /// Indicates whether the <see cref="CountdownEvent"/>'s current count has reached zero.
+        /// </summary>
+        /// <value>
+        /// <c>true</c> if the current count is zero; otherwise, <c>false</c>.
+        /// </value>
+        public bool IsSet
+        {
+            get { return _count == 0; }
+        }
+
+        /// <summary>
+        /// Registers a signal with the <see cref="CountdownEvent"/>, decrementing the value of <see cref="CurrentCount"/>.
+        /// </summary>
+        /// <returns>
+        /// <c>true</c> if the signal caused the count to reach zero and the event was set; otherwise, <c>false</c>.
+        /// </returns>
+        /// <exception cref="ObjectDisposedException">The current instance has already been disposed.</exception>
+        /// <exception cref="InvalidOperationException">The current instance is already set.</exception>
+        public bool Signal()
+        {
+            EnsureNotDisposed();
+
+            if (_count <= 0)
+                throw new InvalidOperationException("Invalid attempt made to decrement the event's count below zero.");
+
+            var newCount = Interlocked.Decrement(ref _count);
+            if (newCount == 0)
+            {
+                _event.Set();
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Increments the <see cref="CountdownEvent"/>'s current count by one.
+        /// </summary>
+        /// <exception cref="ObjectDisposedException">The current instance has already been disposed.</exception>
+        /// <exception cref="InvalidOperationException">The current instance is already set.</exception>
+        /// <exception cref="InvalidOperationException"><see cref="CurrentCount"/> is equal to or greather than <see cref="int.MaxValue"/>.</exception>
+        public void AddCount()
+        {
+            EnsureNotDisposed();
+
+            if (_count == int.MaxValue)
+                throw new InvalidOperationException("TODO");
+
+            Interlocked.Increment(ref _count);
+        }
+
+        /// <summary>
+        /// Blocks the current thread until the <see cref="CountdownEvent"/> is set, using a <see cref="TimeSpan"/>
+        /// to measure the timeout.
+        /// </summary>
+        /// <param name="timeout">A <see cref="TimeSpan"/> that represents the number of milliseconds to wait, or a <see cref="TimeSpan"/> that represents -1 milliseconds to wait indefinitely.</param>
+        /// <returns>
+        /// <c>true</c> if the <see cref="CountdownEvent"/> was set; otherwise, <c>false</c>.
+        /// </returns>
+        /// <exception cref="ObjectDisposedException">The current instance has already been disposed.</exception>
+        public bool Wait(TimeSpan timeout)
+        {
+            EnsureNotDisposed();
+
+            return _event.WaitOne(timeout);
+        }
+
+        /// <summary>
+        /// Releases all resources used by the current instance of the <see cref="CountdownEvent"/> class.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases the unmanaged resources used by the <see cref="CountdownEvent"/>, 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 virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                var theEvent = _event;
+                if (theEvent != null)
+                {
+                    _event = null;
+                    theEvent.Dispose();
+                }
+
+                _disposed = true;
+            }
+        }
+
+        private void EnsureNotDisposed()
+        {
+            if (_disposed)
+                throw new ObjectDisposedException(GetType().Name);
+        }
+    }
+}
+
+#endif // FEATURE_THREAD_COUNTDOWNEVENT

+ 9 - 6
src/Renci.SshNet/ForwardedPort.cs

@@ -73,10 +73,10 @@ namespace Renci.SshNet
         {
             CheckDisposed();
 
-            if (!IsStarted)
-                return;
-
-            StopPort(Session.ConnectionInfo.Timeout);
+            if (IsStarted)
+            {
+                StopPort(Session.ConnectionInfo.Timeout);
+            }
         }
 
         /// <summary>
@@ -111,8 +111,11 @@ namespace Renci.SshNet
                 var session = Session;
                 if (session != null)
                 {
-                    session.ErrorOccured -= Session_ErrorOccured;
-                    StopPort(session.ConnectionInfo.Timeout);
+                    if (IsStarted)
+                    {
+                        StopPort(session.ConnectionInfo.Timeout);
+                    }
+
                     Session = null;
                 }
             }

+ 115 - 72
src/Renci.SshNet/ForwardedPortDynamic.NET.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Diagnostics;
 using System.Linq;
 using System.Text;
 using System.Net;
@@ -14,7 +13,7 @@ namespace Renci.SshNet
     public partial class ForwardedPortDynamic
     {
         private Socket _listener;
-        private int _pendingRequests;
+        private CountdownEvent _pendingRequestsCountdown;
 
         partial void InternalStart()
         {
@@ -39,13 +38,14 @@ namespace Renci.SshNet
             Session.Disconnected += Session_Disconnected;
 
             _listenerCompleted = new ManualResetEvent(false);
+            InitializePendingRequestsCountdown();
 
             ThreadAbstraction.ExecuteThread(() =>
                 {
                     try
                     {
 #if FEATURE_SOCKET_EAP
-                        StartAccept();
+                        StartAccept(null);
 #elif FEATURE_SOCKET_APM
                         _listener.BeginAccept(AcceptCallback, _listener);
 #elif FEATURE_SOCKET_TAP
@@ -53,9 +53,6 @@ namespace Renci.SshNet
 #else
 #error Accepting new socket connections is not implemented.
 #endif
-
-                        // wait until listener is stopped
-                        _listenerCompleted.WaitOne();
                     }
                     catch (ObjectDisposedException)
                     {
@@ -64,48 +61,57 @@ namespace Renci.SshNet
                         //
                         // As we start accepting connection on a separate thread, this is possible
                         // when the listener is stopped right after it was started.
-
-                        // mark listener stopped
-                        _listenerCompleted.Set();
                     }
                     catch (Exception ex)
                     {
                         RaiseExceptionEvent(ex);
-
-                        // mark listener stopped
-                        _listenerCompleted.Set();
                     }
-                    finally
+
+                    // wait until listener is stopped
+                    _listenerCompleted.WaitOne();
+
+                    var session = Session;
+                    if (session != null)
                     {
-                        var session = Session;
-                        if (session != null)
-                        {
-                            session.ErrorOccured -= Session_ErrorOccured;
-                            session.Disconnected -= Session_Disconnected;
-                        }
+                        session.ErrorOccured -= Session_ErrorOccured;
+                        session.Disconnected -= Session_Disconnected;
                     }
                 });
         }
 
         private void Session_Disconnected(object sender, EventArgs e)
         {
-            StopListener();
+            if (IsStarted)
+            {
+                StopListener();
+            }
         }
 
         private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
         {
-            StopListener();
+            if (IsStarted)
+            {
+                StopListener();
+            }
         }
 
 #if FEATURE_SOCKET_EAP
-        private void StartAccept()
+        private void StartAccept(SocketAsyncEventArgs e)
         {
-            var args = new SocketAsyncEventArgs();
-            args.Completed += AcceptCompleted;
+            if (e == null)
+            {
+                e = new SocketAsyncEventArgs();
+                e.Completed += AcceptCompleted;
+            }
+            else
+            {
+                // clear the socket as we're reusing the context object
+                e.AcceptSocket = null;
+            }
 
-            if (!_listener.AcceptAsync(args))
+            if (!_listener.AcceptAsync(e))
             {
-                AcceptCompleted(null, args);
+                AcceptCompleted(null, e);
             }
         }
 
@@ -117,19 +123,22 @@ namespace Renci.SshNet
                 return;
             }
 
+            // capture client socket
+            var clientSocket = acceptAsyncEventArgs.AcceptSocket;
+
             if (acceptAsyncEventArgs.SocketError != SocketError.Success)
             {
                 // accept new connection
-                StartAccept();
-                // dispose broken socket
-                acceptAsyncEventArgs.AcceptSocket.Dispose();
+                StartAccept(acceptAsyncEventArgs);
+                // dispose broken client socket
+                CloseClientSocket(clientSocket);
                 return;
             }
 
             // accept new connection
-            StartAccept();
+            StartAccept(acceptAsyncEventArgs);
             // process connection
-            ProcessAccept(acceptAsyncEventArgs.AcceptSocket);
+            ProcessAccept(clientSocket);
         }
 #elif FEATURE_SOCKET_APM
         private void AcceptCallback(IAsyncResult ar)
@@ -159,7 +168,12 @@ namespace Renci.SshNet
 
         private void ProcessAccept(Socket remoteSocket)
         {
-            Interlocked.Increment(ref _pendingRequests);
+            // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
+            // that same instance; the instance field for the countdown event is re-initialized when the port is restarted
+            // and at that time they may still be pending requests
+            var pendingRequestsCountdown = _pendingRequestsCountdown;
+
+            pendingRequestsCountdown.AddCount();
 
 #if DEBUG_GERT
             Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | " + remoteSocket.RemoteEndPoint + " | ForwardedPortDynamic.ProcessAccept | " + DateTime.Now.ToString("hh:mm:ss.fff"));
@@ -180,7 +194,7 @@ namespace Renci.SshNet
                     {
                         if (!HandleSocks(channel, remoteSocket, Session.ConnectionInfo.Timeout))
                         {
-                            CloseSocket(remoteSocket);
+                            CloseClientSocket(remoteSocket);
                             return;
                         }
 
@@ -191,6 +205,7 @@ namespace Renci.SshNet
                     catch (SocketException ex)
                     {
                         Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | " + ex.SocketErrorCode + " | " + DateTime.Now.ToString("hh:mm:ss.fff") + " | " + ex);
+                        throw;
                     }
 #endif // DEBUG_GERT
                     finally
@@ -211,7 +226,7 @@ namespace Renci.SshNet
                     RaiseExceptionEvent(ex);
 #endif // DEBUG_GERT
                 }
-                CloseSocket(remoteSocket);
+                CloseClientSocket(remoteSocket);
             }
             catch (Exception exp)
             {
@@ -219,11 +234,18 @@ namespace Renci.SshNet
                 Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | " + exp + " | " + DateTime.Now.ToString("hh:mm:ss.fff"));
 #endif // DEBUG_GERT
                 RaiseExceptionEvent(exp);
-                CloseSocket(remoteSocket);
+                CloseClientSocket(remoteSocket);
             }
             finally
             {
-                Interlocked.Decrement(ref _pendingRequests);
+                // take into account that countdown event has since been disposed (after waiting for a given timeout)
+                try
+                {
+                    pendingRequestsCountdown.Signal();
+                }
+                catch (ObjectDisposedException)
+                {
+                }
             }
         }
 
@@ -231,7 +253,7 @@ namespace Renci.SshNet
         {
             // create eventhandler which is to be invoked to interrupt a blocking receive
             // when we're closing the forwarded port
-            EventHandler closeClientSocket = (_, args) => CloseSocket(remoteSocket);
+            EventHandler closeClientSocket = (_, args) => CloseClientSocket(remoteSocket);
 
             Closing += closeClientSocket;
 
@@ -273,61 +295,53 @@ namespace Renci.SshNet
 
         }
 
-        private static void CloseSocket(Socket socket)
+        private static void CloseClientSocket(Socket clientSocket)
         {
 #if DEBUG_GERT
             Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | ForwardedPortDynamic.CloseSocket | " + DateTime.Now.ToString("hh:mm:ss.fff"));
 #endif // DEBUG_GERT
 
-            if (socket.Connected)
+            if (clientSocket.Connected)
             {
-                socket.Shutdown(SocketShutdown.Both);
-                socket.Dispose();
+                try
+                {
+                    clientSocket.Shutdown(SocketShutdown.Both);
+                }
+                catch (Exception)
+                {
+                    // ignore exception when client socket was already closed
+                }
+
             }
+
+            clientSocket.Dispose();
         }
 
         partial void StopListener()
         {
-            //  if the port is not started then there's nothing to stop
-            if (!IsStarted)
-                return;
-
             // close listener socket
-            _listener.Dispose();
+            var listener = _listener;
+            if (listener != null)
+            {
+                listener.Dispose();
+            }
 
             // allow listener thread to stop
-            _listenerCompleted.Set();
+            var listenerCompleted = _listenerCompleted;
+            if (listenerCompleted != null)
+            {
+                listenerCompleted.Set();
+            }
         }
 
         /// <summary>
-        /// Waits for pending requests to finish, and channels to close.
+        /// Waits for pending requests to finish.
         /// </summary>
-        /// <param name="timeout">The maximum time to wait for the forwarded port to stop.</param>
+        /// <param name="timeout">The maximum time to wait for the pending requests to finish.</param>
         partial void InternalStop(TimeSpan timeout)
         {
-            if (timeout == TimeSpan.Zero)
-                return;
-
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
-
-            // break out of loop when one of the following conditions are met:
-            // * the forwarded port is restarted
-            // * all pending requests have been processed and corresponding channel are closed
-            // * the specified timeout has elapsed
-            while (!IsStarted)
-            {
-                // break out of loop when all pending requests have been processed
-                if (Interlocked.CompareExchange(ref _pendingRequests, 0, 0) == 0)
-                    break;
-                // break out of loop when specified timeout has elapsed
-                if (stopWatch.Elapsed >= timeout && timeout != SshNet.Session.InfiniteTimeSpan)
-                    break;
-                // give channels time to process pending requests
-                ThreadAbstraction.Sleep(50);
-            }
-
-            stopWatch.Stop();
+            _pendingRequestsCountdown.Signal();
+            _pendingRequestsCountdown.Wait(timeout);
         }
 
         partial void InternalDispose(bool disposing)
@@ -340,6 +354,13 @@ namespace Renci.SshNet
                     _listener = null;
                     listener.Dispose();
                 }
+
+                var pendingRequestsCountdown = _pendingRequestsCountdown;
+                if (pendingRequestsCountdown != null)
+                {
+                    _pendingRequestsCountdown = null;
+                    pendingRequestsCountdown.Dispose();
+                }
             }
         }
 
@@ -612,6 +633,28 @@ namespace Renci.SshNet
             RaiseExceptionEvent(e.Exception);
         }
 
+        /// <summary>
+        /// Initializes the <see cref="CountdownEvent"/>.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
+        /// of <c>1</c>.
+        /// </para>
+        /// <para>
+        /// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
+        /// initial count of <c>1</c>.
+        /// </para>
+        /// </remarks>
+        private void InitializePendingRequestsCountdown()
+        {
+            var original = Interlocked.Exchange(ref _pendingRequestsCountdown, new CountdownEvent(1));
+            if (original != null)
+            {
+                original.Dispose();
+            }
+        }
+
         /// <summary>
         /// Reads a null terminated string from a socket.
         /// </summary>

+ 6 - 10
src/Renci.SshNet/ForwardedPortDynamic.cs

@@ -67,14 +67,11 @@ namespace Renci.SshNet
         /// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
         protected override void StopPort(TimeSpan timeout)
         {
-            if (IsStarted)
-            {
-                // prevent new requests from getting processed before we signal existing
-                // channels that the port is closing
-                StopListener();
-                // signal existing channels that the port is closing
-                base.StopPort(timeout);
-            }
+            // prevent new requests from getting processed before we signal existing
+            // channels that the port is closing
+            StopListener();
+            // signal existing channels that the port is closing
+            base.StopPort(timeout);
             // wait for open channels to close
             InternalStop(timeout);
         }
@@ -133,6 +130,7 @@ namespace Renci.SshNet
                 return;
 
             base.Dispose(disposing);
+            InternalDispose(disposing);
 
             if (disposing)
             {
@@ -144,8 +142,6 @@ namespace Renci.SshNet
                 }
             }
 
-            InternalDispose(disposing);
-
             _isDisposed = true;
         }
 

+ 115 - 70
src/Renci.SshNet/ForwardedPortLocal.NET.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Diagnostics;
 using System.Net.Sockets;
 using System.Net;
 using System.Threading;
@@ -14,7 +13,7 @@ namespace Renci.SshNet
     public partial class ForwardedPortLocal
     {
         private Socket _listener;
-        private int _pendingRequests;
+        private CountdownEvent _pendingRequestsCountdown;
 
         partial void InternalStart()
         {
@@ -27,7 +26,7 @@ namespace Renci.SshNet
             _listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.NoDelay, true);
 #endif // FEATURE_SOCKET_SETSOCKETOPTION
             _listener.Bind(ep);
-            _listener.Listen(1);
+            _listener.Listen(5);
 
             // update bound port (in case original was passed as zero)
             BoundPort = (uint)((IPEndPoint)_listener.LocalEndPoint).Port;
@@ -36,13 +35,14 @@ namespace Renci.SshNet
             Session.Disconnected += Session_Disconnected;
 
             _listenerTaskCompleted = new ManualResetEvent(false);
+            InitializePendingRequestsCountdown();
 
             ThreadAbstraction.ExecuteThread(() =>
                 {
                     try
                     {
 #if FEATURE_SOCKET_EAP
-                        StartAccept();
+                        StartAccept(null);
 #elif FEATURE_SOCKET_APM
                         _listener.BeginAccept(AcceptCallback, _listener);
 #elif FEATURE_SOCKET_TAP
@@ -50,9 +50,6 @@ namespace Renci.SshNet
 #else
 #error Accepting new socket connections is not implemented.
 #endif
-
-                        // wait until listener is stopped
-                        _listenerTaskCompleted.WaitOne();
                     }
                     catch (ObjectDisposedException)
                     {
@@ -61,62 +58,65 @@ namespace Renci.SshNet
                         //
                         // As we start accepting connection on a separate thread, this is possible
                         // when the listener is stopped right after it was started.
-
-                        // mark listener stopped
-                        _listenerTaskCompleted.Set();
                     }
-                    catch (Exception ex)
-                    {
-                        RaiseExceptionEvent(ex);
 
-                        // mark listener stopped
-                        _listenerTaskCompleted.Set();
-                    }
-                    finally
+                    // wait until listener is stopped
+                    _listenerTaskCompleted.WaitOne();
+
+                    var session = Session;
+                    if (session != null)
                     {
-                        var session = Session;
-                        if (session != null)
-                        {
-                            session.Disconnected -= Session_Disconnected;
-                            session.ErrorOccured -= Session_ErrorOccured;
-                        }
+                        session.Disconnected -= Session_Disconnected;
+                        session.ErrorOccured -= Session_ErrorOccured;
                     }
                 });
         }
 
 #if FEATURE_SOCKET_EAP
-        private void StartAccept()
+        private void StartAccept(SocketAsyncEventArgs e)
         {
-            var args = new SocketAsyncEventArgs();
-            args.Completed += AcceptCompleted;
+            if (e == null)
+            {
+                e = new SocketAsyncEventArgs();
+                e.Completed += AcceptCompleted;
+            }
+            else
+            {
+                // clear the socket as we're reusing the context object
+                e.AcceptSocket = null;
+            }
 
-            if (!_listener.AcceptAsync(args))
+            if (!_listener.AcceptAsync(e))
             {
-                AcceptCompleted(null, args);
+                AcceptCompleted(null, e);
             }
         }
 
+
         private void AcceptCompleted(object sender, SocketAsyncEventArgs acceptAsyncEventArgs)
-        {
+        { 
             if (acceptAsyncEventArgs.SocketError == SocketError.OperationAborted)
             {
                 // server was stopped
                 return;
             }
 
+            // capture client socket
+            var clientSocket = acceptAsyncEventArgs.AcceptSocket;
+
             if (acceptAsyncEventArgs.SocketError != SocketError.Success)
             {
                 // accept new connection
-                StartAccept();
-                // dispose broken socket
-                acceptAsyncEventArgs.AcceptSocket.Dispose();
+                StartAccept(acceptAsyncEventArgs);
+                // close client socket
+                CloseClientSocket(clientSocket);
                 return;
             }
 
             // accept new connection
-            StartAccept();
+            StartAccept(acceptAsyncEventArgs);
             // process connection
-            ProcessAccept(acceptAsyncEventArgs.AcceptSocket);
+            ProcessAccept(clientSocket);
         }
 #elif FEATURE_SOCKET_APM
         private void AcceptCallback(IAsyncResult ar)
@@ -144,9 +144,31 @@ namespace Renci.SshNet
         }
 #endif
 
+        private static void CloseClientSocket(Socket clientSocket)
+        {
+            if (clientSocket.Connected)
+            {
+                try
+                {
+                    clientSocket.Shutdown(SocketShutdown.Send);
+                }
+                catch (Exception)
+                {
+                    // ignore exception when client socket was already closed
+                }
+            }
+
+            clientSocket.Dispose();
+        }
+
         private void ProcessAccept(Socket clientSocket)
         {
-            Interlocked.Increment(ref _pendingRequests);
+            // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
+            // that same instance; the instance field for the countdown event is re-initialized when the port is restarted
+            // and at that time they may still be pending requests
+            var pendingRequestsCountdown = _pendingRequestsCountdown;
+
+            pendingRequestsCountdown.AddCount();
 
             try
             {
@@ -171,61 +193,71 @@ namespace Renci.SshNet
             catch (Exception exp)
             {
                 RaiseExceptionEvent(exp);
-                CloseSocket(clientSocket);
+                CloseClientSocket(clientSocket);
             }
             finally
             {
-                Interlocked.Decrement(ref _pendingRequests);
+                // take into account that countdown event has since been disposed (after waiting for a given timeout)
+                try
+                {
+                    pendingRequestsCountdown.Signal();
+                }
+                catch (ObjectDisposedException)
+                {
+                }
             }
         }
 
-        private static void CloseSocket(Socket socket)
+        /// <summary>
+        /// Initializes the <see cref="CountdownEvent"/>.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
+        /// of <c>1</c>.
+        /// </para>
+        /// <para>
+        /// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
+        /// initial count of <c>1</c>.
+        /// </para>
+        /// </remarks>
+        private void InitializePendingRequestsCountdown()
         {
-            if (socket.Connected)
+            var original = Interlocked.Exchange(ref _pendingRequestsCountdown, new CountdownEvent(1));
+            if (original != null)
             {
-                socket.Shutdown(SocketShutdown.Both);
-                socket.Dispose();
+                original.Dispose();
             }
         }
 
+        /// <summary>
+        /// Waits for pending requests to finish.
+        /// </summary>
+        /// <param name="timeout">The maximum time to wait for the pending requests to finish.</param>
         partial void InternalStop(TimeSpan timeout)
         {
-            if (timeout == TimeSpan.Zero)
-                return;
-
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
-
-            while (true)
-            {
-                // break out of loop when all pending requests have been processed
-                if (Interlocked.CompareExchange(ref _pendingRequests, 0, 0) == 0)
-                    break;
-                // break out of loop when specified timeout has elapsed
-                if (stopWatch.Elapsed >= timeout && timeout != SshNet.Session.InfiniteTimeSpan)
-                    break;
-                // give channels time to process pending requests
-                ThreadAbstraction.Sleep(50);
-            }
-
-            stopWatch.Stop();
+            _pendingRequestsCountdown.Signal();
+            _pendingRequestsCountdown.Wait(timeout);
         }
 
         /// <summary>
         /// Interrupts the listener, and waits for the listener loop to finish.
         /// </summary>
-        /// <remarks>
-        /// When the forwarded port is stopped, then any further action is skipped.
-        /// </remarks>
         partial void StopListener()
         {
-            if (!IsStarted)
-                return;
-
             // close listener socket
-            _listener.Dispose();
+            var listener = _listener;
+            if (listener != null)
+            {
+                listener.Dispose();
+            }
+
             // allow listener thread to stop
-            _listenerTaskCompleted.Set();
+            var listenerTaskCompleted = _listenerTaskCompleted;
+            if (listenerTaskCompleted != null)
+            {
+                listenerTaskCompleted.Set();
+            }
         }
 
         partial void InternalDispose(bool disposing)
@@ -238,17 +270,30 @@ namespace Renci.SshNet
                     _listener = null;
                     listener.Dispose();
                 }
+
+                var pendingRequestsCountdown = _pendingRequestsCountdown;
+                if (pendingRequestsCountdown != null)
+                {
+                    _pendingRequestsCountdown = null;
+                    pendingRequestsCountdown.Dispose();
+                }
             }
         }
 
         private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
         {
-            StopListener();
+            if (IsStarted)
+            {
+                StopListener();
+            }
         }
 
         private void Session_Disconnected(object sender, EventArgs e)
         {
-            StopListener();
+            if (IsStarted)
+            {
+                StopListener();
+            }
         }
 
         private void Channel_Exception(object sender, ExceptionEventArgs e)

+ 6 - 10
src/Renci.SshNet/ForwardedPortLocal.cs

@@ -115,14 +115,11 @@ namespace Renci.SshNet
         /// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
         protected override void StopPort(TimeSpan timeout)
         {
-            if (IsStarted)
-            {
-                // prevent new requests from getting processed before we signal existing
-                // channels that the port is closing
-                StopListener();
-                // signal existing channels that the port is closing
-                base.StopPort(timeout);
-            }
+            // prevent new requests from getting processed before we signal existing
+            // channels that the port is closing
+            StopListener();
+            // signal existing channels that the port is closing
+            base.StopPort(timeout);
             // wait for open channels to close
             InternalStop(timeout);
         }
@@ -174,6 +171,7 @@ namespace Renci.SshNet
                 return;
 
             base.Dispose(disposing);
+            InternalDispose(disposing);
 
             if (disposing)
             {
@@ -185,8 +183,6 @@ namespace Renci.SshNet
                 }
             }
 
-            InternalDispose(disposing);
-
             _isDisposed = true;
         }
 

+ 53 - 23
src/Renci.SshNet/ForwardedPortRemote.cs

@@ -16,7 +16,7 @@ namespace Renci.SshNet
         private bool _requestStatus;
 
         private EventWaitHandle _globalRequestResponse = new AutoResetEvent(false);
-        private int _pendingRequests;
+        private CountdownEvent _pendingRequestsCountdown;
         private bool _isStarted;
 
         /// <summary>
@@ -141,6 +141,8 @@ namespace Renci.SshNet
             Session.RequestFailureReceived += Session_RequestFailure;
             Session.ChannelOpenReceived += Session_ChannelOpening;
 
+            InitializePendingRequestsCountdown();
+
             // send global request to start direct tcpip
             Session.SendMessage(new GlobalRequestMessage(GlobalRequestName.TcpIpForward, true, BoundHost, BoundPort));
             // wat for response on global request to start direct tcpip
@@ -166,10 +168,6 @@ namespace Renci.SshNet
         /// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
         protected override void StopPort(TimeSpan timeout)
         {
-            // if the port not started, then there's nothing to stop
-            if (!IsStarted)
-                return;
-
             // mark forwarded port stopped, this also causes open of new channels to be rejected
             _isStarted = false;
 
@@ -187,21 +185,9 @@ namespace Renci.SshNet
             Session.RequestFailureReceived -= Session_RequestFailure;
             Session.ChannelOpenReceived -= Session_ChannelOpening;
 
-            var startWaiting = DateTime.Now;
-
-            while (true)
-            {
-                // break out of loop when all pending requests have been processed
-                if (Interlocked.CompareExchange(ref _pendingRequests, 0, 0) == 0)
-                    break;
-                // determine time elapsed since waiting for pending requests to finish
-                var elapsed = DateTime.Now - startWaiting;
-                // break out of loop when specified timeout has elapsed
-                if (elapsed >= timeout && timeout != SshNet.Session.InfiniteTimeSpan)
-                    break;
-                // give channels time to process pending requests
-                ThreadAbstraction.Sleep(50);
-            }
+            // wait for pending requests to complete
+            _pendingRequestsCountdown.Signal();
+            _pendingRequestsCountdown.Wait(timeout);
         }
 
         /// <summary>
@@ -231,13 +217,21 @@ namespace Renci.SshNet
 
                     ThreadAbstraction.ExecuteThread(() =>
                         {
-                            Interlocked.Increment(ref _pendingRequests);
+                            // capture the countdown event that we're adding a count to, as we need to make sure that we'll be signaling
+                            // that same instance; the instance field for the countdown event is re-initialize when the port is restarted
+                            // and that time they may still be pending requests
+                            var pendingRequestsCountdown = _pendingRequestsCountdown;
+
+                            pendingRequestsCountdown.AddCount();
 
                             try
                             {
                                 RaiseRequestReceived(info.OriginatorAddress, info.OriginatorPort);
 
-                                using (var channel = Session.CreateChannelForwardedTcpip(channelOpenMessage.LocalChannelNumber, channelOpenMessage.InitialWindowSize, channelOpenMessage.MaximumPacketSize))
+                                using (
+                                    var channel =
+                                        Session.CreateChannelForwardedTcpip(channelOpenMessage.LocalChannelNumber,
+                                            channelOpenMessage.InitialWindowSize, channelOpenMessage.MaximumPacketSize))
                                 {
                                     channel.Exception += Channel_Exception;
                                     channel.Bind(new IPEndPoint(HostAddress, (int) Port), this);
@@ -250,13 +244,42 @@ namespace Renci.SshNet
                             }
                             finally
                             {
-                                Interlocked.Decrement(ref _pendingRequests);
+                                // take into account that countdown event has since been disposed (after waiting for a given timeout)
+                                try
+                                {
+                                    pendingRequestsCountdown.Signal();
+                                }
+                                catch (ObjectDisposedException)
+                                {
+                                }
                             }
                         });
                 }
             }
         }
 
+        /// <summary>
+        /// Initializes the <see cref="CountdownEvent"/>.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// When the port is started for the first time, a <see cref="CountdownEvent"/> is created with an initial count
+        /// of <c>1</c>.
+        /// </para>
+        /// <para>
+        /// On subsequent (re)starts, we'll dispose the current <see cref="CountdownEvent"/> and create a new one with
+        /// initial count of <c>1</c>.
+        /// </para>
+        /// </remarks>
+        private void InitializePendingRequestsCountdown()
+        {
+            var original = Interlocked.Exchange(ref _pendingRequestsCountdown, new CountdownEvent(1));
+            if (original != null)
+            {
+                original.Dispose();
+            }
+        }
+
         private void Channel_Exception(object sender, ExceptionEventArgs exceptionEventArgs)
         {
             RaiseExceptionEvent(exceptionEventArgs.Exception);
@@ -320,6 +343,13 @@ namespace Renci.SshNet
                     _globalRequestResponse = null;
                     globalRequestResponse.Dispose();
                 }
+
+                var pendingRequestsCountdown = _pendingRequestsCountdown;
+                if (pendingRequestsCountdown != null)
+                {
+                    _pendingRequestsCountdown = null;
+                    pendingRequestsCountdown.Dispose();
+                }
             }
 
             _isDisposed = true;

+ 3 - 2
src/Renci.SshNet/Renci.SshNet.csproj

@@ -18,7 +18,7 @@
     <DebugType>full</DebugType>
     <Optimize>false</Optimize>
     <OutputPath>bin\Debug\</OutputPath>
-    <DefineConstants>TRACE;DEBUG;FEATURE_REGEX_COMPILE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_DNS_SYNC;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII</DefineConstants>
+    <DefineConstants>TRACE;DEBUG;FEATURE_REGEX_COMPILE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_DNS_SYNC;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <DocumentationFile>bin\Debug\Renci.SshNet.xml</DocumentationFile>
@@ -29,7 +29,7 @@
     <DebugType>none</DebugType>
     <Optimize>true</Optimize>
     <OutputPath>bin\Release\</OutputPath>
-    <DefineConstants>FEATURE_REGEX_COMPILE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_DNS_SYNC;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII</DefineConstants>
+    <DefineConstants>FEATURE_REGEX_COMPILE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_DNS_SYNC;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <DocumentationFile>bin\Release\Renci.SshNet.xml</DocumentationFile>
@@ -95,6 +95,7 @@
     <Compile Include="Common\ChannelOpenConfirmedEventArgs.cs" />
     <Compile Include="Common\ChannelOpenFailedEventArgs.cs" />
     <Compile Include="Common\ChannelRequestEventArgs.cs" />
+    <Compile Include="Common\CountdownEvent.cs" />
     <Compile Include="Common\ProxyException.cs">
       <SubType>Code</SubType>
     </Compile>