Files
Rob Hague 89f378a0ab Keep APM and sync UploadFile/DownloadFile callbacks on the threadpool (#1805)
* Tweak internal IProgress usage for APM and sync UploadFile/DownloadFile

Changes to support IProgress<> callback on UploadAsync/DownloadAsync meant wrapping
the Action<> callback on existing methods in a Progress<>, which posts the callback
onto the current synchronisation context rather than the threadpool. For the legacy
APM methods (Begin[..]), let's just preserve their old behaviour.

For the synchronous methods, posting to the synchronisation context is probably the
worst choice (because if there is one, the method itself is running there). We can
either revert to the threadpool as well, or take the opportunity to invoke the
callback synchronously, which is a behavioural change but probably the least
surprising behaviour for a synchronous method.

* keep callbacks on the threadpool

Actually, we could call the Download callback synchronously easily enough, but the Upload progress
reports are being made on the message listener thread upon request ack. A more involved
scheme could drain callbacks to fire during the read loop. For now just make it all the
same behaviour as in 2025.1.0.
2026-06-26 19:02:09 +02:00

527 lines
20 KiB
C#

using Renci.SshNet.Common;
using Renci.SshNet.Sftp;
namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
{
/// <summary>
/// Implementation of the SSH File Transfer Protocol (SFTP) over SSH.
/// </summary>
public partial class SftpClientTest : IntegrationTestBase
{
[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_Upload_And_Download_1MB_File()
{
RemoveAllFiles();
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
var uploadedFileName = Path.GetTempFileName();
var remoteFileName = Path.GetRandomFileName();
CreateTestFile(uploadedFileName, 1);
// Calculate has value
var uploadedHash = CalculateMD5(uploadedFileName);
using (var file = File.OpenRead(uploadedFileName))
{
sftp.UploadFile(file, remoteFileName);
}
var downloadedFileName = Path.GetTempFileName();
using (var file = File.OpenWrite(downloadedFileName))
{
sftp.DownloadFile(remoteFileName, file);
}
var downloadedHash = CalculateMD5(downloadedFileName);
sftp.DeleteFile(remoteFileName);
File.Delete(uploadedFileName);
File.Delete(downloadedFileName);
sftp.Disconnect();
Assert.AreEqual(uploadedHash, downloadedHash);
}
}
[TestMethod]
[TestCategory("Sftp")]
public async Task Test_Sftp_Upload_And_Download_Async_1MB_File()
{
RemoveAllFiles();
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
await sftp.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
var uploadedFileName = Path.GetTempFileName();
var remoteFileName = Path.GetRandomFileName();
CreateTestFile(uploadedFileName, 1);
// Calculate has value
var uploadedHash = CalculateMD5(uploadedFileName);
using (var file = File.OpenRead(uploadedFileName))
{
await sftp.UploadFileAsync(file, remoteFileName).ConfigureAwait(false);
}
// uploading again should not throw because of the default canOverride = true
using (var file = File.OpenRead(uploadedFileName))
{
await sftp.UploadFileAsync(file, remoteFileName).ConfigureAwait(false);
}
// uploading with canOverride = false should throw because the file already exists
using (var file = File.OpenRead(uploadedFileName))
{
await Assert.ThrowsAsync<SftpException>(async () => await sftp.UploadFileAsync(file, remoteFileName, canOverride: false).ConfigureAwait(false));
}
var downloadedFileName = Path.GetTempFileName();
using (var file = File.OpenWrite(downloadedFileName))
{
await sftp.DownloadFileAsync(remoteFileName, file).ConfigureAwait(false);
}
var downloadedHash = CalculateMD5(downloadedFileName);
await sftp.DeleteFileAsync(remoteFileName, CancellationToken.None).ConfigureAwait(false);
File.Delete(uploadedFileName);
File.Delete(downloadedFileName);
sftp.Disconnect();
Assert.AreEqual(uploadedHash, downloadedHash);
}
}
[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_Upload_Forbidden()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
var uploadedFileName = Path.GetTempFileName();
var remoteFileName = "/root/1";
CreateTestFile(uploadedFileName, 1);
using (var file = File.OpenRead(uploadedFileName))
{
Assert.ThrowsExactly<SftpPermissionDeniedException>(() => sftp.UploadFile(file, remoteFileName));
}
sftp.Disconnect();
}
}
[TestMethod]
[TestCategory("Sftp")]
public async Task Test_Sftp_UploadAsync_Cancellation_Requested()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
await sftp.ConnectAsync(CancellationToken.None);
var uploadedFileName = Path.GetTempFileName();
var remoteFileName = "/root/1";
CreateTestFile(uploadedFileName, 1);
var cancelledToken = new CancellationToken(true);
using (var file = File.OpenRead(uploadedFileName))
{
await Assert.ThrowsAsync<OperationCanceledException>(() => sftp.UploadFileAsync(file, remoteFileName, cancelledToken));
}
}
}
[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each()
{
var maxFiles = 10;
var maxSize = 5;
RemoveAllFiles();
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.OperationTimeout = TimeSpan.FromMinutes(1);
sftp.Connect();
var testInfoList = new Dictionary<string, TestInfo>();
for (var i = 0; i < maxFiles; i++)
{
var testInfo = new TestInfo
{
UploadedFileName = Path.GetTempFileName(),
DownloadedFileName = Path.GetTempFileName(),
RemoteFileName = Path.GetRandomFileName()
};
CreateTestFile(testInfo.UploadedFileName, maxSize);
// Calculate hash value
testInfo.UploadedHash = CalculateMD5(testInfo.UploadedFileName);
testInfoList.Add(testInfo.RemoteFileName, testInfo);
}
var uploadWaitHandles = new List<WaitHandle>();
// Start file uploads
foreach (var remoteFile in testInfoList.Keys)
{
var testInfo = testInfoList[remoteFile];
testInfo.UploadedFile = File.OpenRead(testInfo.UploadedFileName);
testInfo.UploadResult = sftp.BeginUploadFile(testInfo.UploadedFile,
remoteFile,
null,
null) as SftpUploadAsyncResult;
uploadWaitHandles.Add(testInfo.UploadResult.AsyncWaitHandle);
}
// Wait for upload to finish
var uploadCompleted = false;
while (!uploadCompleted)
{
// Assume upload completed
uploadCompleted = true;
foreach (var testInfo in testInfoList.Values)
{
var sftpResult = testInfo.UploadResult;
if (!testInfo.UploadResult.IsCompleted)
{
uploadCompleted = false;
}
}
Thread.Sleep(500);
}
// End file uploads
foreach (var remoteFile in testInfoList.Keys)
{
var testInfo = testInfoList[remoteFile];
sftp.EndUploadFile(testInfo.UploadResult);
testInfo.UploadedFile.Dispose();
}
// Start file downloads
var downloadWaitHandles = new List<WaitHandle>();
foreach (var remoteFile in testInfoList.Keys)
{
var testInfo = testInfoList[remoteFile];
testInfo.DownloadedFile = File.OpenWrite(testInfo.DownloadedFileName);
testInfo.DownloadResult = sftp.BeginDownloadFile(remoteFile,
testInfo.DownloadedFile,
null,
null) as SftpDownloadAsyncResult;
downloadWaitHandles.Add(testInfo.DownloadResult.AsyncWaitHandle);
}
// Wait for download to finish
var downloadCompleted = false;
while (!downloadCompleted)
{
// Assume download completed
downloadCompleted = true;
foreach (var testInfo in testInfoList.Values)
{
var sftpResult = testInfo.DownloadResult;
if (!testInfo.DownloadResult.IsCompleted)
{
downloadCompleted = false;
}
}
Thread.Sleep(500);
}
var hashMatches = true;
var uploadDownloadSizeOk = true;
// End file downloads
foreach (var remoteFile in testInfoList.Keys)
{
var testInfo = testInfoList[remoteFile];
sftp.EndDownloadFile(testInfo.DownloadResult);
testInfo.DownloadedFile.Dispose();
testInfo.DownloadedHash = CalculateMD5(testInfo.DownloadedFileName);
Console.WriteLine(remoteFile);
Console.WriteLine("UploadedBytes: " + testInfo.UploadResult.UploadedBytes);
Console.WriteLine("DownloadedBytes: " + testInfo.DownloadResult.DownloadedBytes);
Console.WriteLine("UploadedHash: " + testInfo.UploadedHash);
Console.WriteLine("DownloadedHash: " + testInfo.DownloadedHash);
if (!(testInfo.UploadResult.UploadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes == testInfo.UploadResult.UploadedBytes))
{
uploadDownloadSizeOk = false;
}
if (!testInfo.DownloadedHash.Equals(testInfo.UploadedHash))
{
hashMatches = false;
}
}
// Clean up after test
foreach (var remoteFile in testInfoList.Keys)
{
var testInfo = testInfoList[remoteFile];
sftp.DeleteFile(remoteFile);
File.Delete(testInfo.UploadedFileName);
File.Delete(testInfo.DownloadedFileName);
}
sftp.Disconnect();
Assert.IsTrue(hashMatches, "Hash does not match");
Assert.IsTrue(uploadDownloadSizeOk, "Uploaded and downloaded bytes does not match");
}
}
// TODO: Split this test into multiple tests
[TestMethod]
[TestCategory("Sftp")]
[Description("Test that delegates passed to BeginUploadFile, BeginDownloadFile and BeginListDirectory are actually called.")]
public void Test_Sftp_Ensure_Async_Delegates_Called_For_BeginFileUpload_BeginFileDownload_BeginListDirectory()
{
RemoveAllFiles();
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
var remoteFileName = Path.GetRandomFileName();
var localFileName = Path.GetRandomFileName();
using var uploadDelegateEvent = new ManualResetEventSlim();
using var downloadDelegateEvent = new ManualResetEventSlim();
using var listDirectoryDelegateEvent = new ManualResetEventSlim();
using var uploadCallbackEvent = new ManualResetEventSlim();
using var downloadCallbackEvent = new ManualResetEventSlim();
using var listDirectoryCallbackEvent = new ManualResetEventSlim();
IAsyncResult asyncResult;
// Test for BeginUploadFile.
CreateTestFile(localFileName, 1);
var originalContext = SynchronizationContext.Current;
try
{
// Set a throwing context to verify it's not captured by the callback
SynchronizationContext.SetSynchronizationContext(new ThrowingSynchronizationContext());
using (var fileStream = File.OpenRead(localFileName))
{
asyncResult = sftp.BeginUploadFile(fileStream,
remoteFileName,
delegate (IAsyncResult ar)
{
uploadDelegateEvent.Set();
},
state: null,
uploadCallback: _ => uploadCallbackEvent.Set());
sftp.EndUploadFile(asyncResult);
}
}
finally
{
SynchronizationContext.SetSynchronizationContext(originalContext);
}
File.Delete(localFileName);
Assert.IsTrue(uploadDelegateEvent.Wait(1000));
Assert.IsTrue(uploadCallbackEvent.Wait(1000));
// Test for BeginDownloadFile.
asyncResult = null;
try
{
// Set a throwing context to verify it's not captured by the callback
SynchronizationContext.SetSynchronizationContext(new ThrowingSynchronizationContext());
using (var fileStream = File.OpenWrite(localFileName))
{
asyncResult = sftp.BeginDownloadFile(remoteFileName,
fileStream,
delegate (IAsyncResult ar)
{
downloadDelegateEvent.Set();
},
state: null,
downloadCallback: _ => downloadCallbackEvent.Set());
sftp.EndDownloadFile(asyncResult);
}
}
finally
{
SynchronizationContext.SetSynchronizationContext(originalContext);
}
File.Delete(localFileName);
Assert.IsTrue(downloadDelegateEvent.Wait(1000));
Assert.IsTrue(downloadCallbackEvent.Wait(1000));
// Test for BeginListDirectory.
try
{
// Set a throwing context to verify it's not captured by the callback
SynchronizationContext.SetSynchronizationContext(new ThrowingSynchronizationContext());
asyncResult = sftp.BeginListDirectory(sftp.WorkingDirectory,
delegate (IAsyncResult ar)
{
listDirectoryDelegateEvent.Set();
},
state: null,
listCallback: _ => listDirectoryCallbackEvent.Set());
_ = sftp.EndListDirectory(asyncResult);
}
finally
{
SynchronizationContext.SetSynchronizationContext(originalContext);
}
Assert.IsTrue(listDirectoryDelegateEvent.Wait(1000));
Assert.IsTrue(listDirectoryCallbackEvent.Wait(1000));
}
}
[TestMethod]
[TestCategory("Sftp")]
[Description("Test passing null to BeginUploadFile")]
public void Test_Sftp_BeginUploadFile_StreamIsNull()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
Assert.ThrowsExactly<ArgumentNullException>(() => sftp.BeginUploadFile(null, "aaaaa", null, null));
}
}
[TestMethod]
[TestCategory("Sftp")]
[Description("Test passing null to BeginUploadFile")]
public void Test_Sftp_BeginUploadFile_FileNameIsWhiteSpace()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
Assert.ThrowsExactly<ArgumentException>(() => sftp.BeginUploadFile(new MemoryStream(), " ", null, null));
}
}
[TestMethod]
[TestCategory("Sftp")]
[Description("Test passing null to BeginUploadFile")]
public void Test_Sftp_BeginUploadFile_FileNameIsNull()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
Assert.ThrowsExactly<ArgumentNullException>(() => sftp.BeginUploadFile(new MemoryStream(), null, null, null));
}
}
[TestMethod]
[TestCategory("Sftp")]
public void Test_Sftp_EndUploadFile_Invalid_Async_Handle()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
sftp.Connect();
var async1 = sftp.BeginListDirectory("/", null, null);
var filename = Path.GetTempFileName();
CreateTestFile(filename, 100);
using var fileStream = File.OpenRead(filename);
var async2 = sftp.BeginUploadFile(fileStream, "test", null, null);
Assert.ThrowsExactly<ArgumentException>(() => sftp.EndUploadFile(async1));
}
}
[TestMethod]
[TestCategory("Sftp")]
public async Task Test_Sftp_UploadFileAsync_UploadProgress()
{
using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
{
await sftp.ConnectAsync(CancellationToken.None);
var filename = Path.GetTempFileName();
int testFileSizeMB = 1;
CreateTestFile(filename, testFileSizeMB);
using var fileStream = File.OpenRead(filename);
using ManualResetEventSlim finalCallbackCalledEvent = new();
IProgress<UploadFileProgressReport> progress = new Progress<UploadFileProgressReport>(r =>
{
if ((int)r.TotalBytesUploaded == testFileSizeMB * 1024 * 1024)
{
finalCallbackCalledEvent.Set();
}
});
await sftp.UploadFileAsync(fileStream, "test", progress);
// since the callback is queued to the thread pool, wait for the event.
bool callbackCalled = finalCallbackCalledEvent.Wait(5000);
Assert.IsTrue(callbackCalled);
}
}
private sealed class ThrowingSynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
throw new InvalidOperationException();
}
public override void Send(SendOrPostCallback d, object state)
{
throw new InvalidOperationException();
}
}
}
}