Пример #1
0
        /// <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);
        }
Пример #2
0
        /// <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);
        }
Пример #3
0
 /// <summary>
 /// Persists the connections back to Visual Studio.
 /// </summary>
 private void SaveConnections()
 {
     PackageHelper.WriteConnections(connections, disableLogging: true);
 }
Пример #4
0
        /// <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}");
                    }
                }
            }
        }
Пример #5
0
        //--------------------------------------------------------------------
        // Static members

        /// <summary>
        /// Creates a <see cref="ProjectProperties"/> instance holding the
        /// necessary properties from a <see cref="Project"/>.  This must
        /// be called on a UI thread.
        /// </summary>
        /// <param name="solution">The current solution.</param>
        /// <param name="project">The source project.</param>
        /// <returns>The cloned <see cref="ProjectProperties"/>.</returns>
        public static ProjectProperties CopyFrom(Solution solution, Project project)
        {
            Covenant.Requires <ArgumentNullException>(solution != null, nameof(solution));
            Covenant.Requires <ArgumentNullException>(project != null, nameof(project));

            ThreadHelper.ThrowIfNotOnUIThread();

            if (string.IsNullOrEmpty(project.FullName))
            {
                // We'll see this for unsupported Visual Studio projects and will just
                // return project properties indicating this.

                return(new ProjectProperties()
                {
                    Name = project.Name,
                    FullPath = project.FullName,
                    Configuration = null,
                    IsNetCore = false,
                    SdkVersion = null,
                    OutputFolder = null,
                    OutputFileName = null,
                    IsExecutable = false,
                    AssemblyName = null,
                    DebugEnabled = false,
                    DebugConnectionName = null,
                    CommandLineArgs = new List <string>(),
                    EnvironmentVariables = new Dictionary <string, string>(),
                    IsSupportedSdkVersion = false,
                    IsRaspberryCompatible = false,
                    IsAspNet = false,
                    AspPort = 0,
                    AspLaunchBrowser = false,
                    AspRelativeBrowserUri = null
                });
            }

            var projectFolder = Path.GetDirectoryName(project.FullName);
            var isNetCore     = true;
            var netVersion    = (SemanticVersion)null;
            var sdkName       = (string)null;

            // Read the properties we care about from the project.

            var targetFrameworkMonikers = (string)project.Properties.Item("TargetFrameworkMoniker").Value;
            var outputType = (int)project.Properties.Item("OutputType").Value;

            var monikers = targetFrameworkMonikers.Split(',');

            isNetCore = monikers[0] == ".NETCoreApp";

            // Extract the version from the moniker.  This looks like: "Version=v5.0"

            var versionRegex = new Regex(@"(?<version>[0-9\.]+)$");

            netVersion = SemanticVersion.Parse(versionRegex.Match(monikers[1]).Groups["version"].Value);

            // The [dotnet --info] command doesn't work as I expected because it doesn't
            // appear to examine the project file when determining the SDK version.
            //
            //      https://github.com/nforgeio/RaspberryDebugger/issues/16
            //
            // So, we're just going to use the latest known SDK from our catalog instead.
            // This isn't ideal but should work fine for the vast majority of people.

            var targetSdk        = (Sdk)null;
            var targetSdkVersion = (SemanticVersion)null;

            foreach (var sdkItem in PackageHelper.SdkGoodCatalog.Items
                     .Where(item => item.IsStandalone && item.Architecture == SdkArchitecture.ARM32))
            {
                var sdkVersion = SemanticVersion.Parse(sdkItem.Version);

                if (sdkVersion.Major != netVersion.Major || sdkVersion.Minor != netVersion.Minor)
                {
                    continue;
                }

                if (targetSdkVersion == null || sdkVersion > targetSdkVersion)
                {
                    targetSdkVersion = sdkVersion;
                    targetSdk        = new Sdk(sdkItem.Name, sdkItem.Version);;
                }
            }

            sdkName = targetSdk?.Name;

            // Load [Properties/launchSettings.json] if present to obtain the command line
            // arguments and environment variables as well as the target connection.  Note
            // that we're going to use the profile named for the project and ignore any others.
            //
            // The launch settings for Console vs. WebApps are a bit different.  WebApps include
            // a top-level "iisSettings"" property and two profiles: "IIS Express" and the
            // profile with the project name.  We're going to use the presence of the "iisSettings"
            // property to determine that we're dealing with a WebApp and we'll do some additional
            // processing based off of the project profile:
            //
            //      1. Launch the browser if [launchBrowser=true]
            //      2. Extract the site port number from [applicationUrl]
            //      3. Have the app listen on all IP addresses by adding this environment
            //         variable when we :
            //
            //              ASPNETCORE_SERVER.URLS=http://0.0.0.0:<port>

            var launchSettingsPath   = Path.Combine(projectFolder, "Properties", "launchSettings.json");
            var commandLineArgs      = new List <string>();
            var environmentVariables = new Dictionary <string, string>();
            var isAspNet             = false;
            var aspPort               = 0;
            var aspLaunchBrowser      = false;
            var aspRelativeBrowserUri = "/";

            if (File.Exists(launchSettingsPath))
            {
                var settings = JObject.Parse(File.ReadAllText(launchSettingsPath));
                var profiles = settings.Property("profiles");

                if (profiles != null)
                {
                    foreach (var profile in ((JObject)profiles.Value).Properties())
                    {
                        if (profile.Name == project.Name)
                        {
                            var profileObject = (JObject)profile.Value;
                            var environmentVariablesObject = (JObject)profileObject.Property("environmentVariables")?.Value;

                            commandLineArgs = ParseArgs((string)profileObject.Property("commandLineArgs")?.Value);

                            if (environmentVariablesObject != null)
                            {
                                foreach (var variable in environmentVariablesObject.Properties())
                                {
                                    environmentVariables[variable.Name] = (string)variable.Value;
                                }
                            }

                            // Extract additional settings for ASPNET projects.

                            if (settings.Property("iisSettings") != null)
                            {
                                isAspNet = true;

                                // Note that we're going to fall back to port 5000 if there are any
                                // issues parsing the application URL.

                                const int fallbackPort = 5000;

                                var jProperty = profileObject.Property("applicationUrl");

                                if (jProperty != null && jProperty.Value.Type == JTokenType.String)
                                {
                                    try
                                    {
                                        var uri = new Uri((string)jProperty.Value);

                                        aspPort = uri.Port;

                                        if (!NetHelper.IsValidPort(aspPort))
                                        {
                                            aspPort = fallbackPort;
                                        }
                                    }
                                    catch
                                    {
                                        aspPort = fallbackPort;
                                    }
                                }
                                else
                                {
                                    aspPort = fallbackPort;
                                }

                                jProperty = profileObject.Property("launchBrowser");

                                if (jProperty != null && jProperty.Value.Type == JTokenType.Boolean)
                                {
                                    aspLaunchBrowser = (bool)jProperty.Value;
                                }
                            }
                        }
                        else if (profile.Name == "IIS Express")
                        {
                            // For ASPNET apps, this profile may include a "launchUrl" which
                            // specifies the absolute or relative URI to display in a debug
                            // browser launched during debugging.
                            //
                            // We're going to normalize this as a relative URI and save it
                            // so we'll be able to launch the browser on the correct page.

                            var profileObject = (JObject)profile.Value;
                            var jProperty     = profileObject.Property("launchUrl");

                            if (jProperty != null && jProperty.Value.Type == JTokenType.String)
                            {
                                var launchUri = (string)jProperty.Value;

                                if (!string.IsNullOrEmpty(launchUri))
                                {
                                    try
                                    {
                                        var uri = new Uri(launchUri, UriKind.RelativeOrAbsolute);

                                        if (uri.IsAbsoluteUri)
                                        {
                                            aspRelativeBrowserUri = uri.PathAndQuery;
                                        }
                                        else
                                        {
                                            aspRelativeBrowserUri = launchUri;
                                        }
                                    }
                                    catch
                                    {
                                        // We'll fall back to "/" for any URI parsing errors.

                                        aspRelativeBrowserUri = "/";
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // Get the target Raspberry from the debug settings.

            var projects            = PackageHelper.ReadRaspberryProjects(solution);
            var projectSettings     = projects[project.UniqueName];
            var debugEnabled        = projectSettings.EnableRemoteDebugging;
            var debugConnectionName = projectSettings.RemoteDebugTarget;

            // Determine whether the referenced .NET Core SDK is currently supported.

            var sdk = sdkName == null ? null : PackageHelper.SdkGoodCatalog.Items.SingleOrDefault(item => SemanticVersion.Parse(item.Name) == SemanticVersion.Parse(sdkName) && item.Architecture == SdkArchitecture.ARM32);

            var isSupportedSdkVersion = sdk != null;

            // Determine whether the project is Raspberry compatible.

            var isRaspberryCompatible = isNetCore &&
                                        outputType == 1 && // 1=EXE
                                        isSupportedSdkVersion;

            // We need to jump through some hoops to obtain the project GUID.

            var solutionService = RaspberryDebuggerPackage.Instance.SolutionService;

            Covenant.Assert(solutionService.GetProjectOfUniqueName(project.UniqueName, out var hierarchy) == VSConstants.S_OK);
            Covenant.Assert(solutionService.GetGuidOfProject(hierarchy, out var projectGuid) == VSConstants.S_OK);

            // Return the properties.

            return(new ProjectProperties()
            {
                Name = project.Name,
                FullPath = project.FullName,
                Guid = projectGuid,
                Configuration = project.ConfigurationManager.ActiveConfiguration.ConfigurationName,
                IsNetCore = isNetCore,
                SdkVersion = sdk?.Version,
                OutputFolder = Path.Combine(projectFolder, project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString()),
                OutputFileName = (string)project.Properties.Item("OutputFileName").Value,
                IsExecutable = outputType == 1,              // 1=EXE
                AssemblyName = project.Properties.Item("AssemblyName").Value.ToString(),
                DebugEnabled = debugEnabled,
                DebugConnectionName = debugConnectionName,
                CommandLineArgs = commandLineArgs,
                EnvironmentVariables = environmentVariables,
                IsSupportedSdkVersion = isSupportedSdkVersion,
                IsRaspberryCompatible = isRaspberryCompatible,
                IsAspNet = isAspNet,
                AspPort = aspPort,
                AspLaunchBrowser = aspLaunchBrowser,
                AspRelativeBrowserUri = aspRelativeBrowserUri
            });
        }