From 67d58e2fb31afb159de3d007e20841c965ce0f5c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 26 May 2023 21:00:23 +0100 Subject: [PATCH 01/14] Rewrite type loaders using MetadataLoadContext --- Bonsai.Editor/TypeVisualizerDescriptor.cs | 28 ++++++ Bonsai/Bonsai.csproj | 1 + Bonsai/DependencyInspector.cs | 37 +++----- Bonsai/LoaderResource.cs | 27 ++---- Bonsai/PackageAssemblyResolver.cs | 50 +++++++++++ Bonsai/ReflectionHelper.cs | 92 ++++++++++++++++++++ Bonsai/TypeVisualizerLoader.cs | 61 +++++-------- Bonsai/WorkflowElementCategoryConverter.cs | 38 +++++---- Bonsai/WorkflowElementLoader.cs | 99 +++++++++++++--------- 9 files changed, 292 insertions(+), 141 deletions(-) create mode 100644 Bonsai/PackageAssemblyResolver.cs create mode 100644 Bonsai/ReflectionHelper.cs 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/Bonsai.csproj b/Bonsai/Bonsai.csproj index b9b5782a4..8a224038f 100644 --- a/Bonsai/Bonsai.csproj +++ b/Bonsai/Bonsai.csproj @@ -14,6 +14,7 @@ App.manifest + all runtime; build; native; contentfiles; analyzers; buildtransitive 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/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/ReflectionHelper.cs b/Bonsai/ReflectionHelper.cs new file mode 100644 index 000000000..88621010e --- /dev/null +++ b/Bonsai/ReflectionHelper.cs @@ -0,0 +1,92 @@ +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(CustomAttributeData.GetCustomAttributes(type)); + 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.AttributeType.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.AttributeType.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..4558aa338 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 = CustomAttributeData.GetCustomAttributes(assembly); + 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); } } } From 740277996f80d93ec55c7b678b85ac02f22736bb Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 26 May 2023 21:59:57 +0100 Subject: [PATCH 02/14] Internalize all MetadataLoadContext dependencies --- Bonsai/Bonsai.csproj | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Bonsai/Bonsai.csproj b/Bonsai/Bonsai.csproj index 8a224038f..f0842317c 100644 --- a/Bonsai/Bonsai.csproj +++ b/Bonsai/Bonsai.csproj @@ -14,7 +14,7 @@ App.manifest - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -45,6 +45,13 @@ $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework) + + + + + + + From fa3bd5ecf9a1e72f553093814a184b6810c06c03 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 11:20:18 +0100 Subject: [PATCH 03/14] Rewrite bootstrapper logic using child processes --- Bonsai/AppResult.cs | 57 ++++++++++++++++++++++++++++++--------- Bonsai/Program.cs | 66 +++++++++++++++++++++++++-------------------- 2 files changed, 82 insertions(+), 41 deletions(-) diff --git a/Bonsai/AppResult.cs b/Bonsai/AppResult.cs index 9f6445e70..c18d42b5a 100644 --- a/Bonsai/AppResult.cs +++ b/Bonsai/AppResult.cs @@ -1,34 +1,67 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Reactive.Disposables; +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(Stream stream) { - var resultHolder = (ResultHolder)domain.CreateInstanceAndUnwrap( - typeof(ResultHolder).Assembly.FullName, - typeof(ResultHolder).FullName); - return resultHolder.Result; + Values = new(); + if (stream == null) + { + return Disposable.Empty; + } + + var writer = new JsonTextWriter(new StreamWriter(stream)); + return Disposable.Create(() => + { + try { Serializer.Serialize(writer, Values); } + finally { writer.Close(); } + }); } - public static void SetResult(TResult result) + public static IDisposable OpenRead(Stream stream) { - ResultHolder.ResultValue = result; + using var reader = new JsonTextReader(new StreamReader(stream)); + Values = Serializer.Deserialize>(reader); + return Disposable.Empty; } - class ResultHolder : MarshalByRefObject + public static TResult GetResult() { - public static TResult ResultValue; + if (Values == null) + { + throw new InvalidOperationException("No output stream has been opened for reading."); + } - public ResultHolder() + if (Values.TryGetValue(typeof(TResult).FullName, out string value)) { + if (typeof(TResult).IsEnum) + { + return (TResult)Enum.Parse(typeof(TResult), value); + } + + return (TResult)(object)value; } - public TResult Result + return default; + } + + public static void SetResult(TResult result) + { + if (Values == null) { - get { return ResultValue; } + throw new InvalidOperationException("No output stream has been opened for writing."); } + + Values[typeof(TResult).FullName] = result.ToString(); } } } diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 91aaebdec..983872854 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -3,8 +3,10 @@ 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 @@ -27,6 +29,7 @@ static class Program const string ExportImageCommand = "--export-image"; const string ReloadEditorCommand = "--reload-editor"; const string GalleryCommand = "--gallery"; + const string PipeCommand = "--@pipe"; const string EditorDomainName = "EditorDomain"; const string RepositoryPath = "Packages"; const string ExtensionsPath = "Extensions"; @@ -51,6 +54,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 +66,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 +98,8 @@ internal static int Main(string[] args) var packageConfiguration = ConfigurationHelper.Load(); if (!bootstrap) { + using var pipeClient = pipeHandle != null ? new AnonymousPipeClientStream(PipeDirection.Out, pipeHandle) : null; + using var pipeWriter = AppResult.OpenWrite(pipeClient); if (launchResult == EditorResult.Exit) { if (!string.IsNullOrEmpty(initialFileName)) launchResult = EditorResult.ReloadEditor; @@ -170,40 +177,42 @@ internal static int Main(string[] args) args = Array.FindAll(args, arg => arg != DebugScriptCommand); do { - string[] editorArgs; + var editorArgs = new List(args); 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); } - 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 AnonymousPipeServerStream( + PipeDirection.In, + HandleInheritability.Inheritable); + var pipeName = pipeServer.GetClientHandleAsString(); + editorArgs.Add(PipeCommand + ":" + pipeName); - var editorFlags = AppResult.GetResult(editorDomain); - launchResult = AppResult.GetResult(editorDomain); + var setupInfo = new ProcessStartInfo(); + setupInfo.FileName = Assembly.GetEntryAssembly().Location; + setupInfo.Arguments = string.Join(" ", editorArgs); + setupInfo.WorkingDirectory = Environment.CurrentDirectory; + setupInfo.RedirectStandardOutput = true; + setupInfo.RedirectStandardError = true; + setupInfo.UseShellExecute = false; + var process = Process.Start(setupInfo); + process.WaitForExit(); + + using var pipeReader = AppResult.OpenRead(pipeServer); + launchResult = AppResult.GetResult(); if (launchEditor) { if (launchResult == EditorResult.ReloadEditor) launchEditor = false; @@ -215,7 +224,7 @@ 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; @@ -226,13 +235,12 @@ internal static int Main(string[] args) } 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)process.ExitCode; } - - AppDomain.Unload(editorDomain); } while (launchResult != EditorResult.Exit); } From 4f0b377f0f037574afa1de00232f65581e890d2b Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 11:32:27 +0100 Subject: [PATCH 04/14] Add support for child process debugging --- Bonsai/Properties/launchSettings.json | 8 ++++++++ README.md | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 Bonsai/Properties/launchSettings.json 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/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 ------------ From e3f0ffc3484b2fa1db02af8ec4333977070319d4 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 11:52:38 +0100 Subject: [PATCH 05/14] Avoid redirecting standard output --- Bonsai/Program.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 983872854..4a9774c35 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -205,8 +205,6 @@ internal static int Main(string[] args) setupInfo.FileName = Assembly.GetEntryAssembly().Location; setupInfo.Arguments = string.Join(" ", editorArgs); setupInfo.WorkingDirectory = Environment.CurrentDirectory; - setupInfo.RedirectStandardOutput = true; - setupInfo.RedirectStandardError = true; setupInfo.UseShellExecute = false; var process = Process.Start(setupInfo); process.WaitForExit(); From 7fdec81b9fe320b5fa435a638d30e39f1b91cfc2 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 12:15:14 +0100 Subject: [PATCH 06/14] Avoid loading package dependencies on bootstrap --- Bonsai/AppResult.cs | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Bonsai/AppResult.cs b/Bonsai/AppResult.cs index c18d42b5a..1501ff33d 100644 --- a/Bonsai/AppResult.cs +++ b/Bonsai/AppResult.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reactive.Disposables; +using System.Threading; using Newtonsoft.Json; namespace Bonsai @@ -16,11 +16,11 @@ public static IDisposable OpenWrite(Stream stream) Values = new(); if (stream == null) { - return Disposable.Empty; + return EmptyDisposable.Instance; } var writer = new JsonTextWriter(new StreamWriter(stream)); - return Disposable.Create(() => + return new AnonymousDisposable(() => { try { Serializer.Serialize(writer, Values); } finally { writer.Close(); } @@ -31,7 +31,7 @@ public static IDisposable OpenRead(Stream stream) { using var reader = new JsonTextReader(new StreamReader(stream)); Values = Serializer.Deserialize>(reader); - return Disposable.Empty; + return EmptyDisposable.Instance; } public static TResult GetResult() @@ -63,5 +63,33 @@ public static void SetResult(TResult result) 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 EmptyDisposable : IDisposable + { + public static readonly EmptyDisposable Instance = new(); + + private EmptyDisposable() + { + } + + public void Dispose() + { + } + } } } From 9a48b3d253a2afd125d626c4b3091fc37a108d70 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 13:38:45 +0100 Subject: [PATCH 07/14] Ensure working directory is set --- Bonsai/Program.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 4a9774c35..9569daac2 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -178,6 +178,7 @@ internal static int Main(string[] args) do { var editorArgs = new List(args); + var workingDirectory = Environment.CurrentDirectory; if (launchEditor && startScreen) launchResult = EditorResult.Exit; if (launchResult == EditorResult.ExportPackage) editorArgs.AddRange(new[] { initialFileName, ExportPackageCommand }); else if (launchResult == EditorResult.OpenGallery) editorArgs.Add(GalleryCommand); @@ -192,7 +193,11 @@ internal static int Main(string[] args) if (debugScripts) editorArgs.Add(DebugScriptCommand); if (launchResult == EditorResult.ReloadEditor) editorArgs.Add(ReloadEditorCommand); else editorArgs.Add(SuppressBootstrapCommand); - if (!string.IsNullOrEmpty(initialFileName)) editorArgs.Add(initialFileName); + if (!string.IsNullOrEmpty(initialFileName)) + { + editorArgs.Add(initialFileName); + workingDirectory = Path.GetDirectoryName(initialFileName); + } } using var pipeServer = new AnonymousPipeServerStream( @@ -204,7 +209,7 @@ internal static int Main(string[] args) var setupInfo = new ProcessStartInfo(); setupInfo.FileName = Assembly.GetEntryAssembly().Location; setupInfo.Arguments = string.Join(" ", editorArgs); - setupInfo.WorkingDirectory = Environment.CurrentDirectory; + setupInfo.WorkingDirectory = workingDirectory; setupInfo.UseShellExecute = false; var process = Process.Start(setupInfo); process.WaitForExit(); @@ -226,7 +231,6 @@ internal static int Main(string[] args) if (!string.IsNullOrEmpty(result) && File.Exists(result)) { initialFileName = result; - Environment.CurrentDirectory = Path.GetDirectoryName(initialFileName); } } launchResult = EditorResult.ReloadEditor; From d4b6efd39ca9d9007f2cfb484e95cbe9a65bfc66 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 13:48:45 +0100 Subject: [PATCH 08/14] Ensure all local pipe handles are disposed --- Bonsai/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 9569daac2..59b8acb29 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -212,6 +212,7 @@ internal static int Main(string[] args) setupInfo.WorkingDirectory = workingDirectory; setupInfo.UseShellExecute = false; var process = Process.Start(setupInfo); + pipeServer.DisposeLocalCopyOfClientHandle(); process.WaitForExit(); using var pipeReader = AppResult.OpenRead(pipeServer); From 3a039e52675473a1436d9de67ced4e8d4bb32e37 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 19:23:42 +0100 Subject: [PATCH 09/14] Ensure commands are not passed as arguments --- Bonsai.Configuration/CommandLineParser.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); } } From 36f2953027e2efec18166efafa5f70b95059e306 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 19:25:30 +0100 Subject: [PATCH 10/14] Remove unused constants --- Bonsai/Program.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 59b8acb29..699e6f787 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -13,7 +13,6 @@ namespace Bonsai { static class Program { - const string PathEnvironmentVariable = "PATH"; const string StartCommand = "--start"; const string LibraryCommand = "--lib"; const string PropertyCommand = "--property"; @@ -30,7 +29,6 @@ static class Program const string ReloadEditorCommand = "--reload-editor"; const string GalleryCommand = "--gallery"; const string PipeCommand = "--@pipe"; - const string EditorDomainName = "EditorDomain"; const string RepositoryPath = "Packages"; const string ExtensionsPath = "Extensions"; internal const int NormalExitCode = 0; From 5f72834b505d776d9c4c2afc7ac0963db075a177 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 19:26:11 +0100 Subject: [PATCH 11/14] Prefer named to anonymous pipes for compatibility --- Bonsai/AppResult.cs | 14 +++++++++++--- Bonsai/Program.cs | 12 +++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Bonsai/AppResult.cs b/Bonsai/AppResult.cs index 1501ff33d..71c435099 100644 --- a/Bonsai/AppResult.cs +++ b/Bonsai/AppResult.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Pipes; using System.Threading; using Newtonsoft.Json; @@ -11,7 +12,7 @@ static class AppResult static readonly JsonSerializer Serializer = JsonSerializer.CreateDefault(); static Dictionary Values; - public static IDisposable OpenWrite(Stream stream) + public static IDisposable OpenWrite(NamedPipeClientStream stream) { Values = new(); if (stream == null) @@ -19,10 +20,17 @@ public static IDisposable OpenWrite(Stream stream) return EmptyDisposable.Instance; } - var writer = new JsonTextWriter(new StreamWriter(stream)); + stream.Connect(); + var writer = new StreamWriter(stream); return new AnonymousDisposable(() => { - try { Serializer.Serialize(writer, Values); } + try + { + Serializer.Serialize(writer, Values); + writer.Flush(); + try { stream.WaitForPipeDrain(); } + catch (NotSupportedException) { } + } finally { writer.Close(); } }); } diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index 699e6f787..78e56d55d 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -96,7 +96,7 @@ internal static int Main(string[] args) var packageConfiguration = ConfigurationHelper.Load(); if (!bootstrap) { - using var pipeClient = pipeHandle != null ? new AnonymousPipeClientStream(PipeDirection.Out, pipeHandle) : null; + using var pipeClient = pipeHandle != null ? new NamedPipeClientStream(".", pipeHandle, PipeDirection.Out) : null; using var pipeWriter = AppResult.OpenWrite(pipeClient); if (launchResult == EditorResult.Exit) { @@ -172,6 +172,7 @@ 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 { @@ -198,10 +199,7 @@ internal static int Main(string[] args) } } - using var pipeServer = new AnonymousPipeServerStream( - PipeDirection.In, - HandleInheritability.Inheritable); - var pipeName = pipeServer.GetClientHandleAsString(); + using var pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.In); editorArgs.Add(PipeCommand + ":" + pipeName); var setupInfo = new ProcessStartInfo(); @@ -210,10 +208,10 @@ internal static int Main(string[] args) setupInfo.WorkingDirectory = workingDirectory; setupInfo.UseShellExecute = false; var process = Process.Start(setupInfo); - pipeServer.DisposeLocalCopyOfClientHandle(); + pipeServer.WaitForConnection(); + using var pipeReader = AppResult.OpenRead(pipeServer); process.WaitForExit(); - using var pipeReader = AppResult.OpenRead(pipeServer); launchResult = AppResult.GetResult(); if (launchEditor) { From 66797e054eaecca89c10b098506c6648acafba37 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 30 May 2023 21:21:47 +0100 Subject: [PATCH 12/14] Avoid using AttributeType for mono compatibility --- Bonsai.Editor/TypeDefinitionProvider.cs | 5 +++-- Bonsai/ReflectionHelper.cs | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) 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/ReflectionHelper.cs b/Bonsai/ReflectionHelper.cs index 88621010e..6d79c20b1 100644 --- a/Bonsai/ReflectionHelper.cs +++ b/Bonsai/ReflectionHelper.cs @@ -17,7 +17,7 @@ public static CustomAttributeData[] GetCustomAttributesData(this Type type, bool var attributeLists = new List>(); while (type != null) { - attributeLists.Add(CustomAttributeData.GetCustomAttributes(type)); + attributeLists.Add(type.GetCustomAttributesData()); type = inherit ? type.BaseType : null; } @@ -36,7 +36,8 @@ public static CustomAttributeData[] GetCustomAttributesData(this Type type, bool public static IEnumerable OfType(this IEnumerable customAttributes) { var attributeTypeName = typeof(TAttribute).FullName; - return customAttributes.Where(attribute => attribute.AttributeType.FullName == attributeTypeName); + return customAttributes.Where( + attribute => attribute.Constructor.DeclaringType.FullName == attributeTypeName); } public static bool IsDefined(this CustomAttributeData[] customAttributes, Type attributeType) @@ -55,7 +56,7 @@ public static CustomAttributeData GetCustomAttributeData( return Array.Find( customAttributes, - attribute => attribute.AttributeType.FullName == attributeType.FullName); + attribute => attribute.Constructor.DeclaringType.FullName == attributeType.FullName); } public static object GetConstructorArgument(this CustomAttributeData attribute) From 228179082cb1fdb17b017021d39468b9bf4d2c79 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 1 Jun 2023 11:31:19 +0100 Subject: [PATCH 13/14] Use assembly virtual method for mono compatibility --- Bonsai/TypeVisualizerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bonsai/TypeVisualizerLoader.cs b/Bonsai/TypeVisualizerLoader.cs index 4558aa338..e5c99ce23 100644 --- a/Bonsai/TypeVisualizerLoader.cs +++ b/Bonsai/TypeVisualizerLoader.cs @@ -14,7 +14,7 @@ sealed class TypeVisualizerLoader : MarshalByRefObject { static IEnumerable GetCustomAttributeTypes(Assembly assembly) { - var assemblyAttributes = CustomAttributeData.GetCustomAttributes(assembly); + var assemblyAttributes = assembly.GetCustomAttributesData(); var typeVisualizers = assemblyAttributes .OfType() .Select(attribute => new TypeVisualizerDescriptor(attribute)); From 4edc270b12583018d5933c696ddc88d8597dc5fa Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 2 Jun 2023 20:50:27 +0100 Subject: [PATCH 14/14] Avoid reinterpreting process exit codes --- Bonsai/AppResult.cs | 9 ++------- Bonsai/Launcher.cs | 3 ++- Bonsai/Program.cs | 7 +++++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Bonsai/AppResult.cs b/Bonsai/AppResult.cs index 71c435099..fe294e2f9 100644 --- a/Bonsai/AppResult.cs +++ b/Bonsai/AppResult.cs @@ -44,19 +44,14 @@ public static IDisposable OpenRead(Stream stream) public static TResult GetResult() { - if (Values == null) - { - throw new InvalidOperationException("No output stream has been opened for reading."); - } - - if (Values.TryGetValue(typeof(TResult).FullName, out string value)) + if (Values != null && Values.TryGetValue(typeof(TResult).FullName, out string value)) { if (typeof(TResult).IsEnum) { return (TResult)Enum.Parse(typeof(TResult), value); } - return (TResult)(object)value; + return (TResult)Convert.ChangeType(value, typeof(TResult)); } return default; 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/Program.cs b/Bonsai/Program.cs index 78e56d55d..d3d8eed8a 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -114,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; } } @@ -211,6 +212,8 @@ internal static int Main(string[] args) pipeServer.WaitForConnection(); using var pipeReader = AppResult.OpenRead(pipeServer); process.WaitForExit(); + if (process.ExitCode != 0) + return process.ExitCode; launchResult = AppResult.GetResult(); if (launchEditor) @@ -238,7 +241,7 @@ internal static int Main(string[] args) debugScripts = editorFlags.HasFlag(EditorFlags.DebugScripts); updatePackages = editorFlags.HasFlag(EditorFlags.UpdatesAvailable); initialFileName = AppResult.GetResult(); - launchResult = (EditorResult)process.ExitCode; + launchResult = (EditorResult)AppResult.GetResult(); } } while (launchResult != EditorResult.Exit);