Skip to content

Commit

Permalink
Merge pull request #24 from SuessLabs/feature/AsyncUpload
Browse files Browse the repository at this point in the history
Enhancement includes:
* Fixed: Remote folder pre-cleanup.
* Added: Upload files async to reduce locking of Visual Studio
* Added: Removal of `launch.json` if it previously existed
* Added: More output logging.
* Update: v1.8.1
* Update: Enhanced Output 
* Updated: Output Window drop-down title to "Linux Debugger" from "Remote Debugger"
  • Loading branch information
DamianSuess authored May 2, 2022
2 parents edf6a0b + 88f2abc commit 91740b3
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 83 deletions.
4 changes: 2 additions & 2 deletions src/VsLinuxDebugger/Commands.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ private async Task<bool> ExecuteBuildAsync(BuildOptions buildOptions)

if (!dbg.IsProjectValid())
{
Console.WriteLine("No C# startup project/solution loaded.");
Logger.Output("No C# startup project/solution loaded.");
return false;
}

if (!await dbg.BeginAsync(buildOptions))
{
Console.WriteLine("Failed to perform actions.");
Logger.Output("Failed to perform actions.");
return false;
}

Expand Down
11 changes: 5 additions & 6 deletions src/VsLinuxDebugger/Core/LaunchBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,10 @@ public LaunchBuilder(DTE2 dte, Project dteProject, UserOptions o)
public string ProjectName { get; set; }

/// <summary>Full path to the remote assembly. (i.e. `/home/USER/VLSDbg/Proj/ConsoleApp1.dll`)</summary>
public string RemoteDeployAppPath => LinuxPath.Combine(RemoteDeployFolder, $"{AssemblyName}.dll");
public string RemoteDeployAssemblyFilePath => LinuxPath.Combine(RemoteDeployProjectFolder, $"{AssemblyName}.dll");

/// <summary>Folder of our remote assembly. (i.e. `/home/USER/VLSDbg/Proj`)</summary>
public string RemoteDeployFolder =>
LinuxPath.Combine(_opts.RemoteDeployBasePath, ProjectName);
public string RemoteDeployProjectFolder => LinuxPath.Combine(_opts.RemoteDeployBasePath, ProjectName);

public string RemoteDotNetPath => _opts.RemoteDotNetPath;

Expand Down Expand Up @@ -90,7 +89,7 @@ public string GenerateLaunchJson(bool vsdbgLogging = false)

var vsdbgLogPath = "";
if (vsdbgLogging)
vsdbgLogPath = $" --engineLogging={LinuxPath.Combine(RemoteDeployFolder, "_vsdbg.log")}";
vsdbgLogPath = $" --engineLogging={LinuxPath.Combine(RemoteDeployProjectFolder, "_vsdbg.log")}";

