Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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. |
| 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.

Binary file added docs/ScreenShort-Options-Device.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ScreenShort-Options-Local.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/ScreenShort-Options.png
Binary file not shown.
Binary file removed docs/ScreenShort-Options2.png
Binary file not shown.
3 changes: 2 additions & 1 deletion src/Extension/RemoteDebuggerLauncher/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
Expand Down Expand Up @@ -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")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
// </copyright>
// ----------------------------------------------------------------------------

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
{
/// <summary>
/// Factory for creating loggers. Exposed as a MEF component.
/// </summary>
[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();

/// <summary>
/// Initializes a new instance of the <see cref="LoggerFactory"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider to get options.</param>
[ImportingConstructor]
public DebugLoggerFactory(SVsServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
rootPathOverride = null;
}

public DebugLoggerFactory(string rootPathOverride)
{
this.rootPathOverride = rootPathOverride;
}

/// <inheritdoc />
public ILogger CreateLogger(string categoryName)
{
EnsureInitialized();
return loggerFactory.CreateLogger(categoryName);
}

/// <inheritdoc />
public void AddProvider(ILoggerProvider provider)
{
EnsureInitialized();
loggerFactory.AddProvider(provider);
}

/// <inheritdoc />
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<string, LogLevel>()
{
{ "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<ILoggerProvider>(), 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;
}
}
}
58 changes: 58 additions & 0 deletions src/Extension/RemoteDebuggerLauncher/Infrastructure/NullLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
// </copyright>
// ----------------------------------------------------------------------------

using System;
using Microsoft.Extensions.Logging;

namespace RemoteDebuggerLauncher.Infrastructure
{
/// <summary>
/// A logger implementation that does nothing. Used when logging is disabled.
/// </summary>
internal class NullLogger : ILogger
{
/// <summary>
/// Gets the singleton instance of the null logger.
/// </summary>
public static NullLogger Instance { get; } = new NullLogger();

/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
{
return NullScope.Instance;
}

/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return false;
}

/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
// Do nothing
}

/// <summary>
/// A no-op disposable for scope handling.
/// </summary>
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new NullScope();

private NullScope()
{
}

public void Dispose()
{
// Do nothing
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
// </copyright>
// ----------------------------------------------------------------------------

using Microsoft.Extensions.Logging;

namespace RemoteDebuggerLauncher.Infrastructure
{
/// <summary>
/// A logger factory that creates null loggers. Used when logging is disabled.
/// </summary>
internal sealed class NullLoggerFactory : ILoggerFactory
{
/// <inheritdoc />
public ILogger CreateLogger(string categoryName) => NullLogger.Instance;

/// <inheritdoc />
public void AddProvider(ILoggerProvider provider)
{
// EMPTY_BODY
}

/// <inheritdoc />
public void Dispose()
{
// EMPTY_BODY
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// ----------------------------------------------------------------------------

using Microsoft.Extensions.Logging;
using RemoteDebuggerLauncher.Shared;

namespace RemoteDebuggerLauncher
Expand Down Expand Up @@ -85,5 +86,11 @@ internal interface IOptionsPageAccessor
/// </summary>
/// <returns>One of the <see see="PublishMode"/> values.</returns>
PublishMode QueryPublishMode();

/// <summary>
/// Queries the logging level.
/// </summary>
/// <returns>One of the <see cref="LogLevel"/> values.</returns>
LogLevel QueryLogLevel();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
Expand All @@ -7,6 +7,7 @@

using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Shell;
using RemoteDebuggerLauncher.Shared;

Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// ----------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using RemoteDebuggerLauncher.Shared;

namespace RemoteDebuggerLauncher
Expand Down Expand Up @@ -103,6 +104,12 @@ public PublishMode QueryPublishMode()
return GetLocalPage().PublishMode;
}

/// <inheritdoc />
public LogLevel QueryLogLevel()
{
return GetLocalPage().LogLevel;
}

private DeviceOptionsPage GetDevicePage()
{
if (devicePage == null)
Expand Down
3 changes: 3 additions & 0 deletions src/Extension/RemoteDebuggerLauncher/PackageConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public static class Options
/// <summary>The name for the Publish category attribute.</summary>
public const string PageCategoryPublish = "Publish";

/// <summary>The name for the Diagnostics category attribute.</summary>
public const string PageCategoryDiagnostics = "Diagnostics";

/// <summary>The default value for the SSH private key file.</summary>
public const string DefaultValuePrivateKey = @"%userprofile%\.ssh\id_rsa";

Expand Down
Loading
Loading