For me, connecting to a recently built Debian Linux box to download a single file, the ScpClient.ReadByte(Stream) hangs due to an infinite loop waiting for more bytes on the stream, that never come.
The while loop smells, but that wasn't the fix. I didn't want to touch that vital mechanism. I could see that there is a check in the InternalDownload(...) method that will break out of the (different) loop when the download is just for a file.
This check is done using the strong type of FileInfo in the FileSystemInfo instance sent in. For me, this is a DirectoryInfo all the time, since my Windows client copy command doesn't know whether the Linux path is a file or folder (as it doesn't have access to Linux to see). It's PowerShell...
For example: Copy-FromSshSession -ComputerName uktnlx01 -RemotePath /boot/grub/chain.mod -LocalPath c:\
So the user (me) supplies a DirectoryInfo object of the destination folder to copy the Linux path to, be it a file or folder.
Anyway, the InternalDownload method will never see a FileInfo so it keep looping for more. I solved it by adding a boolean flag that is set when the loop sees a directory info message, thus indicating that the download is for an entire folder, i.e. I've let the Linux messages inform the logic, not the client.
Here's the code: Don't know mark-down so I'll try and pretty it up after posting:
private void InternalDownload(ChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
{
DateTime modifiedTime = DateTime.Now;
DateTime accessedTime = DateTime.Now;
var startDirectoryFullName = fileSystemInfo.FullName;
var currentDirectoryFullName = startDirectoryFullName;
var directoryCounter = 0;
bool hasDirectoryMessageBeenSeen = false;
while (true)
{
var message = ReadString(input);
if (message == "E")
{
this.SendConfirmation(channel); // Send reply
directoryCounter--;
currentDirectoryFullName = new DirectoryInfo(currentDirectoryFullName).Parent.FullName;
//if (currentDirectoryFullName == startDirectoryFullName)
if (directoryCounter == 0)
break;
else
continue;
}
var match = _directoryInfoRe.Match(message);
if (match.Success)
{
hasDirectoryMessageBeenSeen = true;
this.SendConfirmation(channel); // Send reply
// Read directory
var mode = long.Parse(match.Result("${mode}"));
var filename = match.Result("${filename}");
DirectoryInfo newDirectoryInfo;
if (directoryCounter > 0)
{
newDirectoryInfo = Directory.CreateDirectory(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, filename));
newDirectoryInfo.LastAccessTime = accessedTime;
newDirectoryInfo.LastWriteTime = modifiedTime;
}
else
{
// Dont create directory for first level
newDirectoryInfo = fileSystemInfo as DirectoryInfo;
}
directoryCounter++;
currentDirectoryFullName = newDirectoryInfo.FullName;
continue;
}
match = _fileInfoRe.Match(message);
if (match.Success)
{
// Read file
this.SendConfirmation(channel); // Send reply
var mode = match.Result("${mode}");
var length = long.Parse(match.Result("${length}"));
var fileName = match.Result("${filename}");
var fileInfo = fileSystemInfo as FileInfo;
if (fileInfo == null)
fileInfo = new FileInfo(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, fileName));
using (var output = fileInfo.OpenWrite())
{
this.InternalFileDownload(channel, input, output, fileName, length);
}
fileInfo.LastAccessTime = accessedTime;
fileInfo.LastWriteTime = modifiedTime;
// Fixes bug. Needs to exit loop after single file copy, so to determine if the copy task was for a single file
// we check whether a directory message has not been seen, which will only happen on single file copy.
//
if (!hasDirectoryMessageBeenSeen)
break;
continue;
}
match = _timestampRe.Match(message);
if (match.Success)
{
// Read timestamp
this.SendConfirmation(channel); // Send reply
var mtime = long.Parse(match.Result("${mtime}"));
var atime = long.Parse(match.Result("${atime}"));
var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
modifiedTime = zeroTime.AddSeconds(mtime);
accessedTime = zeroTime.AddSeconds(atime);
continue;
}
this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
}
}
The while loop smells, but that wasn't the fix. I didn't want to touch that vital mechanism. I could see that there is a check in the InternalDownload(...) method that will break out of the (different) loop when the download is just for a file.
This check is done using the strong type of FileInfo in the FileSystemInfo instance sent in. For me, this is a DirectoryInfo all the time, since my Windows client copy command doesn't know whether the Linux path is a file or folder (as it doesn't have access to Linux to see). It's PowerShell...
For example: Copy-FromSshSession -ComputerName uktnlx01 -RemotePath /boot/grub/chain.mod -LocalPath c:\
So the user (me) supplies a DirectoryInfo object of the destination folder to copy the Linux path to, be it a file or folder.
Anyway, the InternalDownload method will never see a FileInfo so it keep looping for more. I solved it by adding a boolean flag that is set when the loop sees a directory info message, thus indicating that the download is for an entire folder, i.e. I've let the Linux messages inform the logic, not the client.
Here's the code: Don't know mark-down so I'll try and pretty it up after posting:
private void InternalDownload(ChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
{
DateTime modifiedTime = DateTime.Now;
DateTime accessedTime = DateTime.Now;
var startDirectoryFullName = fileSystemInfo.FullName;
var currentDirectoryFullName = startDirectoryFullName;
var directoryCounter = 0;
bool hasDirectoryMessageBeenSeen = false;
while (true)
{
var message = ReadString(input);
if (message == "E")
{
this.SendConfirmation(channel); // Send reply
directoryCounter--;
currentDirectoryFullName = new DirectoryInfo(currentDirectoryFullName).Parent.FullName;
//if (currentDirectoryFullName == startDirectoryFullName)
if (directoryCounter == 0)
break;
else
continue;
}
var match = _directoryInfoRe.Match(message);
if (match.Success)
{
hasDirectoryMessageBeenSeen = true;
this.SendConfirmation(channel); // Send reply
// Read directory
var mode = long.Parse(match.Result("${mode}"));
var filename = match.Result("${filename}");
DirectoryInfo newDirectoryInfo;
if (directoryCounter > 0)
{
newDirectoryInfo = Directory.CreateDirectory(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, filename));
newDirectoryInfo.LastAccessTime = accessedTime;
newDirectoryInfo.LastWriteTime = modifiedTime;
}
else
{
// Dont create directory for first level
newDirectoryInfo = fileSystemInfo as DirectoryInfo;
}
directoryCounter++;
currentDirectoryFullName = newDirectoryInfo.FullName;
continue;
}
match = _fileInfoRe.Match(message);
if (match.Success)
{
// Read file
this.SendConfirmation(channel); // Send reply
var mode = match.Result("${mode}");
var length = long.Parse(match.Result("${length}"));
var fileName = match.Result("${filename}");
var fileInfo = fileSystemInfo as FileInfo;
if (fileInfo == null)
fileInfo = new FileInfo(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, fileName));
using (var output = fileInfo.OpenWrite())
{
this.InternalFileDownload(channel, input, output, fileName, length);
}
fileInfo.LastAccessTime = accessedTime;
fileInfo.LastWriteTime = modifiedTime;
// Fixes bug. Needs to exit loop after single file copy, so to determine if the copy task was for a single file
// we check whether a directory message has not been seen, which will only happen on single file copy.
//
if (!hasDirectoryMessageBeenSeen)
break;
continue;
}
match = _timestampRe.Match(message);
if (match.Success)
{
// Read timestamp
this.SendConfirmation(channel); // Send reply
var mtime = long.Parse(match.Result("${mtime}"));
var atime = long.Parse(match.Result("${atime}"));
var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
modifiedTime = zeroTime.AddSeconds(mtime);
accessedTime = zeroTime.AddSeconds(atime);
continue;
}
this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
}
}