if (!_opts.LocalPlinkEnabled)
{
Expand Down Expand Up @@ -119,7 +118,7 @@ public string GenerateLaunchJson(bool vsdbgLogging = false)
var obj = new Launch(
RemoteDotNetPath,
$"{AssemblyName}.dll", /// RemoteDeployAppPath,
RemoteDeployFolder,
RemoteDeployProjectFolder,
default,
false)
{
Expand All @@ -142,7 +141,7 @@ public string GenerateLaunchJson(bool vsdbgLogging = false)
}
catch (Exception ex)
{
Console.WriteLine($"Error writing 'launch.json' to path, '{outputPath}'!\n{ex.Message}");
Logger.Output($"Error writing 'launch.json' to path, '{outputPath}'!\n{ex.Message}");
outputPath = string.Empty;
}

Expand Down
2 changes: 1 addition & 1 deletion src/VsLinuxDebugger/Core/RemoteDebugger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public async Task<bool> BeginAsync(BuildOptions buildOptions)
{
if (!ssh.Connect())
{
Console.WriteLine("Could not connect to remote device.");
Logger.Output("Could not connect to remote device.");
return false;
}

Expand Down
149 changes: 81 additions & 68 deletions src/VsLinuxDebugger/Core/SshTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Renci.SshNet;
using SharpCompress.Common;
using SharpCompress.Writers;
Expand Down Expand Up @@ -55,12 +53,19 @@ public string Bash(string command)
/// <param name="fullScrub">Clear entire base deployment folder (TRUE) or just our project.</param>
public void CleanDeploymentFolder(bool fullScrub = false)
{
//// Bash($"sudo rm -rf {_opts.RemoteDeployBasePath}/*");

// Whole deployment folder and hidden files
// rm -rf xxx/* == Contents of the folder but not the folder itself
// rm -rf xxx/{*,.*} == All hidden files and folders
var filesAndFolders = "{*,.*}";

if (fullScrub)
Bash($"rm -rf {_opts.RemoteDeployBasePath}/*");
{
Bash($"rm -rf \"{_opts.RemoteDeployBasePath}/{filesAndFolders}\"");
}
else
Bash($"rm -rf \"{_launch.RemoteDeployAppPath}/*\"");
{
Bash($"rm -rf {_launch.RemoteDeployProjectFolder}/{filesAndFolders}");
}
}

public bool Connect()
Expand All @@ -76,7 +81,6 @@ public bool Connect()
else
keyFile = new PrivateKeyFile(_opts.UserPrivateKeyPath, _opts.UserPrivateKeyPassword);
}

}
catch (Exception ex)
{
Expand Down Expand Up @@ -177,30 +181,28 @@ public async Task<string> UploadFilesAsync()
{
try
{
Bash($@"mkdir -p {_launch.RemoteDeployFolder}");
// Clean output folder just incase
//// Bash($@"rm -rf {_launch.RemoteDeployFolder}");

// TODO: Rev1 - Iterate through each file and upload it via SCP client or SFTP.
// TODO: Rev2 - Compress _localHost.OutputDirFullName, upload ZIP, and unzip it.
// TODO: Rev3 - Allow for both SFTP and SCP as a backup. This separating connection to a new disposable class.
//// LogOutput($"Connected to {_connectionInfo.Username}@{_connectionInfo.Host}:{_connectionInfo.Port} via SSH and {(_sftpClient != null ? "SFTP" : "SCP")}");
//// Logger.Output($"Connected to {_connectionInfo.Username}@{_connectionInfo.Host}:{_connectionInfo.Port} via SSH and {(_sftpClient != null ? "SFTP" : "SCP")}");

Bash($@"mkdir -p {_launch.RemoteDeployFolder}");
Bash($@"mkdir -p {_launch.RemoteDeployProjectFolder}");

var srcDirInfo = new DirectoryInfo(_launch.OutputDirFullPath);
if (!srcDirInfo.Exists)
throw new DirectoryNotFoundException($"Directory '{_launch.OutputDirFullPath}' not found!");

// Compress files to upload as single `tar.gz`.
// TODO: Use base folder path: var pathTarGz = $"{_opts.RemoteDeployBasePath}/{_tarGzFileName}";

//// var destTarGz = $"{RemoteDeployPath}/{_tarGzFileName}";
var destTarGz = LinuxPath.Combine(_launch.RemoteDeployFolder, _tarGzFileName);
var destTarGz = LinuxPath.Combine(_launch.RemoteDeployProjectFolder, _tarGzFileName);
Logger.Output($"Destination Tar.GZ: '{destTarGz}'");

var success = PayloadCompressAndUpload(_sftp, srcDirInfo, destTarGz);
var success = await PayloadCompressAndUploadAsync(_sftp, srcDirInfo, destTarGz);

// Decompress file
PayloadDecompress(destTarGz, false);
await PayloadDecompressAsync(destTarGz, false);

return string.Empty;
}
Expand Down Expand Up @@ -295,93 +297,98 @@ private ConcurrentDictionary<string, FileInfo> GetLocalFiles(DirectoryInfo srcDi
return localFileCache;
}

private void LogOutput(string message)
{
Console.WriteLine($">> {message}");
Logger.Output(message);
}

/// <summary>Compress build contents and upload to remote host.</summary>
/// <param name="sftp">SFTP connection.</param>
/// <param name="srcDirInfo">Build (source) contents directory info.</param>
/// <param name="pathBuildTarGz">Upload path and filename of build's tar.gz file.</param>
/// <returns></returns>
private bool PayloadCompressAndUpload(SftpClient sftp, DirectoryInfo srcDirInfo, string pathBuildTarGz)
private async Task<bool> PayloadCompressAndUploadAsync(SftpClient sftp, DirectoryInfo srcDirInfo, string pathBuildTarGz)
{
var success = false;
var localFiles = GetLocalFiles(srcDirInfo);

// TODO: Delta remote files against local files for changes.
using (Stream tarGzStream = new MemoryStream())
{
try
var outputMsg = string.Empty;

await Task.Run(() =>
{
using (var tarGzWriter = WriterFactory.Open(tarGzStream, ArchiveType.Tar, CompressionType.GZip))
try
{
using (MemoryStream fileStream = new MemoryStream())
using (var tarGzWriter = WriterFactory.Open(tarGzStream, ArchiveType.Tar, CompressionType.GZip))
{
using (BinaryWriter fileWriter = new BinaryWriter(fileStream))
using (MemoryStream fileStream = new MemoryStream())
{
fileWriter.Write(localFiles.Count);

var updateFileCount = 0;
long updateFileSize = 0;
var allFileCount = 0;
long allFileSize = 0;

foreach (var file in localFiles)
using (BinaryWriter fileWriter = new BinaryWriter(fileStream))
{
allFileCount++;
allFileSize += file.Value.Length;

// TODO: Add new cache file entry
//// UpdateCacheEntry.WriteToStream(newCacheFileWriter, file.Key, file.Value);
fileWriter.Write(localFiles.Count);

updateFileCount++;
updateFileSize += file.Value.Length;
var updateFileCount = 0;
long updateFileSize = 0;
var allFileCount = 0;
long allFileSize = 0;

try
{
tarGzWriter.Write(file.Key, file.Value);
}
catch (IOException ioEx)
foreach (var file in localFiles)
{
LogOutput($"Exception: {ioEx.Message}");
allFileCount++;
allFileSize += file.Value.Length;

// TODO: Add new cache file entry
//// UpdateCacheEntry.WriteToStream(newCacheFileWriter, file.Key, file.Value);

updateFileCount++;
updateFileSize += file.Value.Length;

try
{
tarGzWriter.Write(file.Key, file.Value);
}
catch (IOException ioEx)
{
outputMsg += $"Exception: {ioEx.Message}{Environment.NewLine}";
}
catch (Exception ex)
{
outputMsg += $"Exception: {ex.Message}{Environment.NewLine}{ex.StackTrace}{Environment.NewLine}";
}
}
catch (Exception ex)
{
LogOutput($"Exception: {ex.Message}\n{ex.StackTrace}");
}
}

LogOutput($"{updateFileCount,7:n0} [{updateFileSize,13:n0} bytes] of {allFileCount,7:n0} [{allFileSize,13:n0} bytes] files need to be updated");
outputMsg += $"Update file count: {updateFileCount}; File Size: [{updateFileSize} bytes] of Total Files: {allFileCount} [{allFileSize} bytes] need to be updated";
}
}
}

success = true;
}
catch (Exception ex)
{
outputMsg += $"Error while compressing file contents. {ex.Message}\n{ex.StackTrace}";
}
});

success = true;
}
catch (Exception ex)
{
LogOutput($"Error while compressing file contents. {ex.Message}\n{ex.StackTrace}");
}
Logger.Output(outputMsg);

// Upload the file
if (success)
{
try
{
var tarGzSize = tarGzStream.Length;
tarGzStream.Seek(0, SeekOrigin.Begin);

sftp.UploadFile(tarGzStream, pathBuildTarGz);
await Task.Run(() =>
{
tarGzStream.Seek(0, SeekOrigin.Begin);

LogOutput($"Uploaded '{_tarGzFileName}' [{tarGzSize,13:n0} bytes].");
sftp.UploadFile(tarGzStream, pathBuildTarGz);
});

Logger.Output($"Uploaded '{_tarGzFileName}' [{tarGzSize,13:n0} bytes].");
success = true;
}
catch (Exception ex)
{
LogOutput($"Error while uploading file. {ex.Message}\n{ex.StackTrace}");
Logger.Output($"Error while uploading file. {ex.Message}\n{ex.StackTrace}");
success = false;
}
}
Expand All @@ -394,20 +401,26 @@ private bool PayloadCompressAndUpload(SftpClient sftp, DirectoryInfo srcDirInfo,
/// <param name="pathBuildTarGz">Path to upload to.</param>
/// <param name="removeTarGz">Remove our build's tar.gz file. Set to FALSE for debugging. (default=true)</param>
/// <returns>Returns true on success.</returns>
private bool PayloadDecompress(string pathBuildTarGz, bool removeTarGz = true)
private async Task<bool> PayloadDecompressAsync(string pathBuildTarGz, bool removeTarGz = true)
{
try
{
var decompressOutput = string.Empty;

var cmd = "set -e";
cmd += $";cd \"{_launch.RemoteDeployFolder}\"";
cmd += $";cd \"{_launch.RemoteDeployProjectFolder}\"";
cmd += $";tar -zxf \"{_tarGzFileName}\"";
////cmd += $";tar -zxf \"{pathBuildTarGz}\"";

if (removeTarGz)
cmd += $";rm \"{pathBuildTarGz}\"";

var output = Bash(cmd);
LogOutput(output);
await Task.Run(() =>
{
decompressOutput = Bash(cmd);
});

Logger.Output($"Payload Decompress results: '{decompressOutput}' (blank=OK)");

return true;
}
Expand Down
12 changes: 9 additions & 3 deletions src/VsLinuxDebugger/Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ private static string FormattedTime
}
}

public static void Init(IServiceProvider provider, OutputWindowType outputType = OutputWindowType.Debug, string name = "Remote Debugger")
public static void Init(
IServiceProvider provider,
OutputWindowType outputType = OutputWindowType.Debug,
string name = "Linux Debugger")
{
_provider = provider;
_outputType = outputType;
Expand All @@ -41,18 +44,21 @@ public static void Init(IServiceProvider provider, OutputWindowType outputType =

public static void Output(string message)
{
var msg = $"{FormattedTime}: {message}{Environment.NewLine}";

try
{
ThreadHelper.ThrowIfNotOnUIThread();

if (HasOutputWindow())
{
_outputPane.OutputStringThreadSafe($"{FormattedTime}: {message}{Environment.NewLine}");
_outputPane.Activate(); // Brings this pane into view
_outputPane.OutputStringThreadSafe(msg);
_outputPane.Activate(); // Brings pane into view
}
}
catch (Exception)
{
Console.Write($"Failed to Output: '{msg}'");
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/VsLinuxDebugger/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.8.0.0")]
[assembly: AssemblyFileVersion("1.8.0.0")]
[assembly: AssemblyVersion("1.8.1.0")]
[assembly: AssemblyFileVersion("1.8.1.0")]
2 changes: 1 addition & 1 deletion src/VsLinuxDebugger/source.extension.vsixmanifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="VsLinuxDebugger.4d7bf4de-5015-4e24-92c0-7f9f3397b2da" Version="1.8.0" Language="en-US" Publisher="Suess Labs" />
<Identity Id="VsLinuxDebugger.4d7bf4de-5015-4e24-92c0-7f9f3397b2da" Version="1.8.1" Language="en-US" Publisher="Suess Labs" />
<DisplayName>VS Linux Debugger</DisplayName>
<Description xml:space="preserve">Remotely deploy and debug your .NET apps visa SSH on your Linux device using Visual Studio 2022. Works with popular Linux distrobutions such as Ubuntu, Raspberry Pi, and more!</Description>
<MoreInfo>https://github.com/SuessLabs/VsLinuxDebug</MoreInfo>
Expand Down

0 comments on commit 91740b3

Please sign in to comment.