diff --git a/paket.dependencies b/paket.dependencies index 39c79408..39cbc071 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -23,6 +23,8 @@ nuget FsPickler.Json ~> 5.3.2 nuget Unofficial.Typography ~> 0.1.0 nuget FuzzySharp ~> 2.0.2 +nuget SingleFileExtractor.Core ~> 2.2.1 + group CodeGenerator framework net8.0 storage: none diff --git a/paket.lock b/paket.lock index 1d76c669..127c631a 100644 --- a/paket.lock +++ b/paket.lock @@ -26,6 +26,8 @@ NUGET Microsoft.NETCore.Platforms (7.0.4) Microsoft.NETCore.Targets (5.0) Newtonsoft.Json (13.0.3) + SingleFileExtractor.Core (2.2.1) + System.Memory (>= 4.5.5) - restriction: || (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) System.Buffers (4.5.1) - restriction: || (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) System.Collections (4.3) Microsoft.NETCore.Platforms (>= 1.1) @@ -87,7 +89,7 @@ NUGET System.Runtime (>= 4.3) System.Runtime.Extensions (>= 4.3) System.Threading (>= 4.3) - System.Memory (4.5.5) - restriction: || (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) + System.Memory (4.5.5) - restriction: || (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) System.Buffers (>= 4.5.1) - restriction: || (&& (== net8.0) (>= monotouch)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp2.0)) (&& (== net8.0) (< netstandard1.1)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= xamarinios)) (&& (== net8.0) (>= xamarinmac)) (&& (== net8.0) (>= xamarintvos)) (&& (== net8.0) (>= xamarinwatchos)) (== netstandard2.0) System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net8.0) (< netcoreapp2.0)) (== netstandard2.0) System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net8.0) (>= monotouch)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp2.0)) (&& (== net8.0) (< netcoreapp2.1)) (&& (== net8.0) (< netstandard1.1)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= uap10.1)) (&& (== net8.0) (>= xamarinios)) (&& (== net8.0) (>= xamarinmac)) (&& (== net8.0) (>= xamarintvos)) (&& (== net8.0) (>= xamarinwatchos)) (== netstandard2.0) diff --git a/src/Aardvark.Base/Introspection/Introspection.cs b/src/Aardvark.Base/Introspection/Introspection.cs index 97ee5abc..86b38744 100644 --- a/src/Aardvark.Base/Introspection/Introspection.cs +++ b/src/Aardvark.Base/Introspection/Introspection.cs @@ -6,15 +6,57 @@ using System.IO.Compression; using System.Linq; using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; +using BundleReader = SingleFileExtractor.Core.ExecutableReader; +using BundleFileEntry = SingleFileExtractor.Core.FileEntry; namespace Aardvark.Base { + internal static class FileUtils + { + public static DateTime GetLastWriteTimeSafe(string path) + { + try + { + return File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MaxValue; + } + catch (Exception) + { + Report.Warn($"Could not get write time for: {path}"); + return DateTime.MaxValue; + } + } + } + + internal static class AssemblyExtenions + { + public static string GetLocationSafe(this Assembly assembly) + { + try + { + var location = assembly.Location; + return location.IsNullOrEmpty() ? null : location; + } + catch { return null; } + } + + public static bool HasLocation(this Assembly assembly) + => assembly.GetLocationSafe() != null; + + public static DateTime GetLastWriteTimeSafe(this Assembly assembly) + { + var location = assembly?.GetLocationSafe() ?? IntrospectionProperties.CurrentEntryBundle; + return FileUtils.GetLastWriteTimeSafe(location); + } + } + public static class CachingProperties { /// @@ -102,7 +144,7 @@ internal static string GetIdentifier(this Assembly asm, NamingScheme scheme) return scheme switch { NamingScheme.Version => asm.GetName().Version.ToString(), - NamingScheme.Timestamp => IntrospectionProperties.GetLastWriteTimeUtc(asm).ToBinary().ToString(), + NamingScheme.Timestamp => asm.GetLastWriteTimeSafe().ToBinary().ToString(), _ => "" }; } @@ -136,10 +178,47 @@ public static class IntrospectionProperties public static bool NativeLibraryUnpackingAllowed = true; - public static string CurrentEntryPath => (CurrentEntryAssembly != null) - ? Path.GetDirectoryName(CurrentEntryAssembly.Location) - : null - ; + public static string CurrentEntryPath + { + get + { + var location = CurrentEntryAssembly?.GetLocationSafe(); + return (location != null) ? Path.GetDirectoryName(location) : null; + } + } + + /// + /// Returns the path to the single file deployed entry bundle if it exists, null otherwise. + /// + public static string CurrentEntryBundle + { + get + { + var entryAssembly = CurrentEntryAssembly; + + // If Location is empty or null, we might have a single-file application + if (entryAssembly != null && !entryAssembly.HasLocation()) + { + var name = entryAssembly.GetName().Name; + var ext = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""; + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, name + ext); + + if (File.Exists(path)) + { + return path; + } + else + { + Report.Warn($"Could not find bundle executable: {path}"); + return null; + } + } + else + { + return null; + } + } + } private static readonly HashSet s_defaultAssemblyBlacklist = new HashSet( @@ -251,28 +330,12 @@ public static bool AssemblyFilter(string name) else return AssemblyFilter(a.GetName().Name); }; + [Obsolete] public static string BundleEntryPoint = ""; - /// - /// Robustly tries to get DateTime also for bundled deployments. Use BundleEntryPoint to register dummy entry as a first fallback. returns DateTime.Now if all fallbacks fail. - /// - public static Func GetLastWriteTimeUtc = (Assembly assembly) => - { - if (!String.IsNullOrEmpty(assembly.Location) && File.Exists(assembly.Location)) // first choise - won't work for bundled deplyoments? - { - return File.GetLastWriteTimeUtc(assembly.Location); - - } - else if (File.Exists(BundleEntryPoint)) // fallback 1 - use bundle entrypoint - { - return File.GetLastWriteTimeUtc(BundleEntryPoint); - } - else - { - // no option left.... - return DateTime.Now; - } - }; + [Obsolete] + public static Func GetLastWriteTimeUtc = + (Assembly assembly) => assembly.GetLastWriteTimeSafe(); } public static class Introspection @@ -446,7 +509,7 @@ static Introspection() { if (typeof(Aardvark).Assembly != null) { - Report.Warn("Assembly.GetEntryAssembly() == null && IntrospectionProperties.CurrentEntryAssembly == null. This might be due to nunit like setups. trying to use typeof.Assembly instead: {0}", typeof(Aardvark).Assembly.Location); + Report.Warn("Assembly.GetEntryAssembly() == null && IntrospectionProperties.CurrentEntryAssembly == null. This might be due to nunit like setups. trying to use typeof.Assembly instead: {0}", typeof(Aardvark).Assembly.GetLocationSafe()); IntrospectionProperties.CustomEntryAssembly = typeof(Aardvark).Assembly; RegisterAllAssembliesInCustomEntryPath(); } else @@ -558,10 +621,9 @@ private static void EnumerateAssemblies(string name, Assembly customAssembly = n private static string GetQueryCacheFilename(Assembly asm, Guid queryGuid) { - var name = Path.GetFileName(asm.Location); + var name = asm.GetName().Name; var id = asm.GetIdentifier(CachingProperties.IntrospectionCacheFileNaming); - var fname = string.Format("{0}_{1}_{2}.txt", name, id, queryGuid); - return Path.Combine(CacheDirectory, fname); + return Path.Combine(CacheDirectory, $"{name}_{id}_{queryGuid}.query"); } private class CacheFileHeader @@ -604,7 +666,7 @@ Func> encode try { cacheFileName = GetQueryCacheFilename(a, discriminator.ToGuid()); - assemblyTimeStamp = IntrospectionProperties.GetLastWriteTimeUtc(a); + assemblyTimeStamp = a.GetLastWriteTimeSafe(); // for standalone deployments cacheFileNames cannot be retrieved robustly - we skip those if (!String.IsNullOrEmpty(cacheFileName) && File.Exists(cacheFileName)) @@ -734,6 +796,7 @@ internal static class Dl } + // TODO: static class [Serializable] public class Aardvark { @@ -778,308 +841,430 @@ public void Serialize(Stream stream) } } - private static string InitializeCacheDirectory() - { - var path = Path.Combine(CachingProperties.CacheDirectory, "Plugins"); - - if (!Directory.Exists(path)) + private static readonly Lazy s_pluginsCacheDirectory = + new (() => { - Directory.CreateDirectory(path); - } + var path = Path.Combine(CachingProperties.CacheDirectory, "Plugins"); - return path; - } + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } - private static readonly Lazy s_cacheDirectory = new Lazy(InitializeCacheDirectory); + return path; + }); /// /// Returns the directory of the plugins cache files. /// - public static string CacheDirectory => s_cacheDirectory.Value; + public static string PluginsCacheDirectory => s_pluginsCacheDirectory.Value; + + [Obsolete("Use PluginsCacheDirectory instead.")] + public static string CacheDirectory => PluginsCacheDirectory; + + private static readonly Lazy s_pluginsCacheFile = + new(() => + { + Assembly entryAssembly = IntrospectionProperties.CurrentEntryAssembly; + string entryAssemblyName = entryAssembly?.GetName().Name ?? "unknown"; + string entryAssemblyId = entryAssembly?.GetIdentifier(CachingProperties.PluginsCacheFileNaming) ?? "unknown"; + string fileName = string.Format("{0}_{1}_plugins.xml", entryAssemblyName, entryAssemblyId); + return Path.Combine(PluginsCacheDirectory, fileName); + }); + + /// + /// Returns the path of the plugins cache file. + /// + public static string PluginsCacheFile => s_pluginsCacheFile.Value; + + [Obsolete("Use PluginsCacheFile instead.")] public string CacheFile = string.Empty; + [Obsolete("Use static methods instead.")] public Aardvark() { - Assembly asm = IntrospectionProperties.CurrentEntryAssembly; - string entryAssemblyName = asm?.GetName().Name ?? "unknown"; - string entryAssemblyId = asm?.GetIdentifier(CachingProperties.PluginsCacheFileNaming) ?? "unknown"; - string fileName = string.Format("{0}_{1}_plugins.xml", entryAssemblyName, entryAssemblyId); - - CacheFile = Path.Combine(CacheDirectory, fileName); } - private PluginCache ReadCacheFile() + private static PluginCache ReadCacheFile() { - if (File.Exists(CacheFile)) + if (File.Exists(PluginsCacheFile)) { try { - using var stream = new FileStream(CacheFile, FileMode.Open); + using var stream = new FileStream(PluginsCacheFile, FileMode.Open); var result = PluginCache.Deserialize(stream); - Report.Line(3, "[ReadCacheFile] loaded cache file: {0}", CacheFile); + Report.Line(3, $"[ReadCacheFile] Loaded plugins cache file: {PluginsCacheFile}"); return result; } catch (Exception e) { - Report.Line(3, "[ReadCacheFile] could not load cache file {0}: {1}", CacheFile, e.Message); + Report.Line(3, $"[ReadCacheFile] Could not load plugins cache file '{PluginsCacheFile}': {e.Message}"); return new PluginCache(); } } else { - Report.Line(3, "[ReadCacheFile] no plugins cache file found at {0}", CacheFile); + Report.Line(3, $"[ReadCacheFile] Using new plugins cache file: {PluginsCacheFile}"); return new PluginCache(); } } - private void WriteCacheFile(PluginCache cache) + + private static void WriteCacheFile(PluginCache cache) { - if (string.IsNullOrEmpty(CacheFile)) + if (string.IsNullOrEmpty(PluginsCacheFile)) { - Report.Warn("Could not write cache file since CacheFile was null or empty"); + Report.Warn("Could not write plugins cache file since Aardvark.PluginCacheFile was null or empty"); } else { try { - if (File.Exists(CacheFile)) File.Delete(CacheFile); + if (File.Exists(PluginsCacheFile)) File.Delete(PluginsCacheFile); - using var stream = new FileStream(CacheFile, FileMode.CreateNew); + using var stream = new FileStream(PluginsCacheFile, FileMode.CreateNew); cache.Serialize(stream); } - catch(Exception ex) + catch(Exception e) { - Report.Warn("Could not write cache file: {0}", ex.Message); + Report.Warn($"Could not write plugins cache file '{PluginsCacheFile}': {e.Message}"); } } } private static readonly Regex versionRx = new Regex(@"^[ \t]*(?[\.A-Za-z_0-9]+)[ \t]*,[ \t]*(v|V)ersion[ \t]*=[ \t]*(?[\.A-Za-z_0-9]+)$"); - private static unsafe bool IsPlugin(string file) + private static unsafe bool ProbeForPlugin(Stream stream) { - try - { - using (var s = File.OpenRead(file)) - using (var v = new System.Reflection.PortableExecutable.PEReader(s)) - { - if (v.PEHeaders.CorHeader == null || !v.HasMetadata) return false; - var data = v.GetMetadata(); - var m = new System.Reflection.Metadata.MetadataReader(data.Pointer, data.Length); + using var v = new PEReader(stream, PEStreamOptions.LeaveOpen); + if (v.PEHeaders.CorHeader == null || !v.HasMetadata) return false; + var data = v.GetMetadata(); + var m = new MetadataReader(data.Pointer, data.Length); - var assdef = m.GetAssemblyDefinition(); - foreach (var att in assdef.GetCustomAttributes()) + var assdef = m.GetAssemblyDefinition(); + foreach (var att in assdef.GetCustomAttributes()) + { + var attDef = m.GetCustomAttribute(att); + if (attDef.Constructor.Kind == HandleKind.MemberReference) + { + var hh = (MemberReferenceHandle)attDef.Constructor; + var e = m.GetMemberReference(hh); + var pp = e.Parent; + if (pp.Kind == HandleKind.TypeReference) { - var attDef = m.GetCustomAttribute(att); - if (attDef.Constructor.Kind == System.Reflection.Metadata.HandleKind.MemberReference) + var attType = m.GetTypeReference((TypeReferenceHandle)pp); + var nameStr = m.GetString(attType.Name); + var nsStr = m.GetString(attType.Namespace); + if (nsStr == "System.Runtime.Versioning" && nameStr == "TargetFrameworkAttribute") { - var hh = (System.Reflection.Metadata.MemberReferenceHandle)attDef.Constructor; - var e = m.GetMemberReference(hh); - var pp = e.Parent; - if (pp.Kind == System.Reflection.Metadata.HandleKind.TypeReference) + var reader = m.GetBlobReader(attDef.Value); + if (reader.ReadUInt16() == 1) { - var attType = m.GetTypeReference((System.Reflection.Metadata.TypeReferenceHandle)pp); - var nameStr = m.GetString(attType.Name); - var nsStr = m.GetString(attType.Namespace); - if (nsStr == "System.Runtime.Versioning" && nameStr == "TargetFrameworkAttribute") + var version = reader.ReadSerializedString(); + var match = versionRx.Match(version); + if (match.Success) { - var reader = m.GetBlobReader(attDef.Value); - if (reader.ReadUInt16() == 1) - { - var version = reader.ReadSerializedString(); - var match = versionRx.Match(version); - if (match.Success) - { - var fwName = match.Groups["name"].Value; - var isLoadable = - (fwName == ".NETCoreApp") || - (fwName == ".NETStandard"); - if (!isLoadable) return false; - } - } + var fwName = match.Groups["name"].Value; + var isLoadable = + (fwName == ".NETCoreApp") || + (fwName == ".NETStandard"); + if (!isLoadable) return false; } } } } + } + } - foreach (var t in m.TypeDefinitions) - { - var def = m.GetTypeDefinition(t); - foreach (var meth in def.GetMethods()) + foreach (var t in m.TypeDefinitions) + { + var def = m.GetTypeDefinition(t); + foreach (var meth in def.GetMethods()) + { + var mdef = m.GetMethodDefinition(meth); + var hasInitAtt = + mdef.GetCustomAttributes().Any(att => { - var mdef = m.GetMethodDefinition(meth); - var hasInitAtt = - mdef.GetCustomAttributes().Any(att => + var attDef = m.GetCustomAttribute(att); + if (attDef.Constructor.Kind == HandleKind.MemberReference) + { + var hh = (MemberReferenceHandle)attDef.Constructor; + var e = m.GetMemberReference(hh); + var pp = e.Parent; + if (pp.Kind == HandleKind.TypeReference) { - var attDef = m.GetCustomAttribute(att); - if (attDef.Constructor.Kind == System.Reflection.Metadata.HandleKind.MemberReference) - { - var hh = (System.Reflection.Metadata.MemberReferenceHandle)attDef.Constructor; - var e = m.GetMemberReference(hh); - var pp = e.Parent; - if (pp.Kind == System.Reflection.Metadata.HandleKind.TypeReference) - { - var attType = m.GetTypeReference((System.Reflection.Metadata.TypeReferenceHandle)pp); - var nameStr = m.GetString(attType.Name); - var nsStr = m.GetString(attType.Namespace); - if (nsStr == "Aardvark.Base" && nameStr == "OnAardvarkInitAttribute") - { - return true; - } - else return false; - } - else return false; - } - else return false; - }); + var attType = m.GetTypeReference((TypeReferenceHandle)pp); + var nameStr = m.GetString(attType.Name); + var nsStr = m.GetString(attType.Namespace); + return nsStr == "Aardvark.Base" && nameStr == nameof(OnAardvarkInitAttribute); + } + else return false; + } + else return false; + }); - if (hasInitAtt) return true; - } - } + if (hasInitAtt) return true; + } + } + + return false; + } + + private abstract class AssemblySource + { + public abstract string Path { get; } + + public abstract DateTime LastModified { get; } + + public abstract Stream OpenRead(); + + public abstract Assembly Load(); + } + + private class FileAssemblySource : AssemblySource + { + public override string Path { get; } - return false; + public override DateTime LastModified { get; } + + public FileAssemblySource(string path) + { + Path = path; + LastModified = FileUtils.GetLastWriteTimeSafe(path); + } + + public override Stream OpenRead() + => File.OpenRead(Path); + + public override Assembly Load() + { +#if NETCOREAPP3_1_OR_GREATER + // In .NET core Assembly.LoadFile uses a separate context, resulting in assemblies being + // potentially loaded multiple times -> leads to problems with static fields in unit tests + // See: https://github.com/dotnet/runtime/issues/39783 + return System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(Path); +#else + return Assembly.LoadFile(Path); +#endif + } + } + + private class BundleAssemblySource : AssemblySource + { + private readonly BundleFileEntry m_entry; + + private byte[] m_data; + + public override string Path { get; } + + public override DateTime LastModified { get; } + + public BundleReader Reader => m_entry.ExecutableReader; + + public BundleAssemblySource(BundleFileEntry entry) + { + var bundlePath = entry.ExecutableReader.FileName; + Path = System.IO.Path.Combine(bundlePath, entry.RelativePath); + LastModified = FileUtils.GetLastWriteTimeSafe(bundlePath); + m_entry = entry; + } + + private byte[] GetData() + { + if (m_data == null) + { + using var s = m_entry.AsStream(); + using var ms = new MemoryStream(s.CanSeek ? (int)s.Length : 0); + s.CopyTo(ms); + m_data = ms.GetBuffer(); } + + return m_data; } - catch(Exception) + + public override Stream OpenRead() + => new MemoryStream(GetData()); + + public override Assembly Load() { - Report.Warn("NO PLUGIN: {0}", file); - return false; +#if NETCOREAPP3_1_OR_GREATER + using var s = OpenRead(); + return System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(s); +#else + return Assembly.Load(GetData()); +#endif } } - public string[] GetPluginAssemblyPaths() + private class AssemblySourceList : List, IDisposable { - var cache = ReadCacheFile(); - var newCache = new PluginCache(); + private BundleReader m_reader; - var folder = Array.Empty(); - - // attach folder contents if possible (e.g. not possible in bundle deployments if no bundle entry is specified) - if (IntrospectionProperties.CurrentEntryPath != null) - folder = Directory.EnumerateFiles(IntrospectionProperties.CurrentEntryPath).ToArray(); - else if (IntrospectionProperties.BundleEntryPoint != null) - folder = Directory.EnumerateFiles(Path.GetDirectoryName(IntrospectionProperties.BundleEntryPoint)).ToArray(); - - string[] assemblies = - folder - .Where(p => { var ext = Path.GetExtension(p).ToLowerInvariant(); return ext == ".dll" || ext == ".exe"; }) - .Where(p => { - var name = Path.GetFileNameWithoutExtension(p); - var f = IntrospectionProperties.AssemblyFilter(name); - if (!f) { Report.Line(4, "[GetPluginAssemblyPaths] Ignoring assembly {0} due to filter", name); } - return f; - }) - .Select(Path.GetFullPath) - .ToArray(); - - var paths = new List(); - - foreach (var fileName in assemblies) - { - var lastWrite = DateTime.MaxValue; - try { lastWrite = File.GetLastWriteTimeUtc(fileName); } - catch(Exception) + public void AddDirectory(string path) + { + foreach (var p in Directory.GetFiles(path)) { - Report.Line(3, "[GetPluginAssemblyPaths] could not get write time for: {0}", fileName); + Add(new FileAssemblySource(p)); } + } + + public void AddBundle(BundleReader reader) + { + if (m_reader != null) + throw new InvalidOperationException("Cannot add multiple bundles."); - bool exists = cache.TryGetValue(fileName, out PluginCache.Data cacheValue); + m_reader = reader; - if (exists && lastWrite <= cacheValue.LastModified) + if (reader.IsSingleFile && reader.IsSupported) { - Report.Line(3, "[GetPluginAssemblyPaths] cache found for: {0}", fileName); - if (cacheValue.IsPlugin) + foreach (var e in reader.Bundle.Files) { - newCache[fileName] = new PluginCache.Data(lastWrite, true); - paths.Add(fileName); + Add(new BundleAssemblySource(e)); } - else + } + else + { + Report.Warn($"Cannot read bundle executable: {reader.FileName}"); + } + } + + public void Dispose() + { + m_reader?.Dispose(); + m_reader = null; + } + } + + private static AssemblySourceList FindAssemblySources() + { + var sources = new AssemblySourceList(); + + try + { + var bundlePath = IntrospectionProperties.CurrentEntryBundle; + + if (bundlePath != null) + { + try + { + var reader = new BundleReader(bundlePath); + sources.AddBundle(reader); + } + catch (Exception e) { - newCache[fileName] = new PluginCache.Data(lastWrite, false); + Report.Warn($"Failed to get assemblies from single file application: {e.Message}"); } } else { - if (exists) - Report.Line(3, "[GetPluginAssemblyPaths] retrying to load because cache outdated {0}", fileName); - else - Report.Line(3, "[GetPluginAssemblyPaths] retrying to load because not in cache {0}", fileName); + var rootPath = IntrospectionProperties.CurrentEntryPath ?? AppDomain.CurrentDomain.BaseDirectory; - if (IsPlugin(fileName)) + try { - Report.Line(3, "[GetPluginAssemblyPaths] plugin found {0}", fileName); - newCache[fileName] = new PluginCache.Data(lastWrite, true); - paths.Add(fileName); + sources.AddDirectory(rootPath); } - else + catch (Exception e) { - newCache[fileName] = new PluginCache.Data(lastWrite, false); + Report.Warn($"Failed to enumerate assemblies in '{rootPath}': {e.Message}"); } } } + catch (Exception e) + { + Report.Warn($"Error while locating plugin assemblies: {e}"); + } - WriteCacheFile(newCache); - return paths.ToArray(); + return sources; } - public static List LoadPlugins() + private static bool IsPlugin(AssemblySource source, PluginCache oldCache, PluginCache newCache) { - //Note: I removed the separate AppDomain for Plugin probing because: - //1) it made problems on startup in some setups - //2) the code below seemed to not do anything in the new AppDomain since the call - // var paths = aardvark.GetPluginAssemblyPaths(); - // was actually executed in this AppDomain. - //Changes are marked with APPD + var ext = Path.GetExtension(source.Path).ToLowerInvariant(); + if (ext != ".dll" && ext != ".exe") return false; + var name = Path.GetFileNameWithoutExtension(source.Path); + if (!IntrospectionProperties.AssemblyFilter(name)) + { + Report.Line(4, $"[IsPlugin] Ignoring assembly {name} due to filter"); + return false; + } - //APPD var setup = new AppDomainSetup(); - //APPD setup.ApplicationBase = IntrospectionProperties.CurrentEntryPath; + bool isPlugin = false; + bool exists = oldCache.TryGetValue(source.Path, out PluginCache.Data cacheValue); - try + if (exists) + { + if (source.LastModified <= cacheValue.LastModified) + { + Report.Line(4, $"[IsPlugin] Cache found for: {source.Path}"); + isPlugin = cacheValue.IsPlugin; + } + else + { + Report.Line(4, $"[IsPlugin] Retrying to load because cache is outdated: {source.Path}"); + } + } + else { - //APPD var d = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, setup); - var aardvark = new Aardvark(); //APPD (Aardvark)d.CreateInstanceAndUnwrap(typeof(Aardvark).Assembly.FullName, typeof(Aardvark).FullName); + Report.Line(4, $"[IsPlugin] Retrying to load because not in cache: {source.Path}"); + + try + { + using var s = source.OpenRead(); + + if (ProbeForPlugin(s)) + { + Report.Line(4, $"[IsPlugin] Plugin found: {source.Path}"); + isPlugin = true; + } + } + catch (Exception e) + { + Report.Line(4, $"[IsPlugin] Error while probing assembly '{source.Path}': {e.Message}"); + } + } - Report.Line(3, "[LoadPlugins] Using plugin cache file name: {0}", aardvark.CacheFile); - var paths = aardvark.GetPluginAssemblyPaths(); - //APPD AppDomain.Unload(d); + newCache[source.Path] = new PluginCache.Data(source.LastModified, isPlugin); + return isPlugin; + } + public static List LoadPlugins() + { + var oldCache = ReadCacheFile(); + var newCache = new PluginCache(); - var assemblies = new List(); + using var sources = FindAssemblySources(); + List assemblies = new (); - foreach (var p in paths) + try + { + foreach (var source in sources) { - try { -#if NETCOREAPP3_1_OR_GREATER - // In .NET core Assembly.LoadFile uses a separate context, resulting in assemblies being - // potentially loaded multiple times -> leads to problems with static fields in unit tests - // See: https://github.com/dotnet/runtime/issues/39783 - var ass = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(p); -#else - var ass = Assembly.LoadFile(p); -#endif - assemblies.Add(ass); + if (!IsPlugin(source, oldCache, newCache)) + continue; + + var asm = source.Load(); + assemblies.Add(asm); } catch (Exception e) { - Report.Line(3, "[LoadPlugins] Could not load assembly: {0}", e.Message); + Report.Warn($"Failed to load plugin assembly '{source.Path}': {e.Message}"); } } - - return assemblies; - } catch(Exception e) + } + finally { - Report.Warn("[LoadPlugins] could not load plugins: {0}", e.Message); - return new List(); + WriteCacheFile(newCache); } - } + return assemblies; + } -#region LdConfig + #region LdConfig private static class LdConfig @@ -1163,9 +1348,9 @@ public static bool TryGetPath(string name, out string path) } -#endregion + #endregion -#region DllMap + #region DllMap private enum OS { @@ -1244,9 +1429,9 @@ private static Dictionary GetSymlinks(XDocument document) } -#endregion + #endregion -#region Symlink + #region Symlink [DllImport("libc")] private static extern int symlink(string src, string linkName); @@ -1295,8 +1480,7 @@ private static void CreateSymlink(string baseDir, string src, string dst) } - -#endregion + #endregion private static void GetPlatformAndArch(out string platform, out string arch) { @@ -1463,10 +1647,7 @@ public static void UnpackNativeDependenciesToBaseDir(Assembly a, string baseDir) public static void UnpackNativeDependencies(Assembly a) { - var baseDir = - IntrospectionProperties.CustomEntryAssembly != null - ? Path.GetDirectoryName(IntrospectionProperties.CustomEntryAssembly.Location) - : AppDomain.CurrentDomain.BaseDirectory; + var baseDir = IntrospectionProperties.CurrentEntryPath ?? AppDomain.CurrentDomain.BaseDirectory; UnpackNativeDependenciesToBaseDir(a,baseDir); } @@ -1567,22 +1748,15 @@ public static IntPtr LoadLibrary(Assembly assembly, string nativeName) } IntPtr ptr = IntPtr.Zero; string probe = Environment.CurrentDirectory; - var nextToAssembly = Array.Empty(); + var paths = new List(capacity: 1); - if (assembly != null) + try { - try { nextToAssembly = new[] { Path.GetFullPath(Path.GetDirectoryName(assembly.Location)) }; } - catch (Exception) { } + var location = assembly?.GetLocationSafe() ?? IntrospectionProperties.CurrentEntryPath ?? AppDomain.CurrentDomain.BaseDirectory; + paths.Add(location); } - else + catch { - try - { - var entry = IntrospectionProperties.CustomEntryAssembly != null ? IntrospectionProperties.CustomEntryAssembly : Assembly.GetEntryAssembly(); - try { nextToAssembly = new[] { Path.GetFullPath(Path.GetDirectoryName(entry.Location)) }; } - catch (Exception) { } - } - catch { } } try @@ -1593,25 +1767,22 @@ public static IntPtr LoadLibrary(Assembly assembly, string nativeName) else if (os == OS.Win32) formats = new[] { "{0}.dll" }; else if (os == OS.MacOS) formats = new[] { "{0}.dylib", "lib{0}.dylib" }; - - string[] paths; if (assembly != null) { if (TryGetNativeLibraryPath(assembly, out var dstFolder)) { - paths = new[] { dstFolder }; + paths.Add(dstFolder); } else { - paths = GetNativeLibraryPaths(); + paths.AddRange(GetNativeLibraryPaths()); } } else { - paths = GetNativeLibraryPaths(); + paths.AddRange(GetNativeLibraryPaths()); } - paths = nextToAssembly.Concat(paths); #if NETCOREAPP3_1_OR_GREATER var realName = Path.GetFileNameWithoutExtension(nativeName); diff --git a/src/Aardvark.Base/paket.references b/src/Aardvark.Base/paket.references index dfb44907..8b8943f8 100644 --- a/src/Aardvark.Base/paket.references +++ b/src/Aardvark.Base/paket.references @@ -1,4 +1,5 @@ Aardvark.Build System.Collections.Immutable System.Reflection.Metadata -System.Text.Json \ No newline at end of file +System.Text.Json +SingleFileExtractor.Core \ No newline at end of file