Skip to content

Commit fbedaab

Browse files
authored
Fix sftp async methods not observing error conditions (#1510)
* Fix sftp async methods not observing error conditions * Update ISftpClient
1 parent 1a8839e commit fbedaab

File tree

7 files changed

+570
-343
lines changed

7 files changed

+570
-343
lines changed

src/Renci.SshNet/ISftpClient.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ public interface ISftpClient : IBaseClient
5656
/// The timeout to wait until an operation completes. The default value is negative
5757
/// one (-1) milliseconds, which indicates an infinite timeout period.
5858
/// </value>
59-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
6059
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> represents a value that is less than -1 or greater than <see cref="int.MaxValue"/> milliseconds.</exception>
6160
TimeSpan OperationTimeout { get; set; }
6261

src/Renci.SshNet/ISubsystemSession.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ namespace Renci.SshNet
1111
internal interface ISubsystemSession : IDisposable
1212
{
1313
/// <summary>
14-
/// Gets or set the number of seconds to wait for an operation to complete.
14+
/// Gets or sets the number of milliseconds to wait for an operation to complete.
1515
/// </summary>
1616
/// <value>
17-
/// The number of seconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.
17+
/// The number of milliseconds to wait for an operation to complete, or <c>-1</c> to wait indefinitely.
1818
/// </value>
19-
int OperationTimeout { get; }
19+
int OperationTimeout { get; set; }
2020

2121
/// <summary>
2222
/// Gets a value indicating whether this session is open.

src/Renci.SshNet/Sftp/SftpSession.cs

Lines changed: 238 additions & 295 deletions
Large diffs are not rendered by default.

src/Renci.SshNet/SftpClient.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,21 @@ public class SftpClient : BaseClient, ISftpClient
4646
/// The timeout to wait until an operation completes. The default value is negative
4747
/// one (-1) milliseconds, which indicates an infinite timeout period.
4848
/// </value>
49-
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
5049
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> represents a value that is less than -1 or greater than <see cref="int.MaxValue"/> milliseconds.</exception>
5150
public TimeSpan OperationTimeout
5251
{
5352
get
5453
{
55-
CheckDisposed();
56-
5754
return TimeSpan.FromMilliseconds(_operationTimeout);
5855
}
5956
set
6057
{
61-
CheckDisposed();
62-
6358
_operationTimeout = value.AsTimeout(nameof(OperationTimeout));
59+
60+
if (_sftpSession is { } sftpSession)
61+
{
62+
sftpSession.OperationTimeout = _operationTimeout;
63+
}
6464
}
6565
}
6666

src/Renci.SshNet/SubsystemSession.cs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Globalization;
33
using System.Runtime.ExceptionServices;
44
using System.Threading;
5+
using System.Threading.Tasks;
56

67
using Renci.SshNet.Abstractions;
78
using Renci.SshNet.Channels;
@@ -29,13 +30,8 @@ internal abstract class SubsystemSession : ISubsystemSession
2930
private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false);
3031
private bool _isDisposed;
3132

32-
/// <summary>
33-
/// Gets or set the number of seconds to wait for an operation to complete.
34-
/// </summary>
35-
/// <value>
36-
/// The number of seconds to wait for an operation to complete, or -1 to wait indefinitely.
37-
/// </value>
38-
public int OperationTimeout { get; private set; }
33+
/// <inheritdoc/>
34+
public int OperationTimeout { get; set; }
3935

4036
/// <summary>
4137
/// Occurs when an error occurred.
@@ -250,6 +246,59 @@ public void WaitOnHandle(WaitHandle waitHandle, int millisecondsTimeout)
250246
}
251247
}
252248

249+
protected async Task<T> WaitOnHandleAsync<T>(TaskCompletionSource<T> tcs, int millisecondsTimeout, CancellationToken cancellationToken)
250+
{
251+
cancellationToken.ThrowIfCancellationRequested();
252+
253+
var errorOccuredReg = ThreadPool.RegisterWaitForSingleObject(
254+
_errorOccuredWaitHandle,
255+
(tcs, _) => ((TaskCompletionSource<T>)tcs).TrySetException(_exception),
256+
state: tcs,
257+
millisecondsTimeOutInterval: -1,
258+
executeOnlyOnce: true);
259+
260+
var sessionDisconnectedReg = ThreadPool.RegisterWaitForSingleObject(
261+
_sessionDisconnectedWaitHandle,
262+
static (tcs, _) => ((TaskCompletionSource<T>)tcs).TrySetException(new SshException("Connection was closed by the server.")),
263+
state: tcs,
264+
millisecondsTimeOutInterval: -1,
265+
executeOnlyOnce: true);
266+
267+
var channelClosedReg = ThreadPool.RegisterWaitForSingleObject(
268+
_channelClosedWaitHandle,
269+
static (tcs, _) => ((TaskCompletionSource<T>)tcs).TrySetException(new SshException("Channel was closed.")),
270+
state: tcs,
271+
millisecondsTimeOutInterval: -1,
272+
executeOnlyOnce: true);
273+
274+
using var timeoutCts = new CancellationTokenSource(millisecondsTimeout);
275+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
276+
277+
using var tokenReg = linkedCts.Token.Register(
278+
static s =>
279+
{
280+
(var tcs, var cancellationToken) = ((TaskCompletionSource<T>, CancellationToken))s;
281+
_ = tcs.TrySetCanceled(cancellationToken);
282+
},
283+
state: (tcs, cancellationToken),
284+
useSynchronizationContext: false);
285+
286+
try
287+
{
288+
return await tcs.Task.ConfigureAwait(false);
289+
}
290+
catch (OperationCanceledException oce) when (timeoutCts.IsCancellationRequested)
291+
{
292+
throw new SshOperationTimeoutException("Operation has timed out.", oce);
293+
}
294+
finally
295+
{
296+
_ = errorOccuredReg.Unregister(waitObject: null);
297+
_ = sessionDisconnectedReg.Unregister(waitObject: null);
298+
_ = channelClosedReg.Unregister(waitObject: null);
299+
}
300+
}
301+
253302
/// <summary>
254303
/// Blocks the current thread until the specified <see cref="WaitHandle"/> gets signaled, using a
255304
/// 32-bit signed integer to specify the time interval in milliseconds.

test/Renci.SshNet.Tests/Classes/SftpClientTest.cs

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -115,37 +115,5 @@ public void OperationTimeout_GreaterThanLowerLimit()
115115
Assert.AreEqual("OperationTimeout", ex.ParamName);
116116
}
117117
}
118-
119-
[TestMethod]
120-
public void OperationTimeout_Disposed()
121-
{
122-
var connectionInfo = new PasswordConnectionInfo("host", 22, "admin", "pwd");
123-
var target = new SftpClient(connectionInfo);
124-
target.Dispose();
125-
126-
// getter
127-
try
128-
{
129-
var actual = target.OperationTimeout;
130-
Assert.Fail("Should have failed, but returned: " + actual);
131-
}
132-
catch (ObjectDisposedException ex)
133-
{
134-
Assert.IsNull(ex.InnerException);
135-
Assert.AreEqual(typeof(SftpClient).FullName, ex.ObjectName);
136-
}
137-
138-
// setter
139-
try
140-
{
141-
target.OperationTimeout = TimeSpan.FromMilliseconds(5);
142-
Assert.Fail();
143-
}
144-
catch (ObjectDisposedException ex)
145-
{
146-
Assert.IsNull(ex.InnerException);
147-
Assert.AreEqual(typeof(SftpClient).FullName, ex.ObjectName);
148-
}
149-
}
150118
}
151119
}

0 commit comments

Comments
 (0)