/// <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); } }
/// <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; }); } }