/// <summary> /// Constructor. /// </summary> /// <param name="projectSettings">The project settings.</param> public SettingsDialog(ProjectSettings projectSettings) { Covenant.Requires <ArgumentNullException>(projectSettings != null); this.projectSettings = projectSettings; InitializeComponent(); // The instructions include "\r\n" sequences that need to be replaced with // actual CR/LF characters. instructionsTextBox.Text = Regex.Unescape(instructionsTextBox.Text); // Initialize the combo box with the available connections and select // the current one. connectionNameToIndex = new Dictionary <string, int>(StringComparer.InvariantCultureIgnoreCase); var connections = PackageHelper.ReadConnections(disableLogging: true); var index = 0; targetComboBox.Items.Clear(); targetComboBox.Items.Add(ProjectSettings.DisabledConnectionName); connectionNameToIndex.Add(ProjectSettings.DisabledConnectionName, index++); targetComboBox.Items.Add(ProjectSettings.DefaultConnectionName); connectionNameToIndex.Add(ProjectSettings.DefaultConnectionName, index++); foreach (var connection in connections.OrderBy(connection => connection.SortKey)) { targetComboBox.Items.Add(connection.Name); connectionNameToIndex.Add(connection.Name, index++); } if (!projectSettings.EnableRemoteDebugging) { targetComboBox.SelectedIndex = connectionNameToIndex[ProjectSettings.DisabledConnectionName]; } else { // If the connection named in the settings exists select it, // otherwise select the default. var selectedConnection = connections.FirstOrDefault(connection => connection.Name.Equals(projectSettings.RemoteDebugTarget, StringComparison.InvariantCultureIgnoreCase)); if (projectSettings.RemoteDebugTarget == null || selectedConnection == null) { targetComboBox.SelectedIndex = connectionNameToIndex[ProjectSettings.DefaultConnectionName]; } else { targetComboBox.SelectedIndex = connectionNameToIndex[selectedConnection.Name]; } } targetGroup.Text = projectSettings.TargetGroup; }
/// <summary> /// 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. /// </summary> /// <param name="projectProperties">The project properties.</param> /// <returns>The connection or <c>null</c> when one couldn't be located.</returns> public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectProperties) { Covenant.Requires <ArgumentNullException>(projectProperties != null, nameof(projectProperties)); var existingConnections = PackageHelper.ReadConnections(); var connectionInfo = (ConnectionInfo)null; if (string.IsNullOrEmpty(projectProperties.DebugConnectionName)) { connectionInfo = existingConnections.SingleOrDefault(info => info.IsDefault); 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); }
/// <summary> /// Reloads the connections into the connection view maintaining the selection /// if the current connection still exists. /// </summary> private void ReloadConnections() { var orgName = ((ConnectionInfo)connectionsView.SelectedObject)?.Name; connections = PackageHelper.ReadConnections(disableLogging: true); connectionsView.SetObjects(connections); connectionsView.SelectedObject = connections.SingleOrDefault(connection => connection.Name == orgName); EnableButtons(); }
/// <summary> /// Reads the Raspberry Pi connections persisted to Visual Studio /// and loads them into the list view. /// </summary> private void LoadConnections() { connections = PackageHelper.ReadConnections(disableLogging: true); // All connections must reference this panel so they can notify // when their [IsDefault] check state changes. foreach (var connection in connections) { connection.ConnectionsPanel = this; } // Load the connections into the view and enable/disable the buttons. connectionsView.SetObjects(connections); EnableButtons(); }
/// <summary> /// Initializes the connection by retrieving status from the remote Raspberry and ensuring /// that any packages required for executing remote commands are installed. This will also /// create and configure a SSH key pair on both the workstation and remote Raspberry if one /// doesn't already exist so that subsequent connections can use key based authentication. /// </summary> /// <returns>The tracking <see cref="Task"/>.</returns> private async Task InitializeAsync() { await PackageHelper.ExecuteWithProgressAsync( $"Connecting to [{Name}]...", async() => { // Disabling this because it looks like SUDO passwork prompting is disabled // by default for Raspberry Pi OS. #if DISABLED // This call ensures that SUDO password prompting is disabled and the // the required hidden folders exist in the user's home directory. DisableSudoPrompt(password); #endif // We need to ensure that [unzip] is installed so that [LinuxSshProxy] command // bundles will work. Log($"[{Name}]: Checking for: [unzip]"); var response = SudoCommand("which unzip"); if (response.ExitCode != 0) { Log($"[{Name}]: Installing: [unzip]"); ThrowOnError(SudoCommand("sudo apt-get update")); ThrowOnError(SudoCommand("sudo apt-get install -yq unzip")); } // We're going to execute a script the gathers everything in a single operation for speed. Log($"[{Name}]: Retrieving status"); var statusScript = $@" # This script will return the status information via STDOUT line-by-line # in this order: # # Chip Architecture # PATH environment variable # Unzip Installed (""unzip"" or ""unzip-missing"") # Debugger Installed (""debugger-installed"" or ""debugger-missing"") # List of installed SDKs names (e.g. 3.1.108) separated by commas # Raspberry Model like: Raspberry Pi 4 Model B Rev 1.2 # Raspberry Revision like: c03112 # # This script also ensures that the [/lib/dotnet] directory exists, that # it has reasonable permissions, and that the folder exists on the system # PATH and that DOTNET_ROOT points to the folder. # Set the SDK and debugger installation paths. DOTNET_ROOT={PackageHelper.RemoteDotnetFolder} DEBUGFOLDER={PackageHelper.RemoteDebuggerFolder} # Get the chip architecture uname -m # Get the current PATH echo $PATH # Detect whether [unzip] is installed. if which unzip &> /dev/nul ; then echo 'unzip' else echo 'unzip-missing' fi # Detect whether the [vsdbg] debugger is installed. if [ -d $DEBUGFOLDER ] ; then echo 'debugger-installed' else echo 'debugger-missing' fi # List the SDK folders. These folder names are the same as the # corresponding SDK name. We'll list the files on one line # with the SDK names separated by commas. We'll return a blank # line if the SDK directory doesn't exist. if [ -d $DOTNET_ROOT/sdk ] ; then ls -m $DOTNET_ROOT/sdk else echo '' fi # Output the Raspberry board model. cat /proc/cpuinfo | grep '^Model\s' | grep -o 'Raspberry.*$' # Output the Raspberry board revision. cat /proc/cpuinfo | grep 'Revision\s' | grep -o '[0-9a-fA-F]*$' # Ensure that the [/lib/dotnet] folder exists, that it's on the # PATH and that DOTNET_ROOT are defined. mkdir -p /lib/dotnet chown root:root /lib/dotnet chmod 755 /lib/dotnet # Set these for the current session: export DOTNET_ROOT={PackageHelper.RemoteDotnetFolder} export PATH=$PATH:$DOTNET_ROOT # and for future sessions too: if ! grep --quiet DOTNET_ROOT /etc/profile ; then echo """" >> /etc/profile echo ""#------------------------------"" >> /etc/profile echo ""# Raspberry Debugger:"" >> /etc/profile echo ""export DOTNET_ROOT=$DOTNET_ROOT"" >> /etc/profile echo ""export PATH=$PATH"" >> /etc/profile echo ""#------------------------------"" >> /etc/profile fi "; Log($"[{Name}]: Fetching status"); response = ThrowOnError(SudoCommand(CommandBundle.FromScript(statusScript))); using (var reader = new StringReader(response.OutputText)) { var architecture = await reader.ReadLineAsync(); var path = await reader.ReadLineAsync(); var hasUnzip = await reader.ReadLineAsync() == "unzip"; var hasDebugger = await reader.ReadLineAsync() == "debugger-installed"; var sdkLine = await reader.ReadLineAsync(); var model = await reader.ReadLineAsync(); var revision = await reader.ReadToEndAsync(); revision = revision.Trim(); // Remove any whitespace at the end. Log($"[{Name}]: architecture: {architecture}"); Log($"[{Name}]: path: {path}"); Log($"[{Name}]: unzip: {hasUnzip}"); Log($"[{Name}]: debugger: {hasDebugger}"); Log($"[{Name}]: sdks: {sdkLine}"); Log($"[{Name}]: model: {model}"); Log($"[{Name}]: revision: {revision}"); // Convert the comma separated SDK names into a [PiSdk] list. var sdks = new List <Sdk>(); foreach (var sdkName in sdkLine.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sdk => sdk.Trim())) { // $todo(jefflill): We're only supporting 32-bit SDKs at this time. var sdkCatalogItem = PackageHelper.SdkCatalog.Items.SingleOrDefault(item => item.Name == sdkName && item.Architecture == SdkArchitecture.ARM32); if (sdkCatalogItem != null) { sdks.Add(new Sdk(sdkName, sdkCatalogItem.Version)); } else { LogWarning($".NET SDK [{sdkName}] is present on [{Name}] but is not known to the RaspberryDebugger extension. Consider updating the extension."); } } PiStatus = new Status( architecture: architecture, path: path, hasUnzip: hasUnzip, hasDebugger: hasDebugger, installedSdks: sdks, model: model, revision: revision ); } }); // Create and configure an SSH key for this connection if one doesn't already exist. if (string.IsNullOrEmpty(connectionInfo.PrivateKeyPath) || !File.Exists(connectionInfo.PrivateKeyPath)) { await PackageHelper.ExecuteWithProgressAsync("Creating SSH keys...", async() => { // Create a 2048-bit private key with no passphrase on the Raspberry // and then download it to our keys folder. The key file name will // be the host name of the Raspberry. LogInfo("Creating SSH keys"); var workstationUser = Environment.GetEnvironmentVariable("USERNAME"); var workstationName = Environment.GetEnvironmentVariable("COMPUTERNAME"); var keyName = Guid.NewGuid().ToString("d"); var homeFolder = LinuxPath.Combine("/", "home", connectionInfo.User); var tempPrivateKeyPath = LinuxPath.Combine(homeFolder, keyName); var tempPublicKeyPath = LinuxPath.Combine(homeFolder, $"{keyName}.pub"); try { var createKeyScript = $@" # Create the key pair if ! ssh-keygen -t rsa -b 2048 -P '' -C '{workstationUser}@{workstationName}' -f {tempPrivateKeyPath} -m pem ; then exit 1 fi # Append the public key to the user's [authorized_keys] file to enable it. mkdir -p {homeFolder}/.ssh touch {homeFolder}/.ssh/authorized_keys cat {tempPublicKeyPath} >> {homeFolder}/.ssh/authorized_keys exit 0 "; ThrowOnError(RunCommand(CommandBundle.FromScript(createKeyScript))); // Download the public and private keys, persist them to the workstation // and then update the connection info. var connections = PackageHelper.ReadConnections(); var existingConnectionInfo = connections.SingleOrDefault(c => c.Name == connectionInfo.Name); var publicKeyPath = Path.Combine(PackageHelper.KeysFolder, $"{connectionInfo.Name}.pub"); var privateKeyPath = Path.Combine(PackageHelper.KeysFolder, connectionInfo.Name); File.WriteAllBytes(publicKeyPath, DownloadBytes(tempPublicKeyPath)); File.WriteAllBytes(privateKeyPath, DownloadBytes(tempPrivateKeyPath)); connectionInfo.PrivateKeyPath = privateKeyPath; connectionInfo.PublicKeyPath = publicKeyPath; if (existingConnectionInfo != null) { existingConnectionInfo.PrivateKeyPath = privateKeyPath; existingConnectionInfo.PublicKeyPath = publicKeyPath; PackageHelper.WriteConnections(connections, disableLogging: true); } } finally { // Delete the temporary key files on the Raspberry. var removeKeyScript = $@" rm -f {tempPrivateKeyPath} rm -f {tempPublicKeyPath} "; ThrowOnError(SudoCommand(CommandBundle.FromScript(removeKeyScript))); } await Task.CompletedTask; }); } }