Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safely handle misconfigured extensions #1161

Merged
merged 2 commits into from
May 26, 2024
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
10 changes: 7 additions & 3 deletions src/app/dev/DevToys.Business/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,22 +200,26 @@ internal async Task RunSmartDetectionAsync(bool isInCompactOverlayMode, bool isD

try
{
using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
using var cancellationTokenSource
= new CancellationTokenSource(
Debugger.IsAttached ? TimeSpan.FromHours(1) : TimeSpan.FromSeconds(2));

CancellationToken cancellationToken = cancellationTokenSource.Token;
INotifyPropertyChanged? selectedMenuBeforeSmartDetection = SelectedMenuItem;
Guard.IsNotNull(selectedMenuBeforeSmartDetection);

// Retrieve clipboard content.
object? rawClipboardData = await _clipboard.GetClipboardDataAsync();

// If the clipboard content has changed since the last time
if (!cancellationTokenSource.Token.IsCancellationRequested && !AreOldAndNewClipboardDataEqual(_oldRawClipboardData, rawClipboardData))
if (!cancellationToken.IsCancellationRequested && !AreOldAndNewClipboardDataEqual(_oldRawClipboardData, rawClipboardData))
{
// Reset recommended tools.
_guiToolProvider.ForEachToolViewItem(toolViewItem => toolViewItem.IsRecommended = false);

// Detect tools to recommend.
IReadOnlyList<SmartDetectedTool> detectedTools
= await _smartDetectionService.DetectAsync(rawClipboardData, strict: true, cancellationTokenSource.Token)
= await _smartDetectionService.DetectAsync(rawClipboardData, strict: true, cancellationToken)
.ConfigureAwait(true);

GuiToolViewItem? firstToolViewItem = null;
Expand Down
27 changes: 23 additions & 4 deletions src/app/dev/DevToys.Core/Tools/GuiToolInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal GuiToolInstance(Lazy<IGuiTool, GuiToolMetadata> guiToolDefinition, Asse

public string LongDisplayTitle => _longDisplayTitle.Value;

public string LongOrShortDisplayTitle => string.IsNullOrWhiteSpace(_longDisplayTitle.Value) ? ShortDisplayTitle : _longDisplayTitle.Value;
public string LongOrShortDisplayTitle => string.IsNullOrWhiteSpace(LongDisplayTitle) ? ShortDisplayTitle : LongDisplayTitle;

public string Description => _descriptionDisplayTitle.Value;

Expand Down Expand Up @@ -164,9 +164,25 @@ public void RebuildView()

private string GetDisplayString(string resourceName)
{
return _resourceManager.Value is not null && !string.IsNullOrWhiteSpace(resourceName)
? _resourceManager.Value.GetString(resourceName) ?? string.Empty
: string.Empty;
if (_resourceManager.Value is null)
{
LogGetMetadataStringFailed(resourceName, InternalComponentName);
return $"[Unable to get the text for '{resourceName}', " +
$"likely because we couldn't find a proper '{nameof(IResourceAssemblyIdentifier)}' " +
$"for the tool '{_guiToolDefinition.Metadata.InternalComponentName}'.]";
}

try
{
return _resourceManager.Value is not null && !string.IsNullOrWhiteSpace(resourceName)
? _resourceManager.Value.GetString(resourceName) ?? $"[Unable to find '{resourceName}' in '{_guiToolDefinition.Metadata.ResourceManagerBaseName}']"
: string.Empty;
}
catch
{
LogGetMetadataStringFailed(resourceName, InternalComponentName);
return $"[Unable to find '{resourceName}' in '{_guiToolDefinition.Metadata.ResourceManagerBaseName}']";
}
}

[LoggerMessage(0, LogLevel.Information, "Initialized '{toolName}' tool instance manager.")]
Expand All @@ -183,4 +199,7 @@ private string GetDisplayString(string resourceName)

[LoggerMessage(4, LogLevel.Error, "Unexpectedly failed to dispose '{toolName}'.")]
partial void LogDisposingToolFailed(Exception ex, string toolName);

[LoggerMessage(5, LogLevel.Error, "Unable to get the string for '{metadataName}' for the tool '{toolName}'.")]
partial void LogGetMetadataStringFailed(string metadataName, string toolName);
}
19 changes: 13 additions & 6 deletions src/app/dev/DevToys.Core/Tools/GuiToolProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ public void DisposeTools()
Assembly? resourceManagerAssembly = null;
if (!string.IsNullOrEmpty(guiToolDefinition.Metadata.ResourceManagerAssemblyIdentifier))
{
resourceManagerAssembly = GetResourceManagerAssembly(guiToolDefinition.Metadata.ResourceManagerAssemblyIdentifier);
resourceManagerAssembly = GetResourceManagerAssembly(guiToolDefinition.Metadata);
}

// Create the tool instance and store it in a menu placement list.
Expand Down Expand Up @@ -584,8 +584,8 @@ private IEnumerable<INotifyPropertyChanged> BuildAllToolsTreeViewItems()

if (toolGroup is null)
{
ThrowHelper.ThrowInvalidDataException($"Unable to find the group named '{tool.GroupName}' that the tool '{tool.InternalComponentName}' is pointing to.");
return null;
LogGroupNotFound(tool.GroupName, tool.InternalComponentName);
continue;
}

// Create a group view presentation, if needed.
Expand Down Expand Up @@ -619,17 +619,18 @@ IEnumerable<GroupViewItem> orderedGroups
return groups.Values.Select(g => g.Value);
}

private Assembly GetResourceManagerAssembly(string resourceManagerAssemblyIdentifier)
private Assembly? GetResourceManagerAssembly(GuiToolMetadata guiToolMetadata)
{
foreach (Lazy<IResourceAssemblyIdentifier, ResourceAssemblyIdentifierMetadata> item in _resourceAssemblyIdentifiers)
{
if (string.Equals(item.Metadata.InternalComponentName, resourceManagerAssemblyIdentifier, StringComparison.Ordinal))
if (string.Equals(item.Metadata.InternalComponentName, guiToolMetadata.ResourceManagerAssemblyIdentifier, StringComparison.Ordinal))
{
return item.Value.GetType().Assembly;
}
}

throw new InvalidDataException($"Unable to find the {nameof(ToolDisplayInformationAttribute.ResourceManagerAssemblyIdentifier)} '{resourceManagerAssemblyIdentifier}'.");
LogResourceManagerAssemblyIdentifierFound(guiToolMetadata.InternalComponentName, guiToolMetadata.ResourceManagerAssemblyIdentifier);
return null;
}

private GroupViewItem CreateFavoriteGroupViewItem(IReadOnlyList<GuiToolViewItem> favoriteTools)
Expand Down Expand Up @@ -777,4 +778,10 @@ private static TextSpan[] CleanUpMatchedSpans(List<TextSpan>? spans)

[LoggerMessage(3, LogLevel.Information, "Instantiated {toolCount} tools in {duration}ms")]
partial void LogToolInstancesCreated(int toolCount, double duration);

[LoggerMessage(4, LogLevel.Error, "Unable to find the group named '{groupName}' that the tool '{toolName}' is pointing to.")]
partial void LogGroupNotFound(string groupName, string toolName);

[LoggerMessage(5, LogLevel.Error, "Unable to find the ResourceManagerAssemblyIdentifier '{resourceManagerAssemblyIdentifier}' that the tool '{toolName}' is pointing to.")]
partial void LogResourceManagerAssemblyIdentifierFound(string toolName, string resourceManagerAssemblyIdentifier);
}
31 changes: 24 additions & 7 deletions src/app/dev/DevToys.Core/Tools/SmartDetectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public sealed partial class SmartDetectionService
_detectorHierarchy = BuildDetectorNodeHierarchy(dataTypeDetectors);

// Create a map of data types to tools.
_dataTypeToToolInstanceMap = BuildDataTypeToToolInstanceMap(guiToolProvider);
_dataTypeToToolInstanceMap = BuildDataTypeToToolInstanceMap(guiToolProvider, dataTypeDetectors);
}

