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