diff --git a/Bonsai.Configuration/CommandLineParser.cs b/Bonsai.Configuration/CommandLineParser.cs index fef24dbda..fed476040 100644 --- a/Bonsai.Configuration/CommandLineParser.cs +++ b/Bonsai.Configuration/CommandLineParser.cs @@ -68,7 +68,10 @@ public void Parse(string[] args) { var argument = string.Empty; if (options.Length > 1) argument = options[1]; - else if (args.Length > i + 1) argument = args[++i]; + else if (args.Length > i + 1 && !args[i + 1].StartsWith(CommandPrefix)) + { + argument = args[++i]; + } command(argument); } } diff --git a/Bonsai.Editor/TypeDefinitionProvider.cs b/Bonsai.Editor/TypeDefinitionProvider.cs index de53a2870..6e7df18ae 100644 --- a/Bonsai.Editor/TypeDefinitionProvider.cs +++ b/Bonsai.Editor/TypeDefinitionProvider.cs @@ -29,8 +29,9 @@ static CodeTypeReference GetTypeReference(Type type, HashSet importNames static CodeAttributeDeclaration GetAttributeDeclaration(CustomAttributeData attribute, HashSet importNamespaces) { - importNamespaces.Add(attribute.AttributeType.Namespace); - var attributeName = attribute.AttributeType.Name; + var attributeType = attribute.Constructor.DeclaringType; + importNamespaces.Add(attributeType.Namespace); + var attributeName = attributeType.Name; var suffix = attributeName.LastIndexOf(nameof(Attribute)); attributeName = suffix >= 0 ? attributeName.Substring(0, suffix) : attributeName; var reference = new CodeTypeReference(attributeName); diff --git a/Bonsai.Editor/TypeVisualizerDescriptor.cs b/Bonsai.Editor/TypeVisualizerDescriptor.cs index 58d4c25a3..892ca854d 100644 --- a/Bonsai.Editor/TypeVisualizerDescriptor.cs +++ b/Bonsai.Editor/TypeVisualizerDescriptor.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace Bonsai.Editor { @@ -8,6 +9,33 @@ public class TypeVisualizerDescriptor public string VisualizerTypeName; public string TargetTypeName; + public TypeVisualizerDescriptor(CustomAttributeData attribute) + { + if (attribute.ConstructorArguments.Count > 0) + { + var constructorArgument = attribute.ConstructorArguments[0]; + if (constructorArgument.ArgumentType.AssemblyQualifiedName == typeof(string).AssemblyQualifiedName) + { + VisualizerTypeName = (string)constructorArgument.Value; + } + else VisualizerTypeName = ((Type)constructorArgument.Value).AssemblyQualifiedName; + } + + for (int i = 0; i < attribute.NamedArguments.Count; i++) + { + var namedArgument = attribute.NamedArguments[i]; + switch (namedArgument.MemberName) + { + case nameof(TypeVisualizerAttribute.TargetTypeName): + TargetTypeName = (string)namedArgument.TypedValue.Value; + break; + case nameof(TypeVisualizerAttribute.Target): + TargetTypeName = ((Type)namedArgument.TypedValue.Value).AssemblyQualifiedName; + break; + } + } + } + public TypeVisualizerDescriptor(TypeVisualizerAttribute typeVisualizer) { TargetTypeName = typeVisualizer.TargetTypeName; diff --git a/Bonsai/AppResult.cs b/Bonsai/AppResult.cs index 9f6445e70..fe294e2f9 100644 --- a/Bonsai/AppResult.cs +++ b/Bonsai/AppResult.cs @@ -1,33 +1,97 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using Newtonsoft.Json; namespace Bonsai { static class AppResult { - public static TResult GetResult(AppDomain domain) + static readonly JsonSerializer Serializer = JsonSerializer.CreateDefault(); + static Dictionary Values; + + public static IDisposable OpenWrite(NamedPipeClientStream stream) + { + Values = new(); + if (stream == null) + { + return EmptyDisposable.Instance; + } + + stream.Connect(); + var writer = new StreamWriter(stream); + return new AnonymousDisposable(() => + { + try + { + Serializer.Serialize(writer, Values); + writer.Flush(); + try { stream.WaitForPipeDrain(); } + catch (NotSupportedException) { } + } + finally { writer.Close(); } + }); + } + + public static IDisposable OpenRead(Stream stream) { - var resultHolder = (ResultHolder)domain.CreateInstanceAndUnwrap( - typeof(ResultHolder).Assembly.FullName, - typeof(ResultHolder).FullName); - return resultHolder.Result; + using var reader = new JsonTextReader(new StreamReader(stream)); + Values = Serializer.Deserialize>(reader); + return EmptyDisposable.Instance; + } + + public static TResult GetResult() + { + if (Values != null && Values.TryGetValue(typeof(TResult).FullName, out string value)) + { + if (typeof(TResult).IsEnum) + { + return (TResult)Enum.Parse(typeof(TResult), value); + } + + return (TResult)Convert.ChangeType(value, typeof(TResult)); + } + + return default; } public static void SetResult(TResult result) { - ResultHolder.ResultValue = result; + if (Values == null) + { + throw new InvalidOperationException("No output stream has been opened for writing."); + } + + Values[typeof(TResult).FullName] = result.ToString(); + } + + class AnonymousDisposable : IDisposable + { + private Action disposeAction; + + public AnonymousDisposable(Action dispose) + { + disposeAction = dispose; + } + + public void Dispose() + { + Interlocked.Exchange(ref disposeAction, null)?.Invoke(); + } } - class ResultHolder : MarshalByRefObject + class EmptyDisposable : IDisposable { - public static TResult ResultValue; + public static readonly EmptyDisposable Instance = new(); - public ResultHolder() + private EmptyDisposable() { } - public TResult Result + public void Dispose() { - get { return ResultValue; } } } } diff --git a/Bonsai/Bonsai.csproj b/Bonsai/Bonsai.csproj index b9b5782a4..f0842317c 100644 --- a/Bonsai/Bonsai.csproj +++ b/Bonsai/Bonsai.csproj @@ -14,6 +14,7 @@ App.manifest + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -44,6 +45,13 @@ $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework) + + + + + + + diff --git a/Bonsai/DependencyInspector.cs b/Bonsai/DependencyInspector.cs index a5391a771..84c93554d 100644 --- a/Bonsai/DependencyInspector.cs +++ b/Bonsai/DependencyInspector.cs @@ -16,8 +16,6 @@ namespace Bonsai { sealed class DependencyInspector : MarshalByRefObject { - readonly ScriptExtensions scriptEnvironment; - readonly PackageConfiguration packageConfiguration; const string XsiAttributeValue = "http://www.w3.org/2001/XMLSchema-instance"; const string WorkflowElementName = "Workflow"; const string ExpressionElementName = "Expression"; @@ -26,14 +24,7 @@ sealed class DependencyInspector : MarshalByRefObject const string TypeAttributeName = "type"; const char AssemblySeparator = ':'; - public DependencyInspector(PackageConfiguration configuration) - { - ConfigurationHelper.SetAssemblyResolve(configuration); - scriptEnvironment = new ScriptExtensions(configuration, null); - packageConfiguration = configuration; - } - - IEnumerable GetVisualizerSettings(VisualizerLayout root) + static IEnumerable GetVisualizerSettings(VisualizerLayout root) { var stack = new Stack(); stack.Push(root); @@ -52,8 +43,10 @@ IEnumerable GetVisualizerSettings(VisualizerLayout roo } } - Configuration.PackageReference[] GetWorkflowPackageDependencies(string[] fileNames) + static Configuration.PackageReference[] GetPackageDependencies(string[] fileNames, PackageConfiguration configuration) { + using var context = LoaderResource.CreateMetadataLoadContext(configuration); + var scriptEnvironment = new ScriptExtensions(configuration, null); var assemblies = new HashSet(); foreach (var path in fileNames) { @@ -76,7 +69,7 @@ Configuration.PackageReference[] GetWorkflowPackageDependencies(string[] fileNam var assemblyName = includePath.Split(new[] { AssemblySeparator }, 2)[0]; if (!string.IsNullOrEmpty(assemblyName)) { - var assembly = Assembly.Load(assemblyName); + var assembly = context.LoadFromAssemblyName(assemblyName); assemblies.Add(assembly); } } @@ -92,7 +85,7 @@ Configuration.PackageReference[] GetWorkflowPackageDependencies(string[] fileNam if (File.Exists(layoutPath)) { var visualizerMap = new Lazy>(() => - TypeVisualizerLoader.GetVisualizerTypes(packageConfiguration) + TypeVisualizerLoader.GetVisualizerTypes(configuration) .Select(descriptor => descriptor.VisualizerTypeName).Distinct() .Select(typeName => Type.GetType(typeName, false)) .Where(type => type != null) @@ -115,16 +108,16 @@ Configuration.PackageReference[] GetWorkflowPackageDependencies(string[] fileNam } } - var packageMap = packageConfiguration.GetPackageReferenceMap(); + var packageMap = configuration.GetPackageReferenceMap(); var dependencies = assemblies.Select(assembly => - packageConfiguration.GetAssemblyPackageReference(assembly.GetName().Name, packageMap)) + configuration.GetAssemblyPackageReference(assembly.GetName().Name, packageMap)) .Where(package => package != null); if (File.Exists(scriptEnvironment.ProjectFileName)) { dependencies = dependencies.Concat( from id in scriptEnvironment.GetPackageReferences() - where packageConfiguration.Packages.Contains(id) - select packageConfiguration.Packages[id]); + where configuration.Packages.Contains(id) + select configuration.Packages[id]); } return dependencies.ToArray(); @@ -134,14 +127,12 @@ public static IObservable GetWorkflowPackageDependencies(stri { if (configuration == null) { - throw new ArgumentNullException("configuration"); + throw new ArgumentNullException(nameof(configuration)); } - return Observable.Using( - () => new LoaderResource(configuration), - resource => from dependency in resource.Loader.GetWorkflowPackageDependencies(fileNames).ToObservable() - let versionRange = new VersionRange(NuGetVersion.Parse(dependency.Version), includeMinVersion: true) - select new PackageDependency(dependency.Id, versionRange)); + return from dependency in GetPackageDependencies(fileNames, configuration).ToObservable() + let versionRange = new VersionRange(NuGetVersion.Parse(dependency.Version), includeMinVersion: true) + select new PackageDependency(dependency.Id, versionRange); } } } diff --git a/Bonsai/Launcher.cs b/Bonsai/Launcher.cs index bfe93fb56..b10c8067d 100644 --- a/Bonsai/Launcher.cs +++ b/Bonsai/Launcher.cs @@ -109,7 +109,8 @@ internal static int LaunchWorkflowEditor( if (scriptExtensions.DebugScripts) editorFlags |= EditorFlags.DebugScripts; AppResult.SetResult(editorFlags); AppResult.SetResult(mainForm.FileName); - return (int)mainForm.EditorResult; + AppResult.SetResult((int)mainForm.EditorResult); + return Program.NormalExitCode; } finally { cancellation.Cancel(); } } diff --git a/Bonsai/LoaderResource.cs b/Bonsai/LoaderResource.cs index 58d5dda26..c91d6947d 100644 --- a/Bonsai/LoaderResource.cs +++ b/Bonsai/LoaderResource.cs @@ -1,30 +1,17 @@ using Bonsai.Configuration; -using System; +using System.IO; using System.Reflection; +using System.Runtime.InteropServices; namespace Bonsai { - class LoaderResource : IDisposable where TLoader : MarshalByRefObject + static class LoaderResource { - AppDomain reflectionDomain; - - public LoaderResource(PackageConfiguration configuration) - { - var currentEvidence = AppDomain.CurrentDomain.Evidence; - var setupInfo = AppDomain.CurrentDomain.SetupInformation; - reflectionDomain = AppDomain.CreateDomain("ReflectionOnly", currentEvidence, setupInfo); - Loader = (TLoader)reflectionDomain.CreateInstanceAndUnwrap( - typeof(TLoader).Assembly.FullName, - typeof(TLoader).FullName, - false, (BindingFlags)0, null, - new[] { configuration }, null, null); - } - - public TLoader Loader { get; private set; } - - public void Dispose() + public static MetadataLoadContext CreateMetadataLoadContext(PackageConfiguration configuration) { - AppDomain.Unload(reflectionDomain); + var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); + var resolver = new PackageAssemblyResolver(configuration, runtimeAssemblies); + return new MetadataLoadContext(resolver); } } } diff --git a/Bonsai/PackageAssemblyResolver.cs b/Bonsai/PackageAssemblyResolver.cs new file mode 100644 index 000000000..516250c9b --- /dev/null +++ b/Bonsai/PackageAssemblyResolver.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Bonsai.Configuration; + +namespace Bonsai +{ + internal class PackageAssemblyResolver : PathAssemblyResolver + { + public PackageAssemblyResolver(PackageConfiguration configuration, IEnumerable assemblyPaths) + : base(assemblyPaths) + { + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + ConfigurationRoot = ConfigurationHelper.GetConfigurationRoot(configuration); + } + + private PackageConfiguration Configuration { get; } + + private string ConfigurationRoot { get; } + + public override Assembly Resolve(MetadataLoadContext context, AssemblyName assemblyName) + { + var assembly = base.Resolve(context, assemblyName); + if (assembly != null) return assembly; + + var assemblyLocation = Configuration.GetAssemblyLocation(assemblyName.Name); + if (assemblyLocation != null) + { + if (assemblyLocation.StartsWith(Uri.UriSchemeFile) && + Uri.TryCreate(assemblyLocation, UriKind.Absolute, out Uri uri)) + { + return context.LoadFromAssemblyPath(uri.LocalPath); + } + + if (!Path.IsPathRooted(assemblyLocation)) + { + assemblyLocation = Path.Combine(ConfigurationRoot, assemblyLocation); + } + + if (File.Exists(assemblyLocation)) + { + return context.LoadFromAssemblyPath(assemblyLocation); + } + } + + return null; + } + } +} diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 91aaebdec..d3d8eed8a 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -3,15 +3,16 @@ using NuGet.Versioning; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Pipes; using System.Reflection; namespace Bonsai { static class Program { - const string PathEnvironmentVariable = "PATH"; const string StartCommand = "--start"; const string LibraryCommand = "--lib"; const string PropertyCommand = "--property"; @@ -27,7 +28,7 @@ static class Program const string ExportImageCommand = "--export-image"; const string ReloadEditorCommand = "--reload-editor"; const string GalleryCommand = "--gallery"; - const string EditorDomainName = "EditorDomain"; + const string PipeCommand = "--@pipe"; const string RepositoryPath = "Packages"; const string ExtensionsPath = "Extensions"; internal const int NormalExitCode = 0; @@ -51,6 +52,7 @@ internal static int Main(string[] args) var launchResult = default(EditorResult); var initialFileName = default(string); var imageFileName = default(string); + var pipeHandle = default(string); var layoutPath = default(string); var libFolders = new List(); var propertyAssignments = new Dictionary(); @@ -62,6 +64,7 @@ internal static int Main(string[] args) parser.RegisterCommand(DebugScriptCommand, () => debugScripts = true); parser.RegisterCommand(SuppressBootstrapCommand, () => bootstrap = false); parser.RegisterCommand(SuppressEditorCommand, () => launchEditor = false); + parser.RegisterCommand(PipeCommand, pipeName => pipeHandle = pipeName); parser.RegisterCommand(ExportImageCommand, fileName => { imageFileName = fileName; exportImage = true; }); parser.RegisterCommand(ExportPackageCommand, () => { launchResult = EditorResult.ExportPackage; bootstrap = false; }); parser.RegisterCommand(ReloadEditorCommand, () => { launchResult = EditorResult.ReloadEditor; bootstrap = false; }); @@ -93,6 +96,8 @@ internal static int Main(string[] args) var packageConfiguration = ConfigurationHelper.Load(); if (!bootstrap) { + using var pipeClient = pipeHandle != null ? new NamedPipeClientStream(".", pipeHandle, PipeDirection.Out) : null; + using var pipeWriter = AppResult.OpenWrite(pipeClient); if (launchResult == EditorResult.Exit) { if (!string.IsNullOrEmpty(initialFileName)) launchResult = EditorResult.ReloadEditor; @@ -109,7 +114,8 @@ internal static int Main(string[] args) } AppResult.SetResult(EditorResult.Exit); AppResult.SetResult(initialFileName); - return (int)launchResult; + AppResult.SetResult((int)launchResult); + return NormalExitCode; } } @@ -167,43 +173,49 @@ internal static int Main(string[] args) catch (AggregateException) { return ErrorExitCode; } var startScreen = launchEditor; + var pipeName = Guid.NewGuid().ToString(); args = Array.FindAll(args, arg => arg != DebugScriptCommand); do { - string[] editorArgs; + var editorArgs = new List(args); + var workingDirectory = Environment.CurrentDirectory; if (launchEditor && startScreen) launchResult = EditorResult.Exit; - if (launchResult == EditorResult.ExportPackage) editorArgs = new[] { initialFileName, ExportPackageCommand }; - else if (launchResult == EditorResult.OpenGallery) editorArgs = new[] { GalleryCommand }; + if (launchResult == EditorResult.ExportPackage) editorArgs.AddRange(new[] { initialFileName, ExportPackageCommand }); + else if (launchResult == EditorResult.OpenGallery) editorArgs.Add(GalleryCommand); else if (launchResult == EditorResult.ManagePackages) { - editorArgs = updatePackages + editorArgs.AddRange(updatePackages ? new[] { PackageManagerCommand + ":" + PackageManagerUpdates } - : new[] { PackageManagerCommand }; + : new[] { PackageManagerCommand }); } else { - var extraArgs = new List(args); - if (debugScripts) extraArgs.Add(DebugScriptCommand); - if (launchResult == EditorResult.ReloadEditor) extraArgs.Add(ReloadEditorCommand); - else extraArgs.Add(SuppressBootstrapCommand); - if (!string.IsNullOrEmpty(initialFileName)) extraArgs.Add(initialFileName); - editorArgs = extraArgs.ToArray(); + if (debugScripts) editorArgs.Add(DebugScriptCommand); + if (launchResult == EditorResult.ReloadEditor) editorArgs.Add(ReloadEditorCommand); + else editorArgs.Add(SuppressBootstrapCommand); + if (!string.IsNullOrEmpty(initialFileName)) + { + editorArgs.Add(initialFileName); + workingDirectory = Path.GetDirectoryName(initialFileName); + } } - var setupInfo = new AppDomainSetup(); - setupInfo.ApplicationBase = editorFolder; - setupInfo.PrivateBinPath = editorFolder; - var currentEvidence = AppDomain.CurrentDomain.Evidence; - var currentPermissionSet = AppDomain.CurrentDomain.PermissionSet; - var currentPath = Environment.GetEnvironmentVariable(PathEnvironmentVariable); - setupInfo.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile; - setupInfo.LoaderOptimization = LoaderOptimization.MultiDomainHost; - var editorDomain = AppDomain.CreateDomain(EditorDomainName, currentEvidence, setupInfo, currentPermissionSet); - var exitCode = (EditorResult)editorDomain.ExecuteAssembly(editorPath, editorArgs); - Environment.SetEnvironmentVariable(PathEnvironmentVariable, currentPath); + using var pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.In); + editorArgs.Add(PipeCommand + ":" + pipeName); + + var setupInfo = new ProcessStartInfo(); + setupInfo.FileName = Assembly.GetEntryAssembly().Location; + setupInfo.Arguments = string.Join(" ", editorArgs); + setupInfo.WorkingDirectory = workingDirectory; + setupInfo.UseShellExecute = false; + var process = Process.Start(setupInfo); + pipeServer.WaitForConnection(); + using var pipeReader = AppResult.OpenRead(pipeServer); + process.WaitForExit(); + if (process.ExitCode != 0) + return process.ExitCode; - var editorFlags = AppResult.GetResult(editorDomain); - launchResult = AppResult.GetResult(editorDomain); + launchResult = AppResult.GetResult(); if (launchEditor) { if (launchResult == EditorResult.ReloadEditor) launchEditor = false; @@ -215,24 +227,22 @@ internal static int Main(string[] args) if (launchResult == EditorResult.OpenGallery || launchResult == EditorResult.ManagePackages) { - var result = AppResult.GetResult(editorDomain); + var result = AppResult.GetResult(); if (!string.IsNullOrEmpty(result) && File.Exists(result)) { initialFileName = result; - Environment.CurrentDirectory = Path.GetDirectoryName(initialFileName); } } launchResult = EditorResult.ReloadEditor; } else { + var editorFlags = AppResult.GetResult(); debugScripts = editorFlags.HasFlag(EditorFlags.DebugScripts); updatePackages = editorFlags.HasFlag(EditorFlags.UpdatesAvailable); - initialFileName = AppResult.GetResult(editorDomain); - launchResult = exitCode; + initialFileName = AppResult.GetResult(); + launchResult = (EditorResult)AppResult.GetResult(); } - - AppDomain.Unload(editorDomain); } while (launchResult != EditorResult.Exit); } diff --git a/Bonsai/Properties/launchSettings.json b/Bonsai/Properties/launchSettings.json new file mode 100644 index 000000000..3d0152158 --- /dev/null +++ b/Bonsai/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Bonsai": { + "commandName": "Project", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/Bonsai/ReflectionHelper.cs b/Bonsai/ReflectionHelper.cs new file mode 100644 index 000000000..6d79c20b1 --- /dev/null +++ b/Bonsai/ReflectionHelper.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Bonsai +{ + static class ReflectionHelper + { + public static CustomAttributeData[] GetCustomAttributesData(this Type type, bool inherit) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + var attributeLists = new List>(); + while (type != null) + { + attributeLists.Add(type.GetCustomAttributesData()); + type = inherit ? type.BaseType : null; + } + + var offset = 0; + var count = attributeLists.Sum(attributes => attributes.Count); + var result = new CustomAttributeData[count]; + for (int i = 0; i < attributeLists.Count; i++) + { + attributeLists[i].CopyTo(result, offset); + offset += attributeLists[i].Count; + } + + return result; + } + + public static IEnumerable OfType(this IEnumerable customAttributes) + { + var attributeTypeName = typeof(TAttribute).FullName; + return customAttributes.Where( + attribute => attribute.Constructor.DeclaringType.FullName == attributeTypeName); + } + + public static bool IsDefined(this CustomAttributeData[] customAttributes, Type attributeType) + { + return GetCustomAttributeData(customAttributes, attributeType) != null; + } + + public static CustomAttributeData GetCustomAttributeData( + this CustomAttributeData[] customAttributes, + Type attributeType) + { + if (customAttributes == null) + { + throw new ArgumentNullException(nameof(customAttributes)); + } + + return Array.Find( + customAttributes, + attribute => attribute.Constructor.DeclaringType.FullName == attributeType.FullName); + } + + public static object GetConstructorArgument(this CustomAttributeData attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return attribute.ConstructorArguments.Count > 0 ? attribute.ConstructorArguments[0].Value : null; + } + + public static bool IsMatchSubclassOf(this Type type, Type baseType) + { + var typeName = baseType.AssemblyQualifiedName; + if (type.AssemblyQualifiedName == typeName) + { + return false; + } + + while (type != null) + { + if (type.AssemblyQualifiedName == typeName) + { + return true; + } + + type = type.BaseType; + } + + return false; + } + } +} diff --git a/Bonsai/TypeVisualizerLoader.cs b/Bonsai/TypeVisualizerLoader.cs index f7cf95e51..e5c99ce23 100644 --- a/Bonsai/TypeVisualizerLoader.cs +++ b/Bonsai/TypeVisualizerLoader.cs @@ -12,76 +12,57 @@ namespace Bonsai { sealed class TypeVisualizerLoader : MarshalByRefObject { - public TypeVisualizerLoader(PackageConfiguration configuration) + static IEnumerable GetCustomAttributeTypes(Assembly assembly) { - ConfigurationHelper.SetAssemblyResolve(configuration); - } - - static IEnumerable GetCustomAttributeTypes(Assembly assembly) - { - Type[] types; - var typeVisualizers = Enumerable.Empty(); - - try { types = assembly.GetTypes(); } - catch (ReflectionTypeLoadException ex) - { - Trace.TraceError(string.Join(Environment.NewLine, ex.LoaderExceptions)); - return typeVisualizers; - } + var assemblyAttributes = assembly.GetCustomAttributesData(); + var typeVisualizers = assemblyAttributes + .OfType() + .Select(attribute => new TypeVisualizerDescriptor(attribute)); + var types = assembly.GetTypes(); for (int i = 0; i < types.Length; i++) { var type = types[i]; if (type.IsPublic && !type.IsAbstract && !type.ContainsGenericParameters) { - var visualizerAttributes = Array.ConvertAll(type.GetCustomAttributes(typeof(TypeVisualizerAttribute), true), attribute => - { - var visualizerAttribute = (TypeVisualizerAttribute)attribute; - visualizerAttribute.TargetTypeName = type.AssemblyQualifiedName; - return visualizerAttribute; - }); - - if (visualizerAttributes.Length > 0) - { - typeVisualizers = typeVisualizers.Concat(visualizerAttributes); - } + var customAttributes = type.GetCustomAttributesData(inherit: true).OfType(); + typeVisualizers = typeVisualizers.Concat(customAttributes.Select( + attribute => new TypeVisualizerDescriptor(attribute) + { + TargetTypeName = type.AssemblyQualifiedName + })); } } return typeVisualizers; } - TypeVisualizerDescriptor[] GetReflectionTypeVisualizerAttributes(string assemblyRef) + static IEnumerable GetReflectionTypeVisualizerTypes(MetadataLoadContext context, string assemblyName) { - var typeVisualizers = Enumerable.Empty(); try { - var assembly = Assembly.Load(assemblyRef); - var visualizerAttributes = assembly.GetCustomAttributes(typeof(TypeVisualizerAttribute), true).Cast(); - typeVisualizers = typeVisualizers.Concat(visualizerAttributes); - typeVisualizers = typeVisualizers.Concat(GetCustomAttributeTypes(assembly)); + var assembly = context.LoadFromAssemblyName(assemblyName); + return GetCustomAttributeTypes(assembly); } catch (FileLoadException ex) { Trace.TraceError("{0}", ex); } catch (FileNotFoundException ex) { Trace.TraceError("{0}", ex); } catch (BadImageFormatException ex) { Trace.TraceError("{0}", ex); } - - return typeVisualizers.Distinct().Select(data => new TypeVisualizerDescriptor(data)).ToArray(); + return Enumerable.Empty(); } public static IObservable GetVisualizerTypes(PackageConfiguration configuration) { if (configuration == null) { - throw new ArgumentNullException("configuration"); + throw new ArgumentNullException(nameof(configuration)); } var assemblies = configuration.AssemblyReferences.Select(reference => reference.AssemblyName); return Observable.Using( - () => new LoaderResource(configuration), - resource => from assemblyRef in assemblies.ToObservable() - let typeVisualizers = resource.Loader.GetReflectionTypeVisualizerAttributes(assemblyRef) - from typeVisualizer in typeVisualizers - select typeVisualizer); + () => LoaderResource.CreateMetadataLoadContext(configuration), + context => from assemblyName in assemblies.ToObservable() + from typeVisualizer in GetReflectionTypeVisualizerTypes(context, assemblyName) + select typeVisualizer); } } } diff --git a/Bonsai/WorkflowElementCategoryConverter.cs b/Bonsai/WorkflowElementCategoryConverter.cs index 032fc4176..8d98e8109 100644 --- a/Bonsai/WorkflowElementCategoryConverter.cs +++ b/Bonsai/WorkflowElementCategoryConverter.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Bonsai.Expressions; -using System.ComponentModel; +using System.Reflection; namespace Bonsai { @@ -9,44 +9,46 @@ static class WorkflowElementCategoryConverter { static bool MatchIgnoredTypes(Type type) { + var typeName = type.AssemblyQualifiedName; #pragma warning disable CS0612 // Type or member is obsolete - return type == typeof(SourceBuilder) || + return typeName == typeof(SourceBuilder).AssemblyQualifiedName || #pragma warning restore CS0612 // Type or member is obsolete - type == typeof(CombinatorBuilder) || - type == typeof(InspectBuilder) || - type == typeof(ExternalizedProperty) || - type == typeof(DisableBuilder); + typeName == typeof(CombinatorBuilder).AssemblyQualifiedName || + typeName == typeof(InspectBuilder).AssemblyQualifiedName || + typeName == typeof(ExternalizedProperty).AssemblyQualifiedName || + typeName == typeof(DisableBuilder).AssemblyQualifiedName; } - static bool MatchAttributeType(Type type, Type attributeType) + static bool MatchAttributeType(CustomAttributeData[] customAttributes, Type attributeType) { - return type.IsDefined(attributeType, true); + return customAttributes.IsDefined(attributeType); } - public static IEnumerable FromType(Type type) + public static IEnumerable FromType(Type type, CustomAttributeData[] customAttributes) { if (MatchIgnoredTypes(type)) yield break; - if (type.IsSubclassOf(typeof(ExpressionBuilder)) || - MatchAttributeType(type, typeof(CombinatorAttribute)) || + if (type.IsMatchSubclassOf(typeof(ExpressionBuilder)) || + MatchAttributeType(customAttributes, typeof(CombinatorAttribute)) || #pragma warning disable CS0612 // Type or member is obsolete - MatchAttributeType(type, typeof(SourceAttribute))) + MatchAttributeType(customAttributes, typeof(SourceAttribute))) #pragma warning restore CS0612 // Type or member is obsolete { - if (type.IsSubclassOf(typeof(WorkflowExpressionBuilder))) + if (type.IsMatchSubclassOf(typeof(WorkflowExpressionBuilder))) { yield return ElementCategory.Nested; } - if (type.IsSubclassOf(typeof(SubjectExpressionBuilder)) || - type == typeof(WorkflowInputBuilder)) + if (type.IsMatchSubclassOf(typeof(SubjectExpressionBuilder)) || + type.AssemblyQualifiedName == typeof(WorkflowInputBuilder).AssemblyQualifiedName) { yield return ~ElementCategory.Combinator; } - var attributes = TypeDescriptor.GetAttributes(type); - var elementCategoryAttribute = (WorkflowElementCategoryAttribute)attributes[typeof(WorkflowElementCategoryAttribute)]; - yield return elementCategoryAttribute.Category; + var elementCategoryAttribute = customAttributes.GetCustomAttributeData(typeof(WorkflowElementCategoryAttribute)); + yield return elementCategoryAttribute != null + ? (ElementCategory)elementCategoryAttribute.GetConstructorArgument() + : WorkflowElementCategoryAttribute.Default.Category; } } } diff --git a/Bonsai/WorkflowElementLoader.cs b/Bonsai/WorkflowElementLoader.cs index bb14645d0..e837bf36e 100644 --- a/Bonsai/WorkflowElementLoader.cs +++ b/Bonsai/WorkflowElementLoader.cs @@ -13,54 +13,74 @@ namespace Bonsai { - sealed class WorkflowElementLoader : MarshalByRefObject + sealed class WorkflowElementLoader { - public WorkflowElementLoader(PackageConfiguration configuration) - { - ConfigurationHelper.SetAssemblyResolve(configuration); - } + const string ExpressionBuilderSuffix = "Builder"; - static bool IsWorkflowElement(Type type) + static bool IsWorkflowElement(Type type, CustomAttributeData[] customAttributes) { - return type.IsSubclassOf(typeof(ExpressionBuilder)) || - type.IsDefined(typeof(CombinatorAttribute), true) || + return type.IsMatchSubclassOf(typeof(ExpressionBuilder)) || + customAttributes.IsDefined(typeof(CombinatorAttribute)) || #pragma warning disable CS0612 // Type or member is obsolete - type.IsDefined(typeof(SourceAttribute), true); + customAttributes.IsDefined(typeof(SourceAttribute)); #pragma warning restore CS0612 // Type or member is obsolete } - static bool IsVisibleElement(Type type) + static bool IsVisibleElement(CustomAttributeData[] customAttributes) { - var visibleAttribute = type.GetCustomAttribute() ?? DesignTimeVisibleAttribute.Default; - return visibleAttribute.Visible; + var visibleAttribute = customAttributes.GetCustomAttributeData(typeof(DesignTimeVisibleAttribute)); + if (visibleAttribute != null) + { + return visibleAttribute.ConstructorArguments.Count > 0 && + (bool)visibleAttribute.ConstructorArguments[0].Value; + } + + return true; } - static IEnumerable GetWorkflowElements(Assembly assembly) + static string RemoveSuffix(string source, string suffix) { - Type[] types; + var suffixStart = source.LastIndexOf(suffix); + return suffixStart >= 0 ? source.Remove(suffixStart) : source; + } - try { types = assembly.GetTypes(); } - catch (ReflectionTypeLoadException ex) + static string GetElementDisplayName(Type type, CustomAttributeData[] customAttributes) + { + var displayNameAttribute = customAttributes.GetCustomAttributeData(typeof(DisplayNameAttribute)); + if (displayNameAttribute != null) { - Trace.TraceError(string.Join(Environment.NewLine, ex.LoaderExceptions)); - yield break; + return (string)displayNameAttribute.GetConstructorArgument() ?? string.Empty; } + return type.IsMatchSubclassOf(typeof(ExpressionBuilder)) + ? RemoveSuffix(type.Name, ExpressionBuilderSuffix) + : type.Name; + } + + static IEnumerable GetWorkflowElements(Assembly assembly) + { + var types = assembly.GetTypes(); for (int i = 0; i < types.Length; i++) { var type = types[i]; - if (type.IsPublic && !type.IsValueType && !type.ContainsGenericParameters && - !type.IsAbstract && IsWorkflowElement(type) && !type.IsDefined(typeof(ObsoleteAttribute)) && - IsVisibleElement(type) && type.GetConstructor(Type.EmptyTypes) != null) + if (!type.IsPublic || type.IsValueType || type.ContainsGenericParameters || type.IsAbstract) { - var descriptionAttribute = (DescriptionAttribute)TypeDescriptor.GetAttributes(type)[typeof(DescriptionAttribute)]; + continue; + } + + var customAttributes = type.GetCustomAttributesData(inherit: true); + if (IsWorkflowElement(type, customAttributes) && + !customAttributes.IsDefined(typeof(ObsoleteAttribute)) && + IsVisibleElement(customAttributes) && type.GetConstructor(Type.EmptyTypes) != null) + { + var descriptionAttribute = customAttributes.GetCustomAttributeData(typeof(DescriptionAttribute)); yield return new WorkflowElementDescriptor { - Name = ExpressionBuilder.GetElementDisplayName(type), + Name = GetElementDisplayName(type, customAttributes), Namespace = type.Namespace, FullyQualifiedName = type.AssemblyQualifiedName, - Description = descriptionAttribute.Description, - ElementTypes = WorkflowElementCategoryConverter.FromType(type).ToArray() + Description = (string)descriptionAttribute?.GetConstructorArgument() ?? string.Empty, + ElementTypes = WorkflowElementCategoryConverter.FromType(type, customAttributes).ToArray() }; } } @@ -79,7 +99,9 @@ static IEnumerable GetWorkflowElements(Assembly assem try { var metadataType = assembly.GetType(name); - if (metadataType != null && metadataType.IsDefined(typeof(ObsoleteAttribute))) + if (metadataType != null && + metadataType.GetCustomAttributesData(inherit: true) + .IsDefined(typeof(ObsoleteAttribute))) { continue; } @@ -87,7 +109,7 @@ static IEnumerable GetWorkflowElements(Assembly assem using (var reader = XmlReader.Create(resourceStream, new XmlReaderSettings { IgnoreWhitespace = true })) { reader.ReadStartElement(typeof(WorkflowBuilder).Name); - if (reader.Name == "Description") + if (reader.Name == nameof(WorkflowBuilder.Description)) { reader.ReadStartElement(); description = reader.Value; @@ -113,36 +135,33 @@ static IEnumerable GetWorkflowElements(Assembly assem } } - WorkflowElementDescriptor[] GetReflectionWorkflowElementTypes(string assemblyRef) + static IEnumerable GetReflectionWorkflowElementTypes(MetadataLoadContext context, string assemblyName) { - var types = Enumerable.Empty(); try { - var assembly = Assembly.Load(assemblyRef); - types = types.Concat(GetWorkflowElements(assembly)); + var assembly = context.LoadFromAssemblyName(assemblyName); + return GetWorkflowElements(assembly); } catch (FileLoadException ex) { Trace.TraceError("{0}", ex); } catch (FileNotFoundException ex) { Trace.TraceError("{0}", ex); } catch (BadImageFormatException ex) { Trace.TraceError("{0}", ex); } - - return types.Distinct().ToArray(); + return Enumerable.Empty(); } public static IObservable> GetWorkflowElementTypes(PackageConfiguration configuration) { if (configuration == null) { - throw new ArgumentNullException("configuration"); + throw new ArgumentNullException(nameof(configuration)); } var assemblies = configuration.AssemblyReferences.Select(reference => reference.AssemblyName); return Observable.Using( - () => new LoaderResource(configuration), - resource => from assemblyRef in assemblies.ToObservable() - from package in resource.Loader - .GetReflectionWorkflowElementTypes(assemblyRef) - .GroupBy(element => element.Namespace) - select package); + () => LoaderResource.CreateMetadataLoadContext(configuration), + context => from assemblyName in assemblies.ToObservable() + from package in GetReflectionWorkflowElementTypes(context, assemblyName) + .GroupBy(element => element.Namespace) + select package); } } } diff --git a/README.md b/README.md index 78e61f1dc..605d766ba 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Building from Source 1. Install the [Wix Toolset build tools](https://wixtoolset.org/releases/) version 3.11 or greater. 2. From Visual Studio menu, select `Extensions` > `Manage Extensions` and install the WiX Toolset Visual Studio 2022 Extension. +### Debugging + +The new bootstrapper logic makes use of isolated child processes to manage local editor extensions. To make it easier to debug the entire process tree we recommend installing the [Child Process Debugging Power Tool](https://devblogs.microsoft.com/devops/introducing-the-child-process-debugging-power-tool/) extension. + Getting Help ------------