/// <summary>
Expand Down Expand Up @@ -231,8 +231,11 @@ var lookup
/// Builds a dictionary that maps data types to a list of GUI tool instances that accept them.
/// </summary>
/// <param name="guiToolProvider">The GUI tool provider.</param>
/// <param name="detectors">The collection of detectors.</param>
/// <returns>A dictionary that maps data types to a list of GUI tool instances that accept them.</returns>
private static Dictionary<string, List<GuiToolInstance>> BuildDataTypeToToolInstanceMap(GuiToolProvider guiToolProvider)
private Dictionary<string, List<GuiToolInstance>> BuildDataTypeToToolInstanceMap(
GuiToolProvider guiToolProvider,
IEnumerable<Lazy<IDataTypeDetector, DataTypeDetectorMetadata>> dataTypeDetectors)
{
// Create a new dictionary with case-insensitive string keys
var dataTypeToToolInstanceMap = new Dictionary<string, List<GuiToolInstance>>(StringComparer.OrdinalIgnoreCase);
Expand All @@ -246,13 +249,24 @@ var lookup
for (int j = 0; j < tool.AcceptedDataTypeNames.Count; j++)
{
string dataType = tool.AcceptedDataTypeNames[j];
if (!dataTypeToToolInstanceMap.TryGetValue(dataType, out List<GuiToolInstance>? toolList))

if (!string.IsNullOrWhiteSpace(dataType))
{
toolList = new();
dataTypeToToolInstanceMap[dataType] = toolList;
}
if (dataTypeDetectors.Any(detector => string.Equals(detector.Metadata.DataTypeName, dataType, StringComparison.CurrentCultureIgnoreCase)))
{
if (!dataTypeToToolInstanceMap.TryGetValue(dataType, out List<GuiToolInstance>? toolList))
{
toolList = new();
dataTypeToToolInstanceMap[dataType] = toolList;
}

toolList.Add(tool);
toolList.Add(tool);
}
else
{
LogDataDetectorNotFound(tool.InternalComponentName, dataType);
}
}
}
}

