From 47968f56bcfbc45aaa5c31ce3fc0800c1d8cf5f6 Mon Sep 17 00:00:00 2001 From: Damian Suess Date: Wed, 30 Mar 2022 18:20:10 -0400 Subject: [PATCH 1/4] Added debugger attach to process (beta phase) --- src/VsLinuxDebugger/Commands.Impl.cs | 9 +- src/VsLinuxDebugger/Core/LaunchBuilder.cs | 101 +++++++++++++++--- src/VsLinuxDebugger/Core/LaunchJson.cs | 53 ++++++++- src/VsLinuxDebugger/Core/LaunchJsonConfig.cs | 24 ----- src/VsLinuxDebugger/Core/RemoteDebugger.cs | 43 ++++++-- src/VsLinuxDebugger/Core/SshTool.cs | 47 ++++++-- src/VsLinuxDebugger/Core/UserOptions.cs | 4 +- src/VsLinuxDebugger/DebuggerPackage.cs | 4 +- src/VsLinuxDebugger/DebuggerPackage.vsct | 19 ++-- .../OptionsPages/OptionsPage.DotNet.cs | 8 +- .../OptionsPages/OptionsPage.Local.cs | 13 ++- .../Properties/AssemblyInfo.cs | 4 +- src/VsLinuxDebugger/VsLinuxDebugger.csproj | 3 +- .../source.extension.vsixmanifest | 46 ++++---- 14 files changed, 274 insertions(+), 104 deletions(-) delete mode 100644 src/VsLinuxDebugger/Core/LaunchJsonConfig.cs diff --git a/src/VsLinuxDebugger/Commands.Impl.cs b/src/VsLinuxDebugger/Commands.Impl.cs index 2404575..bde3f06 100644 --- a/src/VsLinuxDebugger/Commands.Impl.cs +++ b/src/VsLinuxDebugger/Commands.Impl.cs @@ -106,12 +106,15 @@ private void SetMenuTextAndVisibility(object sender, EventArgs e) //// cmd.Enabled = _extension.IsStartupProjectAvailable(); if (cmd.CommandID.ID == CommandIds.CmdShowLog - || cmd.CommandID.ID == CommandIds.CmdBuildDeployDebug || cmd.CommandID.ID == CommandIds.CmdDebugOnly || cmd.CommandID.ID == CommandIds.CmdShowSettings) { cmd.Enabled = false; } + else + { + cmd.Enabled = true; + } } } @@ -119,6 +122,8 @@ private UserOptions ToUserOptions() { return new UserOptions { + DeleteLaunchJsonAfterBuild = Settings.DeleteLaunchJsonAfterBuild, + HostIp = Settings.HostIp, HostPort = Settings.HostPort, @@ -133,7 +138,7 @@ private UserOptions ToUserOptions() RemoteVsDbgPath = Settings.RemoteVsDbgPath, UseCommandLineArgs = Settings.UseCommandLineArgs, - UsePublish = Settings.UsePublish, + //// UsePublish = Settings.UsePublish, UserPrivateKeyEnabled = Settings.UserPrivateKeyEnabled, UserPrivateKeyPath = Settings.UserPrivateKeyPath, diff --git a/src/VsLinuxDebugger/Core/LaunchBuilder.cs b/src/VsLinuxDebugger/Core/LaunchBuilder.cs index fda538b..8d6ba08 100644 --- a/src/VsLinuxDebugger/Core/LaunchBuilder.cs +++ b/src/VsLinuxDebugger/Core/LaunchBuilder.cs @@ -7,17 +7,18 @@ namespace VsLinuxDebugger.Core { + // TODO: Combine with UserOptions to make life easier. public class LaunchBuilder { public const string AdapterFileName = "launch.json"; - private UserOptions _options; + private UserOptions _opts; public LaunchBuilder(DTE2 dte, Project dteProject, UserOptions o) { ThreadHelper.ThrowIfNotOnUIThread(); - _options = o; + _opts = o; AssemblyName = dteProject.Properties.Item("AssemblyName").Value.ToString(); ProjectConfigName = dteProject.ConfigurationManager.ActiveConfiguration.ConfigurationName; @@ -29,48 +30,118 @@ public LaunchBuilder(DTE2 dte, Project dteProject, UserOptions o) OutputDirFullPath = Path.Combine(Path.GetDirectoryName(dteProject.FullName), OutputDirName); } + /// Project assembly name. I.E. "ConsoleApp1" public string AssemblyName { get; set; } public string CommandLineArgs { get; set; } = string.Empty; + /// Remove the `launch.json` file after building. Keep it around for debugging. + public bool DeleteLaunchJsonAfterBuild => _opts.DeleteLaunchJsonAfterBuild; + + /// Full output folder path. I.E. "C:\\path\\Repos\\Porj\\bin\\Debug\\net6.0\\". public string OutputDirFullPath { get; set; } + /// Partial path to the output directory. I.E. "bin\\Debug\\net6.0". public string OutputDirName { get; set; } + /// Configuration build type. I.E. "Debug". public string ProjectConfigName { get; set; } + /// Full project output path. I.E. "C:\\path\\Repos\\Proj\\ConsoleApp1.csproj" public string ProjectFileFullPath { get; set; } + /// Project name (not always the same as AssemblyName). I.E. "Console App1" public string ProjectName { get; set; } + /// Full path to the remote assembly. (i.e. `/home/USER/VLSDbg/Proj/ConsoleApp1.dll`) + public string RemoteDeployAppPath => LinuxPath.Combine(RemoteDeployFolder, $"{AssemblyName}.dll"); + + /// Folder of our remote assembly. (i.e. `/home/USER/VLSDbg/Proj`) + public string RemoteDeployFolder => + LinuxPath.Combine(_opts.RemoteDeployBasePath, ProjectName); + + public string RemoteDotNetPath => _opts.RemoteDotNetPath; + + public string RemoteHostIp => _opts.HostIp; + + public int RemoteHostPort => _opts.HostPort; + + public string RemoteUserName => _opts.UserName; + + public string RemoteUserPass => _opts.UserPass; + + public string RemoteVsDbgPath => _opts.RemoteVsDbgPath; + + /// Solution folder path. I.E. "C:\\path\Repos\" public string SolutionDirPath { get; set; } + /// Full solution output path. I.E. "C:\\path\Repos\Proj.sln" public string SolutionFileFullPath { get; set; } /// Generates the project's `launch.json` file. /// Returns the local path to the file. - public string GenerateLaunchJson() + public string GenerateLaunchJson(bool vsdbgLogging = false) { - ////Adapter => !_options.LocalPlinkEnabled ? "ssh.exe" : ""; - //// - ////AdapterArgs => !_options.LocalPlinkEnabled - //// ? $"-i {SshKeyPath} {_options.UserName}@{_options.HostIp} {_options.RemoteVsDbgPath} --interpreter=vscode" - //// : ""; + string adapter, adapterArgs; + + //// $"-i \"{connectionInfo.PrivateKeyPath}\" -o \"StrictHostKeyChecking no\" {connectionInfo.User}@{connectionInfo.Host} {PackageHelper.RemoteDebuggerPath} --interpreter=vscode {engineLogging}") + var sshPassword = !_opts.UserPrivateKeyEnabled + ? $"-pw {_opts.UserPass}" + : $"-i {_opts.UserPrivateKeyPath}"; - var launch = new LaunchJson(); - var launchCfg = new LaunchJsonConfig(); + var sshEndpoint = $"{_opts.UserName}@{_opts.HostIp}:{_opts.HostPort}"; - var opts = new JsonSerializerOptions + var vsdbgLogPath = ""; + if (vsdbgLogging) + vsdbgLogPath = $" --engineLogging={LinuxPath.Combine(RemoteDeployFolder, "_vsdbg.log")}"; + + if (!_opts.LocalPlinkEnabled) + { + adapter = "ssh.exe"; + adapterArgs = $"{sshPassword} {sshEndpoint} {_opts.RemoteVsDbgPath} --interpreter=vscode {vsdbgLogPath}"; + } + else + { + // TODO: Consider packing PLink.exe + //// "%LocalAppData%\\microsoft\\visualstudio\\16.0_c1d3f8c1\\extensions\\cruad2hg.efs\\plink.exe"; + //// var plinkPath = Path.Combine(GetExtensionDirectory(), "plink.exe").Trim('"'); + + adapter = _opts.LocalPLinkPath; + adapterArgs = $"-ssh -pw {RemoteUserPass} {RemoteUserName}@{RemoteHostIp} -batch -T {RemoteVsDbgPath} --interpreter=vscode {vsdbgLogPath}"; + //// adapterArgs = $"-ssh -pw {_options.UserPass} {_options.UserName}@{_options.HostIp}:{_options.HostPort} -batch -T {_options.RemoteVsDbgPath} --interpreter=vscode"; + } + + var obj = new LaunchJson( + RemoteDotNetPath, + $"{AssemblyName}.dll", /// RemoteDeployAppPath, + RemoteDeployFolder, + string.Empty, + false) + { + Adapter = "ssh.exe", + AdapterArgs = adapterArgs, + }; + + var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, - }; + }); - var json = JsonSerializer.Serialize(launch, opts); + // Create out file + var outputPath = Path.Combine(OutputDirFullPath, "launch.json"); - throw new NotImplementedException(); + try + { + File.WriteAllText(outputPath, json); + } + catch (Exception ex) + { + Console.WriteLine($"Error writing 'launch.json' to path, '{outputPath}'!\n{ex.Message}"); + outputPath = string.Empty; + } - return string.Empty; + return outputPath; } } } diff --git a/src/VsLinuxDebugger/Core/LaunchJson.cs b/src/VsLinuxDebugger/Core/LaunchJson.cs index e321572..645049f 100644 --- a/src/VsLinuxDebugger/Core/LaunchJson.cs +++ b/src/VsLinuxDebugger/Core/LaunchJson.cs @@ -5,9 +5,28 @@ namespace VsLinuxDebugger.Core { public class LaunchJson { - public LaunchJson() + private string _remoteDotNetPath; + private string[] _args; + private string _remoteOutputFolder; + private string _environmentVariables; + private bool _stopAtEntry; + + /// Launch JSON class + /// Remote machine DotNet path + /// Name of app(.dll) or full path on remote machine. + /// Working directory (CWD) where app resides. + /// Custom environment variables. + public LaunchJson(string dotNetPath, string remoteAppFileName, string remoteOutputFolder, string envVariables, bool stopAtEntry = false) { - //// Configurations = new(); + ////string appPath = LinuxPath.Combine(remoteDebugFolder, $"{appName}.dll"); + string appToLaunch = remoteAppFileName; + + System.IO.Path.Combine(""); + _remoteDotNetPath = dotNetPath; + _args = new string[] { appToLaunch }; + _remoteOutputFolder = remoteOutputFolder; + _environmentVariables = envVariables; + _stopAtEntry = stopAtEntry; } public string Version => "0.2.0"; @@ -16,6 +35,34 @@ public LaunchJson() public string AdapterArgs { get; set; } - public List Configurations { get; set; } + public LaunchConfig Configurations => new LaunchConfig + { + Program = _remoteDotNetPath, + Args = _args, + Cwd = _remoteOutputFolder, + StopAtEntry = _stopAtEntry, + Env = _environmentVariables, + }; + + public class LaunchConfig + { + public string Name => "Debug on Linux"; + + public string Type => "coreclr"; + + public string Request => "launch"; + + public string Program { get; set; } //// _remoteDotNetPath; + + public string[] Args { get; set; } //// => _args; + + public string Cwd { get; set; } //// => _remoteDebugFolder; + + public bool StopAtEntry { get; set; } //// => _stopAtEntry; + + public string Console => "internalConsole"; + + public string Env { get; set; } //// => _environmentVariables; + } } } diff --git a/src/VsLinuxDebugger/Core/LaunchJsonConfig.cs b/src/VsLinuxDebugger/Core/LaunchJsonConfig.cs deleted file mode 100644 index 75b6daa..0000000 --- a/src/VsLinuxDebugger/Core/LaunchJsonConfig.cs +++ /dev/null @@ -1,24 +0,0 @@ -// BeginAsync(BuildOptions buildOptions) if (buildOptions.HasFlag(BuildOptions.Debug)) { - // BuildDebugAttacher(); + BuildDebugAttacher(); } } @@ -158,7 +158,8 @@ private void BuildBegin() private void BuildCleanup() { - if (File.Exists(_launchJsonPath)) + // Not really needed + if (_launchBuilder.DeleteLaunchJsonAfterBuild && File.Exists(_launchJsonPath)) File.Delete(_launchJsonPath); //// BuildEvents.OnBuildDone -= BuildEvents_OnBuildDoneAsync; @@ -170,10 +171,16 @@ private void BuildCleanup() /// private void BuildDebugAttacher() { - _launchJsonPath = _launchBuilder.GenerateLaunchJson(); + ////_launchJsonPath = _launchBuilder.GenerateLaunchJson(); - var dte = (DTE2)Package.GetGlobalService(typeof(SDTE)); - dte.ExecuteCommand("DebugAdapterHost.Launch", $"/LaunchJson:\"{_launchJsonPath}\""); + _launchJsonPath = _launchBuilder.GenerateLaunchJson(true); + if (string.IsNullOrEmpty(_launchJsonPath)) + { + LogOutput("Could not generate 'launch.json'. Potential folder creation permissions in project's output directory."); + } + + DTE2 dte2 = (DTE2)Package.GetGlobalService(typeof(SDTE)); + dte2.ExecuteCommand("DebugAdapterHost.Launch", $"/LaunchJson:\"{_launchJsonPath}\""); } private bool Initialize() @@ -196,7 +203,7 @@ private bool Initialize() /* * Borrowed from VSMonoDebugger - * + * public async Task BuildStartupProjectAsync() { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -268,8 +275,30 @@ private bool IsCSharpProject(Project vsProject) private void LogOutput(string message) { - // TODO: Send to VS Output Window + // Reference: + // - https://stackoverflow.com/a/1852535/249492 + // - https://docs.microsoft.com/en-us/visualstudio/extensibility/extending-the-output-window?view=vs-2022 + // - https://github.com/microsoft/VSSDK-Extensibility-Samples/blob/master/Reference_Services/C%23/Reference.Services/HelperFunctions.cs + // Console.WriteLine($">> {message}"); + + // TODO: ERROR, 'generalPane' is NULL! + // 1) Consider passing in IServiceProvider from Commands class + // 2) Use the MS GitHub example + // + ////// TODO: Use main thread + ////////await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); + ////ThreadHelper.ThrowIfNotOnUIThread(); + //// + ////IVsOutputWindow output = Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow; + //// + ////// Guid debugPaneGuid = VSConstants.GUID_OutWindowDebugPane; + ////Guid generalPaneGuid = VSConstants.GUID_OutWindowGeneralPane; + ////IVsOutputWindowPane generalPane; + ////output.GetPane(ref generalPaneGuid, out generalPane); + //// + ////generalPane.OutputStringThreadSafe(message); + ////generalPane.Activate(); // Brings this pane into view } } } diff --git a/src/VsLinuxDebugger/Core/SshTool.cs b/src/VsLinuxDebugger/Core/SshTool.cs index e9de04e..d768827 100644 --- a/src/VsLinuxDebugger/Core/SshTool.cs +++ b/src/VsLinuxDebugger/Core/SshTool.cs @@ -3,6 +3,9 @@ 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; @@ -26,8 +29,6 @@ public SshTool(UserOptions opts, LaunchBuilder launch) _launch = launch; } - public string RemoteDeployPath => $"{_opts.RemoteDeployBasePath}/{_launch.ProjectName}"; - public string Bash(string command) { try @@ -51,10 +52,15 @@ public string Bash(string command) } /// Cleans the contents of the deployment path. - public void CleanDeploymentFolder() + /// Clear entire base deployment folder (TRUE) or just our project. + public void CleanDeploymentFolder(bool fullScrub = false) { //// Bash($"sudo rm -rf {_opts.RemoteDeployBasePath}/*"); - Bash($"rm -rf {_opts.RemoteDeployBasePath}/*"); + + if (fullScrub) + Bash($"rm -rf {_opts.RemoteDeployBasePath}/*"); + else + Bash($"rm -rf \"{_launch.RemoteDeployAppPath}/*\""); } public bool Connect() @@ -165,7 +171,7 @@ public async Task UploadFilesAsync() { try { - Bash($@"mkdir -p {RemoteDeployPath}"); + Bash($@"mkdir -p {_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. @@ -179,7 +185,10 @@ public async Task UploadFilesAsync() // Compress files to upload as single `tar.gz`. // TODO: Use base folder path: var pathTarGz = $"{_opts.RemoteDeployBasePath}/{_tarGzFileName}"; - var destTarGz = $"{RemoteDeployPath}/{_tarGzFileName}"; + //// var destTarGz = $"{RemoteDeployPath}/{_tarGzFileName}"; + var destTarGz = LinuxPath.Combine(_launch.RemoteDeployFolder, _tarGzFileName); + LogOutput($"Destination Tar.GZ: '{destTarGz}'"); + var success = PayloadCompressAndUpload(_sftp, srcDirInfo, destTarGz); // Decompress file @@ -274,8 +283,30 @@ private ConcurrentDictionary GetLocalFiles(DirectoryInfo srcDi private void LogOutput(string message) { - // TODO: Send to VS Output Window + // Reference: + // - https://stackoverflow.com/a/1852535/249492 + // - https://docs.microsoft.com/en-us/visualstudio/extensibility/extending-the-output-window?view=vs-2022 + // - https://github.com/microsoft/VSSDK-Extensibility-Samples/blob/master/Reference_Services/C%23/Reference.Services/HelperFunctions.cs + // Console.WriteLine($">> {message}"); + + // TODO: + // 1) Consider passing in IServiceProvider from Commands class + // 2) Use the MS GitHub example + // + ////// TODO: Use main thread + ////////await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); + ////ThreadHelper.ThrowIfNotOnUIThread(); + //// + ////IVsOutputWindow output = Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow; + //// + ////// Guid debugPaneGuid = VSConstants.GUID_OutWindowDebugPane; + ////Guid generalPaneGuid = VSConstants.GUID_OutWindowGeneralPane; + ////IVsOutputWindowPane generalPane; + ////output.GetPane(ref generalPaneGuid, out generalPane); + //// + ////generalPane.OutputStringThreadSafe(message); + ////generalPane.Activate(); // Brings this pane into view } /// Compress build contents and upload to remote host. @@ -376,7 +407,7 @@ private bool PayloadDecompress(string pathBuildTarGz, bool removeTarGz = true) try { var cmd = "set -e"; - cmd += $";cd \"{RemoteDeployPath}\""; + cmd += $";cd \"{_launch.RemoteDeployFolder}\""; cmd += $";tar -zxf \"{_tarGzFileName}\""; ////cmd += $";tar -zxf \"{pathBuildTarGz}\""; diff --git a/src/VsLinuxDebugger/Core/UserOptions.cs b/src/VsLinuxDebugger/Core/UserOptions.cs index 11e323e..66fdfc3 100644 --- a/src/VsLinuxDebugger/Core/UserOptions.cs +++ b/src/VsLinuxDebugger/Core/UserOptions.cs @@ -2,6 +2,8 @@ { public class UserOptions { + public bool DeleteLaunchJsonAfterBuild { get; set; } + public string HostIp { get; set; } public int HostPort { get; set; } @@ -10,8 +12,6 @@ public class UserOptions public bool RemoteDebugDisplayGui { get; set; } public string RemoteDeployBasePath { get; set; } // TODO: Scrub trailing '/\' chars - ////public string RemoteDeployDebugPath { get; set; } - ////public string RemoteDeployReleasePath { get; set; } public string RemoteDotNetPath { get; set; } public string RemoteVsDbgPath { get; set; } diff --git a/src/VsLinuxDebugger/DebuggerPackage.cs b/src/VsLinuxDebugger/DebuggerPackage.cs index 8150b29..546a64f 100644 --- a/src/VsLinuxDebugger/DebuggerPackage.cs +++ b/src/VsLinuxDebugger/DebuggerPackage.cs @@ -31,6 +31,8 @@ public sealed partial class DebuggerPackage : AsyncPackage /// Package GUID string. public const string PackageGuidString = "19f87f23-7a2c-4279-ac7c-c9267776bbf9"; + public bool DeleteLaunchJsonAfterBuild => _optionsPage.DeleteLaunchJsonAfterBuild; + public string HostIp => _optionsPage.HostIp; public int HostPort => _optionsPage.HostPort; @@ -45,7 +47,7 @@ public sealed partial class DebuggerPackage : AsyncPackage public string RemoteVsDbgPath => _optionsPage.RemoteVsDbgPath; public bool UseCommandLineArgs => _optionsPage.UseCommandLineArgs; - public bool UsePublish => _optionsPage.Publish; + //// public bool UsePublish => _optionsPage.Publish; public string UserGroupName => _optionsPage.UserGroupName; public string UserName => _optionsPage.UserName; diff --git a/src/VsLinuxDebugger/DebuggerPackage.vsct b/src/VsLinuxDebugger/DebuggerPackage.vsct index 6d5a9f0..37de3ec 100644 --- a/src/VsLinuxDebugger/DebuggerPackage.vsct +++ b/src/VsLinuxDebugger/DebuggerPackage.vsct @@ -74,9 +74,9 @@ - + + --> + enable + + + diff --git a/sandbox/ConsoleNet5/Program.cs b/sandbox/ConsoleNet5/Program.cs new file mode 100644 index 0000000..df44e80 --- /dev/null +++ b/sandbox/ConsoleNet5/Program.cs @@ -0,0 +1,18 @@ +using System; + +namespace ConsoleNet5 +{ + public class Program + { + public static void Main(string[] args) + { + // See https://aka.ms/new-console-template for more information + Console.WriteLine("Hello .NET 5, VS Linux Debugger!"); + + Console.WriteLine("Apply breakpoint here!"); + + Console.WriteLine("Press anykey to exit.."); + var x = Console.ReadLine(); + } + } +} diff --git a/sandbox/ConsoleNet6.sln b/sandbox/ConsoleNet6.sln new file mode 100644 index 0000000..22e9503 --- /dev/null +++ b/sandbox/ConsoleNet6.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32317.152 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleNet6", "ConsoleNet6\ConsoleNet6.csproj", "{7342FB7C-71DF-4282-889B-83E189FCBF55}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7342FB7C-71DF-4282-889B-83E189FCBF55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7342FB7C-71DF-4282-889B-83E189FCBF55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7342FB7C-71DF-4282-889B-83E189FCBF55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7342FB7C-71DF-4282-889B-83E189FCBF55}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FD190C6A-36C9-416E-8BB1-E1C7421B6661} + EndGlobalSection +EndGlobal diff --git a/sandbox/ConsoleApp1/ConsoleApp1.csproj b/sandbox/ConsoleNet6/ConsoleNet6.csproj similarity index 100% rename from sandbox/ConsoleApp1/ConsoleApp1.csproj rename to sandbox/ConsoleNet6/ConsoleNet6.csproj diff --git a/sandbox/ConsoleApp1/Program.cs b/sandbox/ConsoleNet6/Program.cs similarity index 77% rename from sandbox/ConsoleApp1/Program.cs rename to sandbox/ConsoleNet6/Program.cs index c201397..2e22deb 100644 --- a/sandbox/ConsoleApp1/Program.cs +++ b/sandbox/ConsoleNet6/Program.cs @@ -1,5 +1,5 @@ // See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello from, VS Linux Debugger!"); +Console.WriteLine("Hello .NET 6, VS Linux Debugger!"); Console.WriteLine("Apply breakpoint here!"); diff --git a/src/VsLinuxDebugger/Core/LinuxPath.cs b/src/VsLinuxDebugger/Core/LinuxPath.cs new file mode 100644 index 0000000..7a765f3 --- /dev/null +++ b/src/VsLinuxDebugger/Core/LinuxPath.cs @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------------- +// FILE: LinuxPath.cs +// CONTRIBUTOR: Jeff Lill +// COPYRIGHT: Copyright (c) 2005-2022 by neonFORGE LLC. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// References: +// - https://github.com/nforgeio/neonKUBE/blob/master/Lib/Neon.Common/IO/LinuxPath.cs +// - https://github.com/nforgeio/neonKUBE/blob/master/Lib/Neon.Common/Diagnostics/Covenant.cs + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace VsLinuxDebugger.Core +{ + /// + /// Implements functionality much like , except for + /// this class is oriented towards handling Linux-style paths on + /// a remote (possibly a Windows) machine. + /// + public static class LinuxPath + { + /// + /// Changes the file extension. + /// + /// The file path. + /// The new extension. + /// The modified path. + public static string ChangeExtension(string path, string extension) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + Covenant.Requires(!string.IsNullOrEmpty(extension), nameof(extension)); + + return Path.ChangeExtension(Normalize(path), extension).ToLinux(); + } + + /// + /// Combines an array of strings into a path. + /// + /// The paths. + /// The combined paths. + public static string Combine(params string[] paths) + { + Covenant.Requires(paths != null, nameof(paths)); + + return Path.Combine(paths).ToLinux(); + } + + /// + /// Extracts the directory portion of a path. + /// + /// The path. + /// The directory portion. + public static string GetDirectoryName(string path) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + + return Path.GetDirectoryName(Normalize(path)).ToLinux(); + } + + /// + /// Returns the file extension from a path. + /// + /// The path. + /// The file extension. + public static string GetExtension(string path) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + + return Path.GetExtension(Normalize(path)); + } + + /// + /// Returns the file name and extension from a path. + /// + /// The path. + /// The file name and extension. + public static string GetFileName(string path) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + + return Path.GetFileName(Normalize(path)); + } + + /// + /// Returns the file name from a path without the extension. + /// + /// The path. + /// The file name without the extension. + public static string GetFileNameWithoutExtension(string path) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + + return Path.GetFileNameWithoutExtension(Normalize(path)); + } + + /// + /// Determines whether a path has a file extension. + /// + /// The path. + /// true if the path has an extension. + public static bool HasExtension(string path) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + + return Path.HasExtension(Normalize(path)); + } + + /// + /// Determines whether the path is rooted. + /// + /// The path. + /// true ifc the path is rooted. + public static bool IsPathRooted(string path) + { + Covenant.Requires(!string.IsNullOrEmpty(path), nameof(path)); + + return Normalize(path).ToLinux().StartsWith("/"); + } + + /// + /// Ensures that the path passed is suitable for non-Windows platforms + /// by conmverting any backslashes to forward slashes. + /// + /// The input path (or null). + /// The normalized path. + private static string Normalize(string path) + { + if (path == null || RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return path; + } + + return path.Replace('\\', '/'); + } + + /// + /// Converts a Windows style path to Linux. + /// + /// The source path. + /// The converted path. + private static string ToLinux(this string path) + { + return path.Replace('\\', '/'); + } + + /// A simple, lightweight, and partial implementation of the Microsoft Dev Labs Contract class. + /// + /// + /// This class is intended to be a drop-in replacement for code contract assertions by simply + /// searching and replacing "Contract." with "." in all source code. + /// In my experience, code contracts slow down build times too much and often obsfucate + /// async methods such that they cannot be debugged effectively using the debugger. + /// Code Contracts are also somewhat of a pain to configure as project propoerties. + /// + /// + /// This class includes the , + /// and methods that can be used to capture validation + /// requirements in code, but these methods don't currently generate any code. + /// + /// + private class Covenant + { + private static Type[] _oneStringArg = new Type[] { typeof(string) }; + private static Type[] _twoStringArgs = new Type[] { typeof(string), typeof(string) }; + + /// + /// Verifies a method pre-condition throwing a custom exception. + /// + /// The exception to be thrown if the condition is false. + /// The condition to be tested. + /// The first optional string argument to the exception constructor. + /// The second optional string argument to the exception constructor. + /// + /// + /// This method throws a instance when + /// is false. Up to two string arguments may be passed to the exception constructor when an + /// appropriate constructor exists, otherwise these arguments will be ignored. + /// + /// + public static void Requires(bool condition, string arg1 = null, string arg2 = null) + where TException : Exception, new() + { + if (condition) + { + return; + } + + var exceptionType = typeof(TException); + + // Look for a constructor with two string parameters. + + var constructor = exceptionType.GetConstructor(_twoStringArgs); + + if (constructor != null) + { + throw (Exception)constructor.Invoke(new object[] { arg1, arg2 }); + } + + // Look for a constructor with one string parameter. + + constructor = exceptionType.GetConstructor(_oneStringArg); + + if (constructor != null) + { + throw (Exception)constructor.Invoke(new object[] { arg1 }); + } + + // Fall back to the default constructor. + + throw new TException(); + } + } + } +} diff --git a/src/VsLinuxDebugger/Extensions/JsonExtension.cs b/src/VsLinuxDebugger/Extensions/JsonExtension.cs new file mode 100644 index 0000000..f826aee --- /dev/null +++ b/src/VsLinuxDebugger/Extensions/JsonExtension.cs @@ -0,0 +1,385 @@ +/* +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +*/ +namespace VsLinuxDebugger.Extensions +{ + /* + /// + /// System.Text.Json extension. + /// License: MIT + /// https://github.com/andrewjpoole/jsonelement.extensions/blob/master/AJP.JsonElementExtensions/JsonElementExtensions.cs + /// + public static class JsonExtension + { + /// + /// Method which recreates a new JsonElement from an existing one, with an extra null valued property added to the start of the list of properties. + /// If you care about where the new property should appear in the list of properties, use InsertNullProperty(), although its a slightly more expensive operation. + /// If you have multiple changes to make to the JsonElement, please consider/test using ParseAsJsonStringAndMutate() + /// so that all changes can be done together, with only one roudtrip process. + /// + /// A string containing the name of the property to add + /// The json serializer options to respect. + /// A new JsonElement containing the old properties plus the new property + public static JsonElement AddNullProperty(this JsonElement jElement, string name, JsonSerializerOptions options = null) => + jElement.ParseAsJsonStringAndMutate((utf8JsonWriter, _) => HandleNull(utf8JsonWriter, name, options)); + + /// + /// Method which recreates a new JsonElement from an existing one, with an extra property added to the start of the list of properties. + /// If you care about where the new property should appear in the list of properties, use InsertProperty(), although its a slightly more expensive operation. + /// If you have multiple changes to make to the JsonElement, please consider/test using ParseAsJsonStringAndMutate() + /// so that all changes can be done together, with only one roudtrip process. + /// + /// A string containing the name of the property to add + /// The value of the property to add, primitives and simple objects are supported. + /// The serializer options to respect when converting values. + /// A new JsonElement containing the old properties plus the new property + public static JsonElement AddProperty(this JsonElement jElement, string name, object value, JsonSerializerOptions options = null) => + jElement.ParseAsJsonStringAndMutate((utf8JsonWriter, _) => + { + if (value is null) + { + HandleNull(utf8JsonWriter, name, options); + return; + } + + utf8JsonWriter.WritePropertyName(name); + RenderValue(utf8JsonWriter, value, options); + }); + + /// + /// Method which attempts to convert a given JsonElement into the specified type + /// + /// The JsonElement to convert + /// JsonSerializerOptions to use + /// The specified type + /// + public static T ConvertToObject(this JsonElement jElement, JsonSerializerOptions options = null) + { + var arrayBufferWriter = new ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(arrayBufferWriter)) + jElement.WriteTo(writer); + + return JsonSerializer.Deserialize(arrayBufferWriter.WrittenSpan, options); + } + + /// + /// Method which attempts to convert a given JsonDocument into the specified type + /// + /// The JsonDocument to convert + /// JsonSerializerOptions to use + /// The specified type + /// An instance of the specified type from the supplied JsonDocument + /// Thrown if the JsonDocument cannot be dserialised into the specified type + public static T ConvertToObject(this JsonDocument jDocument, JsonSerializerOptions options = null) + { + if (jDocument == null) + throw new ArgumentNullException(nameof(jDocument)); + + return jDocument.RootElement.ConvertToObject(options); + } + + /// + /// Method which returns a list of property name and value, from a given object + /// + public static IEnumerable<(string Name, object Value)> GetProperties(this object source) + { + if (source is IDictionary dictionary) + { + return dictionary.Select(x => (x.Key, x.Value)); + } + + return source.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => !p.GetGetMethod().GetParameters().Any()) + .Select(x => (x.Name, x.GetValue(source))); + } + + /// + /// Method which recreates a new JsonElement from an existing one, with an extra null property inserted at a specified position in the list of properties. + /// If you don't care about where the new property should appear in the list of properties, or if you care more about performance, + /// use AddNullProperty(), which is a slightly less expensive operation. + /// + /// + /// + /// + /// + public static JsonElement InsertNullProperty(this JsonElement jElement, string name, int insertAt, JsonSerializerOptions options = null) => + jElement.ParseAsJsonStringAndMutatePreservingOrder(props => props.Insert(name, null, insertAt), options); + + /// + /// Method which recreates a new JsonElement from an existing one, with an extra property inserted at a specified position in the list of properties. + /// If you don't care about where the new property should appear in the list of properties, or if you care more about performance, + /// use AddProperty(), which is a slightly less expensive operation. + /// + /// + /// + /// + /// + /// + public static JsonElement InsertProperty(this JsonElement jElement, string name, object value, int insertAt, JsonSerializerOptions options = null) => + jElement.ParseAsJsonStringAndMutatePreservingOrder(props => props.Insert(name, value, insertAt), options); + + /// + /// Method which recreates a new JsonElement from an existing one, with the opportunity to add new properties to the start of the object + /// and remove existing properties. New properties will be added to the beginning of the list, if you care about the order of the properties, + /// use ParseAsJsonStringAndMutatePreservingOrder() however that method is slightly more expensive in terms of time and allocation. + /// + /// An Action of Utf8JsonWriter and List of strings. + /// The Utf8JsonWriter allows the calling code to write additional properties, its possible to add highly complex nested structures, + /// the list of strings is a list names of any existing properties to be removed from the resulting JsonElement + /// A new JsonElement + public static JsonElement ParseAsJsonStringAndMutate(this JsonElement jElement, Action> mutate) + { + if (jElement.ValueKind != JsonValueKind.Object) + throw new Exception("Only able to add properties to json objects (i.e. jElement.ValueKind == JsonValueKind.Object)"); + + var arrayBufferWriter = new ArrayBufferWriter(); + using (var jsonWriter = new Utf8JsonWriter(arrayBufferWriter)) + { + jsonWriter.WriteStartObject(); + + var namesOfPropertiesToRemove = new List(); + + mutate?.Invoke(jsonWriter, namesOfPropertiesToRemove); + + foreach (var jProp in jElement.EnumerateObject()) + { + if (!(namesOfPropertiesToRemove.Contains(jProp.Name))) + { + jProp.WriteTo(jsonWriter); + } + } + jsonWriter.WriteEndObject(); + } + var resultJson = Encoding.UTF8.GetString(arrayBufferWriter.WrittenSpan); + return JsonDocument.Parse(resultJson).RootElement; + } + + /// + /// Method which recreates a new JsonElement from an existing one, + /// with the opportunity to add, remove and change properties while preserving the order of the properties. + /// This method is slightly more expensive in terms of time and allocation, than ParseAsJsonStringAndMutate() + /// + /// An Action on a list of Name/Value. + /// This list contains the properties from the JsonElement in order, items can be added, removed or updated. + /// The resulting JsonElement will be built from the mutated list of properties. + /// + /// JsonSerializerOptions that will be respected when a value is rendered. + /// + public static JsonElement ParseAsJsonStringAndMutatePreservingOrder(this JsonElement jElement, Action mutateProps, JsonSerializerOptions options) + { + if (jElement.ValueKind != JsonValueKind.Object) + throw new Exception("Only able to add properties to json objects (i.e. jElement.ValueKind == JsonValueKind.Object)"); + + var props = new PropertyList(); + foreach (var prop in jElement.EnumerateObject()) + { + props.Add(prop.Name, prop.Value); + } + + mutateProps.Invoke(props); + + var arrayBufferWriter = new ArrayBufferWriter(); + using (var jsonWriter = new Utf8JsonWriter(arrayBufferWriter)) + { + jsonWriter.WriteStartObject(); + + foreach (Property prop in props) + { + if (prop.Value is null) + { + HandleNull(jsonWriter, prop.Name, options); + } + else + { + jsonWriter.WritePropertyName(options?.PropertyNamingPolicy?.ConvertName(prop.Name) ?? prop.Name); + if (prop.Value is JsonElement jProp) + { + jProp.WriteTo(jsonWriter); + } + else + { + RenderValue(jsonWriter, prop.Value, options); + } + } + } + + jsonWriter.WriteEndObject(); + } + + var resultJson = Encoding.UTF8.GetString(arrayBufferWriter.WrittenSpan); + return JsonDocument.Parse(resultJson).RootElement; + } + + /// + /// Method which recreates a new JsonElement from an existing one, but without some of the exiting properties + /// + /// A list of names of the properties to remove + /// A new JsonElement without the properties listed + public static JsonElement RemoveProperties(this JsonElement jElement, IEnumerable propertyNamesToRemove) => + jElement.ParseAsJsonStringAndMutate((writer, namesOfPropertiesToRemove) => namesOfPropertiesToRemove.AddRange(propertyNamesToRemove)); + + /// + /// Method which recreates a new JsonElement from an existing one, but without one of the exiting properties + /// + /// A string containing the name of the property to remove + /// A new JsonElement containing the old properties apart from the named property to remove + public static JsonElement RemoveProperty(this JsonElement jElement, string nameOfPropertyToRemove) => + jElement.ParseAsJsonStringAndMutate((writer, namesOfPropertiesToRemove) => namesOfPropertiesToRemove.Add(nameOfPropertyToRemove)); + + /// + /// Method which recreates a new JsonElement from an existing one, with a property updated, preserving the order of the list of properties. + /// If you don't care about preserving the order of the list of properties, or if you care more about performance, + /// you could use ParseAsJsonStringAndMutate() which is a slightly less expensive operation. + /// + /// + /// + /// + /// + public static JsonElement UpdateProperty(this JsonElement jElement, string nameOfPropertyToUpdate, object newValue, JsonSerializerOptions options = null) + => jElement.ParseAsJsonStringAndMutatePreservingOrder(props => + { + var propToUpdate = props.FirstOrDefault(p => p.Name == nameOfPropertyToUpdate); + if (propToUpdate is null) + throw new ArgumentException($"Could not find a property named {nameOfPropertyToUpdate} in the list."); + propToUpdate.Value = newValue; + }, options ?? new JsonSerializerOptions()); + + private static void HandleNull(Utf8JsonWriter writer, string propName, JsonSerializerOptions options = null) + { + if (options?.IgnoreNullValues == true) + return; + + writer.WriteNull(options?.PropertyNamingPolicy?.ConvertName(propName) ?? propName); + } + + private static void RenderValue(this Utf8JsonWriter writer, object value, JsonSerializerOptions options = null) + { + // The value is not a primitive. + if (Convert.GetTypeCode(value) == TypeCode.Object && !(value is IEnumerable) && !(value is JsonElement)) + { + writer.WriteStartObject(); + foreach (var (propName, propValue) in value.GetProperties()) + { + if (propValue is null) + { + HandleNull(writer, propName, options); + continue; + } + + writer.WritePropertyName(options?.PropertyNamingPolicy.ConvertName(propName) ?? propName); + writer.RenderValue(propValue); + } + + writer.WriteEndObject(); + return; + } + + switch (value) + { + case string v: + writer.WriteStringValue(v); + break; + + case bool v: + writer.WriteBooleanValue(v); + break; + + case decimal v: + writer.WriteNumberValue(v); + break; + + case int v: + writer.WriteNumberValue(v); + break; + + case double v: + writer.WriteNumberValue(v); + break; + + case float v: + writer.WriteNumberValue(v); + break; + + case DateTime v: + writer.WriteStringValue(v); + break; + + case Guid v: + writer.WriteStringValue(v); + break; + + case JsonElement v: + writer.WriteStartObject(); + foreach (var jProp in v.EnumerateObject()) + { + jProp.WriteTo(writer); + } + + writer.WriteEndObject(); + break; + + case IEnumerable arr: + writer.WriteStartArray(); + arr.ToList() + .ForEach(obj => RenderValue(writer, obj)); + writer.WriteEndArray(); + break; + + default: + writer.WriteStringValue(value.ToString()); + break; + } + } + + public class Property + { + public string Name { get; set; } + + public object Value { get; set; } + } + + public class PropertyList : IEnumerable + { + private readonly List _properties = new List(); + + public void Add(string name, object value) + { + _properties.Add(new Property { Name = name, Value = value }); + } + + public Property GetByName(string name) + { + return _properties.FirstOrDefault(p => p.Name == name); + } + + public IEnumerator GetEnumerator() + { + return _properties.GetEnumerator(); + } + + public void Insert(string name, object value, int insertAt) + { + _properties.Insert(insertAt, new Property { Name = name, Value = value }); + } + + public void Remove(string name) + { + var propertyToRemove = _properties.First(p => p.Name == name); + _properties.Remove(propertyToRemove); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _properties.GetEnumerator(); + } + } + } + */ +} From 6ec15b1c0ac5034a8223f23709f0c4f1768120d5 Mon Sep 17 00:00:00 2001 From: Damian Suess Date: Wed, 30 Mar 2022 18:24:08 -0400 Subject: [PATCH 3/4] readme --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index eaeb58f..a2da51a 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,7 @@ This project was inspired by [VS Mono Debugger](https://github.com/GordianDotNet Now developers can build, deploy and debug projects on their remote Linux (Ubuntu, Raspberry PI, etc) devices! Customize your SSH connection to use either a _password_ or a _private key_. ### Work in Progress + This project is currently in the early alpha stages, so only Building and Deployment is available. This extension aims to allow you to automatically attach for debugging over the network. For now, that step is still manual. On the plus side, we just saved you 1.5 min of manual upload and `chown -R`. ### Usage @@ -37,7 +38,7 @@ In order to get this project moving, the following must be done. * [X] Store settings (globally; per-project) * [X] IP, User, Pass, default-folder `"~/VsLinuxDbg/(proj-name)"` * [X] Perform upload to remote machine -* [ ] Attach to process +* [ ] Attach to process - _in testing phase_ ## Developers Wanted From afc1175cf44538687b44a557c505e5f4e2c1b38e Mon Sep 17 00:00:00 2001 From: Damian Suess Date: Wed, 30 Mar 2022 18:26:26 -0400 Subject: [PATCH 4/4] . --- sandbox/TestLinuxDbg.sln | 30 - sandbox/src/Extensions.cs | 125 ---- sandbox/src/LocalHost.cs | 148 ---- sandbox/src/OptionsPages/LocalOptionsPage.cs | 38 -- sandbox/src/OptionsPages/RemoteOptionsPage.cs | 55 -- sandbox/src/Properties/AssemblyInfo.cs | 33 - sandbox/src/RemoteDebugCommand.cs | 639 ------------------ sandbox/src/RemoteDebugger.png | Bin 25407 -> 0 bytes sandbox/src/Resources/RemoteDebug16x16.png | Bin 813 -> 0 bytes sandbox/src/Resources/RemoteDebugger.png | Bin 25407 -> 0 bytes sandbox/src/TestLinuxDbg.csproj | 122 ---- sandbox/src/VSLinuxDebuggerPackage.cs | 86 --- sandbox/src/VSLinuxDebuggerPackage.vsct | 116 ---- sandbox/src/source.extension.vsixmanifest | 27 - 14 files changed, 1419 deletions(-) delete mode 100644 sandbox/TestLinuxDbg.sln delete mode 100644 sandbox/src/Extensions.cs delete mode 100644 sandbox/src/LocalHost.cs delete mode 100644 sandbox/src/OptionsPages/LocalOptionsPage.cs delete mode 100644 sandbox/src/OptionsPages/RemoteOptionsPage.cs delete mode 100644 sandbox/src/Properties/AssemblyInfo.cs delete mode 100644 sandbox/src/RemoteDebugCommand.cs delete mode 100644 sandbox/src/RemoteDebugger.png delete mode 100644 sandbox/src/Resources/RemoteDebug16x16.png delete mode 100644 sandbox/src/Resources/RemoteDebugger.png delete mode 100644 sandbox/src/TestLinuxDbg.csproj delete mode 100644 sandbox/src/VSLinuxDebuggerPackage.cs delete mode 100644 sandbox/src/VSLinuxDebuggerPackage.vsct delete mode 100644 sandbox/src/source.extension.vsixmanifest diff --git a/sandbox/TestLinuxDbg.sln b/sandbox/TestLinuxDbg.sln deleted file mode 100644 index 14dbb6f..0000000 --- a/sandbox/TestLinuxDbg.sln +++ /dev/null @@ -1,30 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32317.152 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestLinuxDbg", "src\TestLinuxDbg.csproj", "{22B0448E-2491-44D2-B061-D8A218943621}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E314478E-11B3-4830-9382-67E6D8773B90}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {22B0448E-2491-44D2-B061-D8A218943621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22B0448E-2491-44D2-B061-D8A218943621}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22B0448E-2491-44D2-B061-D8A218943621}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22B0448E-2491-44D2-B061-D8A218943621}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7702277A-2C89-4C0A-9BA8-AA6C94B4550A} - EndGlobalSection -EndGlobal diff --git a/sandbox/src/Extensions.cs b/sandbox/src/Extensions.cs deleted file mode 100644 index 581e527..0000000 --- a/sandbox/src/Extensions.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using EnvDTE; -using Microsoft.VisualStudio.Shell; -using Process = System.Diagnostics.Process; - -namespace VSLinuxDebugger -{ - public static class Extensions - { - public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - void Process_Exited(object sender, EventArgs e) - { - tcs.TrySetResult(true); - } - - process.EnableRaisingEvents = true; - process.Exited += Process_Exited; - - try - { - if (process.HasExited) - { - return process.ExitCode; - } - - using (cancellationToken.Register(() => tcs.TrySetCanceled())) - { - await tcs.Task.ConfigureAwait(false); - } - } - finally - { - process.Exited -= Process_Exited; - } - - return process.ExitCode; - } - - /// - /// Gets the startup project for the given solution. - /// - /// The solution. - /// null if the startup project cannot be found. - public static Project GetStartupProject(this Solution solution) - { - ThreadHelper.ThrowIfNotOnUIThread(); - Project ret = null; - - if (solution?.SolutionBuild?.StartupProjects != null) - { - string uniqueName = (string)((object[])solution.SolutionBuild.StartupProjects)[0]; - - // Can't use the solution.Item(uniqueName) here since that doesn't work - // for projects under solution folders. - ret = GetProject(solution, uniqueName); - } - - return ret; - } - - /// - /// Gets the project located in the given solution. - /// - /// The solution. - /// The unique name of the project. - /// null if the project could not be found. - private static Project GetProject(Solution solution, string uniqueName) - { - ThreadHelper.ThrowIfNotOnUIThread(); - Project ret = null; - - if (solution != null && uniqueName != null) - { - foreach (Project p in solution.Projects) - { - ret = GetSubProject(p, uniqueName); - - if (ret != null) - break; - } - } - - return ret; - } - - /// - /// Gets a project located under another project item. - /// - /// The project to start the search from. - /// Unique name of the project. - /// null if the project can't be found. - /// Only works for solution folders. - private static Project GetSubProject(Project project, string uniqueName) - { - ThreadHelper.ThrowIfNotOnUIThread(); - Project ret = null; - - if (project != null) - { - if (project.UniqueName == uniqueName) - { - ret = project; - } - else if (project.Kind == Constants.vsProjectKindSolutionItems) - { - // Solution folder - foreach (ProjectItem projectItem in project.ProjectItems) - { - ret = GetSubProject(projectItem.SubProject, uniqueName); - - if (ret != null) - break; - } - } - } - - return ret; - } - } -} diff --git a/sandbox/src/LocalHost.cs b/sandbox/src/LocalHost.cs deleted file mode 100644 index 4d98b27..0000000 --- a/sandbox/src/LocalHost.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json.Linq; - -namespace VSLinuxDebugger -{ - internal class LocalHost - { - internal LocalHost(string remoteUserName, string remoteUserPass, - string remoteIP, - string remoteVsDbgPath, string remoteDotnetPath, - string remoteDebugFolderPath, - bool useSshKey = false, - bool usePlink = false) - { - _remoteUserName = remoteUserName; - _remoteUserPass = remoteUserPass; - _remoteUseSshKey = useSshKey; - _remoteUsePLink = usePlink; - _remoteIP = remoteIP; - _remoteHostPort = 22; - _remoteVsDbgPath = remoteVsDbgPath; - _remoteDotnetPath = remoteDotnetPath; - _remoteDebugFolderPath = remoteDebugFolderPath; - } - - private readonly string _remoteUserName; - private readonly string _remoteUserPass; - private readonly bool _remoteUseSshKey; - private readonly bool _remoteUsePLink; - private readonly string _remoteIP; - private readonly uint _remoteHostPort; - private readonly string _remoteVsDbgPath; - private readonly string _remoteDotnetPath; - private readonly string _remoteDebugFolderPath; - - internal static string DEBUG_ADAPTER_HOST_FILENAME => "launch.json"; - internal static string HOME_DIR_PATH => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - internal static string SSH_KEY_PATH => Path.Combine(HOME_DIR_PATH, ".ssh\\id_rsa"); // TODO: Make path dynamic - - internal string DebugAdapterHostFilePath { get; set; } - internal string ProjectName { get; set; } - internal string Assemblyname { get; set; } - internal string ProjectFullName { get; set; } - internal string SolutionFullName { get; set; } - internal string SolutionDirPath { get; set; } - internal string ProjectConfigName { get; set; } - internal string OutputDirName { get; set; } - internal string OutputDirFullName { get; set; } - - /// Space delimited string containing command line arguments - internal string CommandLineArguments { get; set; } = String.Empty; - - public string GetExtensionDirectory() - { - try - { - var uri = new Uri(typeof(VSLinuxDebuggerPackage).Assembly.CodeBase, UriKind.Absolute); - return Path.GetDirectoryName(uri.LocalPath); - } - catch - { - return null; - } - } - - /// Generates a temporary json file and returns its path - /// Full path to the generated json file - internal string ToJson() - { - var sshPassword = !_remoteUseSshKey - ? $"-pw {_remoteUserPass}" - : $"-i {SSH_KEY_PATH}"; - - var sshEndpoint = $"{_remoteUserName}@{_remoteIP}:{_remoteHostPort}"; - - dynamic json = new JObject(); - json.version = "0.2.0"; - - if (!_remoteUsePLink) - { - json.adapter = "ssh.exe"; - json.adapterArgs = $"{sshPassword} {sshEndpoint} {_remoteVsDbgPath} --interpreter=vscode"; - } - else - { - //// "%LocalAppData%\\microsoft\\visualstudio\\16.0_c1d3f8c1\\extensions\\cruad2hg.efs\\plink.exe"; - var plinkPath = Path.Combine(GetExtensionDirectory(), "plink.exe").Trim('"'); - json.adapter = plinkPath; - json.adapter = @"C:\work\tools\PuTTY\PLINK.EXE"; // For testing only - json.adapterArgs = $"-ssh -pw {_remoteUserPass} {_remoteUserName}@{_remoteIP} -batch -T {_remoteVsDbgPath} --interpreter=vscode"; - //// json.adapterArgs = $"-ssh -pw {_remoteUserPass} {_remoteUserName}@{_remoteIP}:22 -batch -T {_remoteVsDbgPath} --interpreter=vscode"; - } - - json.configurations = new JArray() as dynamic; - dynamic config = new JObject(); - config.project = "default"; - config.type = "coreclr"; - config.request = "launch"; - config.program = _remoteDotnetPath; - - ////var jarrObj = new JArray($"./{Assemblyname}.dll"); - var jarrObj = new JArray($"{Assemblyname}.dll"); - if (CommandLineArguments.Length > 0) - { - foreach (var arg in CommandLineArguments.Split(' ')) - { - jarrObj.Add(arg); - } - } - - config.args = jarrObj; - config.cwd = _remoteDebugFolderPath; - config.stopAtEntry = "false"; - config.console = "internalConsole"; - json.configurations.Add(config); - - string tempJsonPath = Path.Combine(Path.GetTempPath(), DEBUG_ADAPTER_HOST_FILENAME); - File.WriteAllText(tempJsonPath, Convert.ToString(json)); - - return tempJsonPath; - - /* - { - "version": "0.2.0", - "adapter": "%LocalAppData%\\microsoft\\visualstudio\\16.0_c1d3f8c1\\extensions\\cruad2hg.efs\\plink.exe", - "adapterArgs": "-pw {_remoteUserPass} {_remoteUserName}@{_remoteIP}:22 -batch -T vsdbg --interpreter=vscode", - "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "dotnet", - "args": [ - "{Assemblyname}.dll", - "" - ], - "cwd": "./MonoDebugTemp/", - "console": "internalConsole", - "stopAtEntry": true - } - ] - } - */ - } - } -} diff --git a/sandbox/src/OptionsPages/LocalOptionsPage.cs b/sandbox/src/OptionsPages/LocalOptionsPage.cs deleted file mode 100644 index fe7f2ce..0000000 --- a/sandbox/src/OptionsPages/LocalOptionsPage.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.ComponentModel; -using Microsoft.VisualStudio.Shell; - -namespace VSLinuxDebugger.OptionsPages -{ - public class LocalOptionsPage : DialogPage - { - [Category("Local Machine")] - [DisplayName("Publish")] - [Description("Publish the solution instead of building. Apply setting for ASP.NET/Blazor projects.")] - public bool Publish { get; set; } = false; - - [Category("Local Machine")] - [DisplayName("Use Command Line Arguments")] - [Description( - "Apply command line arguments from Visual Studio Project Settings. " + - "(Experimental : Project Settings -> Debugging -> Command Line Arguments)")] - public bool UseCommandLineArgs { get; set; } = false; - - [Category("Local Machine")] - [DisplayName("Display GUI")] - [Description( - "Display application on remote machine. This is helpful for debugging " + - "GUI applications on remote devices.")] - public bool DisplayInGui { get; set; } = true; - - [Category("Local Machine")] - [DisplayName("Use PLink instead of SSH")] - [Description("Set to TRUE to debug with PLINK.EXE and FALSE for SSH.")] - public bool UsePLinkForDebugging { get; set; } = false; - - // TODO: Move to menu items. - [Category("Local Machine")] - [DisplayName("Build and deploy only.")] - [Description("Does not perform debugging operation.")] - public bool NoDebug { get; set; } = false; - } -} diff --git a/sandbox/src/OptionsPages/RemoteOptionsPage.cs b/sandbox/src/OptionsPages/RemoteOptionsPage.cs deleted file mode 100644 index 3078da6..0000000 --- a/sandbox/src/OptionsPages/RemoteOptionsPage.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel; -using Microsoft.VisualStudio.Shell; - -namespace VSLinuxDebugger.OptionsPage -{ - public class RemoteOptionsPage : DialogPage - { - [Category("Remote Machine Settings")] - [DisplayName("IP Address")] - [Description("Remote IP Address")] - public string IP { get; set; } = "192.168.1.205"; - - [Category("Remote Machine")] - [DisplayName("Host Port Number (22)")] - [Description("Remote Host Port Number (SSH Default is 22)")] - public uint HostPort { get; set; } = 22; - - [Category("Remote Machine")] - [DisplayName("User Name")] - [Description("Remote Machine User Name")] - public string UserName { get; set; } = "pi"; - - [Category("Remote Machine")] - [DisplayName("User Password")] - [Description("Remote Machine User Password")] - public string UserPass { get; set; } = "raspberry"; - - [Category("Remote Machine")] - [DisplayName("Use SSSH Key File")] - [Description("Use SSH Key for connecting to remote machine.")] - public bool UseSshKeyFile { get; set; } = false; - - [Category("Remote Machine")] - [DisplayName("Group Name")] - [Description("Remote Machine Group Name. For RaspberryPI you may use, 'pi'.")] - public string GroupName { get; set; } = ""; - - [Category("Remote Machine")] - [DisplayName("Visual Studio Debugger Path")] - [Description("Remote Machine Visual Studio Debugger Path")] - public string VsDbgPath { get; set; } = "~/.vs-debugger/vs2022"; - - ////public string VsDbgPath { get; set; } = "~/.vsdbg/vsdbg"; - - [Category("Remote Machine")] - [DisplayName(".Net Path")] - [Description("Remote Machine .Net Path")] - public string DotNetPath { get; set; } = "~/.dotnet/dotnet"; - - [Category("Remote Machine")] - [DisplayName("Project folder")] - [Description("Folder for to transfer files to. For HOME folder, use './VLSDbg' and not '~/VLSDbg'")] - public string AppFolderPath { get; set; } = $"./VSLinuxDbg"; // "VSLDebugger" - } -} diff --git a/sandbox/src/Properties/AssemblyInfo.cs b/sandbox/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 26d2c5c..0000000 --- a/sandbox/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("VSLinuxDebugger")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Xeno Innovations, Inc.")] -[assembly: AssemblyProduct("VSLinuxDebugger")] -[assembly: AssemblyCopyright("Copyright 2021 Xeno Innovations, Inc.")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// 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.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/sandbox/src/RemoteDebugCommand.cs b/sandbox/src/RemoteDebugCommand.cs deleted file mode 100644 index 1101412..0000000 --- a/sandbox/src/RemoteDebugCommand.cs +++ /dev/null @@ -1,639 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using EnvDTE; -using EnvDTE80; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; -using Newtonsoft.Json.Linq; -using Renci.SshNet; -using SharpCompress.Common; -using SharpCompress.Writers; -using Process = System.Diagnostics.Process; -using Task = System.Threading.Tasks.Task; - -namespace VSLinuxDebugger -{ - /// - /// Command handler - /// - internal sealed class RemoteDebugCommand - { - /// Command ID. - public const int CommandId = 0x0100; - - /// Command menu group (command set GUID). - public static readonly Guid CommandSet = new Guid("5b4eaa99-73ea-49a5-99c3-bd64eecafa37"); - - /// VS Package that provides this command, not null. - private readonly AsyncPackage _package; - - private readonly string _tarGzFileName = "vsldBuildContents.tar.gz"; - - private VSLinuxDebuggerPackage Settings => _package as VSLinuxDebuggerPackage; - private LocalHost _localhost; - private bool _isBuildSucceeded; - private string _launchJsonPath = string.Empty; - - public static BuildEvents BuildEvents { get; set; } - - /// - /// Initializes a new instance of the class. - /// Adds our command handlers for menu (commands must exist in the command table file) - /// - /// Owner package, not null. - /// Command service to add command to, not null. - private RemoteDebugCommand(AsyncPackage package, OleMenuCommandService commandService) - { - _package = package ?? throw new ArgumentNullException(nameof(package)); - commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); - - var menuCommandID = new CommandID(CommandSet, CommandId); - var menuItem = new MenuCommand(this.Execute, menuCommandID); - - commandService.AddCommand(menuItem); - } - - /// - /// Initializes the singleton instance of the command. - /// - /// Owner package, not null. - public static async Task InitializeAsync(AsyncPackage package) - { - // Switch to the main thread - the call to AddCommand in RemoteDebug's constructor requires the UI thread. - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); - var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)).ConfigureAwait(false) as OleMenuCommandService; - Instance = new RemoteDebugCommand(package, commandService); - } - - /// - /// Gets the instance of the command. - /// - public static RemoteDebugCommand Instance { get; private set; } - - /// - /// Wrapper around a (alert) messagebox - /// - /// - private void MsgBox(string message, string title = "Error") => VsShellUtilities.ShowMessageBox( - _package, - message, - title, - OLEMSGICON.OLEMSGICON_INFO, - OLEMSGBUTTON.OLEMSGBUTTON_OK, - OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); - - /// - /// This function is the callback used to execute the command when the menu item is clicked. - /// See the constructor to see how the menu item is associated with this function using - /// OleMenuCommandService service and MenuCommand class. - /// - private async void Execute(object sender, EventArgs e) - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - try - { - bool connected = await Task.Run(() => CheckConnWithRemote()).ConfigureAwait(true); - } - catch (Exception connection_exception) - { - MsgBox($"Cannot connect to {Settings.UserName}:{Settings.IP}.:" + connection_exception.ToString()); - return; - } - - if (!InitSolution()) - { - MsgBox("Please select a startup project"); - } - else - { - await Task.Run(() => - { - TryInstallVsDbg(); - MkDir(); - Clean(); - }).ConfigureAwait(true); - - if (!Settings.Publish) - { - Bash($"export DISPLAY=:0"); - Build(); // once this finishes it will raise an event; see BuildEvents_OnBuildDone - } - else - { - int exitcode = await PublishAsync().ConfigureAwait(true); - - if (exitcode != 0) - { - MsgBox("File transfer to Remote Machine failed"); - } - else - { - string errormessage = await TransferFiles2Async().ConfigureAwait(true); - - if (errormessage != "") - { - MsgBox("Build failed: " + errormessage); - } - else - { - if (Settings.NoDebug) - { - MsgBox("Files successfully transfered to remote machine", "Success"); - } - else - { - Bash($"export DISPLAY=:0"); - Debug(); - } - } - } - } - } - } - - /// - /// Must be called on the UI thread - /// - private bool InitSolution() - { - ThreadHelper.ThrowIfNotOnUIThread(); - var dte = (DTE2)Package.GetGlobalService(typeof(SDTE)); - var project = dte.Solution.GetStartupProject(); - - if (project == null) - { - return false; - } - - _localhost = new LocalHost(Settings.UserName, Settings.UserPass, Settings.IP, Settings.VsDbgPath, Settings.DotnetPath, Settings.DebugFolderPath, Settings.UseSshKeyFile, Settings.UsePlinkForDebugging); - - _localhost.ProjectFullName = project.FullName; - _localhost.ProjectName = project.Name; - _localhost.Assemblyname = project.Properties.Item("AssemblyName").Value.ToString(); - _localhost.SolutionFullName = dte.Solution.FullName; - _localhost.SolutionDirPath = Path.GetDirectoryName(_localhost.SolutionFullName); - _localhost.ProjectConfigName = project.ConfigurationManager.ActiveConfiguration.ConfigurationName; - _localhost.OutputDirName = project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString(); - _localhost.OutputDirFullName = Path.Combine(Path.GetDirectoryName(project.FullName), _localhost.OutputDirName); - - if (Settings.UseCommandLineArgs) - { - _localhost.CommandLineArguments = GetArgs(_localhost.SolutionDirPath); - } - - string debugtext = $"ProjectFullName: {_localhost.ProjectFullName} \nProjectName: {_localhost.ProjectName} \n" + - $"SolutionFullName: {_localhost.SolutionFullName} \nSolutionDirPath:{_localhost.SolutionDirPath} \n" + - $"ProjectConfigname: {_localhost.ProjectConfigName} \nOutputDirName: {_localhost.OutputDirName} \nOutputDirFullName: {_localhost.OutputDirFullName}"; - - return true; - } - - /// - /// Checks if a connection with the remote machine can be established - /// - /// - private bool CheckConnWithRemote() - { - try - { - Bash("echo hello"); - return true; - } - catch (Exception) - { - throw; - } - } - - /// - /// create debug/release folders and take ownership - /// - private void MkDir() - { - Bash($"sudo mkdir -p {Settings.DebugFolderPath}"); - Bash($"sudo mkdir -p {Settings.ReleaseFolderPath}"); - ////Bash($"sudo chown -R {Settings.UserName}:{Settings.GroupName} {Settings.AppFolderPath}"); - - var group = string.IsNullOrEmpty(Settings.GroupName) ? string.Empty : $":{Settings.GroupName}"; - Bash($"sudo chown -R {Settings.UserName}{group} {Settings.AppFolderPath}"); - } - - /// - /// clean everything in the debug directory - /// - private void Clean() => Bash($"sudo rm -rf {Settings.DebugFolderPath}/*"); - - /// - /// Instals VS Debugger if it doesn't exist already - /// - private void TryInstallVsDbg() - { - string arch = Bash("uname -m").Trim('\n'); - - // It seems like the latest versions of getvsdbgsh actually figures out the right version itself - Bash("[ -d ~/.vsdbg ] || curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/.vsdbg"); - - /*switch (arch) - { - case "arm7l": - Bash("[ -d ~/.vsdbg ] || curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -r linux-arm -v latest -l ~/.vsdbg"); - break; - - case "aarch64": - Bash("[ -d ~/.vsdbg ] || curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -r linux-arm64 -v latest -l ~/.vsdbg"); - break; - - default: - break; - }*/ - } - - private void Build() - { - ThreadHelper.ThrowIfNotOnUIThread(); - var dte = (DTE)Package.GetGlobalService(typeof(DTE)); - BuildEvents = dte.Events.BuildEvents; - // For some reason, cleanup isn't actually always ran when there has been an error. - // This removes the fact that if you run a debug attempt, get a file error, that you don't get 2 message boxes, 3 message boxes, etc for each attempt. - BuildEvents.OnBuildDone -= BuildEvents_OnBuildDoneAsync; - BuildEvents.OnBuildDone += BuildEvents_OnBuildDoneAsync; - BuildEvents.OnBuildProjConfigDone -= BuildEvents_OnBuildProjConfigDone; - BuildEvents.OnBuildProjConfigDone += BuildEvents_OnBuildProjConfigDone; - dte.SuppressUI = false; - dte.Solution.SolutionBuild.BuildProject(_localhost.ProjectConfigName, _localhost.ProjectFullName); - } - - private void LogOutput(string message) - { - //// if (Settings.LogVerbose) - Console.WriteLine(message); - } - - /// - /// Publish the solution. Publishing is done via an external process - /// - /// - private async Task PublishAsync() - { - using (var process = new Process()) - { - process.StartInfo.FileName = "dotnet"; - process.StartInfo.Arguments = $@"publish -c {_localhost.ProjectConfigName} -r linux-arm --self-contained=false -o {_localhost.OutputDirFullName} {_localhost.ProjectFullName}"; - process.StartInfo.CreateNoWindow = true; - process.Start(); - - return await process.WaitForExitAsync().ConfigureAwait(true); - } - } - - private async Task TransferFiles2Async() - { - LogOutput($"Connecting to {Settings.UserName}@{Settings.IP}:{Settings.HostPort}..."); - - try - { - Bash($@"mkdir -p {Settings.DebugFolderPath}"); - - // 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")}"); - - // Sample SCP Connection: - //// LogOutput($"Error: {ex.Message} Is SFTP supported for {username}@{host}:{port}? We are using SCP instead!"); - //// _scpClient = (keyFile == null) - //// ? new ScpClient(host, port, username, password) - //// : new ScpClient(host, port, username, keyFile); - //// _scpClient.Connect(); - - using (var sftp = !Settings.UseSshKeyFile - ? new Renci.SshNet.SftpClient(Settings.IP, 22, Settings.UserName, Settings.UserPass) - : null) //// new SftpClient(Settings.IP, Settings.HostPort, Settings.UserName, Settings.SshKeyFile)) - { - sftp.Connect(); - LogOutput("Connected to via SSH and SFTP"); - - var srcDirInfo = new DirectoryInfo(_localhost.OutputDirFullName); - if (!srcDirInfo.Exists) - throw new DirectoryNotFoundException($"Directory '{_localhost.OutputDirFullName}' not found!"); - - // Compress files to upload as single `tar.gz`. - // TODO: Use base folder path: var pathTarGz = $"{Settings.AppFolderPath}/{_tarGzFileName}"; - var pathTarGz = $"{Settings.DebugFolderPath}/{_tarGzFileName}"; - var success = PayloadCompressAndUpload(sftp, srcDirInfo, pathTarGz); - - // Decompress file - PayloadDecompress(pathTarGz, false); - }; - - return string.Empty; - } - catch (Exception ex) - { - return ex.ToString(); - } - } - - /// Compress build contents and upload to remote host. - /// SFTP connection. - /// Build (source) contents directory info. - /// Upload path and filename of build's tar.gz file. - /// - private bool PayloadCompressAndUpload(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 - { - using (var tarGzWriter = WriterFactory.Open(tarGzStream, ArchiveType.Tar, CompressionType.GZip)) - { - using (MemoryStream fileStream = new MemoryStream()) - { - using (BinaryWriter fileWriter = new BinaryWriter(fileStream)) - { - fileWriter.Write(localFiles.Count); - - var updateFileCount = 0; - long updateFileSize = 0; - var allFileCount = 0; - long allFileSize = 0; - - foreach (var file in localFiles) - { - 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) - { - LogOutput($"Exception: {ioEx.Message}"); - } - 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"); - } - } - } - - success = true; - } - catch (Exception ex) - { - LogOutput($"Error while compressing file contents. {ex.Message}\n{ex.StackTrace}"); - } - - // Upload the file - if (success) - { - try - { - var tarGzSize = tarGzStream.Length; - tarGzStream.Seek(0, SeekOrigin.Begin); - - sftp.UploadFile(tarGzStream, pathBuildTarGz); - - LogOutput($"Uploaded '{_tarGzFileName}' [{tarGzSize,13:n0} bytes]."); - success = true; - } - catch (Exception ex) - { - LogOutput($"Error while uploading file. {ex.Message}\n{ex.StackTrace}"); - success = false; - } - } - } - - return success; - } - - /// Unpack build contents. - /// Path to upload to. - /// Remove our build's tar.gz file. Set to FALSE for debugging. (default=true) - /// Returns true on success. - private bool PayloadDecompress(string pathBuildTarGz, bool removeTarGz = true) - { - try - { - var cmd = $"set -e;cd \"{Settings.DebugFolderPath}\""; - cmd += $";tar -zxf \"{pathBuildTarGz}\""; - - if (removeTarGz) - cmd += $";rm \"{pathBuildTarGz}\""; - - var output = Bash(cmd); - LogOutput(output); - - return true; - } - catch (Exception) - { - return false; - } - } - - /// Get all files to transfer. - /// - /// Collection of file names and their full path. - private ConcurrentDictionary GetLocalFiles(DirectoryInfo srcDirInfo) - { - var startIndex = srcDirInfo.FullName.Length; - var localFileCache = new ConcurrentDictionary(); - Parallel.ForEach(GetFiles(srcDirInfo.FullName), file => - { - var cleanedRelativeFilePath = file.Substring(startIndex); - cleanedRelativeFilePath = cleanedRelativeFilePath.Replace("\\", "/").TrimStart('/'); - localFileCache[cleanedRelativeFilePath] = new FileInfo(file); - }); - - LogOutput($"Local file cache created"); - return localFileCache; - } - - /// Get all files, including subdirectories. - /// Base path. - /// Collection of files in folder path. - private IEnumerable GetFiles(string path) - { - Queue queue = new Queue(); - queue.Enqueue(path); - - while (queue.Count > 0) - { - path = queue.Dequeue(); - try - { - foreach (string subDir in Directory.GetDirectories(path)) - queue.Enqueue(subDir); - } - catch (Exception ex) - { - Console.Error.WriteLine(ex); - } - - string[] files = null; - try - { - files = Directory.GetFiles(path); - } - catch (Exception ex) - { - Console.Error.WriteLine(ex); - } - - if (files != null) - { - for (int i = 0; i < files.Length; i++) - yield return files[i]; - } - } - } - - /// - /// Start debugging using the remote visual studio server adapter - /// - private void Debug() - { - _launchJsonPath = _localhost.ToJson(); - - var dte = (DTE2)Package.GetGlobalService(typeof(SDTE)); - dte.ExecuteCommand("DebugAdapterHost.Launch", $"/LaunchJson:\"{_launchJsonPath}\""); - } - - private void Cleanup() - { - File.Delete(_launchJsonPath); - - BuildEvents.OnBuildDone -= BuildEvents_OnBuildDoneAsync; - BuildEvents.OnBuildProjConfigDone -= BuildEvents_OnBuildProjConfigDone; - } - - private string Bash(string cmd) - { - try - { - // TODO: Assign port. new SshClient(host, port, userName, userPass); - using (var client = Settings.UseSshKeyFile ? - new SshClient(Settings.IP, Settings.UserName, new PrivateKeyFile[] { new PrivateKeyFile(LocalHost.SSH_KEY_PATH) }) : - new SshClient(Settings.IP, Settings.UserName, Settings.UserPass)) - { - client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(5); - client.Connect(); - var sshcmd = client.RunCommand(cmd); - client.Disconnect(); - - return sshcmd.Result; - } - } - catch (Exception) - { - throw; - } - } - - /// - /// The build is finised sucessfully only when the startup project has been compiled without any errors - /// - private void BuildEvents_OnBuildProjConfigDone(string project, string projectConfig, string platform, string solutionConfig, bool success) - { - string debugtext = $"Project: {project} --- Success: {success}\n"; - - if (!success) - { - Cleanup(); - } - - _isBuildSucceeded = Path.GetFileName(project) == _localhost.ProjectName + ".csproj" && success; - } - - /// - /// Build finished. We can now transfer the files to the remote host and start debugging the program - /// - private async void BuildEvents_OnBuildDoneAsync(vsBuildScope scope, vsBuildAction action) - { - if (_isBuildSucceeded) - { - string errormessage = await TransferFiles2Async().ConfigureAwait(true); - //// string errormessage = await TransferFilesAsync().ConfigureAwait(true); - - if (errormessage == "") - { - if (Settings.NoDebug) - { - MsgBox("Files sucessfully transfered to remote machine", "Success"); - } - else - { - Debug(); - } - - Cleanup(); - } - else - { - MsgBox($"Transferring files failed: {errormessage}"); - } - } - } - - /// - /// Tries to search for a file "launchSettings.json", which is used by Visual Studio to store the command line arguments - /// - /// - /// If using multiple debugging profiles, this will not work - /// The debugging command arguments set in Project Settings -> Debug -> Command Line Arguments - private string GetArgs(string slnDirPath) - { - string args = String.Empty; - string[] launchSettingsOccurences = Directory.GetFiles(slnDirPath, "launchSettings.json", SearchOption.AllDirectories); - - if (launchSettingsOccurences.Length == 1) - { - var jobj = JObject.Parse(File.ReadAllText(launchSettingsOccurences[0])); - - var commandLineArgsOccurences = jobj.SelectTokens("$..commandLineArgs") - .Select(t => t.Value()) - .ToList(); - - if (commandLineArgsOccurences.Count > 1) - { - MsgBox( - "Multiple debugging profiles detected. Due to Visual Studio API limitations, command line arguments cannot be used. \n" + - "Please turn off Command Line Arguments in the extension settings page"); - } - else if (commandLineArgsOccurences.Count == 1) - { - args = commandLineArgsOccurences[0]; - } - } - else if (launchSettingsOccurences.Length > 1) - { - MsgBox("Cannot read command line arguments"); - } - - return args; - } - } -} diff --git a/sandbox/src/RemoteDebugger.png b/sandbox/src/RemoteDebugger.png deleted file mode 100644 index efc00151da1852819ad89c25b20c159e90fb7ce9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25407 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 yS?Rtj6_;B6>;HnP!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=d3- zBeIx*f$sf-Ln{LV)sD1jC>nC}Q!>*kacc;~>+LolAm0fo0?Z*WfcHaTFhX${Qqu!G&Nv5t*kOqGt)Cl7)%X~ z4qONb1ga57G6$+AJTs*v1EfTzhWQRqi6oK|-^|?9lFEWq2C%F23-Z$KH--73nd2J* zRK;LuYGhz)U|?ZvKFPpo15k-Dk~zVdRls1@Gc)yea{4Pie;-iV4oNyR$de&0Gba@o z1bX_V1v&YNDaHDxX8NIC=~Yn?Zu*ACW|sQ81*J(jnZ+6LmIkI~hI&A^_}{F`0-Chb z)5S5w!hh=|Tdo!Zf!27l3dUTWoE#?2w`*)GUX@JKv~9enutTzU(hu&}zYb1FH2N&j zcHE6!?!(9T=Ut??yTFnGH9 KxvXxVyW%>)`GVgEY?H@B44WM(mz^ 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 yS?Rtj6_;B6>;HnP - - - 16.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - Debug - AnyCPU - 2.0 - {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - {22B0448E-2491-44D2-B061-D8A218943621} - Library - Properties - VSRemoteDebugger - VSRemoteDebugger - v4.8 - true - true - true - false - false - true - true - Program - $(DevEnvDir)devenv.exe - /rootsuffix Exp - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - Component - - - Component - - - - - - - - Designer - - - - - - - - - - - - - - 17.0.31902.203 - - - compile; build; native; contentfiles; analyzers; buildtransitive - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - 13.0.1 - - - 0.30.1 - - - 2020.0.1 - - - - - Menus.ctmenu - - - - - - Always - true - - - Always - true - - - - - - - \ No newline at end of file diff --git a/sandbox/src/VSLinuxDebuggerPackage.cs b/sandbox/src/VSLinuxDebuggerPackage.cs deleted file mode 100644 index 8a0d9b4..0000000 --- a/sandbox/src/VSLinuxDebuggerPackage.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; -using Microsoft.VisualStudio.Shell; -using VSLinuxDebugger.OptionsPage; -using VSLinuxDebugger.OptionsPages; -using Task = System.Threading.Tasks.Task; - -namespace VSLinuxDebugger -{ - /// - /// This is the class that implements the package exposed by this assembly. - /// - /// - /// - /// The minimum requirement for a class to be considered a valid package for Visual Studio - /// is to implement the IVsPackage interface and register itself with the shell. - /// This package uses the helper classes defined inside the Managed Package Framework (MPF) - /// to do it: it derives from the Package class that provides the implementation of the - /// IVsPackage interface and uses the registration attributes defined in the framework to - /// register itself and its components with the shell. These attributes tell the pkgdef creation - /// utility what data to put into .pkgdef file. - /// - /// - /// To get loaded into VS, the package must be referred by <Asset Type="Microsoft.VisualStudio.VsPackage" ...> in .vsixmanifest file. - /// - /// - [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] - [Guid(PackageGuidString)] - [ProvideMenuResource("Menus.ctmenu", 1)] - [ProvideOptionPage(typeof(RemoteOptionsPage), "Linux TEST Debugger", "Remote Machine", 0, 0, true)] - [ProvideOptionPage(typeof(LocalOptionsPage), "Linux TEST Debugger", "Local Machine", 0, 0, true)] - public sealed class VSLinuxDebuggerPackage : AsyncPackage - { - private RemoteOptionsPage RemotePage => (RemoteOptionsPage)GetDialogPage(typeof(RemoteOptionsPage)); - - private LocalOptionsPage LocalPage => (LocalOptionsPage)GetDialogPage(typeof(LocalOptionsPage)); - - public string IP => RemotePage.IP; - - public int HostPort => 22; - - public string UserName => RemotePage.UserName; - - public string UserPass => RemotePage.UserPass; - - public string GroupName => RemotePage.GroupName; - - public bool UseSshKeyFile => RemotePage.UseSshKeyFile; - - public bool UsePlinkForDebugging => LocalPage.UsePLinkForDebugging; - - public string VsDbgPath => RemotePage.VsDbgPath; - public string DotnetPath => RemotePage.DotNetPath; - public string AppFolderPath => RemotePage.AppFolderPath; - public string DebugFolderPath => $"{AppFolderPath}/TMP"; //// AppFolderPath + "/debug"; - public string ReleaseFolderPath => $"{AppFolderPath}/TMP"; //// AppFolderPath + "/release"; - public bool Publish => LocalPage.Publish; - public bool UseCommandLineArgs => LocalPage.UseCommandLineArgs; - public bool NoDebug => LocalPage.NoDebug; - - /// - /// VSLinuxDebuggerPackage GUID string. - /// - public const string PackageGuidString = "27375819-9dd0-4292-a612-2300250b06b8"; - - #region Package Members - - /// - /// Initialization of the package; this method is called right after the package is sited, so this is the place - /// where you can put all the initialization code that rely on services provided by VisualStudio. - /// - /// A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down. - /// A provider for progress updates. - /// A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method. - protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) - { - // When initialized asynchronously, the current thread may be a background thread at this point. - // Do any initialization that requires the UI thread after switching to the UI thread. - await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - await RemoteDebugCommand.InitializeAsync(this).ConfigureAwait(false); - } - - #endregion Package Members - } -} diff --git a/sandbox/src/VSLinuxDebuggerPackage.vsct b/sandbox/src/VSLinuxDebuggerPackage.vsct deleted file mode 100644 index 1629adb..0000000 --- a/sandbox/src/VSLinuxDebuggerPackage.vsct +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sandbox/src/source.extension.vsixmanifest b/sandbox/src/source.extension.vsixmanifest deleted file mode 100644 index 16a22c5..0000000 --- a/sandbox/src/source.extension.vsixmanifest +++ /dev/null @@ -1,27 +0,0 @@ - - - - - VSLinuxDebugger - Remote Linux Debugging tool for VS2022. - Resources\RemoteDebugger.png - Resources\RemoteDebugger.png - - - - x86 - - - amd64 - - - - - - - - - - - - \ No newline at end of file