using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
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 ConcurrentDictionary> namespaces =
new ConcurrentDictionary>();
//private static Dictionary> generics;
private static AssemblyLoadEventHandler lhandler;
private static ResolveEventHandler rhandler;
// updated only under GIL?
private static Dictionary probed = new Dictionary(32);
// modified from event handlers below, potentially triggered from different .NET threads
private static ConcurrentQueue assemblies;
internal static List pypath;
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()
{
assemblies = new ConcurrentQueue();
pypath = new List(16);
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)
{
string name = args.Name.ToLower();
foreach (Assembly a in assemblies)
{
string full = a.FullName.ToLower();
if (full.StartsWith(name))
{
return a;
}
}
return LoadAssemblyPath(args.Name);
}
///
/// 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()
{
IntPtr 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++)
{
IntPtr 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(string name)
{
char sep = Path.DirectorySeparatorChar;
foreach (string head in pypath)
{
string path;
if (head == null || head.Length == 0)
{
path = name;
}
else
{
path = head + sep + name;
}
string temp = path + ".dll";
if (File.Exists(temp))
{
return temp;
}
temp = path + ".exe";
if (File.Exists(temp))
{
return temp;
}
}
return null;
}
///
/// Loads an assembly from the application directory or the GAC
/// given a simple assembly name. Returns the assembly if loaded.
///
public static Assembly LoadAssembly(string name)
{
Assembly assembly = null;
try
{
assembly = Assembly.Load(name);
}
catch (Exception)
{
//if (!(e is System.IO.FileNotFoundException))
//{
// throw;
//}
}
return assembly;
}
///
/// Loads an assembly using an augmented search path (the python path).
///
public static Assembly LoadAssemblyPath(string name)
{
string path = FindAssembly(name);
Assembly assembly = null;
if (path != null)
{
try
{
assembly = Assembly.LoadFrom(path);
}
catch (Exception)
{
}
}
return assembly;
}
///
/// Loads an assembly using full path.
///
///
///
public static Assembly LoadAssemblyFullPath(string name)
{
Assembly assembly = null;
if (Path.IsPathRooted(name))
{
if (!Path.HasExtension(name))
{
name = name + ".dll";
}
if (File.Exists(name))
{
try
{
assembly = Assembly.LoadFrom(name);
}
catch (Exception)
{
}
}
}
return assembly;
}
///
/// 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;
}
///
/// Given a qualified name of the form A.B.C.D, attempt to load
/// an assembly named after each of A.B.C.D, A.B.C, A.B, A. This
/// will only actually probe for the assembly once for each unique
/// namespace. Returns true if any assemblies were loaded.
///
///
/// TODO item 3 "* Deprecate implicit loading of assemblies":
/// Set the fromFile flag if the name of the loaded assembly matches
/// the fully qualified name that was requested if the framework
/// actually loads an assembly.
/// Call ONLY for namespaces that HAVE NOT been cached yet.
///
public static bool LoadImplicit(string name, bool warn = true)
{
string[] names = name.Split('.');
var loaded = false;
var s = "";
Assembly lastAssembly = null;
HashSet assembliesSet = null;
for (var i = 0; i < names.Length; i++)
{
s = i == 0 ? names[0] : s + "." + names[i];
if (!probed.ContainsKey(s))
{
if (assembliesSet == null)
{
assembliesSet = new HashSet(AppDomain.CurrentDomain.GetAssemblies());
}
Assembly a = FindLoadedAssembly(s);
if (a == null)
{
a = LoadAssemblyPath(s);
}
if (a == null)
{
a = LoadAssembly(s);
}
if (a != null && !assembliesSet.Contains(a))
{
loaded = true;
lastAssembly = a;
}
probed[s] = 1;
}
}
// Deprecation warning
if (warn && loaded)
{
string location = Path.GetFileNameWithoutExtension(lastAssembly.Location);
string deprWarning = "The module was found, but not in a referenced namespace.\n" +
$"Implicit loading is deprecated. Please use clr.AddReference('{location}').";
Exceptions.deprecation(deprWarning);
}
return loaded;
}
///
/// 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)
{
// 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];
namespaces.TryAdd(s, new ConcurrentDictionary());
}
}
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 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 System.Type object for a given qualified name,
/// looking in the currently loaded assemblies for the named
/// type. Returns null if the named type cannot be found.
///
public static Type LookupType(string qname)
{
foreach (Assembly assembly in assemblies)
{
Type type = assembly.GetType(qname);
if (type != null)
{
return type;
}
}
return null;
}
internal static Type[] GetTypes(Assembly a)
{
if (a.IsDynamic)
{
try
{
return a.GetTypes();
}
catch (ReflectionTypeLoadException exc)
{
// Return all types that were successfully loaded
return exc.Types.Where(x => x != null).ToArray();
}
}
else
{
try
{
return a.GetExportedTypes();
}
catch (FileNotFoundException)
{
return new Type[0];
}
}
}
}
}