Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added cancellation support for ScpClient stream upload/download #856

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Renci.SshNet/Renci.SshNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
<DefineConstants>FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;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;FEATURE_ECDSA</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net40' ">
<DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;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;FEATURE_ECDSA</DefineConstants>
<DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;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;FEATURE_ECDSA;FEATURE_TPL</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
<DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_REFLECTION_TYPEINFO;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_TAP;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512</DefineConstants>
<DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_REFLECTION_TYPEINFO;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_TAP;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_TPL</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP;FEATURE_STREAM_APM;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_ECDSA</DefineConstants>
<DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP;FEATURE_STREAM_APM;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_ECDSA;FEATURE_TPL</DefineConstants>
</PropertyGroup>
</Project>
167 changes: 164 additions & 3 deletions src/Renci.SshNet/ScpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
using System.Net;
using System.Collections.Generic;

#if FEATURE_TPL
using System.Threading;
#endif

namespace Renci.SshNet
{
/// <summary>
Expand Down Expand Up @@ -90,7 +94,7 @@ public IRemotePathTransformation RemotePathTransformation
/// </summary>
public event EventHandler<ScpUploadEventArgs> Uploading;

#region Constructors
#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
Expand Down Expand Up @@ -195,8 +199,10 @@ internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServ
_remotePathTransformation = serviceFactory.CreateRemotePathDoubleQuoteTransformation();
}

#endregion
#endregion


#if FEATURE_TPL
/// <summary>
/// Uploads the specified stream to the remote host.
/// </summary>
Expand All @@ -208,7 +214,71 @@ internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServ
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Upload(Stream source, string path)
{
var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Upload(source, path, CancellationToken.None);
}

#endif

#if FEATURE_TPL
/// <summary>
/// Uploads the specified stream to the remote host.
/// </summary>
/// <param name="source">The <see cref="Stream"/> to upload.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <param name="cancellationToken">cancellation token</param>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
/// <exception cref="OperationCanceledException">Download cancelled exception</exception>
public void Upload(Stream source, string path,CancellationToken cancellationToken )
{
try
{
var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);

using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
using (cancellationToken.Register(() => { channel.Dispose(); input.Dispose(); }))
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.Open();

// Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
// that we expect the target to be a directory.
if (!channel.SendExecRequest(string.Format("scp -t -d {0}",
_remotePathTransformation.Transform(posixPath.Directory))))
{
throw SecureExecutionRequestRejectedException();
}

CheckReturnCode(input);

UploadFileModeAndName(channel, input, source.Length, posixPath.File);
UploadFileContent(channel, input, source, posixPath.File);
}
}
catch(Exception)
{
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException();
else
throw;
}
}
#else
/// <summary>
/// Uploads the specified stream to the remote host.
/// </summary>
/// <param name="source">The <see cref="Stream"/> to upload.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Upload(Stream source, string path)
{
var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);

using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
Expand All @@ -227,8 +297,98 @@ public void Upload(Stream source, string path)
UploadFileModeAndName(channel, input, source.Length, posixPath.File);
UploadFileContent(channel, input, source, posixPath.File);
}

}
#endif

#if FEATURE_TPL
/// <summary>
/// Downloads the specified file from the remote host to the stream.
/// </summary>
/// <param name="filename">A relative or absolute path for the remote file.</param>
/// <param name="destination">The <see cref="Stream"/> to download the remote file to.</param>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="destination"/> is <c>null</c>.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Download(string filename, Stream destination)
{
Download(filename,destination,CancellationToken.None);
}
#endif

#if FEATURE_TPL
/// <summary>
/// Downloads the specified file from the remote host to the stream.
/// </summary>
/// <param name="filename">A relative or absolute path for the remote file.</param>
/// <param name="destination">The <see cref="Stream"/> to download the remote file to.</param>
/// <param name="cancellationToken">cancellation token</param>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="destination"/> is <c>null</c>.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
/// <exception cref="OperationCanceledException">Download cancelled exception</exception>
public void Download(string filename, Stream destination, CancellationToken cancellationToken)
{
if (filename.IsNullOrWhiteSpace())
throw new ArgumentException("filename");

if (destination == null)
throw new ArgumentNullException("destination");

try
{
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
using (cancellationToken.Register(() =>
{
channel.Dispose();
input.Dispose();
}))
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.Open();

// Send channel command request
if (!channel.SendExecRequest(string.Format("scp -f {0}",
_remotePathTransformation.Transform(filename))))
{
throw SecureExecutionRequestRejectedException();
}

SendSuccessConfirmation(channel); // Send reply

var message = ReadString(input);
var match = FileInfoRe.Match(message);

if (match.Success)
{
// Read file
SendSuccessConfirmation(channel); // Send reply

var length = long.Parse(match.Result("${length}"));
var fileName = match.Result("${filename}");

InternalDownload(channel, input, destination, fileName, length);
}
else
{
SendErrorConfirmation(channel,
string.Format("\"{0}\" is not valid protocol message.", message));
}
}
}
catch (Exception)
{
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException();
else
throw;
}
}

#else
/// <summary>
/// Downloads the specified file from the remote host to the stream.
/// </summary>
Expand Down Expand Up @@ -278,6 +438,7 @@ public void Download(string filename, Stream destination)
}
}
}
#endif

/// <summary>
/// Sets mode, size and name of file being upload.
Expand Down