/// <summary> /// 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 /// </summary> /// <param name="dte">The DTE.</param> /// <param name="solution">The solution.</param> /// <param name="project">The project.</param> /// <param name="projectProperties">The project properties.</param> /// <returns><c>true</c> on success.</returns> public static async Task <bool> PublishProjectAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) { Covenant.Requires <ArgumentNullException>(dte != null, nameof(dte)); Covenant.Requires <ArgumentNullException>(solution != null, nameof(solution)); Covenant.Requires <ArgumentNullException>(project != null, nameof(project)); Covenant.Requires <ArgumentNullException>(projectProperties != null, nameof(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 projectGuid = projectProperties.Guid; var solutionService4 = (IVsSolution4)await RaspberryDebuggerPackage.Instance.GetServiceAsync(typeof(SVsSolution)); if (solutionService4 == null) { Covenant.Assert(solutionService4 != null, $"Service [{typeof(SVsSolution).Name}] is not available."); } solutionService4.EnsureProjectIsLoaded(ref projectGuid, (uint)__VSBSLFLAGS.VSBSLFLAGS_LoadAllPendingProjects); // Build the project to ensure that there are no compile-time errors. Log.Info($"Building: {projectProperties.FullPath}"); solution.SolutionBuild.BuildProject(solution.SolutionBuild.ActiveConfiguration.Name, project.UniqueName, WaitForBuildToFinish: true); var errorList = dte.ToolWindows.ErrorList.ErrorItems; if (errorList.Count > 0) { for (int i = 1; i <= errorList.Count; i++) { var error = errorList.Item(i); Log.Error($"{error.FileName}({error.Line},{error.Column}: {error.Description})"); } Log.Error($"Build failed: [{errorList.Count}] errors"); Log.Error($"See the Build/Output panel for more information"); return(false); } Log.Info("Build succeeded"); // 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: {projectProperties.FullPath}"); await Task.Yield(); var 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 <string>(StringComparer.InvariantCultureIgnoreCase); var environmentVariables = new Dictionary <string, string>(); 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); } } try { var response = await NeonHelper.ExecuteCaptureAsync( "dotnet", new object[] { "publish", "--configuration", projectProperties.Configuration, "--runtime", projectProperties.Runtime, "--no-self-contained", "--output", projectProperties.PublishFolder, projectProperties.FullPath }, environmentVariables : environmentVariables); await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); if (response.ExitCode == 0) { Log.Error("Publish succeeded"); return(true); } Log.Error($"Publish failed: ExitCode={response.ExitCode}"); Log.WriteLine(response.AllText); return(false); } catch (Exception e) { Log.Error(NeonHelper.ExceptionError(e)); return(false); } }
/// <summary> /// Establishes a connection to the Raspberry and ensures that the Raspberry has /// the target SDK, <b>vsdbg</b> installed and also handles uploading of the project /// binaries. /// </summary> /// <param name="connectionInfo">The connection info.</param> /// <param name="targetSdk">The target SDK.</param> /// <param name="projectProperties">The project properties.</param> /// <param name="projectSettings">The project's Raspberry debug settings.</param> /// <returns>The <see cref="Connection"/> or <c>null</c> if there was an error.</returns> public static async Task <Connection> InitializeConnectionAsync(ConnectionInfo connectionInfo, Sdk targetSdk, ProjectProperties projectProperties, ProjectSettings projectSettings) { Covenant.Requires <ArgumentNullException>(connectionInfo != null, nameof(connectionInfo)); Covenant.Requires <ArgumentNullException>(targetSdk != null, nameof(targetSdk)); Covenant.Requires <ArgumentNullException>(projectProperties != null, nameof(projectProperties)); Covenant.Requires <ArgumentNullException>(projectSettings != null, nameof(projectSettings)); var connection = await Connection.ConnectAsync(connectionInfo, projectSettings : projectSettings); // .NET Core only supports Raspberry models 3 and 4. if (!connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi 3 Model") && !connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi 4 Model")) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); MessageBoxEx.Show( $"Your [{connection.PiStatus.RaspberryModel}] is not supported. .NET Core requires a Raspberry Model 3 or 4.", $"Raspberry Not Supported", MessageBoxButtons.OK, MessageBoxIcon.Error); connection.Dispose(); return(null); } // Ensure that the SDK is installed. if (!await connection.InstallSdkAsync(targetSdk.Version)) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); MessageBoxEx.Show( $"Cannot install the .NET SDK [v{targetSdk.Version}] on the Raspberry.\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. if (!await connection.InstallDebuggerAsync()) { 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); } // Upload the program binaries. if (!await connection.UploadProgramAsync(projectProperties.Name, projectProperties.AssemblyName, projectProperties.PublishFolder)) { 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); } return(connection); }
/// <summary> /// 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 /// </summary> /// <param name="dte"></param> /// <param name="solution"></param> /// <param name="project"></param> /// <param name="projectProperties"></param> /// <returns></returns> public static async Task <bool> PublishProjectWithUIAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) { Covenant.Requires <ArgumentNullException>(dte != null, nameof(dte)); Covenant.Requires <ArgumentNullException>(solution != null, nameof(solution)); Covenant.Requires <ArgumentNullException>(project != null, nameof(project)); Covenant.Requires <ArgumentNullException>(projectProperties != null, nameof(projectProperties)); await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); if (!await PublishProjectAsync(dte, solution, project, projectProperties)) { MessageBox.Show( "[dotnet publish] failed for the project.\r\n\r\nLook at the Output/Debug panel for more details.", "Publish Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); return(false); } else { return(true); } }
/// <summary> /// Attempts to locate the startup project to be debugged, ensuring that it's /// eligable for Raspberry debugging. /// </summary> /// <param name="dte">The IDE.</param> /// <returns>The target project or <c>null</c> if there isn't a startup project or it wasn't eligible.</returns> 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 .NET Core 3.1 or .NET 5 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 (string.IsNullOrEmpty(projectProperties.SdkVersion)) { MessageBox.Show( "The .NET Core SDK version could not be identified.", "Invalid Project Type", MessageBoxButtons.OK, MessageBoxIcon.Error); return(null); } var sdkVersion = Version.Parse(projectProperties.SdkVersion); if (!projectProperties.IsSupportedSdkVersion) { MessageBox.Show( $"The .NET Core SDK [{sdkVersion}] is not currently supported. Only .NET Core versions [v3.1] or later will ever be supported\r\n\r\nNote that we currently support only offical 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:\r\n\t\nhttps://github.com/nforgeio/RaspberryDebugger/issues", "SDK Not Supported", MessageBoxButtons.OK, MessageBoxIcon.Error); return(null); } if (projectProperties.AssemblyName.Contains(' ')) { MessageBox.Show( $"Your assembly name [{projectProperties.AssemblyName}] includes a space. This isn't supported.", "Unsupported Assembly Name", MessageBoxButtons.OK, MessageBoxIcon.Error); return(null); } return(project); }
/// <summary> /// Creates the temporary launch settings file we'll use starting <b>vsdbg</b> on /// the Raspberry for this command. /// </summary> /// <param name="connectionInfo">The connection information.</param> /// <param name="projectProperties">The project properties.</param> /// <returns>The <see cref="TempFile"/> referencing the created launch file.</returns> private async Task <TempFile> CreateLaunchSettingsAsync(ConnectionInfo connectionInfo, ProjectProperties projectProperties) { Covenant.Requires <ArgumentNullException>(connectionInfo != null, nameof(connectionInfo)); Covenant.Requires <ArgumentNullException>(projectProperties != null, nameof(projectProperties)); var systemRoot = Environment.GetFolderPath(Environment.SpecialFolder.Windows); var debugFolder = LinuxPath.Combine(PackageHelper.RemoteDebugBinaryRoot(connectionInfo.User), projectProperties.Name); // NOTE: // // We're having the remote [vsdbg] debugger launch our program as: // // dotnet program.dll args // // where: // // dotnet - is the fully qualified path to the dotnet SDK tool on the remote machine // program.dll - is the fully qualified path to our program DLL // args - are the arguments to be passed to our program // // This means that we need add [program.dll] as the first argument, followed // by the program arguments. var args = new JArray(); args.Add(LinuxPath.Combine(debugFolder, projectProperties.AssemblyName + ".dll")); foreach (var arg in projectProperties.CommandLineArgs) { args.Add(arg); } var environmentVariables = new JObject(); foreach (var variable in projectProperties.EnvironmentVariables) { environmentVariables.Add(variable.Key, variable.Value); } // For ASPNET apps, set the [ASPNETCORE_URLS] environment variable // to [http://0.0.0.0:PORT] so that the app running on the Raspberry will // be reachable from the development workstation. Note that we don't // support HTTPS at this time. if (projectProperties.IsAspNet) { environmentVariables["ASPNETCORE_URLS"] = $"http://0.0.0.0:{projectProperties.AspPort}"; } // Construct the debug launch JSON file. var engineLogging = string.Empty; #if DISABLED // Uncomment this to have the remote debugger log the traffic it // sees from Visual Studio for debugging purposes. The log file // is persisted to the program folder on the Raspberry. engineLogging = $"--engineLogging={debugFolder}/__vsdbg-log.txt"; #endif var settings = new JObject ( new JProperty("version", "0.2.1"), new JProperty("adapter", Path.Combine(systemRoot, "Sysnative", "OpenSSH", "ssh.exe")), new JProperty("adapterArgs", $"-i \"{connectionInfo.PrivateKeyPath}\" -o \"StrictHostKeyChecking no\" {connectionInfo.User}@{connectionInfo.Host} {PackageHelper.RemoteDebuggerPath} --interpreter=vscode {engineLogging}"), new JProperty("configurations", new JArray ( new JObject ( new JProperty("name", "Debug on Raspberry"), new JProperty("type", "coreclr"), new JProperty("request", "launch"), new JProperty("program", PackageHelper.RemoteDotnetCommand), new JProperty("args", args), new JProperty("cwd", debugFolder), new JProperty("stopAtEntry", "false"), new JProperty("console", "internalConsole"), new JProperty("env", environmentVariables) ) ) ) ); var tempFile = new TempFile(".launch.json"); using (var stream = new FileStream(tempFile.Path, FileMode.CreateNew, FileAccess.ReadWrite)) { await stream.WriteAsync(Encoding.UTF8.GetBytes(settings.ToString())); } return(tempFile); }
/// <summary> /// This function is the callback used to execute the command when the menu item is clicked. /// See the constructor to see how the menu item is associated with this function using /// OleMenuCommandService service and MenuCommand class. /// </summary> /// <param name="sender">Event sender.</param> /// <param name="e">Event args.</param> #pragma warning disable VSTHRD100 private async void Execute(object sender, EventArgs e) #pragma warning restore VSTHRD100 { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); if (!await DebugHelper.EnsureOpenSshAsync()) { return; } var project = DebugHelper.GetTargetProject(dte); if (project == null) { return; } var projectProperties = ProjectProperties.CopyFrom(dte.Solution, project); //----------------------------------------------------------------- if (!await DebugHelper.PublishProjectWithUIAsync(dte, dte.Solution, project, projectProperties)) { return; } var connectionInfo = DebugHelper.GetDebugConnectionInfo(projectProperties); if (connectionInfo == null) { return; } // Identify the most recent SDK installed on the workstation that has the same // major and minor version numbers as the project. We'll ensure that the same // SDK is installed on the Raspberry (further below). var targetSdk = DebugHelper.GetTargetSdk(projectProperties); if (targetSdk == null) { return; } // Establish a Raspberry connection to handle some things before we start the debugger. var connection = await DebugHelper.InitializeConnectionAsync(connectionInfo, targetSdk, projectProperties, PackageHelper.GetProjectSettings(dte.Solution, project)); if (connection == null) { return; } using (connection) { // Generate a temporary [launch.json] file and launch the debugger. using (var tempFile = await CreateLaunchSettingsAsync(connectionInfo, projectProperties)) { dte.ExecuteCommand("DebugAdapterHost.Launch", $"/LaunchJson:\"{tempFile.Path}\""); } // Launch the browser for ASPNET apps if requested. Note that we're going to do this // on a background task to poll the Raspberry, waiting for the app to create the create // the LISTENING socket. if (projectProperties.IsAspNet && projectProperties.AspLaunchBrowser) { var baseUri = $"http://{connectionInfo.Host}:{projectProperties.AspPort}"; var launchReady = false; await NeonHelper.WaitForAsync( async() => { if (dte.Mode != vsIDEMode.vsIDEModeDebug) { // The developer must have stopped debugging before the ASPNET // application was able to begin servicing requests. return(true); } try { var appListeningScript = $@" if lsof -i -P -n | grep --quiet 'TCP \*:{projectProperties.AspPort} (LISTEN)' ; then exit 0 else exit 1 fi "; var response = connection.SudoCommand(CommandBundle.FromScript(appListeningScript)); if (response.ExitCode != 0) { return(false); } // Wait just a bit longer to give the application a chance to // perform any additional initialization. await Task.Delay(TimeSpan.FromSeconds(1)); launchReady = true; return(true); } catch { return(false); } }, timeout : TimeSpan.FromSeconds(30), pollInterval : TimeSpan.FromSeconds(0.5)); if (launchReady) { NeonHelper.OpenBrowser($"{baseUri}{projectProperties.AspRelativeBrowserUri}"); } } } }
/// <summary> /// 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 /// </summary> /// <param name="dte">The DTE.</param> /// <param name="solution">The solution.</param> /// <param name="project">The project.</param> /// <param name="projectProperties">The project properties.</param> /// <returns><c>true</c> on success.</returns> public static async Task <bool> PublishProjectAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties) { Covenant.Requires <ArgumentNullException>(dte != null, nameof(dte)); Covenant.Requires <ArgumentNullException>(solution != null, nameof(solution)); Covenant.Requires <ArgumentNullException>(project != null, nameof(project)); Covenant.Requires <ArgumentNullException>(projectProperties != null, nameof(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(); solution.SolutionBuild.BuildProject(solution.SolutionBuild.ActiveConfiguration.Name, project.UniqueName, WaitForBuildToFinish: true); var errorList = dte.ToolWindows.ErrorList.ErrorItems; if (errorList.Count > 0) { return(false); } await Task.Yield(); // Publish the project so all required binaries and assets end up // in the output folder. Log.Info($"Publishing: {projectProperties.FullPath}"); var response = await NeonHelper.ExecuteCaptureAsync( "dotnet", new object[] { "publish", "--configuration", projectProperties.Configuration, "--runtime", projectProperties.Runtime, "--no-self-contained", "--output", projectProperties.PublishFolder, projectProperties.FullPath }); if (response.ExitCode == 0) { return(true); } Log.Error("Build Failed!"); Log.WriteLine(response.AllText); return(false); }