示例#1
0
        /// <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>
        /// 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;
                });
            }
        }
 /// <summary>
 /// Persists the connections back to Visual Studio.
 /// </summary>
 private void SaveConnections()
 {
     PackageHelper.WriteConnections(connections, disableLogging: true);
 }