/// <summary>
        /// Verifies the connection settings by establishing a connection to the Raspberry Pi.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">The arguments.</param>
#pragma warning disable VSTHRD100
        private async void verifyButton_Click(object sender, EventArgs args)
#pragma warning restore VSTHRD100
        {
            if (SelectedConnection == null)
            {
                return;
            }

            Log.Info($"[{SelectedConnection.Name}]: Verify Connection");

            var currentConnection = SelectedConnection;
            var exception         = (Exception)null;

            await PackageHelper.ExecuteWithProgressAsync("Verify connection", async() =>
            {
                try
                {
                    using (var connection = await Connection.ConnectAsync(currentConnection))
                    {
                        exception = null;
                    }
                }
                catch (Exception e)
                {
                    exception = e;

                    Log.Exception(e);
                }
            });

            if (exception == null)
            {
                // Reload the connections to pick any changes (like adding the SSH key).

                ReloadConnections();

                MessageBox.Show(this,
                                $"[{SelectedConnection.Name}] Connection is OK!",
                                $"Success",
                                MessageBoxButtons.OK,
                                MessageBoxIcon.Information);
            }
            else
            {
                MessageBox.Show(this,
                                $"Connection Failed:\r\n\r\n{exception.GetType().FullName}\r\n{exception.Message}\r\n\r\nView the Debug Output for more details.",
                                $"Connection Failed",
                                MessageBoxButtons.OK,
                                MessageBoxIcon.Error);
            }
        }
Exemple #2
0
        /// <summary>
        /// Ensures that the native Windows OpenSSH client is installed, prompting
        /// the user to install it if necessary.
        /// </summary>
        /// <returns><c>true</c> if OpenSSH is installed.</returns>
        public static async Task <bool> EnsureOpenSshAsync()
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            Log.Info("Checking for native Windows OpenSSH client");

            var openSshPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Sysnative", "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.\r\n\r\nWould 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);
            }
        }
        /// <summary>
        /// Installs the <b>vsdbg</b> debugger on the Raspberry if it's not already installed.
        /// </summary>
        /// <returns><c>true</c> on success.</returns>
        public async Task <bool> InstallDebuggerAsync()
        {
            if (PiStatus.HasDebugger)
            {
                return(await Task.FromResult(true));
            }

            LogInfo($"Installing VSDBG to: [{PackageHelper.RemoteDebuggerFolder}]");

            return(await PackageHelper.ExecuteWithProgressAsync <bool>($"Installing [vsdbg] debugger...",
                                                                       async() =>
            {
                var installScript =
                    $@"
if ! curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l {PackageHelper.RemoteDebuggerFolder} ; then
    exit 1
fi

exit 0
";
                try
                {
                    var response = SudoCommand(CommandBundle.FromScript(installScript));

                    if (response.ExitCode == 0)
                    {
                        // Indicate that debugger is now installed.

                        PiStatus.HasDebugger = true;
                        return await Task.FromResult(true);
                    }
                    else
                    {
                        LogError(response.AllText);
                        return await Task.FromResult(false);
                    }
                }
                catch (Exception e)
                {
                    LogException(e);
                    return await Task.FromResult(false);
                }
            }));
        }
        /// <summary>
        /// Installs the specified .NET Core SDK on the Raspberry if it's not already installed.
        /// </summary>
        /// <param name="sdkVersion">The SDK version.</param>
        /// <returns><c>true</c> on success.</returns>
        public async Task <bool> InstallSdkAsync(string sdkVersion)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(sdkVersion), nameof(sdkVersion));

            // $todo(jefflill):
            //
            // Note that we're going to install that standalone SDK for the SDK
            // version rather than the SDK that shipped with Visual Studio.  I'm
            // assuming that the Visual Studio SDKs might have extra stuff we don't
            // need and it's also possible that the Visual Studio SDK for the SDK
            // version may not have shipped yet.
            //
            // We may want to re-evaluate this in the future.

            if (PiStatus.InstalledSdks.Any(sdk => sdk.Version == sdkVersion))
            {
                return(await Task.FromResult(true));    // Already installed
            }

            LogInfo($".NET Core SDK [v{sdkVersion}] is not installed.");

            // Locate the standalone SDK for the request .NET version.

            var targetSdk = PackageHelper.SdkCatalog.Items.SingleOrDefault(item => item.IsStandalone && item.Version == sdkVersion && item.Architecture == SdkArchitecture.ARM32);

            if (targetSdk == null)
            {
                // Fall back to the Visual Studio SDK, if there is one.

                targetSdk = PackageHelper.SdkCatalog.Items.SingleOrDefault(item => item.Version == sdkVersion);
                LogInfo($"Cannot find standalone SDK for [{sdkVersion}] for falling back to [{targetSdk.Name}], version [v{targetSdk.Version}].");
            }

            if (targetSdk == null)
            {
                LogError($"RasberryDebug is unaware of .NET Core SDK [v{sdkVersion}].");
                LogError($"Try updating the RasberryDebug extension or report this issue at:");
                LogError($"https://github.com/nforgeio/RaspberryDebugger/issues");

                return(await Task.FromResult(false));
            }

            // Install the SDK.

            LogInfo($"Installing SDK v{targetSdk.Version}");

            return(await PackageHelper.ExecuteWithProgressAsync <bool>($"Download and install SDK v{targetSdk.Version} on Raspberry...",
                                                                       async() =>
            {
                var installScript =
                    $@"
export DOTNET_ROOT={PackageHelper.RemoteDotnetFolder}

# Ensure that the packages required by .NET Core are installed:
#
#       https://docs.microsoft.com/en-us/dotnet/core/install/linux-debian#dependencies

if ! apt-get update ; then
    exit 1
fi

if ! apt-get install -yq libc6 libgcc1 libgssapi-krb5-2 libicu-dev libssl1.1 libstdc++6 zlib1g libgdiplus ; then
    exit 1
fi

# Remove any existing SDK download.  This might be present if a
# previous installation attempt failed.

if ! rm -f /tmp/dotnet-sdk.tar.gz ; then
    exit 1
fi

# Download the SDK installation file to a temporary file.

if ! wget --quiet -O /tmp/dotnet-sdk.tar.gz {targetSdk.Link} ; then
    exit 1
fi

# Verify the SHA512.

orgDir=$cwd
cd /tmp

if ! echo '{targetSdk.SHA512}  dotnet-sdk.tar.gz' | sha512sum --check - ; then
    cd $orgDir
    exit 1
fi

cd $orgDir

# Make sure the installation directory exists.

if ! mkdir -p $DOTNET_ROOT ; then
    exit 1
fi

# Unpack the SDK to the installation directory.

if ! tar -zxf /tmp/dotnet-sdk.tar.gz -C $DOTNET_ROOT --no-same-owner ; then
    exit 1
fi

# Remove the temporary installation file.

if ! rm /tmp/dotnet-sdk.tar.gz ; then
    exit 1
fi

exit 0
";
                try
                {
                    var response = SudoCommand(CommandBundle.FromScript(installScript));

                    if (response.ExitCode == 0)
                    {
                        // Add the newly installed SDK to the list of installed SDKs.

                        PiStatus.InstalledSdks.Add(new Sdk(targetSdk.Name, targetSdk.Version));
                        return await Task.FromResult(true);
                    }
                    else
                    {
                        LogError(response.AllText);
                        return await Task.FromResult(false);
                    }
                }
                catch (Exception e)
                {
                    LogException(e);
                    return await Task.FromResult(false);
                }
            }));
        }
        /// <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;
                });
            }
        }