diff --git a/RaspberryDebugger/Commands/SettingsCommand.cs b/RaspberryDebugger/Commands/SettingsCommand.cs index 1578214..494d0ca 100644 --- a/RaspberryDebugger/Commands/SettingsCommand.cs +++ b/RaspberryDebugger/Commands/SettingsCommand.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // FILE: SettingsCommand.cs // CONTRIBUTOR: Jeff Lill // COPYRIGHT: Copyright (c) 2021 by neonFORGE, LLC. All rights reserved. @@ -69,7 +69,7 @@ private SettingsCommand(AsyncPackage package, OleMenuCommandService commandServi { var command = (OleMenuCommand)s; - command.Visible = PackageHelper.IsActiveProjectRaspberryCompatible(dte); + command.Visible = PackageHelper.IsActiveProjectRaspberryExecutable(dte); }; commandService?.AddCommand(menuItem); diff --git a/RaspberryDebugger/DebugHelper.cs b/RaspberryDebugger/DebugHelper.cs index 92121a1..01a578f 100644 --- a/RaspberryDebugger/DebugHelper.cs +++ b/RaspberryDebugger/DebugHelper.cs @@ -49,7 +49,7 @@ internal static class DebugHelper /// /// Track the last output file that was uploaded. /// - private static DirectoryInfo LastUploadedDirInfo { get; set; } + private static DirectoryInfo LastUploadedPublishDirInfo { get; set; } /// /// Ensures that the native Windows OpenSSH client is installed, prompting @@ -302,7 +302,7 @@ private static async Task BuildProjectAsync(DTE2 dte, Solution solution, P } // Build the project to ensure that there are no compile-time errors. - Log.Info($"Build Started: {projectProperties?.FullPath}, Configuration: {projectProperties.Configuration}"); + Log.Info($"Build Started: {project.FullName}, Configuration: {projectProperties.ConfigurationName}, Platform: {projectProperties.PlatformName}"); solution?.SolutionBuild.BuildProject( solution.SolutionBuild.ActiveConfiguration.Name, project?.UniqueName, WaitForBuildToFinish: true); @@ -338,7 +338,7 @@ private static async Task PublishProjectAsync(DTE2 dte, Solution solution, // these can cause conflicts when we invoke [dotnet] below to // publish the project. - Log.Info($"Publishing Started: {projectProperties?.FullPath}, Runtime: {projectProperties.Runtime}"); + Log.Info($"Publishing Started: {project?.FullName}, RuntimeIdentifier: {projectProperties.RuntimeIdentifier}"); const string allowedVariableNames = """ @@ -413,24 +413,24 @@ private static async Task PublishProjectAsync(DTE2 dte, Solution solution, var options = new List() { "publish", - "--configuration", projectProperties.Configuration + "--configuration", projectProperties.ConfigurationName }; - if ( (bool)( projectProperties?.Framework.HasValue ) ) + if ( (bool)( projectProperties?.TargetFramework.HasValue ) ) { options.AddRange( [ - "--framework", projectProperties.Framework == DotNetFrameworks.DotNet_8 + "--framework", projectProperties.TargetFramework == DotNetFrameworks.DotNet_8 ? "net8.0" : "net9.0", // short term hack ]); } options.AddRange( [ - "--runtime", projectProperties.Runtime, + "--runtime", projectProperties.RuntimeIdentifier, "--no-self-contained", "--output", projectProperties.PublishFolder, - projectProperties.FullPath + project?.FullName ] ); Log.Info("dotnet Command:"); @@ -616,23 +616,22 @@ public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectPro return null; } - var dirInfo = new DirectoryInfo(projectProperties.OutputFolder); + // look at the Build output and see if it's changed. + var dirInfo = new DirectoryInfo(projectProperties.PublishFolder); bool shouldUploadProgram = - LastUploadedDirInfo == null || - LastUploadedDirInfo.FullName != dirInfo.FullName || - LastUploadedDirInfo.LastWriteTime != dirInfo.LastWriteTime; - - var outputFileName = Path.Combine( projectProperties.OutputFolder, projectProperties.OutputFileName ); - + LastUploadedPublishDirInfo == null || + LastUploadedPublishDirInfo.FullName != dirInfo.FullName || + LastUploadedPublishDirInfo.LastWriteTime != dirInfo.LastWriteTime; + if (!shouldUploadProgram) { - Log.Info($"Skipping upload of {outputFileName}, {dirInfo.LastWriteTime}"); + Log.Info($"Skipping upload of {projectProperties.PublishFolder}, {dirInfo.LastWriteTime}"); return connection; } - Log.Info($"Uploading {outputFileName}, {dirInfo.LastWriteTime}"); + Log.Info($"Uploading {projectProperties.PublishFolder}, {dirInfo.LastWriteTime}"); // Upload the program binaries. if (await connection.UploadProgramAsync( @@ -640,7 +639,7 @@ public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectPro projectProperties?.AssemblyName, projectProperties?.PublishFolder)) { - LastUploadedDirInfo = dirInfo; + LastUploadedPublishDirInfo = dirInfo; return connection; } diff --git a/RaspberryDebugger/Models/Connection/ConnectionInfo.cs b/RaspberryDebugger/Models/Connection/ConnectionInfo.cs index 415b226..f326bb8 100644 --- a/RaspberryDebugger/Models/Connection/ConnectionInfo.cs +++ b/RaspberryDebugger/Models/Connection/ConnectionInfo.cs @@ -1,154 +1,174 @@ -//----------------------------------------------------------------------------- -// FILE: ConnectionInfo.cs -// CONTRIBUTOR: Jeff Lill -// COPYRIGHT: Copyright (c) 2021 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. -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using Neon.Net; -using Newtonsoft.Json; -using RaspberryDebugger.OptionsPages; - -namespace RaspberryDebugger.Models.Connection -{ - /// - /// Describes a Raspberry Pi connection's network details and credentials. - /// - internal class ConnectionInfo - { - private bool isDefault; - private string host; - private string user = "pi"; - private string cachedName; - - /// - /// Returns the value to be used for sorting the connection. - /// - [JsonIgnore] - public string SortKey => Name.ToLowerInvariant(); - - /// - /// Returns the connection name like: user@host - /// - [JsonIgnore] - public string Name - { - get - { - if (cachedName != null) - { - return cachedName; - } - - return cachedName = $"{User}@{Host}"; - } - } - - /// - /// Indicates that this is the default connection. - /// - [JsonProperty(PropertyName = "IsDefault", Required = Required.Always)] - public bool IsDefault - { - get => isDefault; - - set - { - if (isDefault == value) return; - - isDefault = value; - ConnectionsPanel?.ConnectionIsDefaultChanged(this); - } - } - - /// - /// The Raspberry Pi host name or IP address. - /// - [JsonProperty(PropertyName = "Host", Required = Required.Always)] - public string Host - { - get => host; - - set - { - host = value; - cachedName = null; - } - } - - /// - /// The SSH port. - /// - [JsonProperty(PropertyName = "Port", Required = Required.Always)] - public int Port { get; set; } = NetworkPorts.SSH; - - /// - /// The user name. - /// - [JsonProperty(PropertyName = "User", Required = Required.Always)] - public string User - { - get => user; - - set - { - user = value; - cachedName = null; - } - } - - /// - /// The password. - /// - [JsonProperty(PropertyName = "Password", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - [DefaultValue(null)] - public string Password { get; set; } = "raspberry"; - - /// - /// Path to the private key for this connection or null when one - /// hasn't been initialized yet. - /// - [JsonProperty(PropertyName = "PrivateKeyPath", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - [DefaultValue(null)] - public string PrivateKeyPath { get; set; } - - /// - /// Path to the public key for this connection or null when one - /// hasn't been initialized yet. - /// - [JsonProperty(PropertyName = "PublicKeyPath", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - [DefaultValue(null)] - public string PublicKeyPath { get; set; } - - /// - /// Describes the authentication method. - /// - [JsonIgnore] - public string Authentication => string.IsNullOrEmpty(PrivateKeyPath) ? "PASSWORD" : "SSH KEY"; - - /// - /// - /// This is a bit of a hack to call the - /// when the user changes the state of the property. The sender will be - /// the changed and the arguments will be empty. - /// - /// - /// This is a bit of hack because the control doesn't - /// appear to have a check box changed event. - /// - /// - [SuppressMessage("ReSharper", "InvalidXmlDocComment")] - internal ConnectionsPanel ConnectionsPanel { get; set; } - } -} +//----------------------------------------------------------------------------- +// FILE: ConnectionInfo.cs +// CONTRIBUTOR: Jeff Lill +// COPYRIGHT: Copyright (c) 2021 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. +using Neon.Net; +using Newtonsoft.Json; +using RaspberryDebugger.Models.Sdk; +using RaspberryDebugger.OptionsPages; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace RaspberryDebugger.Models.Connection +{ + /// + /// Describes a Raspberry Pi connection's network details and credentials. + /// + internal class ConnectionInfo + { + private bool isDefault; + private string host; + private string user = "pi"; + private string cachedName; + private SdkArchitecture architecture = SdkArchitecture.Arm32; + + /// + /// Returns the value to be used for sorting the connection. + /// + [JsonIgnore] + public string SortKey => Name.ToLowerInvariant(); + + /// + /// Returns the connection name like: user@host + /// + [JsonIgnore] + public string Name + { + get + { + if (cachedName != null) + { + return cachedName; + } + + return cachedName = $"{User}@{Host}"; + } + } + + /// + /// Indicates that this is the default connection. + /// + [JsonProperty(PropertyName = "IsDefault", Required = Required.Always)] + public bool IsDefault + { + get => isDefault; + + set + { + if (isDefault == value) return; + + isDefault = value; + ConnectionsPanel?.ConnectionIsDefaultChanged(this); + } + } + + /// + /// The Raspberry Pi host name or IP address. + /// + [JsonProperty(PropertyName = "Host", Required = Required.Always)] + public string Host + { + get => host; + + set + { + host = value; + cachedName = null; + } + } + + /// + /// The SSH port. + /// + [JsonProperty(PropertyName = "Port", Required = Required.Always)] + public int Port { get; set; } = NetworkPorts.SSH; + + /// + /// Provides an architecture setting for this device so that build settings can be + /// pared correctly. + /// + [JsonProperty(PropertyName = "Architecture", Required = Required.Default)] + public SdkArchitecture Architecture + { + get => architecture; + + set + { + if (architecture == value) return; + + architecture = value; + ConnectionsPanel?.ConnectionIsDefaultChanged(this); + } + } + + /// + /// The user name. + /// + [JsonProperty(PropertyName = "User", Required = Required.Always)] + public string User + { + get => user; + + set + { + user = value; + cachedName = null; + } + } + + /// + /// The password. + /// + [JsonProperty(PropertyName = "Password", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(null)] + public string Password { get; set; } = "raspberry"; + + /// + /// Path to the private key for this connection or null when one + /// hasn't been initialized yet. + /// + [JsonProperty(PropertyName = "PrivateKeyPath", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(null)] + public string PrivateKeyPath { get; set; } + + /// + /// Path to the public key for this connection or null when one + /// hasn't been initialized yet. + /// + [JsonProperty(PropertyName = "PublicKeyPath", Required = Required.Default, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(null)] + public string PublicKeyPath { get; set; } + + /// + /// Describes the authentication method. + /// + [JsonIgnore] + public string Authentication => string.IsNullOrEmpty(PrivateKeyPath) ? "PASSWORD" : "SSH KEY"; + + /// + /// + /// This is a bit of a hack to call the + /// when the user changes the state of the property. The sender will be + /// the changed and the arguments will be empty. + /// + /// + /// This is a bit of hack because the control doesn't + /// appear to have a check box changed event. + /// + /// + [SuppressMessage("ReSharper", "InvalidXmlDocComment")] + internal ConnectionsPanel ConnectionsPanel { get; set; } + } +} diff --git a/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs b/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs index e2dc15f..a9c2a12 100644 --- a/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs +++ b/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs @@ -72,30 +72,31 @@ public static ProjectProperties CopyFrom(Solution solution, EnvDTE.Project proje return new ProjectProperties() { - Name = project?.Name, - FullPath = project?.FullName, - Configuration = null, - IsNetCore = false, - SdkVersion = null, - OutputFolder = null, - OutputFileName = null, - IsExecutable = false, - Runtime = string.Empty, - AssemblyName = null, - DebugEnabled = false, - DebugConnectionName = null, - CommandLineArgs = new List(), - EnvironmentVariables = new Dictionary(), + Name = project?.Name, + FullPath = project?.FullName, + OutputPath = string.Empty, + ConfigurationName = null, + Guid = Guid.Empty, + IsNetCore = false, + SdkVersion = null, + OutputFileName = null, + IsExecutable = false, + RuntimeIdentifier = string.Empty, + AssemblyName = null, + DebugEnabled = false, + DebugConnectionName = null, + CommandLineArgs = new List(), + EnvironmentVariables = new Dictionary(), IsSupportedSdkVersion = false, IsRaspberryCompatible = false, - IsAspNet = false, - AspPort = 0, - AspLaunchBrowser = false, - AspRelativeBrowserUri = null + IsAspNet = false, + AspPort = 0, + AspLaunchBrowser = false, + AspRelativeBrowserUri = null, }; } - var projectFolder = Path.GetDirectoryName(project.FullName); + string fullPath = project.Properties.Item("FullPath").Value.ToString(); // Read the properties we care about from the project. var targetFrameworkMonikers = (string)project.Properties.Item("TargetFrameworkMonikers").Value; @@ -129,8 +130,8 @@ public static ProjectProperties CopyFrom(Solution solution, EnvDTE.Project proje // // ASPNETCORE_SERVER.URLS=http://0.0.0.0: - var launchSettingsPath = Path.Combine(projectFolder ?? string.Empty, "Properties", "launchSettings.json"); - var activeDebugProfile = (string)project.Properties.Item("ActiveDebugProfile").Value; + var launchSettingsPath = Path.Combine(fullPath ?? string.Empty, @"Properties/launchSettings.json"); + var activeDebugProfile = project.Properties.Item("ActiveDebugProfile").Value.ToString(); var commandLineArgs = new List(); var environmentVariables = new Dictionary(); var isAspNet = false; @@ -271,7 +272,7 @@ string SetLaunchUrl(JObject profileObject) platformTarget.Contains( "arm" ); // linux-arm64 - var runtime = string.IsNullOrEmpty( platformTarget ) ? platformTarget : $"linux-{platformTarget}"; + var runtimeIdentifier = string.IsNullOrEmpty( platformTarget ) ? platformTarget : $"linux-{platformTarget}"; // We need to jump through some hoops to obtain the project GUID. var solutionService = RaspberryDebuggerPackage.Instance.SolutionService; @@ -283,17 +284,18 @@ string SetLaunchUrl(JObject profileObject) return new ProjectProperties() { Name = project.Name, - FullPath = project.FullName, + FullPath = fullPath, + OutputPath = project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString(), + OutputFileName = project.Properties.Item("OutputFileName").Value.ToString(), Guid = projectGuid, - Configuration = project.ConfigurationManager.ActiveConfiguration.ConfigurationName, + ConfigurationName = project.ConfigurationManager.ActiveConfiguration.ConfigurationName, + PlatformName = project.ConfigurationManager.ActiveConfiguration.PlatformName, ActiveDebugProfile = activeDebugProfile, IsNetCore = isNetCore, - Framework = targetFramework, + TargetFramework = targetFramework, SdkVersion = new Version( netVersion.Major, netVersion.Minor), - OutputFolder = Path.Combine(projectFolder, project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString()), - OutputFileName = (string)project.Properties.Item("OutputFileName").Value, IsExecutable = outputType == 1, // 1=EXE - Runtime = runtime, + RuntimeIdentifier = runtimeIdentifier, AssemblyName = project.Properties.Item("AssemblyName").Value.ToString(), DebugEnabled = debugEnabled, DebugConnectionName = debugConnectionName, @@ -441,6 +443,11 @@ private static List ParseArgs(string commandLine) /// public string FullPath { get; private set; } + /// + /// Returns the relative path to the output directory. . + /// + public string OutputPath { get; private set; } + /// /// Returns the project's GUID. /// @@ -466,17 +473,17 @@ private static List ParseArgs(string commandLine) /// /// Returns the project's build configuration. /// - public string Configuration { get; private set; } + public string ConfigurationName { get; private set; } /// - /// Returns the Selected Debug profile. + /// Returns the project's build platform. /// - public string ActiveDebugProfile { get; private set; } - + public string PlatformName { get; private set; } + /// - /// Returns the fully qualified path to the project's output directory. + /// Returns the Selected Debug profile. /// - public string OutputFolder { get; private set; } + public string ActiveDebugProfile { get; private set; } /// /// Indicates that the program is an executable as opposed to something @@ -485,19 +492,24 @@ private static List ParseArgs(string commandLine) public bool IsExecutable { get; private set; } /// - /// Returns the publish runtime. + /// Returns the derived Runtime Identifier. /// - public string Runtime { get; set; } + public string RuntimeIdentifier { get; set; } /// /// Returns the framework version. /// - public DotNetFrameworks? Framework { get; set; } = null; + public DotNetFrameworks? TargetFramework { get; set; } = null; + + /// + /// Returns the publication folder. + /// + public string PublishFolder => Path.Combine(this.FullPath, this.OutputPath, this.RuntimeIdentifier); /// /// Returns the publication folder. /// - public string PublishFolder => Path.Combine(OutputFolder, Runtime); + public string OutputFolder => Path.Combine(this.FullPath, this.OutputPath); /// /// Returns the name of the output assembly. diff --git a/RaspberryDebugger/PackageHelper.cs b/RaspberryDebugger/PackageHelper.cs index d00e596..b867014 100644 --- a/RaspberryDebugger/PackageHelper.cs +++ b/RaspberryDebugger/PackageHelper.cs @@ -1,660 +1,662 @@ -//----------------------------------------------------------------------------- -// FILE: PackageHelper.cs -// CONTRIBUTOR: Jeff Lill -// COPYRIGHT: Copyright (c) 2021 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. -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Diagnostics; -using System.Windows.Forms; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Diagnostics.Contracts; - -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; - -using EnvDTE; -using EnvDTE80; -using Microsoft.VisualStudio.Threading; -using Neon.IO; -using Neon.Common; - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -using RaspberryDebugger.Dialogs; -using RaspberryDebugger.Extensions; -using RaspberryDebugger.Models.Sdk; -using RaspberryDebugger.Models.Connection; -using RaspberryDebugger.Models.Project; -using RaspberryDebugger.Models.VisualStudio; -using VersionsService = RaspberryDebugger.Web; - -namespace RaspberryDebugger -{ - /// - /// Package specific constants. - /// - internal static class PackageHelper - { - /// - /// The path to the folder holding the Raspberry SSH private keys. - /// - public static readonly string KeysFolder; - - /// - /// The path to the JSON file defining the Raspberry Pi connections. - /// - private static readonly string ConnectionsPath; - - /// - /// Directory on the Raspberry Pi where .NET Core SDKs will be installed along with the - /// vsdbg remote debugger. - /// - public const string RemoteDotnetFolder = "/lib/dotnet"; - - /// - /// Fully qualified path to the dotnet executable on the Raspberry. - /// - public const string RemoteDotnetCommand = "/lib/dotnet/dotnet"; - - /// - /// Directory on the Raspberry Pi where the vsdbg remote debugger will be installed. - /// Currently the VSIX is only targeted to VS2022 so keep the version selector fixed. - /// TODO: Use RaspberryDebuggerPackage.VisualStudioVersion for DIR.. - /// - public const string RemoteDebuggerFolder = "~/.vs-debugger/vs2022"; - - /// - /// Path to the vsdbg program on the remote machine. - /// - public const string RemoteDebuggerPath = RemoteDebuggerFolder + "/vsdbg"; - - /// - /// Returns the root directory on the Raspberry Pi where the folder where - /// program binaries will be uploaded for the named user. Each program will - /// have a sub directory named for the program. - /// - public static string RemoteDebugBinaryRoot(string username) - { - Covenant.Requires(!string.IsNullOrEmpty(username), nameof(username)); - - return LinuxPath.Combine("/", "home", username, "vsdbg"); - } - - /// - /// Static constructor. - /// - static PackageHelper() - { - // Initialize the settings path and folders. - var settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".raspberry"); - - if (!Directory.Exists(settingsFolder)) - { - Directory.CreateDirectory(settingsFolder); - } - - KeysFolder = Path.Combine(settingsFolder, "keys"); - - if (!Directory.Exists(KeysFolder)) - { - Directory.CreateDirectory(KeysFolder); - } - - ConnectionsPath = Path.Combine(settingsFolder, "connections.json"); - } - - /// - /// Reads the persisted connection settings. - /// - /// Optionally disable logging. - /// The connections. - public static List ReadConnections(bool disableLogging = false) - { - if (!disableLogging) - { - Log.Info("Reading connections"); - } - - try - { - if (!File.Exists(ConnectionsPath)) - { - return new List(); - } - - var connections = NeonHelper.JsonDeserialize>(File.ReadAllText(ConnectionsPath)) ?? - new List(); - - // Ensure that at least one connection is marked as default. We'll - // select the first one as sorted by name if necessary. - if (connections.Count > 0 && !connections.Any(connection => connection.IsDefault)) - { - connections.OrderBy(connection => connection.Name - .ToLowerInvariant()) - .Single().IsDefault = true; - } - - return connections; - } - catch (Exception e) - { - if (!disableLogging) - { - Log.Exception(e); - } - - throw; - } - } - - /// - /// Persists the connections passed. - /// - /// The connections. - /// Optionally disable logging. - public static void WriteConnections(List connections, bool disableLogging = false) - { - if (!disableLogging) - { - Log.Info("Writing connections"); - } - - try - { - connections ??= new List(); - - // Ensure that at least one connection is marked as default. We'll - // select the first one as sorted by name if necessary. - if (connections.Count > 0 && !connections.Any(connection => connection.IsDefault)) - { - connections.OrderBy(connection => connection.Name.ToLowerInvariant()).First().IsDefault = true; - } - - File.WriteAllText(ConnectionsPath, NeonHelper.JsonSerialize(connections, Formatting.Indented)); - } - catch (Exception e) - { - if (!disableLogging) Log.Exception(e); - - throw; - } - } - - /// - /// Returns the current Visual Studio startup project for a solution. - /// - /// The current solution (or null). - /// The startup project or null. - /// - /// - /// The active project may be different from the startup project. Users select - /// the startup project explicitly and that project will remain selected until - /// the user selects another. The active project is determined by the current - /// document. - /// - /// - public static Project GetStartupProject(Solution solution) - { - ThreadHelper.ThrowIfNotOnUIThread(); - - if (solution?.SolutionBuild?.StartupProjects == null) - { - return null; - } - - var projectName = (string)((object[])solution.SolutionBuild.StartupProjects).FirstOrDefault(); - var startupProject = (Project)null; - - foreach (Project project in solution.Projects) - { - if (project.UniqueName == projectName) - { - startupProject = project; - } - else if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems) - { - startupProject = FindInSubProjects(project, projectName); - } - - if (startupProject != null) - { - break; - } - } - - return startupProject; - } - - /// - /// Returns a solution's active project. - /// - /// The active or null for none. - /// - /// - /// The active project may be different from the startup project. Users select - /// the startup project explicitly and that project will remain selected until - /// the user selects another. The active project is determined by the current - /// document. - /// - /// - private static Project GetActiveProject(DTE2 dte) - { - Covenant.Requires(dte != null, nameof(dte)); - - ThreadHelper.ThrowIfNotOnUIThread(); - - var activeSolutionProjects = (Array)dte?.ActiveSolutionProjects; - - return activeSolutionProjects is { Length: > 0 } - ? (Project)activeSolutionProjects.GetValue(0) - : null; - } - - /// - /// Determines whether the active project is a candidate for debugging on - /// a Raspberry. Currently, the project must target .NET Core 3.1 or - /// greater and be an executable. - /// - /// - /// - /// true if there's an active project and it satisfies the criterion. - /// - public static bool IsActiveProjectRaspberryCompatible(DTE2 dte) - { - Covenant.Requires(dte != null, nameof(dte)); - ThreadHelper.ThrowIfNotOnUIThread(); - - var activeProject = GetActiveProject(dte); - - if (activeProject == null) - { - return false; - } - - var projectProperties = ProjectProperties.CopyFrom(dte?.Solution, activeProject); - - return projectProperties.IsRaspberryCompatible; - } - - /// - /// Searches a project's sub project for a project matching a path. - /// - /// The parent project. - /// The desired project name. - /// The or null. - private static Project FindInSubProjects(Project parentProject, string projectName) - { - ThreadHelper.ThrowIfNotOnUIThread(); - - if (parentProject == null) - { - return null; - } - - if (parentProject.UniqueName == projectName) - { - return parentProject; - } - - if (parentProject.Kind != EnvDTE.Constants.vsProjectKindSolutionItems) return null; - var project = (Project)null; - - // The project is actually a solution folder so recursively - // search any sub projects. - - foreach (ProjectItem projectItem in parentProject.ProjectItems) - { - if (projectItem.SubProject == null) continue; - - project = FindInSubProjects(projectItem.SubProject, projectName); - - if (project != null) - { - break; - } - } - - return project; - } - - /// - /// Adds any projects suitable for debugging on a Raspberry to the - /// list, recursing into projects that are actually solution folders as required. - /// - /// The list where discovered projects will be added. - /// The parent solution. - /// The project or solution folder. - private static void GetSolutionProjects(List solutionProjects, Solution solution, Project project) - { - Covenant.Requires(solutionProjects != null, nameof(solutionProjects)); - Covenant.Requires(solution != null, nameof(solution)); - Covenant.Requires(project != null, nameof(project)); - - ThreadHelper.ThrowIfNotOnUIThread(); - - if (project?.Kind == EnvDTE.Constants.vsProjectKindSolutionItems) - { - foreach (ProjectItem projectItem in project.ProjectItems) - { - if ( projectItem.SubProject == null) continue; - - GetSolutionProjects(solutionProjects, solution, projectItem.SubProject); - } - } - else - { - var projectProperties = ProjectProperties.CopyFrom(solution, project); - - if (projectProperties.IsRaspberryCompatible) - { - solutionProjects?.Add(project); - } - } - } - - /// - /// Returns the path to the $/.vs/raspberry-projects.json file for - /// the current solution. - /// - /// The current solution. - /// The file path. - private static string GetRaspberryProjectsPath(Solution solution) - { - Covenant.Requires(solution != null); - ThreadHelper.ThrowIfNotOnUIThread(); - - return Path.Combine(Path.GetDirectoryName(solution?.FullName) ?? string.Empty, ".vs", "raspberry-projects.json"); - } - - /// - /// Reads the $/.vs/raspberry-projects.json file from the current - /// solution's directory. - /// - /// The current solution. - /// The projects read or an object with no projects if the file doesn't exist. - public static RaspberryProjects ReadRaspberryProjects(Solution solution) - { - Covenant.Requires(solution != null); - - ThreadHelper.ThrowIfNotOnUIThread(); - - var path = GetRaspberryProjectsPath(solution); - - return File.Exists(path) - ? NeonHelper.JsonDeserialize(File.ReadAllText(path)) - : new RaspberryProjects(); - } - - /// - /// Persists the project information passed to the $/.vs/raspberry-projects.json file. - /// - /// The current solution. - /// The projects. - public static void WriteRaspberryProjects(Solution solution, RaspberryProjects projects) - { - Covenant.Requires(solution != null); - Covenant.Requires(projects != null); - - ThreadHelper.ThrowIfNotOnUIThread(); - - // Prune any projects with GUIDs that are no longer present in - // the solution so these don't accumulate. Note that we need to - // recurse into solution folders to look for any projects there. - var solutionProjects = new List(); - - if (solution?.Projects != null) - { - foreach (Project project in solution.Projects) - { - GetSolutionProjects(solutionProjects, solution, project); - } - } - - var solutionProjectIds = new HashSet(); - var delList = new List(); - - foreach (var project in solutionProjects) - { - solutionProjectIds.Add(project.UniqueName); - } - - if (projects?.Keys != null) - { - delList.AddRange((projects.Keys).Where(projectId => !solutionProjectIds.Contains(projectId))); - } - - foreach (var projectId in delList) - { - projects?.Remove(projectId); - } - - // Write the file, ensuring that the parent directories exist. - var path = GetRaspberryProjectsPath(solution); - - Directory.CreateDirectory(Path.GetDirectoryName(path) ?? string.Empty); - File.WriteAllText(path, NeonHelper.JsonSerialize(projects, Formatting.Indented)); - } - - /// - /// Returns the project settings for a specific project. - /// - /// The current solution. - /// The target project. - /// The project settings. - public static ProjectSettings GetProjectSettings(Solution solution, Project project) - { - Covenant.Requires(solution != null); - Covenant.Requires(project != null); - - ThreadHelper.ThrowIfNotOnUIThread(); - var raspberryProjects = ReadRaspberryProjects(solution); - - return raspberryProjects[project?.UniqueName]; - } - - //--------------------------------------------------------------------- - // Progress related code - private const string ProgressCaption = "Raspberry Debugger"; - - private static IVsThreadedWaitDialog2 _progressDialog; - private static readonly Stack OperationStack = new(); - private static string _rootDescription; - - /// - /// Executes an asynchronous action that does not return a result within the context of a - /// Visual Studio progress dialog. You may make nested calls and this may also be called - /// from any thread. - /// - /// The operation description. - /// The action. - /// The tracking . - public static async Task ExecuteWithProgressAsync(string description, Func action) - { - Covenant.Requires(!string.IsNullOrEmpty(description), nameof(description)); - Covenant.Requires(action != null, nameof(action)); - - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - if (_progressDialog == null) - { - Covenant.Assert(OperationStack.Count == 0); - - _rootDescription = description; - OperationStack.Push(description); - - var dialogFactory = (IVsThreadedWaitDialogFactory)Package - .GetGlobalService((typeof(SVsThreadedWaitDialogFactory))); - - dialogFactory.CreateInstance(out _progressDialog); - - _progressDialog.StartWaitDialog( - szWaitCaption: ProgressCaption, - szWaitMessage: description, - szProgressText: null, - varStatusBmpAnim: null, - szStatusBarText: null, - iDelayToShowDialog: 0, - fIsCancelable: false, - fShowMarqueeProgress: true); - } - else - { - Covenant.Assert(OperationStack.Count > 0); - - OperationStack.Push(description); - - _progressDialog.UpdateProgress( - szUpdatedWaitMessage: ProgressCaption, - szProgressText: description, - szStatusBarText: null, - iCurrentStep: 0, - iTotalSteps: 0, - fDisableCancel: true, - pfCanceled: out _); - } - - var orgCursor = Cursor.Current; - - try - { - Cursor.Current = Cursors.WaitCursor; - - if (action != null) await action().ConfigureAwait(false); - } - finally - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - Cursor.Current = orgCursor; - - OperationStack.Pop(); - - if (OperationStack.Count == 0) - { - _progressDialog.EndWaitDialog(out _); - - _progressDialog = null; - _rootDescription = null; - } - else - { - _progressDialog.UpdateProgress( - szUpdatedWaitMessage: ProgressCaption, - szProgressText: description, - szStatusBarText: null, - iCurrentStep: 0, - iTotalSteps: 0, - fDisableCancel: true, - pfCanceled: out _); - } - } - } - - /// - /// Executes an asynchronous action that does not return a result within the context of a - /// Visual Studio progress dialog. You may make nested calls and this may also be called - /// from any thread. - /// - /// The action result type. - /// The operation description. - /// The action. - /// The tracking . - public static async Task ExecuteWithProgressAsync(string description, Func> action) - { - Covenant.Requires(!string.IsNullOrEmpty(description), nameof(description)); - Covenant.Requires(action != null, nameof(action)); - - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - if (_progressDialog == null) - { - Covenant.Assert(OperationStack.Count == 0); - - _rootDescription = description; - OperationStack.Push(description); - - var dialogFactory = (IVsThreadedWaitDialogFactory)Package - .GetGlobalService((typeof(SVsThreadedWaitDialogFactory))); - - dialogFactory.CreateInstance(out _progressDialog); - - _progressDialog.StartWaitDialog( - szWaitCaption: ProgressCaption, - szWaitMessage: description, - szProgressText: null, - varStatusBmpAnim: null, - szStatusBarText: $"[Raspberry Debugger]{description}", - iDelayToShowDialog: 0, - fIsCancelable: false, - fShowMarqueeProgress: true); - } - else - { - Covenant.Assert(OperationStack.Count > 0); - - OperationStack.Push(description); - - _progressDialog.UpdateProgress( - szUpdatedWaitMessage: ProgressCaption, - szProgressText: description, - szStatusBarText: null, - iCurrentStep: 0, - iTotalSteps: 0, - fDisableCancel: true, - pfCanceled: out _); - } - - var orgCursor = Cursor.Current; - - try - { - Cursor.Current = Cursors.WaitCursor; - Debug.Assert(action != null, nameof(action) + " != null"); - return await action().ConfigureAwait(false); - } - finally - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - Cursor.Current = orgCursor; - - var currentDescription = OperationStack.Pop(); - - if (OperationStack.Count == 0) - { - _progressDialog.EndWaitDialog(out _); - - _progressDialog = null; - _rootDescription = null; - } - else - { - _progressDialog.UpdateProgress( - szUpdatedWaitMessage: currentDescription, - szProgressText: null, - szStatusBarText: _rootDescription, - iCurrentStep: 0, - iTotalSteps: 0, - fDisableCancel: true, - pfCanceled: out _); - } - } - } - } +//----------------------------------------------------------------------------- +// FILE: PackageHelper.cs +// CONTRIBUTOR: Jeff Lill +// COPYRIGHT: Copyright (c) 2021 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. +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Diagnostics; +using System.Windows.Forms; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.Contracts; + +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +using EnvDTE; +using EnvDTE80; +using Microsoft.VisualStudio.Threading; +using Neon.IO; +using Neon.Common; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +using RaspberryDebugger.Dialogs; +using RaspberryDebugger.Extensions; +using RaspberryDebugger.Models.Sdk; +using RaspberryDebugger.Models.Connection; +using RaspberryDebugger.Models.Project; +using RaspberryDebugger.Models.VisualStudio; +using VersionsService = RaspberryDebugger.Web; + +namespace RaspberryDebugger +{ + /// + /// Package specific constants. + /// + internal static class PackageHelper + { + /// + /// The path to the folder holding the Raspberry SSH private keys. + /// + public static readonly string KeysFolder; + + /// + /// The path to the JSON file defining the Raspberry Pi connections. + /// + private static readonly string ConnectionsPath; + + /// + /// Directory on the Raspberry Pi where .NET Core SDKs will be installed along with the + /// vsdbg remote debugger. + /// + public const string RemoteDotnetFolder = "/lib/dotnet"; + + /// + /// Fully qualified path to the dotnet executable on the Raspberry. + /// + public const string RemoteDotnetCommand = "/lib/dotnet/dotnet"; + + /// + /// Directory on the Raspberry Pi where the vsdbg remote debugger will be installed. + /// Currently the VSIX is only targeted to VS2022 so keep the version selector fixed. + /// TODO: Use RaspberryDebuggerPackage.VisualStudioVersion for DIR.. + /// + public const string RemoteDebuggerFolder = "~/.vs-debugger/vs2022"; + + /// + /// Path to the vsdbg program on the remote machine. + /// + public const string RemoteDebuggerPath = RemoteDebuggerFolder + "/vsdbg"; + + /// + /// Returns the root directory on the Raspberry Pi where the folder where + /// program binaries will be uploaded for the named user. Each program will + /// have a sub directory named for the program. + /// + public static string RemoteDebugBinaryRoot(string username) + { + Covenant.Requires(!string.IsNullOrEmpty(username), nameof(username)); + + return LinuxPath.Combine("/", "home", username, "vsdbg"); + } + + /// + /// Static constructor. + /// + static PackageHelper() + { + // Initialize the settings path and folders. + var settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".raspberry"); + + if (!Directory.Exists(settingsFolder)) + { + Directory.CreateDirectory(settingsFolder); + } + + KeysFolder = Path.Combine(settingsFolder, "keys"); + + if (!Directory.Exists(KeysFolder)) + { + Directory.CreateDirectory(KeysFolder); + } + + ConnectionsPath = Path.Combine(settingsFolder, "connections.json"); + } + + /// + /// Reads the persisted connection settings. + /// + /// Optionally disable logging. + /// The connections. + public static List ReadConnections(bool disableLogging = false) + { + if (!disableLogging) + { + Log.Info("Reading connections"); + } + + try + { + if (!File.Exists(ConnectionsPath)) + { + return new List(); + } + + var connections = NeonHelper.JsonDeserialize>(File.ReadAllText(ConnectionsPath)) ?? + new List(); + + // Ensure that at least one connection is marked as default. We'll + // select the first one as sorted by name if necessary. + if (connections.Count > 0 && !connections.Any(connection => connection.IsDefault)) + { + connections.OrderBy(connection => connection.Name + .ToLowerInvariant()) + .Single().IsDefault = true; + } + + return connections; + } + catch (Exception e) + { + if (!disableLogging) + { + Log.Exception(e); + } + + throw; + } + } + + /// + /// Persists the connections passed. + /// + /// The connections. + /// Optionally disable logging. + public static void WriteConnections(List connections, bool disableLogging = false) + { + if (!disableLogging) + { + Log.Info("Writing connections"); + } + + try + { + connections ??= new List(); + + // Ensure that at least one connection is marked as default. We'll + // select the first one as sorted by name if necessary. + if (connections.Count > 0 && !connections.Any(connection => connection.IsDefault)) + { + connections.OrderBy(connection => connection.Name.ToLowerInvariant()).First().IsDefault = true; + } + + File.WriteAllText(ConnectionsPath, NeonHelper.JsonSerialize(connections, Formatting.Indented)); + } + catch (Exception e) + { + if (!disableLogging) Log.Exception(e); + + throw; + } + } + + /// + /// Returns the current Visual Studio startup project for a solution. + /// + /// The current solution (or null). + /// The startup project or null. + /// + /// + /// The active project may be different from the startup project. Users select + /// the startup project explicitly and that project will remain selected until + /// the user selects another. The active project is determined by the current + /// document. + /// + /// + public static Project GetStartupProject(Solution solution) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (solution?.SolutionBuild?.StartupProjects == null) + { + return null; + } + + var projectName = (string)((object[])solution.SolutionBuild.StartupProjects).FirstOrDefault(); + var startupProject = (Project)null; + + foreach (Project project in solution.Projects) + { + if (project.UniqueName == projectName) + { + startupProject = project; + } + else if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems) + { + startupProject = FindInSubProjects(project, projectName); + } + + if (startupProject != null) + { + break; + } + } + + return startupProject; + } + + /// + /// Returns a solution's active project. + /// + /// The active or null for none. + /// + /// + /// The active project may be different from the startup project. Users select + /// the startup project explicitly and that project will remain selected until + /// the user selects another. The active project is determined by the current + /// document. + /// + /// + private static Project GetActiveProject(DTE2 dte) + { + Covenant.Requires(dte != null, nameof(dte)); + + ThreadHelper.ThrowIfNotOnUIThread(); + + var activeSolutionProjects = (Array)dte?.ActiveSolutionProjects; + + return activeSolutionProjects is { Length: > 0 } + ? (Project)activeSolutionProjects.GetValue(0) + : null; + } + + /// + /// Determines whether the active project is a candidate for debugging on + /// a Raspberry. Currently, the project must target .NET Core 3.1 or + /// greater and be an executable. + /// + /// + /// + /// true if there's an active project and it satisfies the criterion. + /// + public static bool IsActiveProjectRaspberryExecutable(DTE2 dte) + { + Covenant.Requires(dte != null, nameof(dte)); + ThreadHelper.ThrowIfNotOnUIThread(); + + var activeProject = GetActiveProject(dte); + + if (activeProject == null) + { + return false; + } + + var projectProperties = ProjectProperties.CopyFrom(dte?.Solution, activeProject); + + return projectProperties.IsNetCore && + projectProperties.IsExecutable && + projectProperties.IsSupportedSdkVersion; + } + + /// + /// Searches a project's sub project for a project matching a path. + /// + /// The parent project. + /// The desired project name. + /// The or null. + private static Project FindInSubProjects(Project parentProject, string projectName) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (parentProject == null) + { + return null; + } + + if (parentProject.UniqueName == projectName) + { + return parentProject; + } + + if (parentProject.Kind != EnvDTE.Constants.vsProjectKindSolutionItems) return null; + var project = (Project)null; + + // The project is actually a solution folder so recursively + // search any sub projects. + + foreach (ProjectItem projectItem in parentProject.ProjectItems) + { + if (projectItem.SubProject == null) continue; + + project = FindInSubProjects(projectItem.SubProject, projectName); + + if (project != null) + { + break; + } + } + + return project; + } + + /// + /// Adds any projects suitable for debugging on a Raspberry to the + /// list, recursing into projects that are actually solution folders as required. + /// + /// The list where discovered projects will be added. + /// The parent solution. + /// The project or solution folder. + private static void GetSolutionProjects(List solutionProjects, Solution solution, Project project) + { + Covenant.Requires(solutionProjects != null, nameof(solutionProjects)); + Covenant.Requires(solution != null, nameof(solution)); + Covenant.Requires(project != null, nameof(project)); + + ThreadHelper.ThrowIfNotOnUIThread(); + + if (project?.Kind == EnvDTE.Constants.vsProjectKindSolutionItems) + { + foreach (ProjectItem projectItem in project.ProjectItems) + { + if ( projectItem.SubProject == null) continue; + + GetSolutionProjects(solutionProjects, solution, projectItem.SubProject); + } + } + else + { + var projectProperties = ProjectProperties.CopyFrom(solution, project); + + if (projectProperties.IsRaspberryCompatible) + { + solutionProjects?.Add(project); + } + } + } + + /// + /// Returns the path to the $/.vs/raspberry-projects.json file for + /// the current solution. + /// + /// The current solution. + /// The file path. + private static string GetRaspberryProjectsPath(Solution solution) + { + Covenant.Requires(solution != null); + ThreadHelper.ThrowIfNotOnUIThread(); + + return Path.Combine(Path.GetDirectoryName(solution?.FullName) ?? string.Empty, ".vs", "raspberry-projects.json"); + } + + /// + /// Reads the $/.vs/raspberry-projects.json file from the current + /// solution's directory. + /// + /// The current solution. + /// The projects read or an object with no projects if the file doesn't exist. + public static RaspberryProjects ReadRaspberryProjects(Solution solution) + { + Covenant.Requires(solution != null); + + ThreadHelper.ThrowIfNotOnUIThread(); + + var path = GetRaspberryProjectsPath(solution); + + return File.Exists(path) + ? NeonHelper.JsonDeserialize(File.ReadAllText(path)) + : new RaspberryProjects(); + } + + /// + /// Persists the project information passed to the $/.vs/raspberry-projects.json file. + /// + /// The current solution. + /// The projects. + public static void WriteRaspberryProjects(Solution solution, RaspberryProjects projects) + { + Covenant.Requires(solution != null); + Covenant.Requires(projects != null); + + ThreadHelper.ThrowIfNotOnUIThread(); + + // Prune any projects with GUIDs that are no longer present in + // the solution so these don't accumulate. Note that we need to + // recurse into solution folders to look for any projects there. + var solutionProjects = new List(); + + if (solution?.Projects != null) + { + foreach (Project project in solution.Projects) + { + GetSolutionProjects(solutionProjects, solution, project); + } + } + + var solutionProjectIds = new HashSet(); + var delList = new List(); + + foreach (var project in solutionProjects) + { + solutionProjectIds.Add(project.UniqueName); + } + + if (projects?.Keys != null) + { + delList.AddRange((projects.Keys).Where(projectId => !solutionProjectIds.Contains(projectId))); + } + + foreach (var projectId in delList) + { + projects?.Remove(projectId); + } + + // Write the file, ensuring that the parent directories exist. + var path = GetRaspberryProjectsPath(solution); + + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? string.Empty); + File.WriteAllText(path, NeonHelper.JsonSerialize(projects, Formatting.Indented)); + } + + /// + /// Returns the project settings for a specific project. + /// + /// The current solution. + /// The target project. + /// The project settings. + public static ProjectSettings GetProjectSettings(Solution solution, Project project) + { + Covenant.Requires(solution != null); + Covenant.Requires(project != null); + + ThreadHelper.ThrowIfNotOnUIThread(); + var raspberryProjects = ReadRaspberryProjects(solution); + + return raspberryProjects[project?.UniqueName]; + } + + //--------------------------------------------------------------------- + // Progress related code + private const string ProgressCaption = "Raspberry Debugger"; + + private static IVsThreadedWaitDialog2 _progressDialog; + private static readonly Stack OperationStack = new(); + private static string _rootDescription; + + /// + /// Executes an asynchronous action that does not return a result within the context of a + /// Visual Studio progress dialog. You may make nested calls and this may also be called + /// from any thread. + /// + /// The operation description. + /// The action. + /// The tracking . + public static async Task ExecuteWithProgressAsync(string description, Func action) + { + Covenant.Requires(!string.IsNullOrEmpty(description), nameof(description)); + Covenant.Requires(action != null, nameof(action)); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (_progressDialog == null) + { + Covenant.Assert(OperationStack.Count == 0); + + _rootDescription = description; + OperationStack.Push(description); + + var dialogFactory = (IVsThreadedWaitDialogFactory)Package + .GetGlobalService((typeof(SVsThreadedWaitDialogFactory))); + + dialogFactory.CreateInstance(out _progressDialog); + + _progressDialog.StartWaitDialog( + szWaitCaption: ProgressCaption, + szWaitMessage: description, + szProgressText: null, + varStatusBmpAnim: null, + szStatusBarText: null, + iDelayToShowDialog: 0, + fIsCancelable: false, + fShowMarqueeProgress: true); + } + else + { + Covenant.Assert(OperationStack.Count > 0); + + OperationStack.Push(description); + + _progressDialog.UpdateProgress( + szUpdatedWaitMessage: ProgressCaption, + szProgressText: description, + szStatusBarText: null, + iCurrentStep: 0, + iTotalSteps: 0, + fDisableCancel: true, + pfCanceled: out _); + } + + var orgCursor = Cursor.Current; + + try + { + Cursor.Current = Cursors.WaitCursor; + + if (action != null) await action().ConfigureAwait(false); + } + finally + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + Cursor.Current = orgCursor; + + OperationStack.Pop(); + + if (OperationStack.Count == 0) + { + _progressDialog.EndWaitDialog(out _); + + _progressDialog = null; + _rootDescription = null; + } + else + { + _progressDialog.UpdateProgress( + szUpdatedWaitMessage: ProgressCaption, + szProgressText: description, + szStatusBarText: null, + iCurrentStep: 0, + iTotalSteps: 0, + fDisableCancel: true, + pfCanceled: out _); + } + } + } + + /// + /// Executes an asynchronous action that does not return a result within the context of a + /// Visual Studio progress dialog. You may make nested calls and this may also be called + /// from any thread. + /// + /// The action result type. + /// The operation description. + /// The action. + /// The tracking . + public static async Task ExecuteWithProgressAsync(string description, Func> action) + { + Covenant.Requires(!string.IsNullOrEmpty(description), nameof(description)); + Covenant.Requires(action != null, nameof(action)); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (_progressDialog == null) + { + Covenant.Assert(OperationStack.Count == 0); + + _rootDescription = description; + OperationStack.Push(description); + + var dialogFactory = (IVsThreadedWaitDialogFactory)Package + .GetGlobalService((typeof(SVsThreadedWaitDialogFactory))); + + dialogFactory.CreateInstance(out _progressDialog); + + _progressDialog.StartWaitDialog( + szWaitCaption: ProgressCaption, + szWaitMessage: description, + szProgressText: null, + varStatusBmpAnim: null, + szStatusBarText: $"[Raspberry Debugger]{description}", + iDelayToShowDialog: 0, + fIsCancelable: false, + fShowMarqueeProgress: true); + } + else + { + Covenant.Assert(OperationStack.Count > 0); + + OperationStack.Push(description); + + _progressDialog.UpdateProgress( + szUpdatedWaitMessage: ProgressCaption, + szProgressText: description, + szStatusBarText: null, + iCurrentStep: 0, + iTotalSteps: 0, + fDisableCancel: true, + pfCanceled: out _); + } + + var orgCursor = Cursor.Current; + + try + { + Cursor.Current = Cursors.WaitCursor; + Debug.Assert(action != null, nameof(action) + " != null"); + return await action().ConfigureAwait(false); + } + finally + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + Cursor.Current = orgCursor; + + var currentDescription = OperationStack.Pop(); + + if (OperationStack.Count == 0) + { + _progressDialog.EndWaitDialog(out _); + + _progressDialog = null; + _rootDescription = null; + } + else + { + _progressDialog.UpdateProgress( + szUpdatedWaitMessage: currentDescription, + szProgressText: null, + szStatusBarText: _rootDescription, + iCurrentStep: 0, + iTotalSteps: 0, + fDisableCancel: true, + pfCanceled: out _); + } + } + } + } } \ No newline at end of file diff --git a/RaspberryDebugger/RaspberryDebuggerPackage.cs b/RaspberryDebugger/RaspberryDebuggerPackage.cs index 1096b7d..f11c0f4 100644 --- a/RaspberryDebugger/RaspberryDebuggerPackage.cs +++ b/RaspberryDebugger/RaspberryDebuggerPackage.cs @@ -36,6 +36,8 @@ namespace RaspberryDebugger /// /// Implements a VSIX package that automates debugging C# .NET Core applications remotely /// on Raspberry Pi OS. + /// + /// C:\Program Files\Microsoft Visual Studio\2022\Professional\VSSDK\VisualStudioIntegration\Common\Inc /// [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)]