Expand All @@ -265,6 +279,9 @@ var lookup
[LoggerMessage(1, LogLevel.Error, $"Error while building data detectors.")]
partial void LogBuildDetectorNodeHierarchyError(Exception ex);

[LoggerMessage(2, LogLevel.Warning, "Unable to find the data type detector '{dataTypeName}' for the tool '{toolName}'.")]
partial void LogDataDetectorNotFound(string toolName, string dataTypeName);

private sealed class DetectorNode
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
using System.Reflection;
using System.Resources;
using DevToys.Api;
using Microsoft.Extensions.Logging;

namespace DevToys.CLI;

internal sealed class CommandToICommandLineToolMap
internal sealed partial class CommandToICommandLineToolMap
{
private readonly ILogger _logger;

internal CommandToICommandLineToolMap(ICommandLineTool commandLineTool, CommandLineToolMetadata metadata)
{
Guard.IsNotNull(commandLineTool);
Guard.IsNotNull(metadata);

_logger = this.Log();

// Get the resource manager, if possible.
ResourceManager? resourceManager = GetResourceManager(commandLineTool, metadata);

Expand Down Expand Up @@ -61,13 +66,31 @@ internal CommandToICommandLineToolMap(ICommandLineTool commandLineTool, CommandL
return resourceManager;
}

private static string GetCommandDescription(ResourceManager? resourceManager, CommandLineToolMetadata metadata)
private string GetCommandDescription(ResourceManager? resourceManager, CommandLineToolMetadata metadata)
{
string? commandDescription
= resourceManager is not null && !string.IsNullOrWhiteSpace(metadata.DescriptionResourceName)
? resourceManager.GetString(metadata.DescriptionResourceName) ?? string.Empty
: string.Empty;
if (resourceManager is null)
{
LogGetMetadataStringFailed(metadata.DescriptionResourceName, metadata.InternalComponentName);
return $"[Unable to get the text for '{metadata.DescriptionResourceName}', " +
$"likely because we couldn't find a proper '{nameof(IResourceAssemblyIdentifier)}' " +
$"for the tool '{metadata.InternalComponentName}'.]";
}

return commandDescription;
try
{
string? commandDescription
= !string.IsNullOrWhiteSpace(metadata.DescriptionResourceName)
? resourceManager.GetString(metadata.DescriptionResourceName) ?? $"[Unable to find '{metadata.DescriptionResourceName}' in '{metadata.ResourceManagerBaseName}']"
: string.Empty;
return commandDescription;
}
catch
{
LogGetMetadataStringFailed(metadata.DescriptionResourceName, metadata.InternalComponentName);
return $"[Unable to find '{metadata.DescriptionResourceName}' in '{metadata.ResourceManagerBaseName}']";
}
}

[LoggerMessage(0, LogLevel.Error, "Unable to get the string for '{metadataName}' for the tool '{toolName}'.")]
partial void LogGetMetadataStringFailed(string metadataName, string toolName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System.Resources;
using DevToys.Api;
using DevToys.CLI.Core;
using Microsoft.Extensions.Logging;

namespace DevToys.CLI;

internal sealed class OptionToICommandLineToolMap
internal sealed partial class OptionToICommandLineToolMap
{
private readonly ILogger _logger;
private static readonly Type optionType = typeof(Option<>);

private readonly ICommandLineTool _commandLineTool;
Expand All @@ -24,6 +26,7 @@ internal sealed class OptionToICommandLineToolMap
Guard.IsNotNull(property);
Guard.IsNotNull(commandLineOptionAttribute);

_logger = this.Log();
_property = property;
_commandLineTool = commandLineTool;

Expand Down Expand Up @@ -59,7 +62,7 @@ internal void SetPropertyValue(object? value)
_property.SetValue(_commandLineTool, value);
}

private static string? GetOptionDescription(ICommandLineTool commandLineTool, CommandLineOptionAttribute commandLineOptionAttribute, ResourceManager? parentResourceManager)
private string? GetOptionDescription(ICommandLineTool commandLineTool, CommandLineOptionAttribute commandLineOptionAttribute, ResourceManager? parentResourceManager)
{
string? optionDescription = null;
if (!string.IsNullOrWhiteSpace(commandLineOptionAttribute.DescriptionResourceName))
Expand All @@ -78,6 +81,12 @@ internal void SetPropertyValue(object? value)
{
optionDescription = optionResourceManager.GetString(commandLineOptionAttribute.DescriptionResourceName);
}

if (optionDescription is null)
{
LogGetMetadataStringFailed(commandLineOptionAttribute.DescriptionResourceName);
optionDescription = $"[Unable to find '{commandLineOptionAttribute.DescriptionResourceName}' in '{commandLineOptionAttribute.ResourceManagerBaseName}']";
}
}

return optionDescription;
Expand Down Expand Up @@ -223,4 +232,7 @@ internal void SetPropertyValue(object? value)
result.ErrorMessage = result.LocalizationResources.ArgumentConversionCannotParse(result.ToString(), typeof(OneOfOption));
return null;
}

[LoggerMessage(0, LogLevel.Error, "Unable to get the string for '{metadataName}'.")]
partial void LogGetMetadataStringFailed(string metadataName);
}
5 changes: 5 additions & 0 deletions src/app/dev/platforms/desktop/DevToys.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ private static void Main(string[] args)

LogAppShuttingDown(logger);
loggerFactory.Dispose();

if (Debugger.IsAttached)
{
Console.ReadKey();
}
}

private static async Task MainAsync(string[] args)
Expand Down
Loading