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
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)]