X Tutup
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; namespace Python.Runtime { /// /// The AssemblyManager maintains information about loaded assemblies /// namespaces and provides an interface for name-based type lookup. /// internal class AssemblyManager { // modified from event handlers below, potentially triggered from different .NET threads // therefore this should be a ConcurrentDictionary // // WARNING: Dangerous if cross-app domain usage is ever supported // Reusing the dictionary with assemblies accross multiple initializations is problematic. // Loading happens from CurrentDomain (see line 53). And if the first call is from AppDomain that is later unloaded, // than it can end up referring to assemblies that are already unloaded (default behavior after unload appDomain - // unless LoaderOptimization.MultiDomain is used); // So for multidomain support it is better to have the dict. recreated for each app-domain initialization private static readonly ConcurrentDictionary> namespaces = new(); #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. // domain-level handlers are initialized in Initialize private static AssemblyLoadEventHandler lhandler; private static ResolveEventHandler rhandler; #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. // updated only under GIL? private static readonly Dictionary probed = new(32); // modified from event handlers below, potentially triggered from different .NET threads private static readonly ConcurrentQueue assemblies = new(); internal static readonly List pypath = new (capacity: 16); private AssemblyManager() { } /// /// Initialization performed on startup of the Python runtime. Here we /// scan all of the currently loaded assemblies to determine exported /// names, and register to be notified of new assembly loads. /// internal static void Initialize() { pypath.Clear(); AppDomain domain = AppDomain.CurrentDomain; lhandler = new AssemblyLoadEventHandler(AssemblyLoadHandler); domain.AssemblyLoad += lhandler; rhandler = new ResolveEventHandler(ResolveHandler); domain.AssemblyResolve += rhandler; Assembly[] items = domain.GetAssemblies(); foreach (Assembly a in items) { try { ScanAssembly(a); assemblies.Enqueue(a); } catch (Exception ex) { Debug.WriteLine("Error scanning assembly {0}. {1}", a, ex); } } } /// /// Cleanup resources upon shutdown of the Python runtime. /// internal static void Shutdown() { AppDomain domain = AppDomain.CurrentDomain; domain.AssemblyLoad -= lhandler; domain.AssemblyResolve -= rhandler; } /// /// Event handler for assembly load events. At the time the Python /// runtime loads, we scan the app domain to map the assemblies that /// are loaded at the time. We also have to register this event handler /// so that we can know about assemblies that get loaded after the /// Python runtime is initialized. /// private static void AssemblyLoadHandler(object ob, AssemblyLoadEventArgs args) { Assembly assembly = args.LoadedAssembly; assemblies.Enqueue(assembly); ScanAssembly(assembly); } /// /// Event handler for assembly resolve events. This is needed because /// we augment the assembly search path with the PYTHONPATH when we /// load an assembly from Python. Because of that, we need to listen /// for failed loads, because they might be dependencies of something /// we loaded from Python which also needs to be found on PYTHONPATH. /// private static Assembly? ResolveHandler(object ob, ResolveEventArgs args) { var name = new AssemblyName(args.Name); foreach (var alreadyLoaded in assemblies) { if (AssemblyName.ReferenceMatchesDefinition(name, alreadyLoaded.GetName())) { return alreadyLoaded; } } return LoadAssemblyPath(name.Name); } internal static AssemblyName? TryParseAssemblyName(string name) { // workaround for https://github.com/dotnet/runtime/issues/123951 if (name.IndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }) >= 0) { return null; } try { return new AssemblyName(name); } catch (FileLoadException) { return null; } } /// /// We __really__ want to avoid using Python objects or APIs when /// probing for assemblies to load, since our ResolveHandler may be /// called in contexts where we don't have the Python GIL and can't /// even safely try to get it without risking a deadlock ;( /// To work around that, we update a managed copy of sys.path (which /// is the main thing we care about) when UpdatePath is called. The /// import hook calls this whenever it knows its about to use the /// assembly manager, which lets us keep up with changes to sys.path /// in a relatively lightweight and low-overhead way. /// internal static void UpdatePath() { BorrowedReference list = Runtime.PySys_GetObject("path"); var count = Runtime.PyList_Size(list); if (count != pypath.Count) { pypath.Clear(); probed.Clear(); for (var i = 0; i < count; i++) { BorrowedReference item = Runtime.PyList_GetItem(list, i); string? path = Runtime.GetManagedString(item); if (path != null) { pypath.Add(path); } } } } /// /// Given an assembly name, try to find this assembly file using the /// PYTHONPATH. If not found, return null to indicate implicit load /// using standard load semantics (app base directory then GAC, etc.) /// public static string FindAssembly(AssemblyName name) { if (name is null) throw new ArgumentNullException(nameof(name)); return FindAssembly(name.Name); } /// /// Given an assembly name, try to find this assembly file using the /// PYTHONPATH. If not found, return null to indicate implicit load /// using standard load semantics (app base directory then GAC, etc.) /// public static string FindAssembly(string name) { if (name is null) throw new ArgumentNullException(nameof(name)); return FindAssemblyCandidates(name).FirstOrDefault(); } static IEnumerable FindAssemblyCandidates(string name) { foreach (string head in pypath) { string path; if (head == null || head.Length == 0) { path = name; } else { int invalidCharIndex = head.IndexOfAny(Path.GetInvalidPathChars()); if (invalidCharIndex >= 0) { using var importWarning = Runtime.PyObject_GetAttrString(Exceptions.exceptions_module, "ImportWarning"); Exceptions.warn($"Path entry '{head}' has invalid char at position {invalidCharIndex}", importWarning.BorrowOrThrow()); continue; } path = Path.Combine(head, name); } string temp = path + ".dll"; if (File.Exists(temp)) { yield return temp; } temp = path + ".exe"; if (File.Exists(temp)) { yield return temp; } } } /// /// Loads an assembly from the application directory or the GAC /// given its name. Returns the assembly if loaded. /// public static Assembly LoadAssembly(AssemblyName name) { return Assembly.Load(name); } /// /// Loads an assembly using an augmented search path (the python path). /// public static Assembly? LoadAssemblyPath(string name) { string path = FindAssembly(name); if (path == null) return null; return Assembly.LoadFrom(path); } /// /// Loads an assembly using full path. /// /// /// public static Assembly? LoadAssemblyFullPath(string name) { if (Path.IsPathRooted(name)) { if (File.Exists(name)) { return Assembly.LoadFrom(name); } } return null; } /// /// Returns an assembly that's already been loaded /// public static Assembly? FindLoadedAssembly(string name) { foreach (Assembly a in assemblies) { if (a.GetName().Name == name) { return a; } } return null; } /// /// Scans an assembly for exported namespaces, adding them to the /// mapping of valid namespaces. Note that for a given namespace /// a.b.c.d, each of a, a.b, a.b.c and a.b.c.d are considered to /// be valid namespaces (to better match Python import semantics). /// internal static void ScanAssembly(Assembly assembly) { if (assembly.GetCustomAttribute()?.Export == false) { return; } // A couple of things we want to do here: first, we want to // gather a list of all of the namespaces contributed to by // the assembly. foreach (Type t in GetTypes(assembly)) { string ns = t.Namespace ?? ""; if (!namespaces.ContainsKey(ns)) { string[] names = ns.Split('.'); var s = ""; for (var n = 0; n < names.Length; n++) { s = n == 0 ? names[0] : s + "." + names[n]; if (namespaces.TryAdd(s, new ConcurrentDictionary())) { ImportHook.AddNamespace(s); } } } if (ns != null) { namespaces[ns].TryAdd(assembly, string.Empty); } if (ns != null && t.IsGenericTypeDefinition) { GenericUtil.Register(t); } } } public static AssemblyName[] ListAssemblies() { var names = new List(assemblies.Count); foreach (Assembly assembly in assemblies) { names.Add(assembly.GetName()); } return names.ToArray(); } /// /// Returns true if the given qualified name matches a namespace /// exported by an assembly loaded in the current app domain. /// public static bool IsValidNamespace(string name) { return !string.IsNullOrEmpty(name) && namespaces.ContainsKey(name); } /// /// Returns an enumerable collection containing the namepsaces exported /// by loaded assemblies in the current app domain. /// public static IEnumerable GetNamespaces () { return namespaces.Keys; } /// /// Returns list of assemblies that declare types in a given namespace /// public static IEnumerable GetAssemblies(string nsname) { return !namespaces.ContainsKey(nsname) ? new List() : namespaces[nsname].Keys; } /// /// Returns the current list of valid names for the input namespace. /// public static List GetNames(string nsname) { //Dictionary seen = new Dictionary(); var names = new List(8); List? g = GenericUtil.GetGenericBaseNames(nsname); if (g != null) { foreach (string n in g) { names.Add(n); } } if (namespaces.ContainsKey(nsname)) { foreach (Assembly a in namespaces[nsname].Keys) { foreach (Type t in GetTypes(a)) { if ((t.Namespace ?? "") == nsname && !t.IsNested) { names.Add(t.Name); } } } int nslen = nsname.Length; foreach (string key in namespaces.Keys) { if (key.Length > nslen && key.StartsWith(nsname)) { //string tail = key.Substring(nslen); if (key.IndexOf('.') == -1) { names.Add(key); } } } } return names; } /// /// Returns the objects for the given qualified name, /// looking in the currently loaded assemblies for the named /// type. /// public static IEnumerable LookupTypes(string qualifiedName) => assemblies.Select(assembly => assembly.GetType(qualifiedName)).Where(type => type != null && IsExported(type)); internal static Type[] GetTypes(Assembly a) { if (a.IsDynamic) { try { return a.GetTypes().Where(IsExported).ToArray(); } catch (ReflectionTypeLoadException exc) { // Return all types that were successfully loaded return exc.Types.Where(x => x != null && IsExported(x)).ToArray(); } } else { try { return a.GetExportedTypes().Where(IsExported).ToArray(); } catch (FileNotFoundException) { return new Type[0]; } } } static bool IsExported(Type type) => type.GetCustomAttribute()?.Export != false; } }
X Tutup