From 8b7d261dca2434858e2c53843fa774b2268f3fa8 Mon Sep 17 00:00:00 2001 From: Bryan Roth Date: Tue, 8 Apr 2025 15:24:14 -0400 Subject: [PATCH 1/2] Add Framework to the ProjectProperties. Use where necessary and log the Publish command line. --- RaspberryDebugger/DebugHelper.cs | 1323 ++++++++--------- .../Models/VisualStudio/ProjectProperties.cs | 23 +- 2 files changed, 679 insertions(+), 667 deletions(-) diff --git a/RaspberryDebugger/DebugHelper.cs b/RaspberryDebugger/DebugHelper.cs index 8842e0b..92121a1 100644 --- a/RaspberryDebugger/DebugHelper.cs +++ b/RaspberryDebugger/DebugHelper.cs @@ -1,662 +1,661 @@ -//----------------------------------------------------------------------------- -// FILE: DebugHelper.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. -// ReSharper disable StringLiteralTypo -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Forms; - -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; -using EnvDTE; -using EnvDTE80; - -using Neon.Common; -using Neon.Windows; -using RaspberryDebugger.Dialogs; -using RaspberryDebugger.Models.Connection; -using RaspberryDebugger.Models.Project; -using RaspberryDebugger.Models.Raspberry; -using RaspberryDebugger.Models.VisualStudio; -using Task = System.Threading.Tasks.Task; - -namespace RaspberryDebugger -{ - /// - /// Remote debugger related utilities. - /// - internal static class DebugHelper - { - private const string SupportedVersions = ".NET Core 3.1 or .NET 5 + 6"; - - /// - /// Track the last output file that was uploaded. - /// - private static DirectoryInfo LastUploadedDirInfo { get; set; } - - /// - /// Ensures that the native Windows OpenSSH client is installed, prompting - /// the user to install it if necessary. - /// - /// true if OpenSSH is installed. - public static async Task EnsureOpenSshAsync() - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - Log.Info("Checking for native Windows OpenSSH client"); - - var openSshPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "OpenSSH", "ssh.exe"); - - if (!File.Exists(openSshPath)) - { - Log.WriteLine("Raspberry debugging requires the native Windows OpenSSH client. See this:"); - Log.WriteLine("https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540"); - - var button = MessageBox.Show( - @"Raspberry debugging requires the Windows OpenSSH client. - Would you like to install this now (restart required)?", - @"Windows OpenSSH Client Required", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question, - MessageBoxDefaultButton.Button2); - - if (button != DialogResult.Yes) - { - return false; - } - - // Install via Powershell: https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540 - - await PackageHelper.ExecuteWithProgressAsync("Installing OpenSSH Client", - async () => - { - using (var powershell = new PowerShell()) - { - Log.Info("Installing OpenSSH"); - Log.Info(powershell.Execute("Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0")); - } - - await Task.CompletedTask; - }); - - MessageBox.Show( - @"Restart Windows to complete the OpenSSH Client installation.", - @"Restart Required", - MessageBoxButtons.OK); - - return false; - } - else - { - return true; - } - } - - /// - /// Attempts to locate the startup project to be debugged, ensuring that it's - /// eligable for Raspberry debugging. - /// - /// The IDE. - /// The target project or null if there isn't a startup project or it wasn't eligible. - public static Project GetTargetProject(DTE2 dte) - { - ThreadHelper.ThrowIfNotOnUIThread(); - - // Identify the current startup project (if any). - - if (dte.Solution == null) - { - MessageBox.Show( - @"Please open a Visual Studio solution.", - @"Solution Required", - MessageBoxButtons.OK, - MessageBoxIcon.Information); - - return null; - } - - var project = PackageHelper.GetStartupProject(dte.Solution); - - if (project == null) - { - MessageBox.Show( - @"Please select a startup project for your solution.", - @"Startup Project Required", - MessageBoxButtons.OK, - MessageBoxIcon.Information); - - return null; - } - - // We need to capture the relevant project properties while we're still - // on the UI thread so we'll have them on background threads. - - var projectProperties = ProjectProperties.CopyFrom(dte.Solution, project); - - if (!projectProperties.IsNetCore) - { - MessageBox.Show( - $@"Only {SupportedVersions} projects are supported for Raspberry debugging.", - @"Invalid Project Type", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return null; - } - - if (!projectProperties.IsExecutable) - { - MessageBox.Show( - @"Only projects types that generate an executable program are supported for Raspberry debugging.", - @"Invalid Project Type", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return null; - } - - if ( projectProperties.SdkVersion == null ) - { - MessageBox.Show( - @"The .NET Core SDK version could not be identified.", - @"Invalid Project Type", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return null; - } - - if (!projectProperties.IsSupportedSdkVersion) - { - MessageBox.Show( - $@"The .NET Core SDK [{projectProperties.SdkVersion}] is not currently supported. Only .NET Core versions [v3.1] or later will ever be supported - Note that we currently support only official SDKs (not previews or release candidates) and we check for new .NET Core SDKs every week or two. - Submit an issue if you really need support for a new SDK ASAP: - https://github.com/nforgeio/RaspberryDebugger/issues", - @"SDK Not Supported", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return null; - } - - if (!projectProperties.AssemblyName.Contains(' ')) return project; - - MessageBox.Show( - $@"Your assembly name [{projectProperties.AssemblyName}] includes a space. This isn't supported.", - @"Unsupported Assembly Name", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return null; - - } - - /// - /// Builds and publishes a project locally to prepare it for being uploaded to the Raspberry. This method - /// will display an error message box to the user on failures - /// - /// - /// - /// - /// - /// - public static async Task PublishProjectWithUiAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) - { - Covenant.Requires(dte != null, nameof(dte)); - Covenant.Requires(solution != null, nameof(solution)); - Covenant.Requires(project != null, nameof(project)); - Covenant.Requires(projectProperties != null, nameof(projectProperties)); - - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - if (!await BuildProjectAsync(dte, solution, project, projectProperties)) - { - // bring ErrorList window to the top of the z order. - dte.Application.ExecuteCommand("View.ErrorList", " "); - - await Task.Yield(); - - MessageBox.Show( - """ - There were one or more errors building the project. - Please review the Error List. - """, - @"Build Failed", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return false; - } - - await Task.Yield(); - - if (!await PublishProjectAsync(dte, solution, project, projectProperties)) - { - // bring Output window to the top of the z order. - dte.Application.ExecuteCommand("View.Output", " "); - - await Task.Yield(); - - MessageBox.Show( - """ - There were one or more errors Publishing the project. - Please review the Output Window for more details. - """, - @"Publish Failed", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return false; - } - - return true; - } - - /// - /// Builds and publishes a project locally to prepare it for being uploaded to the Raspberry. This method - /// does not display error message box to the user on failures - /// - /// The DTE. - /// The solution. - /// The project. - /// The project properties. - /// true on success. - private static async Task BuildProjectAsync(DTE2 dte, Solution solution, Project project, - ProjectProperties projectProperties) - { - // Build the project within the context of VS to ensure that all changed - // files are saved and all dependencies are built first. Then we'll - // verify that there were no errors before proceeding. - - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - // Ensure that the project is completely loaded by Visual Studio. I've seen - // random crashes when building or publishing projects when VS is still loading - // projects. - - var solutionService4 = - (IVsSolution4)await RaspberryDebuggerPackage.Instance.GetServiceAsync(typeof(SVsSolution)); - - if (solutionService4 == null) - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - Covenant.Assert(solutionService4 != null, $"Service [{nameof(SVsSolution)}] is not available."); - } - - // Build the project to ensure that there are no compile-time errors. - Log.Info($"Build Started: {projectProperties?.FullPath}, Configuration: {projectProperties.Configuration}"); - - solution?.SolutionBuild.BuildProject( - solution.SolutionBuild.ActiveConfiguration.Name, project?.UniqueName, WaitForBuildToFinish: true); - - // if any projects failed to build - if (solution.SolutionBuild.LastBuildInfo != 0) - { - return false; - } - - Log.Info($"Build succeeded"); - - return true; - } - - /// - /// Builds and publishes a project locally to prepare it for being uploaded to the Raspberry. This method - /// does not display error message box to the user on failures - /// - /// The DTE. - /// The solution. - /// The project. - /// The project properties. - /// true on success. - private static async Task PublishProjectAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) - { - // Publish the project so all required binaries and assets end up - // in the output folder. - // - // Note that we're taking care to only forward a few standard - // environment variables because Visual Studio seems to communicate - // with dotnet related processes with environment variables and - // these can cause conflicts when we invoke [dotnet] below to - // publish the project. - - Log.Info($"Publishing Started: {projectProperties?.FullPath}, Runtime: {projectProperties.Runtime}"); - - const string allowedVariableNames = - """ - ALLUSERSPROFILE - APPDATA - architecture - architecture_bits - CommonProgramFiles - CommonProgramFiles(x86) - CommonProgramW6432 - COMPUTERNAME - ComSpec - DOTNETPATH - DOTNET_CLI_TELEMETRY_OPTOUT - DriverData - HOME - HOMEDRIVE - HOMEPATH - LOCALAPPDATA - NUMBER_OF_PROCESSORS - OS - Path - PATHEXT - POWERSHELL_DISTRIBUTION_CHANNEL - PROCESSOR_ARCHITECTURE - PROCESSOR_IDENTIFIER - PROCESSOR_LEVEL - PROCESSOR_REVISION - ProgramData - ProgramFiles - ProgramFiles(x86) - ProgramW6432 - PUBLIC - SystemDrive - SystemRoot - TEMP - USERDOMAIN - USERDOMAIN_ROAMINGPROFILE - USERNAME - USERPROFILE - windir - """; - - var allowedVariables = new HashSet(StringComparer.InvariantCultureIgnoreCase); - var environmentVariables = new Dictionary(); - - using (var reader = new StringReader(allowedVariableNames)) - { - foreach (var line in reader.Lines()) - { - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - allowedVariables.Add(line.Trim()); - } - } - - foreach (string variable in Environment.GetEnvironmentVariables().Keys) - { - if (allowedVariables.Contains(variable)) - { - environmentVariables[variable] = Environment.GetEnvironmentVariable(variable); - } - } - - ExecuteResponse response; - - try - { - if (!string.IsNullOrEmpty(projectProperties?.Framework)) - { - response = await NeonHelper.ExecuteCaptureAsync( - "dotnet", - new object[] - { - "publish", - "--configuration", projectProperties.Configuration, - "--framework", projectProperties.Framework, - "--runtime", projectProperties.Runtime, - "--no-self-contained", - "--output", projectProperties.PublishFolder, - projectProperties.FullPath - }, - environmentVariables: environmentVariables).ConfigureAwait(false); - } - else - { - response = await NeonHelper.ExecuteCaptureAsync( - "dotnet", - new object[] - { - "publish", - "--configuration", projectProperties?.Configuration, - "--runtime", projectProperties?.Runtime, - "--no-self-contained", - "--output", projectProperties?.PublishFolder, - projectProperties?.FullPath - }, - environmentVariables: environmentVariables).ConfigureAwait(false); - } - - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - if (response.ExitCode == 0) - { - Log.Info("Publish Succeeded"); - return true; - } - - Log.Error($"Publish failed: ExitCode={response.ExitCode}"); - Log.WriteLine(response.AllText); - } - catch (Exception e) - { - Log.Error($"Publish failed:"); - Log.Error(NeonHelper.ExceptionError(e)); - } - - return false; - } - - /// - /// Maps the debug connection name we got from the project properties (if any) to - /// one of our Raspberry connections. If no name is specified, we'll - /// use the default connection or prompt the user to create a connection. - /// We'll display an error if a connection is specified and but doesn't exist. - /// - /// The project properties. - /// The connection or null when one couldn't be located. - public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectProperties) - { - Covenant.Requires(projectProperties != null, nameof(projectProperties)); - var existingConnections = PackageHelper.ReadConnections(); - ConnectionInfo connectionInfo; - - if (string.IsNullOrEmpty(projectProperties?.DebugConnectionName)) - { - connectionInfo = existingConnections.SingleOrDefault(info => info.IsDefault); - - // ReSharper disable once InvertIf - if (connectionInfo == null) - { - if (MessageBoxEx.Show( - "Raspberry connection information required. Would you like to create a connection now?", - "Raspberry Connection Required", - MessageBoxButtons.YesNo, - MessageBoxIcon.Error, - MessageBoxDefaultButton.Button1) == DialogResult.No) - { - return null; - } - - connectionInfo = new ConnectionInfo(); - - var connectionDialog = new ConnectionDialog(connectionInfo, edit: false, existingConnections: existingConnections); - - if (connectionDialog.ShowDialog() == DialogResult.OK) - { - existingConnections.Add(connectionInfo); - PackageHelper.WriteConnections(existingConnections, disableLogging: true); - } - else - { - return null; - } - } - } - else - { - connectionInfo = existingConnections.SingleOrDefault(info => info.Name.Equals(projectProperties.DebugConnectionName, StringComparison.InvariantCultureIgnoreCase)); - - if (connectionInfo == null) - { - MessageBoxEx.Show( - $"The [{projectProperties.DebugConnectionName}] Raspberry connection does not exist.\r\n\r\nPlease add the connection via:\r\n\r\nTools/Options/Raspberry Debugger", - "Cannot Find Raspberry Connection", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - return null; - } - } - - return connectionInfo; - } - - /// - /// Establishes a connection to the Raspberry and ensures that the Raspberry has - /// the target SDK, vsdbg installed and also handles uploading of the project - /// binaries. - /// - /// The connection info. - /// The project properties. - /// The project's Raspberry debug settings. - /// The or null if there was an error. - public static async Task InitializeConnectionAsync(ConnectionInfo connectionInfo, ProjectProperties projectProperties, ProjectSettings projectSettings) - { - Covenant.Requires(connectionInfo != null, nameof(connectionInfo)); - Covenant.Requires(projectProperties != null, nameof(projectProperties)); - Covenant.Requires(projectSettings != null, nameof(projectSettings)); - - var connection = await Connection.Connection.ConnectAsync(connectionInfo, projectSettings: projectSettings); - - // device not found - if(connection == null) return null; - - var raspberryModel = new RaspberryModelCheck(); - - // .NET Core only supports Raspberry models 3 and 4. - if(raspberryModel.IsNotSupported(connection.PiStatus.RaspberryModel)) - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - MessageBoxEx.Show( - $"Your [{raspberryModel.ActualType}] is not supported." + - $" This .NET version requires a {string.Join(" or ", raspberryModel.Supported)}.", - "Raspberry Not Supported", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - connection.Dispose(); - - return null; - } - - // Ensure that the SDK is installed. - if (!await connection.SetupSdkAsync(projectProperties.SdkVersion, connection.PiStatus)) - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - MessageBoxEx.Show( - $"Installation of the .NET SDK {projectProperties.SdkVersion} on the Raspberry was unsuccessful.\r\n\r\nCheck the Debug Output for more details.", - "SDK Installation Failed", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - connection.Dispose(); - - return null; - } - - // Ensure that the debugger is installed. - // TODO: Visual Studio tries to update/install the package each time it attaches. - // Maybe only try the update/install once per session/device? Lots of permutations to track tho. - // Would need to quietly fail if no internet connection. - if (!await connection.SetupDebuggerAsync()) - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - MessageBoxEx.Show( - "Cannot install the VSDBG debugger on the Raspberry.\r\n\r\nCheck the Debug Output for more details.", - "Debugger Installation Failed", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - connection.Dispose(); - - return null; - } - - // Ensure that linux libraries are installed. - if (!await connection.SetupLinuxDependenciesAsync()) - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - MessageBoxEx.Show( - "Cannot install the Linux dependencies on the Raspberry.\r\n\r\nCheck the Debug Output for more details.", - "Linux Dependencies Installation Failed", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - connection.Dispose(); - - return null; - } - - var dirInfo = new DirectoryInfo(projectProperties.OutputFolder); - - bool shouldUploadProgram = - LastUploadedDirInfo == null || - LastUploadedDirInfo.FullName != dirInfo.FullName || - LastUploadedDirInfo.LastWriteTime != dirInfo.LastWriteTime; - - var outputFileName = Path.Combine( projectProperties.OutputFolder, projectProperties.OutputFileName ); - - if (!shouldUploadProgram) - { - Log.Info($"Skipping upload of {outputFileName}, {dirInfo.LastWriteTime}"); - - return connection; - } - - Log.Info($"Uploading {outputFileName}, {dirInfo.LastWriteTime}"); - - // Upload the program binaries. - if (await connection.UploadProgramAsync( - projectProperties?.Name, - projectProperties?.AssemblyName, - projectProperties?.PublishFolder)) - { - LastUploadedDirInfo = dirInfo; - - return connection; - } - - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - MessageBoxEx.Show( - "Cannot upload the program binaries to the Raspberry.\r\n\r\nCheck the Debug Output for more details.", - "Debugger Installation Failed", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - - connection.Dispose(); - - return null; - } - } -} +//----------------------------------------------------------------------------- +// FILE: DebugHelper.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. +// ReSharper disable StringLiteralTypo +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using EnvDTE; +using EnvDTE80; + +using Neon.Common; +using Neon.Windows; +using RaspberryDebugger.Dialogs; +using RaspberryDebugger.Models.Connection; +using RaspberryDebugger.Models.Project; +using RaspberryDebugger.Models.Raspberry; +using RaspberryDebugger.Models.VisualStudio; +using Task = System.Threading.Tasks.Task; + +namespace RaspberryDebugger +{ + /// + /// Remote debugger related utilities. + /// + internal static class DebugHelper + { + private const string SupportedVersions = ".NET Core 3.1 or .NET 5 + 6"; + + /// + /// Track the last output file that was uploaded. + /// + private static DirectoryInfo LastUploadedDirInfo { get; set; } + + /// + /// Ensures that the native Windows OpenSSH client is installed, prompting + /// the user to install it if necessary. + /// + /// true if OpenSSH is installed. + public static async Task EnsureOpenSshAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + Log.Info("Checking for native Windows OpenSSH client"); + + var openSshPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "OpenSSH", "ssh.exe"); + + if (!File.Exists(openSshPath)) + { + Log.WriteLine("Raspberry debugging requires the native Windows OpenSSH client. See this:"); + Log.WriteLine("https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540"); + + var button = MessageBox.Show( + @"Raspberry debugging requires the Windows OpenSSH client. + Would you like to install this now (restart required)?", + @"Windows OpenSSH Client Required", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question, + MessageBoxDefaultButton.Button2); + + if (button != DialogResult.Yes) + { + return false; + } + + // Install via Powershell: https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540 + + await PackageHelper.ExecuteWithProgressAsync("Installing OpenSSH Client", + async () => + { + using (var powershell = new PowerShell()) + { + Log.Info("Installing OpenSSH"); + Log.Info(powershell.Execute("Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0")); + } + + await Task.CompletedTask; + }); + + MessageBox.Show( + @"Restart Windows to complete the OpenSSH Client installation.", + @"Restart Required", + MessageBoxButtons.OK); + + return false; + } + else + { + return true; + } + } + + /// + /// Attempts to locate the startup project to be debugged, ensuring that it's + /// eligable for Raspberry debugging. + /// + /// The IDE. + /// The target project or null if there isn't a startup project or it wasn't eligible. + public static Project GetTargetProject(DTE2 dte) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + // Identify the current startup project (if any). + + if (dte.Solution == null) + { + MessageBox.Show( + @"Please open a Visual Studio solution.", + @"Solution Required", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + return null; + } + + var project = PackageHelper.GetStartupProject(dte.Solution); + + if (project == null) + { + MessageBox.Show( + @"Please select a startup project for your solution.", + @"Startup Project Required", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + return null; + } + + // We need to capture the relevant project properties while we're still + // on the UI thread so we'll have them on background threads. + + var projectProperties = ProjectProperties.CopyFrom(dte.Solution, project); + + if (!projectProperties.IsNetCore) + { + MessageBox.Show( + $@"Only {SupportedVersions} projects are supported for Raspberry debugging.", + @"Invalid Project Type", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return null; + } + + if (!projectProperties.IsExecutable) + { + MessageBox.Show( + @"Only projects types that generate an executable program are supported for Raspberry debugging.", + @"Invalid Project Type", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return null; + } + + if ( projectProperties.SdkVersion == null ) + { + MessageBox.Show( + @"The .NET Core SDK version could not be identified.", + @"Invalid Project Type", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return null; + } + + if (!projectProperties.IsSupportedSdkVersion) + { + MessageBox.Show( + $@"The .NET Core SDK [{projectProperties.SdkVersion}] is not currently supported. Only .NET Core versions [v3.1] or later will ever be supported + Note that we currently support only official SDKs (not previews or release candidates) and we check for new .NET Core SDKs every week or two. + Submit an issue if you really need support for a new SDK ASAP: + https://github.com/nforgeio/RaspberryDebugger/issues", + @"SDK Not Supported", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return null; + } + + if (!projectProperties.AssemblyName.Contains(' ')) return project; + + MessageBox.Show( + $@"Your assembly name [{projectProperties.AssemblyName}] includes a space. This isn't supported.", + @"Unsupported Assembly Name", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return null; + + } + + /// + /// Builds and publishes a project locally to prepare it for being uploaded to the Raspberry. This method + /// will display an error message box to the user on failures + /// + /// + /// + /// + /// + /// + public static async Task PublishProjectWithUiAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) + { + Covenant.Requires(dte != null, nameof(dte)); + Covenant.Requires(solution != null, nameof(solution)); + Covenant.Requires(project != null, nameof(project)); + Covenant.Requires(projectProperties != null, nameof(projectProperties)); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (!await BuildProjectAsync(dte, solution, project, projectProperties)) + { + // bring ErrorList window to the top of the z order. + dte.Application.ExecuteCommand("View.ErrorList", " "); + + await Task.Yield(); + + MessageBox.Show( + """ + There were one or more errors building the project. + Please review the Error List. + """, + @"Build Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return false; + } + + await Task.Yield(); + + if (!await PublishProjectAsync(dte, solution, project, projectProperties)) + { + // bring Output window to the top of the z order. + dte.Application.ExecuteCommand("View.Output", " "); + + await Task.Yield(); + + MessageBox.Show( + """ + There were one or more errors Publishing the project. + Please review the Output Window for more details. + """, + @"Publish Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return false; + } + + return true; + } + + /// + /// Builds and publishes a project locally to prepare it for being uploaded to the Raspberry. This method + /// does not display error message box to the user on failures + /// + /// The DTE. + /// The solution. + /// The project. + /// The project properties. + /// true on success. + private static async Task BuildProjectAsync(DTE2 dte, Solution solution, Project project, + ProjectProperties projectProperties) + { + // Build the project within the context of VS to ensure that all changed + // files are saved and all dependencies are built first. Then we'll + // verify that there were no errors before proceeding. + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // Ensure that the project is completely loaded by Visual Studio. I've seen + // random crashes when building or publishing projects when VS is still loading + // projects. + + var solutionService4 = + (IVsSolution4)await RaspberryDebuggerPackage.Instance.GetServiceAsync(typeof(SVsSolution)); + + if (solutionService4 == null) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + Covenant.Assert(solutionService4 != null, $"Service [{nameof(SVsSolution)}] is not available."); + } + + // Build the project to ensure that there are no compile-time errors. + Log.Info($"Build Started: {projectProperties?.FullPath}, Configuration: {projectProperties.Configuration}"); + + solution?.SolutionBuild.BuildProject( + solution.SolutionBuild.ActiveConfiguration.Name, project?.UniqueName, WaitForBuildToFinish: true); + + // if any projects failed to build + if (solution.SolutionBuild.LastBuildInfo != 0) + { + return false; + } + + Log.Info($"Build succeeded"); + + return true; + } + + /// + /// Builds and publishes a project locally to prepare it for being uploaded to the Raspberry. This method + /// does not display error message box to the user on failures + /// + /// The DTE. + /// The solution. + /// The project. + /// The project properties. + /// true on success. + private static async Task PublishProjectAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) + { + // Publish the project so all required binaries and assets end up + // in the output folder. + // + // Note that we're taking care to only forward a few standard + // environment variables because Visual Studio seems to communicate + // with dotnet related processes with environment variables and + // these can cause conflicts when we invoke [dotnet] below to + // publish the project. + + Log.Info($"Publishing Started: {projectProperties?.FullPath}, Runtime: {projectProperties.Runtime}"); + + const string allowedVariableNames = + """ + ALLUSERSPROFILE + APPDATA + architecture + architecture_bits + CommonProgramFiles + CommonProgramFiles(x86) + CommonProgramW6432 + COMPUTERNAME + ComSpec + DOTNETPATH + DOTNET_CLI_TELEMETRY_OPTOUT + DriverData + HOME + HOMEDRIVE + HOMEPATH + LOCALAPPDATA + NUMBER_OF_PROCESSORS + OS + Path + PATHEXT + POWERSHELL_DISTRIBUTION_CHANNEL + PROCESSOR_ARCHITECTURE + PROCESSOR_IDENTIFIER + PROCESSOR_LEVEL + PROCESSOR_REVISION + ProgramData + ProgramFiles + ProgramFiles(x86) + ProgramW6432 + PUBLIC + SystemDrive + SystemRoot + TEMP + USERDOMAIN + USERDOMAIN_ROAMINGPROFILE + USERNAME + USERPROFILE + windir + """; + + var allowedVariables = new HashSet(StringComparer.InvariantCultureIgnoreCase); + var environmentVariables = new Dictionary(); + + using (var reader = new StringReader(allowedVariableNames)) + { + foreach (var line in reader.Lines()) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + allowedVariables.Add(line.Trim()); + } + } + + foreach (string variable in Environment.GetEnvironmentVariables().Keys) + { + if (allowedVariables.Contains(variable)) + { + environmentVariables[variable] = Environment.GetEnvironmentVariable(variable); + } + } + + ExecuteResponse response; + + try + { + var options = new List() + { + "publish", + "--configuration", projectProperties.Configuration + }; + + if ( (bool)( projectProperties?.Framework.HasValue ) ) + { + options.AddRange( + [ + "--framework", projectProperties.Framework == DotNetFrameworks.DotNet_8 + ? "net8.0" + : "net9.0", // short term hack + ]); + } + + options.AddRange( [ + "--runtime", projectProperties.Runtime, + "--no-self-contained", + "--output", projectProperties.PublishFolder, + projectProperties.FullPath + ] ); + + Log.Info("dotnet Command:"); + Log.WriteLine( string.Join(" ", options.Select( p => p.ToString() ).ToArray())); + + response = await NeonHelper.ExecuteCaptureAsync( + "dotnet", + options.ToArray(), + environmentVariables: environmentVariables).ConfigureAwait(false); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (response.ExitCode == 0) + { + Log.Info("Publish Succeeded"); + return true; + } + + Log.Error($"Publish failed: ExitCode={response.ExitCode}"); + Log.WriteLine(response.AllText); + } + catch (Exception e) + { + Log.Error($"Publish failed:"); + Log.Error(NeonHelper.ExceptionError(e)); + } + + return false; + } + + /// + /// Maps the debug connection name we got from the project properties (if any) to + /// one of our Raspberry connections. If no name is specified, we'll + /// use the default connection or prompt the user to create a connection. + /// We'll display an error if a connection is specified and but doesn't exist. + /// + /// The project properties. + /// The connection or null when one couldn't be located. + public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectProperties) + { + Covenant.Requires(projectProperties != null, nameof(projectProperties)); + var existingConnections = PackageHelper.ReadConnections(); + ConnectionInfo connectionInfo; + + if (string.IsNullOrEmpty(projectProperties?.DebugConnectionName)) + { + connectionInfo = existingConnections.SingleOrDefault(info => info.IsDefault); + + // ReSharper disable once InvertIf + if (connectionInfo == null) + { + if (MessageBoxEx.Show( + "Raspberry connection information required. Would you like to create a connection now?", + "Raspberry Connection Required", + MessageBoxButtons.YesNo, + MessageBoxIcon.Error, + MessageBoxDefaultButton.Button1) == DialogResult.No) + { + return null; + } + + connectionInfo = new ConnectionInfo(); + + var connectionDialog = new ConnectionDialog(connectionInfo, edit: false, existingConnections: existingConnections); + + if (connectionDialog.ShowDialog() == DialogResult.OK) + { + existingConnections.Add(connectionInfo); + PackageHelper.WriteConnections(existingConnections, disableLogging: true); + } + else + { + return null; + } + } + } + else + { + connectionInfo = existingConnections.SingleOrDefault(info => info.Name.Equals(projectProperties.DebugConnectionName, StringComparison.InvariantCultureIgnoreCase)); + + if (connectionInfo == null) + { + MessageBoxEx.Show( + $"The [{projectProperties.DebugConnectionName}] Raspberry connection does not exist.\r\n\r\nPlease add the connection via:\r\n\r\nTools/Options/Raspberry Debugger", + "Cannot Find Raspberry Connection", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + return null; + } + } + + return connectionInfo; + } + + /// + /// Establishes a connection to the Raspberry and ensures that the Raspberry has + /// the target SDK, vsdbg installed and also handles uploading of the project + /// binaries. + /// + /// The connection info. + /// The project properties. + /// The project's Raspberry debug settings. + /// The or null if there was an error. + public static async Task InitializeConnectionAsync(ConnectionInfo connectionInfo, ProjectProperties projectProperties, ProjectSettings projectSettings) + { + Covenant.Requires(connectionInfo != null, nameof(connectionInfo)); + Covenant.Requires(projectProperties != null, nameof(projectProperties)); + Covenant.Requires(projectSettings != null, nameof(projectSettings)); + + var connection = await Connection.Connection.ConnectAsync(connectionInfo, projectSettings: projectSettings); + + // device not found + if(connection == null) return null; + + var raspberryModel = new RaspberryModelCheck(); + + // .NET Core only supports Raspberry models 3 and 4. + if(raspberryModel.IsNotSupported(connection.PiStatus.RaspberryModel)) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + MessageBoxEx.Show( + $"Your [{raspberryModel.ActualType}] is not supported." + + $" This .NET version requires a {string.Join(" or ", raspberryModel.Supported)}.", + "Raspberry Not Supported", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + connection.Dispose(); + + return null; + } + + // Ensure that the SDK is installed. + if (!await connection.SetupSdkAsync(projectProperties.SdkVersion, connection.PiStatus)) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + MessageBoxEx.Show( + $"Installation of the .NET SDK {projectProperties.SdkVersion} on the Raspberry was unsuccessful.\r\n\r\nCheck the Debug Output for more details.", + "SDK Installation Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + connection.Dispose(); + + return null; + } + + // Ensure that the debugger is installed. + // TODO: Visual Studio tries to update/install the package each time it attaches. + // Maybe only try the update/install once per session/device? Lots of permutations to track tho. + // Would need to quietly fail if no internet connection. + if (!await connection.SetupDebuggerAsync()) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + MessageBoxEx.Show( + "Cannot install the VSDBG debugger on the Raspberry.\r\n\r\nCheck the Debug Output for more details.", + "Debugger Installation Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + connection.Dispose(); + + return null; + } + + // Ensure that linux libraries are installed. + if (!await connection.SetupLinuxDependenciesAsync()) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + MessageBoxEx.Show( + "Cannot install the Linux dependencies on the Raspberry.\r\n\r\nCheck the Debug Output for more details.", + "Linux Dependencies Installation Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + connection.Dispose(); + + return null; + } + + var dirInfo = new DirectoryInfo(projectProperties.OutputFolder); + + bool shouldUploadProgram = + LastUploadedDirInfo == null || + LastUploadedDirInfo.FullName != dirInfo.FullName || + LastUploadedDirInfo.LastWriteTime != dirInfo.LastWriteTime; + + var outputFileName = Path.Combine( projectProperties.OutputFolder, projectProperties.OutputFileName ); + + if (!shouldUploadProgram) + { + Log.Info($"Skipping upload of {outputFileName}, {dirInfo.LastWriteTime}"); + + return connection; + } + + Log.Info($"Uploading {outputFileName}, {dirInfo.LastWriteTime}"); + + // Upload the program binaries. + if (await connection.UploadProgramAsync( + projectProperties?.Name, + projectProperties?.AssemblyName, + projectProperties?.PublishFolder)) + { + LastUploadedDirInfo = dirInfo; + + return connection; + } + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + MessageBoxEx.Show( + "Cannot upload the program binaries to the Raspberry.\r\n\r\nCheck the Debug Output for more details.", + "Debugger Installation Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + connection.Dispose(); + + return null; + } + } +} diff --git a/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs b/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs index da9b401..e0415c3 100644 --- a/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs +++ b/RaspberryDebugger/Models/VisualStudio/ProjectProperties.cs @@ -30,6 +30,15 @@ namespace RaspberryDebugger.Models.VisualStudio { + /// + /// Determined through experimentation. + /// + enum DotNetFrameworks + { + DotNet_8 = 524288, + DotNet_9 = 589824, + } + /// /// The Visual Studio class properties can only be /// accessed from the UI thread, so we'll use this class to capture the @@ -89,16 +98,19 @@ public static ProjectProperties CopyFrom(Solution solution, EnvDTE.Project proje var projectFolder = Path.GetDirectoryName(project.FullName); // Read the properties we care about from the project. - var targetFrameworkMonikers = (string)project.Properties.Item("TargetFrameworkMoniker").Value; + var targetFrameworkMonikers = (string)project.Properties.Item("TargetFrameworkMonikers").Value; + var targetFrameworkMoniker = (string)project.Properties.Item("TargetFrameworkMoniker").Value; var outputType = (int)project.Properties.Item("OutputType").Value; - var monikers = targetFrameworkMonikers.Split(','); + var targetFramework = (DotNetFrameworks)((int)project.Properties.Item("TargetFramework").Value); + + var moniker = targetFrameworkMoniker.Split(','); - var isNetCore = monikers[0] == ".NETCoreApp"; + var isNetCore = moniker[0] == ".NETCoreApp"; // Extract the version from the moniker. This looks like: "Version=v5.0" var versionRegex = new Regex(@"(?[0-9\.]+)$"); - var netVersion = SemanticVersion.Parse(versionRegex.Match(monikers[1]).Groups["version"].Value); + var netVersion = SemanticVersion.Parse(versionRegex.Match(moniker[1]).Groups["version"].Value); // Load [Properties/launchSettings.json] if present to obtain the command line // arguments and environment variables as well as the target connection. Note @@ -275,6 +287,7 @@ string SetLaunchUrl(JObject profileObject) Guid = projectGuid, Configuration = project.ConfigurationManager.ActiveConfiguration.ConfigurationName, IsNetCore = isNetCore, + Framework = 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, @@ -473,7 +486,7 @@ private static List ParseArgs(string commandLine) /// /// Returns the framework version. /// - public string Framework => null; + public DotNetFrameworks? Framework { get; set; } = null; /// /// Returns the publication folder. From 4374ba1e50da768ad8457abb099288d37811760e Mon Sep 17 00:00:00 2001 From: Bryan Roth Date: Tue, 8 Apr 2025 15:55:55 -0400 Subject: [PATCH 2/2] Use NBGV to set version. --- RaspberryDebugger/Properties/AssemblyInfo.cs | 13 ------------- RaspberryDebugger/RaspberryDebugger.csproj | 16 +++++++++------- version.json | 13 +++++++++++++ 3 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 version.json diff --git a/RaspberryDebugger/Properties/AssemblyInfo.cs b/RaspberryDebugger/Properties/AssemblyInfo.cs index 04e9b79..035ff33 100644 --- a/RaspberryDebugger/Properties/AssemblyInfo.cs +++ b/RaspberryDebugger/Properties/AssemblyInfo.cs @@ -34,16 +34,3 @@ // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.3.1.0")] -[assembly: AssemblyFileVersion("3.3.1.0")] diff --git a/RaspberryDebugger/RaspberryDebugger.csproj b/RaspberryDebugger/RaspberryDebugger.csproj index 4c0aeb9..c9aa357 100644 --- a/RaspberryDebugger/RaspberryDebugger.csproj +++ b/RaspberryDebugger/RaspberryDebugger.csproj @@ -148,6 +148,13 @@ compile; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all +