diff --git a/README.md b/README.md index 2f2a6890..14fef87f 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ To use the plugin, follow the steps below. #### 1. Configure Options (optional) Open Visual Studio 2022, navigate to Tools -> Options -> Remote Debugger Launcher -> Device -![VS Options](docs/ScreenShort-Options.png) +![VS Options](docs/ScreenShort-Options-Device.png) Configure the default values for Credentials, Folders and SSH setting. These values will be applied if the Launch Profile does not configure them. #### 2. Create and configure the Launch Profile diff --git a/docs/Options.md b/docs/Options.md index 4925193f..ecb2f00a 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -3,7 +3,7 @@ The plugin provides a set of global options in the Visual Studio under Tools -> This page describes the details of the options. ## Device options -![Device Options](ScreenShort-Options.png) +![Device Options](ScreenShort-Options-Device.png) The Device page holds all the configuration options related to device connection settings: @@ -29,12 +29,21 @@ The values configured in this section will only be applied the launch profile ha | Host port | The SSH port where the device listens for SSH connections. | ## Local options -![Local Optioons](ScreenShort-Options2.png) +![Local Optioons](ScreenShort-Options-Local.png) The values configured in this section will only be applied the launch profile has no value (launchprofile.json) does not configure it. ### Credentials | Setting | Description | |:------- |:-------------------- | -| Publish mode | The mode (self contained vs framework dependat) how a .NET app is beeing published | -| Publish on deploy | The flag wheter dotnet publish should be run on deploy. | \ No newline at end of file +| Publish mode | The mode (self contained vs framework dependent) how a .NET app is being published | +| Publish on deploy | The flag whether dotnet publish should be run on deploy. | + +### Diagnostics +| Setting | Description | +|:------- |:-------------------- | +| Log Level | The amount of debug logging the extension should produce. 'None' disables logging. | + +The log files are stored in %localappdata%\RemoteDebuggerLauncher\Logfiles and are kept for 31 days. +If you change the log level after using the extension (deploy, debug, ...), please restart Visual Studio to apply the new log level. + diff --git a/docs/ScreenShort-Options-Device.png b/docs/ScreenShort-Options-Device.png new file mode 100644 index 00000000..48a3626c Binary files /dev/null and b/docs/ScreenShort-Options-Device.png differ diff --git a/docs/ScreenShort-Options-Local.png b/docs/ScreenShort-Options-Local.png new file mode 100644 index 00000000..05184127 Binary files /dev/null and b/docs/ScreenShort-Options-Local.png differ diff --git a/docs/ScreenShort-Options.png b/docs/ScreenShort-Options.png deleted file mode 100644 index 4d94b2f5..00000000 Binary files a/docs/ScreenShort-Options.png and /dev/null differ diff --git a/docs/ScreenShort-Options2.png b/docs/ScreenShort-Options2.png deleted file mode 100644 index af0041f3..00000000 Binary files a/docs/ScreenShort-Options2.png and /dev/null differ diff --git a/src/Extension/RemoteDebuggerLauncher/GlobalSuppressions.cs b/src/Extension/RemoteDebuggerLauncher/GlobalSuppressions.cs index b4fe5168..d2ff286d 100644 --- a/src/Extension/RemoteDebuggerLauncher/GlobalSuppressions.cs +++ b/src/Extension/RemoteDebuggerLauncher/GlobalSuppressions.cs @@ -1,4 +1,4 @@ -// ---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- // // Copyright (c) Michael Koster. All rights reserved. // Licensed under the MIT License. @@ -39,3 +39,4 @@ [assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "False positive", Scope = "member", Target = "~M:RemoteDebuggerLauncher.RemoteOperations.SecureShellRemoteBulkCopyRsyncSessionService.StartRsyncFileSessionAsync(System.String,System.String,RemoteDebuggerLauncher.IOutputPaneWriterService)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "By design to ignore all failures", Scope = "member", Target = "~M:RemoteDebuggerLauncher.RemoteOperations.SecureShellRemoteBulkCopyRsyncSessionService.StartRsyncDirectorySessionAsync(System.String,System.String,RemoteDebuggerLauncher.IOutputPaneWriterService)~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "By design to ignore all failures", Scope = "member", Target = "~M:RemoteDebuggerLauncher.RemoteOperations.SecureShellRemoteBulkCopyRsyncSessionService.StartRsyncFileSessionAsync(System.String,System.String,RemoteDebuggerLauncher.IOutputPaneWriterService)~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive", Scope = "member", Target = "~M:RemoteDebuggerLauncher.Logging.DebugLoggerFactory.EnsureInitialized")] diff --git a/src/Extension/RemoteDebuggerLauncher/Infrastructure/DebugLoggerFactory.cs b/src/Extension/RemoteDebuggerLauncher/Infrastructure/DebugLoggerFactory.cs new file mode 100644 index 00000000..5b0adeb2 --- /dev/null +++ b/src/Extension/RemoteDebuggerLauncher/Infrastructure/DebugLoggerFactory.cs @@ -0,0 +1,155 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (c) Michael Koster. All rights reserved. +// Licensed under the MIT License. +// +// ---------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Composition; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Debug; +using Microsoft.VisualStudio.Shell; + +namespace RemoteDebuggerLauncher.Infrastructure +{ + /// + /// Factory for creating loggers. Exposed as a MEF component. + /// + [Export(typeof(ILoggerFactory))] + internal sealed class DebugLoggerFactory : ILoggerFactory + { + private readonly SVsServiceProvider serviceProvider; + private readonly string rootPathOverride; + private ILoggerFactory loggerFactory; + private bool initialized = false; + private readonly object lockObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// The service provider to get options. + [ImportingConstructor] + public DebugLoggerFactory(SVsServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + rootPathOverride = null; + } + + public DebugLoggerFactory(string rootPathOverride) + { + this.rootPathOverride = rootPathOverride; + } + + /// + public ILogger CreateLogger(string categoryName) + { + EnsureInitialized(); + return loggerFactory.CreateLogger(categoryName); + } + + /// + public void AddProvider(ILoggerProvider provider) + { + EnsureInitialized(); + loggerFactory.AddProvider(provider); + } + + /// + public void Dispose() + { + loggerFactory?.Dispose(); + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "To prevent unexpected failures")] + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Handled by the logging framework")] + private void EnsureInitialized() + { + lock (lockObject) + { + if (initialized) + { + return; + } + + try + { + // Get the options page accessor service + LogLevel minLogLevel = LogLevel.None; + + if (serviceProvider.GetService(typeof(SOptionsPageAccessor)) is IOptionsPageAccessor optionsPageAccessor) + { + minLogLevel = optionsPageAccessor.QueryLogLevel(); + } + + if (minLogLevel == LogLevel.None) + { + // Create a null logger factory when logging is disabled + loggerFactory = new NullLoggerFactory(); + } + else + { + // Step 1: Prepare the logging configuration + // Create log file path + var logFilePath = CreateLogFilePath(); + + // Create the filter options + var filterOptions = new LoggerFilterOptions + { + MinLevel = minLogLevel + }; + + // Create the level overrides dictionary + var levelOverrides = new Dictionary() + { + { "Microsoft", LogLevel.Warning } + }; + + // Limit the logfile size to 1MB and keep up to 31 files + const long FileSizeLimitBytes = 1048576L; + const int FileCountLimit = 31; + + // Step 2: Create the logger factory, add providers and configure them + loggerFactory = new LoggerFactory(Enumerable.Empty(), filterOptions); + + // Add Debug logger provider for Output/Debug window, does not support filtering + loggerFactory.AddProvider(new DebugLoggerProvider()); + + // Add the Serilog file logger provider + _ = loggerFactory.AddFile( + logFilePath, + minimumLevel: minLogLevel, + levelOverrides: levelOverrides, + fileSizeLimitBytes: FileSizeLimitBytes, + retainedFileCountLimit: FileCountLimit, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {SourceContext}::{Message}{NewLine}{Exception}"); + } + } + catch + { + // If we can't configure logging, use null logger factory + loggerFactory = new NullLoggerFactory(); + } + + initialized = true; + } + } + + private string CreateLogFilePath() + { + // Prepare log file path + var localAppData = rootPathOverride ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var logDirectory = Path.Combine(localAppData, PackageConstants.FileSystem.StorageFolder, "Logfiles"); + var logFilePath = Path.Combine(logDirectory, "Debug-{Date}.log"); + + // Ensure directory exists + _ = Directory.CreateDirectory(logDirectory); + + return logFilePath; + } + } +} diff --git a/src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLogger.cs b/src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLogger.cs new file mode 100644 index 00000000..b8331213 --- /dev/null +++ b/src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLogger.cs @@ -0,0 +1,58 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (c) Michael Koster. All rights reserved. +// Licensed under the MIT License. +// +// ---------------------------------------------------------------------------- + +using System; +using Microsoft.Extensions.Logging; + +namespace RemoteDebuggerLauncher.Infrastructure +{ + /// + /// A logger implementation that does nothing. Used when logging is disabled. + /// + internal class NullLogger : ILogger + { + /// + /// Gets the singleton instance of the null logger. + /// + public static NullLogger Instance { get; } = new NullLogger(); + + /// + public IDisposable BeginScope(TState state) + { + return NullScope.Instance; + } + + /// + public bool IsEnabled(LogLevel logLevel) + { + return false; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + // Do nothing + } + + /// + /// A no-op disposable for scope handling. + /// + private sealed class NullScope : IDisposable + { + public static NullScope Instance { get; } = new NullScope(); + + private NullScope() + { + } + + public void Dispose() + { + // Do nothing + } + } + } +} diff --git a/src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLoggerFactory.cs b/src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLoggerFactory.cs new file mode 100644 index 00000000..82e61ea6 --- /dev/null +++ b/src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLoggerFactory.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (c) Michael Koster. All rights reserved. +// Licensed under the MIT License. +// +// ---------------------------------------------------------------------------- + +using Microsoft.Extensions.Logging; + +namespace RemoteDebuggerLauncher.Infrastructure +{ + /// + /// A logger factory that creates null loggers. Used when logging is disabled. + /// + internal sealed class NullLoggerFactory : ILoggerFactory + { + /// + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + + /// + public void AddProvider(ILoggerProvider provider) + { + // EMPTY_BODY + } + + /// + public void Dispose() + { + // EMPTY_BODY + } + } +} diff --git a/src/Extension/RemoteDebuggerLauncher/Options/IOptionsPageAccessor.cs b/src/Extension/RemoteDebuggerLauncher/Options/IOptionsPageAccessor.cs index 86d682c9..229103b8 100644 --- a/src/Extension/RemoteDebuggerLauncher/Options/IOptionsPageAccessor.cs +++ b/src/Extension/RemoteDebuggerLauncher/Options/IOptionsPageAccessor.cs @@ -5,6 +5,7 @@ // // ---------------------------------------------------------------------------- +using Microsoft.Extensions.Logging; using RemoteDebuggerLauncher.Shared; namespace RemoteDebuggerLauncher @@ -85,5 +86,11 @@ internal interface IOptionsPageAccessor /// /// One of the values. PublishMode QueryPublishMode(); + + /// + /// Queries the logging level. + /// + /// One of the values. + LogLevel QueryLogLevel(); } } \ No newline at end of file diff --git a/src/Extension/RemoteDebuggerLauncher/Options/LocalOptionsPage.cs b/src/Extension/RemoteDebuggerLauncher/Options/LocalOptionsPage.cs index 28203e07..1bae89a4 100644 --- a/src/Extension/RemoteDebuggerLauncher/Options/LocalOptionsPage.cs +++ b/src/Extension/RemoteDebuggerLauncher/Options/LocalOptionsPage.cs @@ -1,4 +1,4 @@ -// ---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- // // Copyright (c) Michael Koster. All rights reserved. // Licensed under the MIT License. @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Shell; using RemoteDebuggerLauncher.Shared; @@ -28,5 +29,11 @@ internal class LocalOptionsPage : DialogPage [DisplayName("Publish mode")] [Description("The type of application the publish step should produce, either self contained (includes the runtime) or framework dependent (requires .NET to be installed on the device.")] public PublishMode PublishMode { get; set; } = PublishMode.FrameworkDependent; + + [Category(PackageConstants.Options.PageCategoryDiagnostics)] + [DisplayName("Log level")] + [Description("The minimum logging level for diagnostics. Set to 'None' to disable logging. (requires restart)")] + [DefaultValue(LogLevel.None)] + public LogLevel LogLevel { get; set; } = LogLevel.None; } } diff --git a/src/Extension/RemoteDebuggerLauncher/Options/OptionsPageAccessorService.cs b/src/Extension/RemoteDebuggerLauncher/Options/OptionsPageAccessorService.cs index 6749754c..f49c86a2 100644 --- a/src/Extension/RemoteDebuggerLauncher/Options/OptionsPageAccessorService.cs +++ b/src/Extension/RemoteDebuggerLauncher/Options/OptionsPageAccessorService.cs @@ -6,6 +6,7 @@ // ---------------------------------------------------------------------------- using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; using RemoteDebuggerLauncher.Shared; namespace RemoteDebuggerLauncher @@ -103,6 +104,12 @@ public PublishMode QueryPublishMode() return GetLocalPage().PublishMode; } + /// + public LogLevel QueryLogLevel() + { + return GetLocalPage().LogLevel; + } + private DeviceOptionsPage GetDevicePage() { if (devicePage == null) diff --git a/src/Extension/RemoteDebuggerLauncher/PackageConstants.cs b/src/Extension/RemoteDebuggerLauncher/PackageConstants.cs index db3de686..09599d81 100644 --- a/src/Extension/RemoteDebuggerLauncher/PackageConstants.cs +++ b/src/Extension/RemoteDebuggerLauncher/PackageConstants.cs @@ -76,6 +76,9 @@ public static class Options /// The name for the Publish category attribute. public const string PageCategoryPublish = "Publish"; + /// The name for the Diagnostics category attribute. + public const string PageCategoryDiagnostics = "Diagnostics"; + /// The default value for the SSH private key file. public const string DefaultValuePrivateKey = @"%userprofile%\.ssh\id_rsa"; diff --git a/src/Extension/RemoteDebuggerLauncher/RemoteDebuggerLauncher.csproj b/src/Extension/RemoteDebuggerLauncher/RemoteDebuggerLauncher.csproj index 2adfb83b..e5cf5c14 100644 --- a/src/Extension/RemoteDebuggerLauncher/RemoteDebuggerLauncher.csproj +++ b/src/Extension/RemoteDebuggerLauncher/RemoteDebuggerLauncher.csproj @@ -134,6 +134,9 @@ + + + @@ -340,6 +343,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + 8.0.1 + + + 8.0.1 + @@ -358,6 +367,9 @@ + + 3.0.0 + diff --git a/src/Extension/RemoteDebuggerLauncher/RemoteOperations/SecureShellKeySetupService.cs b/src/Extension/RemoteDebuggerLauncher/RemoteOperations/SecureShellKeySetupService.cs index 165e6e42..aa6fd0db 100644 --- a/src/Extension/RemoteDebuggerLauncher/RemoteOperations/SecureShellKeySetupService.cs +++ b/src/Extension/RemoteDebuggerLauncher/RemoteOperations/SecureShellKeySetupService.cs @@ -11,6 +11,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using RemoteDebuggerLauncher.Infrastructure; @@ -26,13 +27,14 @@ namespace RemoteDebuggerLauncher.RemoteOperations [Export(typeof(ISecureShellKeySetupService))] internal class SecureShellKeySetupService : ISecureShellKeySetupService { - private readonly IVsFacadeFactory factory; + private readonly ILogger logger; [ImportingConstructor] - public SecureShellKeySetupService(IVsFacadeFactory factory) + public SecureShellKeySetupService(IVsFacadeFactory factory, ILoggerFactory loggerFactory) { this.factory = factory; + logger = loggerFactory.CreateLogger(nameof(SecureShellKeySetupService)); } /// @@ -44,6 +46,7 @@ public SecureShellKeySetupService(IVsFacadeFactory factory) /// public async Task RegisterServerFingerprintAsync(SecureShellKeySetupSettings settings) { + logger.LogInformation("RegisterServerFingerprintAsync: Starting server fingerprint registration Device={UserName}@{HostName}:{HostPort}", settings.UserName, settings.HostName, settings.HostPort); Statusbar.SetText(Resources.RemoteCommandSetupSshCommandStatusbarScanProgress); OutputPaneWriter.WriteLine(Resources.CommonStartSessionMarker); @@ -51,22 +54,30 @@ public async Task RegisterServerFingerprintAsync(SecureShellKeySetupSettings set var (success, keyscanStdError) = await RegisterServerFingerprintWithKeyScanAsync(settings); if (!success) { + logger.LogWarning("Failed to register server fingerprint using ssh-keyscan, trying interactive connection"); // Try 2: Establish an interactive SSH connection to the server to get the fingerprint success = await RegisterServerFingerprintWithConnectionAsync(settings); } if (!success) { + logger.LogError("RegisterServerFingerprintAsync: FAILED to register server fingerprint. keyscanError={StdError}", keyscanStdError); + OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintFailed1, settings.UserName, settings.HostName, settings.HostPort); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintFailed2); OutputPaneWriter.WriteLine(keyscanStdError); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintFailed3); } + else + { + logger.LogInformation("RegisterServerFingerprintAsync: Successfully registered server fingerprint"); + } } /// public async Task AuthorizeKeyAsync(SecureShellKeySetupSettings settings) { + logger.LogInformation("AuthorizeKeyAsync: Starting SSH key authorization for {UserName}@{HostName}:{HostPort}", settings.UserName, settings.HostName, settings.HostPort); Statusbar.SetText(Resources.RemoteCommandSetupSshCommandStatusbarAuthorizeProgress); OutputPaneWriter.WriteLine(Resources.CommonStartSessionMarker); @@ -77,10 +88,12 @@ public async Task AuthorizeKeyAsync(SecureShellKeySetupSettings settings) Resources.RemoteCommandSetupSshPhase1TryAuthenticatePrivateKeyFailed); if (success) { + logger.LogInformation("AuthorizeKeyAsync: SSH key already authorized, no action needed"); return; } // Step 2: try authenticate with the supplied username/password + logger.LogDebug("AuthorizeKeyAsync: Private key authentication failed, trying password authentication"); using (var sshClient = await TryEstablishConnectionWithPasswordAsync(settings)) { // Step 3: register the public key with the target @@ -104,6 +117,7 @@ public async Task AuthorizeKeyAsync(SecureShellKeySetupSettings settings) private async Task<(bool, string)> RegisterServerFingerprintWithKeyScanAsync(SecureShellKeySetupSettings settings) { + logger.LogTrace("RegisterServerFingerprintWithKeyScanAsync: Begin."); var defaultKeysFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), PackageConstants.SecureShell.DefaultKeyPairFolder); var knownHostsFilePath = Path.Combine(defaultKeysFolder, PackageConstants.SecureShell.DefaultKnownHostsFileName); @@ -119,6 +133,7 @@ public async Task AuthorizeKeyAsync(SecureShellKeySetupSettings settings) OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintScan1, settings.UserName, settings.HostName, settings.HostPort); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintScan2, arguments); + logger.LogDebug("RegisterServerFingerprintWithKeyScanAsync: Executing '{Exe} {Arguments}", PackageConstants.SecureShell.KeyScanExecutable, arguments); using (var process = Process.Start(startInfo)) { var stdOutput = await process.StandardOutput.ReadToEndAsync(); @@ -127,14 +142,18 @@ public async Task AuthorizeKeyAsync(SecureShellKeySetupSettings settings) await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + logger.LogDebug("RegisterServerFingerprintWithKeyScanAsync: keyscan completed. ExitCode={ExitCode}, StdOutput={StdOutput}, StdError={StdError}", exitCode, stdOutput, stdError); + if (exitCode == 0) { if (FileHelper.ContainsText(knownHostsFilePath, settings.HostName)) { + logger.LogInformation("RegisterServerFingerprintWithKeyScanAsync: Host already present in known_hosts file, skipping addition"); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintSkip, settings.UserName, settings.HostName, settings.HostPort); } else { + logger.LogInformation("RegisterServerFingerprintWithKeyScanAsync: Adding host to known_hosts file"); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshScanProgressFingerprintAdd, settings.UserName, settings.HostName, settings.HostPort); File.AppendAllText(knownHostsFilePath, stdOutput); } @@ -237,6 +256,8 @@ private async Task RegisterServerFingerprintWithConnectionAsync(SecureShel private Task TryEstablishConnectionWithKeyAsync(SecureShellKeySetupSettings settings, string progressText, string successText, string failureText) { + logger.LogDebug("TryEstablishConnectionWithKeyAsync: Begin."); + OutputPaneWriter.WriteLine(progressText, settings.UserName, settings.HostName, settings.HostPort); try @@ -254,11 +275,14 @@ private Task TryEstablishConnectionWithKeyAsync(SecureShellKeySetupSetting catch (SshAuthenticationException expectedException) { // This is expected + logger.LogDebug("TryEstablishConnectionWithKeyAsync: expected SshAuthenticationException. Message={Message}, return=false", expectedException.Message); + OutputPaneWriter.WriteLine(failureText, expectedException.Message); return Task.FromResult(false); } catch (Exception ex) { + logger.LogError(ex, "TryEstablishConnectionWithKeyAsync: unexpected exception"); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshConnectionFailed, ex.Message); throw new SecureShellSessionException(ex.Message, ex); } @@ -266,6 +290,8 @@ private Task TryEstablishConnectionWithKeyAsync(SecureShellKeySetupSetting private Task TryEstablishConnectionWithPasswordAsync(SecureShellKeySetupSettings settings) { + logger.LogDebug("TryEstablishConnectionWithPasswordAsync: Begin."); + OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase2TryAuthenticatePasswordProgress, settings.UserName, settings.HostName, settings.HostPort); SshClient sshClient = null; @@ -276,15 +302,20 @@ private Task TryEstablishConnectionWithPasswordAsync(SecureShellKeySe sshClient.Connect(); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase2TryAuthenticatePasswordSuccess); + + logger.LogDebug("TryEstablishConnectionWithPasswordAsync: Connected successfully."); } catch (SshAuthenticationException expectedException) { + logger.LogWarning("TryEstablishConnectionWithPasswordAsync: SshAuthenticationException. Message={Message}, return=null", expectedException.Message); + sshClient?.Dispose(); sshClient = null; OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase2TryAuthenticatePasswordFailed, expectedException.Message); } catch (Exception ex) { + logger.LogError(ex, "TryEstablishConnectionWithPasswordAsync: unexpected exception."); sshClient?.Dispose(); throw new SecureShellSessionException(ex.Message, ex); } @@ -296,30 +327,38 @@ private Task RegisterPublicKeyAsync(SecureShellKeySetupSettings settings, { try { + logger.LogDebug("RegisterPublicKeyAsync: Begin."); + OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase3AddKeyProgress, settings.UserName, settings.HostNameIPv4, settings.HostPort); string publicKeyData = File.ReadAllText(settings.PublicKeyFile).Trim(); + logger.LogDebug("RegisterPublicKeyAsync: executing 'mkdir -p ~/.ssh && echo ... >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'"); using (var command = client.RunCommand($"mkdir -p ~/.ssh && echo {publicKeyData} >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys")) { if (command.ExitStatus != 0) { + logger.LogError("RegisterPublicKeyAsync: FAILED to add public key. ExitStatus={ExitStatus}, Error={Error}, return=false", command.ExitStatus, command.Error); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase3AddKeyFailed, command.Error); return Task.FromResult(false); } } + logger.LogDebug("RegisterPublicKeyAsync: End. returns=true"); + OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase3AddKeySuccess); return Task.FromResult(true); } catch (SshException ex) { + logger.LogWarning("RegisterPublicKeyAsync: SshException. Message={Message}, return=false", ex.Message); OutputPaneWriter.WriteLine(Resources.RemoteCommandSetupSshPhase3AddKeyFailed, ex.Message); return Task.FromResult(false); } catch (Exception ex) { + logger.LogError(ex, "RegisterPublicKeyAsync: unexpected exception"); throw new SecureShellSessionException(ex.Message, ex); } }