diff --git a/Directory.Packages.props b/Directory.Packages.props index 02ecbcfbec..f207629a4b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,12 +23,12 @@ + - @@ -45,6 +45,7 @@ + diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 0665244f3f..0ae19f9e17 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -23,7 +23,6 @@ using System.Linq; using System.Reflection; using System.Runtime.Loader; -using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Documents; @@ -36,8 +35,6 @@ using Medo.Application; -using Microsoft.VisualStudio.Composition; - using TomsToolbox.Wpf.Styles; using ICSharpCode.ILSpyX.TreeView; @@ -47,6 +44,11 @@ using System.Globalization; using System.Threading; +using Microsoft.Extensions.DependencyInjection; + +using TomsToolbox.Composition.MicrosoftExtensions; +using TomsToolbox.Essentials; + namespace ICSharpCode.ILSpy { /// @@ -59,10 +61,9 @@ public partial class App : Application public static IExportProvider ExportProvider { get; private set; } - internal class ExceptionData + internal record ExceptionData(Exception Exception) { - public Exception Exception; - public string PluginName; + public string PluginName { get; init; } } public App() @@ -80,6 +81,14 @@ public App() InitializeComponent(); + if (!InitializeDependencyInjection(SettingsService.Instance)) + { + // There is something completely wrong with DI, probably some service registration is missing => nothing we can do to recover, so stop and shut down. + Exit += (_, _) => MessageBox.Show(StartupExceptions.FormatExceptions(), "Sorry we crashed!"); + Shutdown(1); + return; + } + if (!Debugger.IsAttached) { AppDomain.CurrentDomain.UnhandledException += ShowErrorBox; @@ -92,8 +101,6 @@ public App() Resources.RegisterDefaultStyles(); - InitializeMef().GetAwaiter().GetResult(); - // Register the export provider so that it can be accessed from WPF/XAML components. ExportProviderLocator.Register(ExportProvider); // Add data templates registered via MEF. @@ -103,7 +110,7 @@ public App() ThemeManager.Current.Theme = sessionSettings.Theme; if (!string.IsNullOrEmpty(sessionSettings.CurrentCulture)) { - Thread.CurrentThread.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(sessionSettings.CurrentCulture); + Thread.CurrentThread.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = new(sessionSettings.CurrentCulture); } EventManager.RegisterClassHandler(typeof(Window), @@ -125,6 +132,13 @@ public App() SettingsService.Instance.AssemblyListManager.CreateDefaultAssemblyLists(); } + public new static App Current => (App)Application.Current; + + public new MainWindow MainWindow { + get => (MainWindow)base.MainWindow; + set => base.MainWindow = value; + } + private static void SingleInstance_NewInstanceDetected(object sender, NewInstanceEventArgs e) => ExportProvider.GetExportedValue().HandleSingleInstanceCommandLineArguments(e.Args).HandleExceptions(); static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName) @@ -136,22 +150,17 @@ static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyN return context.LoadFromAssemblyPath(assemblyFileName); } - private static async Task InitializeMef() + private static bool InitializeDependencyInjection(SettingsService settingsService) { // Add custom logic for resolution of dependencies. // This necessary because the AssemblyLoadContext.LoadFromAssemblyPath and related methods, // do not automatically load dependencies. AssemblyLoadContext.Default.Resolving += ResolvePluginDependencies; - // Cannot show MessageBox here, because WPF would crash with a XamlParseException - // Remember and show exceptions in text output, once MainWindow is properly initialized try { - // Set up VS MEF. For now, only do MEF1 part discovery, since that was in use before. - // To support both MEF1 and MEF2 parts, just change this to: - // var discovery = PartDiscovery.Combine(new AttributedPartDiscoveryV1(Resolver.DefaultInstance), - // new AttributedPartDiscovery(Resolver.DefaultInstance)); - var discovery = new AttributedPartDiscoveryV1(Resolver.DefaultInstance); - var catalog = ComposableCatalog.Create(Resolver.DefaultInstance); + var services = new ServiceCollection(); + + var pluginDir = Path.GetDirectoryName(typeof(App).Module.FullyQualifiedName); if (pluginDir != null) { @@ -160,46 +169,39 @@ private static async Task InitializeMef() var name = Path.GetFileNameWithoutExtension(plugin); try { - var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(plugin); - var parts = await discovery.CreatePartsAsync(asm); - catalog = catalog.AddParts(parts); + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(plugin); + services.BindExports(assembly); } catch (Exception ex) { - StartupExceptions.Add(new ExceptionData { Exception = ex, PluginName = name }); + // Cannot show MessageBox here, because WPF would crash with a XamlParseException + // Remember and show exceptions in text output, once MainWindow is properly initialized + StartupExceptions.Add(new(ex) { PluginName = name }); } } } + // Add the built-in parts: First, from ILSpyX - var xParts = await discovery.CreatePartsAsync(typeof(IAnalyzer).Assembly); - catalog = catalog.AddParts(xParts); + services.BindExports(typeof(IAnalyzer).Assembly); // Then from ILSpy itself - var createdParts = await discovery.CreatePartsAsync(Assembly.GetExecutingAssembly()); - catalog = catalog.AddParts(createdParts); - - // If/When the project switches to .NET Standard/Core, this will be needed to allow metadata interfaces (as opposed - // to metadata classes). When running on .NET Framework, it's automatic. - // catalog.WithDesktopSupport(); - // If/When any part needs to import ICompositionService, this will be needed: - // catalog.WithCompositionService(); - var config = CompositionConfiguration.Create(catalog); - var exportProviderFactory = config.CreateExportProviderFactory(); - ExportProvider = new ExportProviderAdapter(exportProviderFactory.CreateExportProvider()); - - // This throws exceptions for composition failures. Alternatively, the configuration's CompositionErrors property - // could be used to log the errors directly. Used at the end so that it does not prevent the export provider setup. - config.ThrowOnErrors(); - } - catch (CompositionFailedException ex) when (ex.InnerException is AggregateException agex) - { - foreach (var inner in agex.InnerExceptions) - { - StartupExceptions.Add(new ExceptionData { Exception = inner }); - } + services.BindExports(Assembly.GetExecutingAssembly()); + // Add the settings service + services.AddSingleton(settingsService); + + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + + ExportProvider = new ExportProviderAdapter(serviceProvider); + + return true; } catch (Exception ex) { - StartupExceptions.Add(new ExceptionData { Exception = ex }); + if (ex is AggregateException aggregate) + StartupExceptions.AddRange(aggregate.InnerExceptions.Select(item => new ExceptionData(ex))); + else + StartupExceptions.Add(new(ex)); + + return false; } } @@ -207,15 +209,7 @@ protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); - var output = new StringBuilder(); - - if (StartupExceptions.FormatExceptions(output)) - { - MessageBox.Show(output.ToString(), "Sorry we crashed!"); - Environment.Exit(1); - } - - MainWindow = new MainWindow(); + MainWindow = new(); MainWindow.Show(); } diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index b94668f14e..33570c34fc 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -21,18 +21,17 @@ using System.Collections.Specialized; using System.ComponentModel; using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; -using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Navigation; using System.Windows.Threading; -using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Documentation; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; @@ -160,13 +159,15 @@ private bool HandleCommandLineArguments(CommandLineArguments args) /// Called on startup or when passed arguments via WndProc from a second instance. /// In the format case, spySettings is non-null; in the latter it is null. /// - private void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ISettingsProvider? spySettings = null) + private async Task HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ISettingsProvider? spySettings = null) { var sessionSettings = SettingsService.Instance.SessionSettings; var relevantAssemblies = commandLineLoadedAssemblies.ToList(); commandLineLoadedAssemblies.Clear(); // clear references once we don't need them anymore - NavigateOnLaunch(args.NavigateTo, sessionSettings.ActiveTreeViewPath, spySettings, relevantAssemblies); + + await NavigateOnLaunch(args.NavigateTo, sessionSettings.ActiveTreeViewPath, spySettings, relevantAssemblies); + if (args.Search != null) { var searchPane = App.ExportProvider.GetExportedValue(); @@ -180,7 +181,7 @@ public async Task HandleSingleInstanceCommandLineArguments(string[] args) { var cmdArgs = CommandLineArguments.Create(args); - await Dispatcher.InvokeAsync(() => { + await Dispatcher.InvokeAsync(async () => { if (!HandleCommandLineArguments(cmdArgs)) return; @@ -192,11 +193,11 @@ await Dispatcher.InvokeAsync(() => { window.WindowState = WindowState.Normal; } - HandleCommandLineArgumentsAfterShowList(cmdArgs); + await HandleCommandLineArgumentsAfterShowList(cmdArgs); }); } - private async void NavigateOnLaunch(string? navigateTo, string[]? activeTreeViewPath, ISettingsProvider? spySettings, List relevantAssemblies) + private async Task NavigateOnLaunch(string? navigateTo, string[]? activeTreeViewPath, ISettingsProvider? spySettings, List relevantAssemblies) { var initialSelection = SelectedItem; if (navigateTo != null) @@ -386,26 +387,31 @@ public void Initialize() Dispatcher.BeginInvoke(DispatcherPriority.Loaded, OpenAssemblies); } - private void OpenAssemblies() + private async Task OpenAssemblies() { - HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, SettingsService.Instance.SpySettings); + await HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, SettingsService.Instance.SpySettings); - AvalonEditTextOutput output = new(); - if (FormatExceptions(App.StartupExceptions.ToArray(), output)) + if (FormatExceptions(App.StartupExceptions.ToArray(), out var output)) { + output.Title = "Startup errors"; + + DockWorkspace.Instance.AddTabPage(); DockWorkspace.Instance.ShowText(output); } } - private static bool FormatExceptions(App.ExceptionData[] exceptions, ITextOutput output) + private static bool FormatExceptions(App.ExceptionData[] exceptions, [NotNullWhen(true)] out AvalonEditTextOutput? output) { - var stringBuilder = new StringBuilder(); - var result = exceptions.FormatExceptions(stringBuilder); - if (result) - { - output.Write(stringBuilder.ToString()); - } - return result; + output = null; + + var result = exceptions.FormatExceptions(); + if (result.IsNullOrEmpty()) + return false; + + output = new(); + output.Write(result); + return true; + } private void ShowAssemblyList(string name) diff --git a/ILSpy/ExportProviderAdapter.cs b/ILSpy/ExportProviderAdapter.cs deleted file mode 100644 index d883248c4c..0000000000 --- a/ILSpy/ExportProviderAdapter.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2024 Tom Englert for the SharpDevelop Team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this -// software and associated documentation files (the "Software"), to deal in the Software -// without restriction, including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons -// to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE -// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -using Microsoft.VisualStudio.Composition; - -using TomsToolbox.Composition; -using TomsToolbox.Essentials; - -namespace ICSharpCode.ILSpy; - -#nullable enable - -/// -/// Adapter for Microsoft.VisualStudio.Composition. to . -/// -public sealed class ExportProviderAdapter : IExportProvider -{ - private static readonly Type DefaultMetadataType = typeof(Dictionary); - - private readonly ExportProvider _exportProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The export provider. - public ExportProviderAdapter(ExportProvider exportProvider) - { - _exportProvider = exportProvider; - } - - event EventHandler? IExportProvider.ExportsChanged { add { } remove { } } - - T IExportProvider.GetExportedValue(string? contractName) where T : class - { - return _exportProvider.GetExportedValue(contractName) ?? throw new InvalidOperationException($"No export found for type {typeof(T).FullName} with contract '{contractName}'"); - } - - T? IExportProvider.GetExportedValueOrDefault(string? contractName) where T : class - { - return _exportProvider.GetExportedValues(contractName).SingleOrDefault(); - } - - bool IExportProvider.TryGetExportedValue(string? contractName, [NotNullWhen(true)] out T? value) where T : class - { - value = _exportProvider.GetExportedValues(contractName).SingleOrDefault(); - - return !Equals(value, default(T)); - } - - IEnumerable IExportProvider.GetExportedValues(string? contractName) where T : class - { - return _exportProvider.GetExportedValues(contractName); - } - - IEnumerable IExportProvider.GetExportedValues(Type contractType, string? contractName) - { - return _exportProvider - .GetExports(contractType, DefaultMetadataType, contractName) - .Select(item => item.Value) - .ExceptNullItems(); - } - - IEnumerable> IExportProvider.GetExports(Type contractType, string? contractName) - { - return _exportProvider - .GetExports(contractType, DefaultMetadataType, contractName) - .Select(item => new ExportAdapter(() => item.Value, new MetadataAdapter((IDictionary)item.Metadata))); - } - - IEnumerable> IExportProvider.GetExports(string? contractName) where T : class - { - return _exportProvider - .GetExports(typeof(T), DefaultMetadataType, contractName) - .Select(item => new ExportAdapter(() => (T?)item.Value, new MetadataAdapter((IDictionary)item.Metadata))); - } - - IEnumerable> IExportProvider.GetExports(string? contractName) where T : class where TMetadataView : class - { - return _exportProvider - .GetExports(contractName) - .Select(item => new ExportAdapter(() => item.Value, item.Metadata)); - } -} \ No newline at end of file diff --git a/ILSpy/ExtensionMethods.cs b/ILSpy/ExtensionMethods.cs index 39d623fd9a..1d49f41ada 100644 --- a/ILSpy/ExtensionMethods.cs +++ b/ILSpy/ExtensionMethods.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; @@ -166,34 +167,37 @@ public static double ToGray(this Color? color) return color?.R * 0.3 + color?.G * 0.6 + color?.B * 0.1 ?? 0.0; } - internal static bool FormatExceptions(this IList exceptions, StringBuilder output) + internal static string? FormatExceptions(this IList exceptions) { if (exceptions.Count == 0) - return false; - bool first = true; + return null; - foreach (var item in exceptions) - { - if (first) - first = false; - else - output.AppendLine("-------------------------------------------------"); + string delimiter = $"-------------------------------------------------{Environment.NewLine}"; + + return string.Join(delimiter, exceptions.Select(FormatException)); + } + + private static string FormatException(App.ExceptionData item) + { + var output = new StringBuilder(); + + if (!item.PluginName.IsNullOrEmpty()) output.AppendLine("Error(s) loading plugin: " + item.PluginName); - if (item.Exception is System.Reflection.ReflectionTypeLoadException exception) - { - foreach (var ex in exception.LoaderExceptions.ExceptNullItems()) - { - output.AppendLine(ex.ToString()); - output.AppendLine(); - } - } - else + + if (item.Exception is System.Reflection.ReflectionTypeLoadException exception) + { + foreach (var ex in exception.LoaderExceptions.ExceptNullItems()) { - output.AppendLine(item.Exception.ToString()); + output.AppendLine(ex.ToString()); + output.AppendLine(); } } + else + { + output.AppendLine(item.Exception.ToString()); + } - return true; + return output.ToString(); } public static IDisposable PreserveFocus(this IInputElement? inputElement, bool preserve = true) diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 9b0dd62593..5908e748df 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -45,10 +45,11 @@ - + +