// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Tap.Shared;
namespace OpenTap
{
///
/// Static class that searches for and loads OpenTAP plugins.
///
public static class PluginManager
{
private static readonly TraceSource log = Log.CreateSource("PluginManager");
private static ManualResetEventSlim searchTask = new ManualResetEventSlim(true);
private static PluginSearcher searcher;
static TapAssemblyResolver assemblyResolver;
///
/// Specifies the directories to be searched for plugins.
/// Any additional directories should be added before calling ,
/// , , ,
/// or
///
public static List DirectoriesToSearch { get; private set; }
///
/// Function signature for assembly load filters.
///
/// The name of the assembly (not the full name).
/// The assembly version.
/// true if the assembly should be loaded. False if not.
public delegate bool AssemblyLoadFilterDelegate(string assemblyName, Version version);
///
/// Adds a function that is used to filter whether an assembly should be loaded. This can be used to control which assemblies gets loaded.
/// This is for very advanced usage only. The filters are used in the order in which they are added.
///
/// The additional filter to use.
static public void AddAssemblyLoadFilter(AssemblyLoadFilterDelegate filter)
{
if (filter == null)
throw new ArgumentNullException("filter");
assemblyLoadFilters.Add(filter);
}
static List assemblyLoadFilters = new List { (asm,version) => true };
static bool shouldLoadAssembly(string asmName, Version asmVersion)
{
foreach (var filter in assemblyLoadFilters)
if (!filter(asmName, asmVersion))
return false;
return true;
}
///
/// Returns a list of types that implement a specified plugin base type.
/// This will load the assembly containing the type, if not already loaded.
/// This will search for plugins if not done already (e.g. using )
///
/// only looks for types descending from pluginBaseType.
public static ReadOnlyCollection GetPlugins(Type pluginBaseType)
{
return PluginFetcher.GetPlugins(pluginBaseType);
}
/// Class for caching the result of GetPlugins(type).
static class PluginFetcher
{
public static ReadOnlyCollection GetPlugins(Type pluginBaseType)
{
// If the searcher has changed, or if a search is currently happening, invalidate plugins.
if (searcher != lastUsedSearcher || !searchTask.Wait(0))
{
lock (pluginsSelection)
{
lastUsedSearcher = searcher;
pluginsSelection.InvalidateAll();
}
}
return pluginsSelection.Invoke(pluginBaseType);
}
static readonly ReadOnlyCollection emptyTypes = Array.Empty().ToList().AsReadOnly();
static ReadOnlyCollection getPlugins(Type pluginBaseType)
{
if (pluginBaseType == null)
throw new ArgumentNullException("pluginBaseType");
PluginSearcher searcher = GetSearcher(); // Wait for the search to complete (and get the result)
var unloadedPlugins = PluginManager.GetPlugins(searcher, pluginBaseType.FullName);
if (unloadedPlugins.Count == 0)
return emptyTypes;
int notLoadedTypesCnt = unloadedPlugins.Count(pl => pl.Status == LoadStatus.NotLoaded);
if(notLoadedTypesCnt > 0)
{
var notLoadedAssembliesCnt = unloadedPlugins.Select(x => x.Assembly).Distinct().Where(asm => asm.Status == LoadStatus.NotLoaded).ToArray();
if (notLoadedAssembliesCnt.Length > 0)
{
notLoadedAssembliesCnt.AsParallel().ForAll(asm => asm.Load());
}
}
IEnumerable plugins = unloadedPlugins;
if (notLoadedTypesCnt > 8)
{
// only find types in parallel if there are sufficiently many.
plugins = plugins
.AsParallel() // This is around 50% faster when many plugins are loaded in parallel.
.AsOrdered(); // ensure the order is the same as before.
}
return plugins
.Select(td => td.Load())
.Where(x => x != null)
.ToList()
.AsReadOnly();
}
static PluginSearcher lastUsedSearcher = null;
static Memorizer> pluginsSelection = new Memorizer>(getPlugins);
}
static ICollection GetPlugins(PluginSearcher searcher, string baseTypeFullName)
{
TypeData baseType;
if (!searcher.AllTypes.TryGetValue(baseTypeFullName, out baseType))
return Array.Empty();
if (baseType.DerivedTypes == null)
return Array.Empty();
var specializations = new List();
foreach (TypeData st in baseType.DerivedTypes)
{
if (st.TypeAttributes.HasFlag(TypeAttributes.Interface) || st.TypeAttributes.HasFlag(TypeAttributes.Abstract))
continue;
if(shouldLoadAssembly(st.Assembly.Name, st.Assembly.Version))
specializations.Add(st);
}
return specializations;
}
static object startSearcherLock = new object();
///
/// Returns the used to search for plugins.
/// This will search for plugins if not done already (i.e. call and wait for )
///
public static PluginSearcher GetSearcher()
{
searchTask.Wait();
if (searcher == null)
{
// If the search task has not been started, do it now.
lock (startSearcherLock)
{
if(searcher == null)
Search();
}
}
return searcher; // Wait for the search to complete (and get the result)
}
///
/// Gets all plugins. I.e. all types that descend from .
/// Abstract types and interfaces are not included.
/// This does not require/cause the assembly containing the type to be loaded.
/// This will search for plugins if not done already (i.e. call and wait for )
/// Only C#/.NET types are returned. To also get dynamic types (from custom s) use instead.
///
public static ReadOnlyCollection GetAllPlugins()
{
PluginSearcher searcher = GetSearcher();
return searcher.PluginTypes
.Where(st =>
!st.TypeAttributes.HasFlag(TypeAttributes.Interface)
&& !st.TypeAttributes.HasFlag(TypeAttributes.Abstract)
&& st.Status != LoadStatus.FailedToLoad)
.ToList().AsReadOnly();
}
///
/// Returns a list of types that implement a specified plugin base type.
/// This will load the assembly containing the type, if not already loaded.
/// This will search for plugins if not done already (i.e. call and wait for )
/// Only C#/.NET types are returned. To also get dynamic types (from custom s) use instead.
///
///
/// This is just to provide a more convenient syntax compared to . The funcionallity is identical.
///
/// find types that descends from this type.
/// A read-only collection of types.
public static ReadOnlyCollection GetPlugins()
{
return StaticPluginTypeCache.Get();
}
///
/// Cache structure to lock-free optimize BaseType type lookups.
///
///
class StaticPluginTypeCache
{
static ReadOnlyCollection list;
public static ReadOnlyCollection Get()
{
return list ??= GetPlugins(typeof(T));
}
static StaticPluginTypeCache()
{
CacheState.Updated += (s, e) => list = null;
}
}
///
/// Gets the AssemblyData for the OpenTap.dll assembly.
/// This will search for plugins if not done already (i.e. call and wait for )
///
public static AssemblyData GetOpenTapAssembly()
{
PluginSearcher searcher = GetSearcher();
return searcher.PluginMarkerType.Assembly;
}
///
/// Start a search task that finds plugins to the platform.
/// This call is not blocking, some other calls to PluginManager will automatically
/// wait for this task to finish (or even start it if it hasn't been already). These calls
/// include , ,
/// , and
///
public static Task SearchAsync()
{
searchTask.Reset();
CacheState.OnUpdated();
return TapThread.StartAwaitable(Search);
}
///Searches for plugins.
public static void Search()
{
searchTask.Reset();
searcher ??= new();
assemblyResolver.Invalidate(DirectoriesToSearch);
CacheState.OnUpdated();
try
{
IEnumerable fileNames = assemblyResolver.GetAssembliesToSearch();
searcher = SearchAndAddToStore(fileNames);
}
catch (Exception e)
{
log.Error("Caught exception while searching for plugins: '{0}'", e.Message);
log.Debug(e);
}
finally
{
searchTask.Set();
}
}
static bool isLoaded = false;
static object loadLock = new object();
/// Sets up the PluginManager assembly resolution systems. Under normal circumstances it is not needed to call this method directly.
internal static void Load()
{
lock (loadLock)
{
if (isLoaded) return;
isLoaded = true;
SessionLogs.Initialize();
string tapEnginePath = Assembly.GetExecutingAssembly().Location;
if(String.IsNullOrEmpty(tapEnginePath))
{
// if OpenTap.dll was loaded from memory/bytes instead of from a file, it does not have a location.
// This is the case if the process was launched through tap.exe.
// In that case just use the location of tap.exe, it is the same
tapEnginePath = Assembly.GetEntryAssembly().Location;
}
DirectoriesToSearch = new List { Path.GetDirectoryName(tapEnginePath) };
assemblyResolver = new TapAssemblyResolver(DirectoriesToSearch);
// Custom Assembly resolvers.
AppDomain.CurrentDomain.GetAssemblies().ToList().ForEach(assemblyResolver.AddAssembly);
AppDomain.CurrentDomain.AssemblyLoad += (s, args) => assemblyResolver.AddAssembly(args.LoadedAssembly);
AppDomain.CurrentDomain.AssemblyResolve += (s, args) => assemblyResolver.Resolve(args.Name, false);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (s, args) => assemblyResolver.Resolve(args.Name, true);
}
}
/// Calls PluginManager.Load
static PluginManager()
{
CacheState.Updated += (s, e) =>
{
PluginsChanged?.Invoke(s, new PluginsChangedEventArgs());
};
Load();
}
/// Ensures that the plugin manager is initialized.
public static void Initialize()
{
// Forces the static constuctore to be called. Intentionally left empty.
}
///
/// Searches the files in fileNames for dlls implementing
/// and puts the implementation in the appropriate list.
///
/// List of files to search.
static PluginSearcher SearchAndAddToStore(IEnumerable _fileNames)
{
var fileNames = _fileNames.ToList();
Stopwatch timer = Stopwatch.StartNew();
// Create a new searcher based on the current searcher.
// Loaded plugins will be 'absorbed'. This is needed in case a loaded plugin was uninstalled.
var newSearcher = new PluginSearcher(searcher);
try
{
var w2 = Stopwatch.StartNew();
IEnumerable foundPluginTypes = newSearcher.Search(fileNames);
IEnumerable foundAssemblies = foundPluginTypes.Select(p => p.Assembly).Distinct();
log.Debug(w2, "Found {0} plugin assemblies containing {1} plugin types.", foundAssemblies.Count(), foundPluginTypes.Count());
foreach (AssemblyData asm in foundAssemblies)
{
assemblyResolver.AddAssembly(asm.Name, asm.Location);
if (asm.Location.Contains(AppDomain.CurrentDomain.BaseDirectory))
log.Debug("Found version {0,-16} of {1}", asm.SemanticVersion?.ToString() ?? asm.Version?.ToString(), Path.GetFileName(asm.Location));
else
// log full path of assembly if it was loaded with --search from another directory.
log.Debug("Found version {0,-16} of {1} from {2}", asm.SemanticVersion?.ToString() ?? asm.Version?.ToString(), Path.GetFileName(asm.Location), asm.Location);
}
}
catch (Exception ex)
{
log.Error($"Plugin search failed: {ex.Message}");
log.Debug(ex);
}
log.Debug(timer, "Searched {0} Assemblies.", fileNames.Count);
if (GetPlugins(newSearcher, typeof(IInstrument).FullName).Count == 0)
log.Warning("No instruments found.");
if (GetPlugins(newSearcher, typeof(ITestStep).FullName).Count == 0)
log.Warning("No TestSteps found.");
return newSearcher;
}
private static Type locateType(string typeName)
{
PluginSearcher searcher = GetSearcher();
if (string.IsNullOrWhiteSpace(typeName))
return null;
if ((typeName.Length > 2) && (typeName[typeName.Length - 2] == '[') && (typeName[typeName.Length - 1] == ']'))
{
var elemType = LocateType(typeName.Substring(0, typeName.Length - 2));
if (elemType != null)
return elemType.MakeArrayType();
else return null;
}
else if (searcher.AllTypes.ContainsKey(typeName))
{
TypeData t = searcher.AllTypes[typeName];
var loaded = t.Load();
if (loaded != null)
return loaded;
}
if (typeName.Contains(","))
{
var x = locateType(typeName.Split(',')[0]);
if (x != null) return x;
}
return Type.GetType(typeName);
}
///
/// Searches through found plugins. Returning the System.Type matching the given name if such a type is found
/// in any assembly in or mscorelib - otherwise null (e.g. for types located in the GAC)
/// This will load the assembly containing the type, if not already loaded.
/// This will search for plugins if not done already (i.e. call and wait for )
///
public static Type LocateType(string typeName)
{
// For compatibility with 8.x testplans in which e.g. OpenTap.BasicSteps.DelayStep was called Keysight.Tap.BasicSteps.DelayStep
var type = locateType(typeName);
if (type == null && typeName.StartsWith("Keysight.Tap."))
type = locateType(typeName.Replace("Keysight.Tap.", "OpenTap."));
return type;
}
///
/// Searches through found plugins. Returns the matching the given name if such a type is found
/// in any assembly in - otherwise null (e.g. for types located in the GAC).
/// This does not require/cause the assembly containing the type to be loaded.
/// This will search for plugins if not done already (i.e. call and wait for )
///
[Obsolete("This only returns C#/.NET types. Use TypeData.GetDerivedTypes instead.")]
public static TypeData LocateTypeData(string typeName)
{
PluginSearcher searcher = GetSearcher();
TypeData type;
searcher.AllTypes.TryGetValue(typeName, out type);
return type;
}
#region Version ResultParameters
static Memorizer AssemblyVersions = new Memorizer(GetVersionResultParameter);
internal static readonly CacheObservable CacheState = new CacheObservable();
/// This event is invoked when the types found by the plugin manager has changed.
public static EventHandler PluginsChanged;
private static ResultParameter GetVersionResultParameter(Assembly assembly)
{
string hash = " - ";
using (var file = new FileStream(assembly.Location, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4 * 4096))
{
byte[] hashValue = BitConverter.GetBytes(MurMurHash3.Hash(file));
for (int i = 0; i < 4; i++)
{
hash += String.Format("{0:x2}", hashValue[i]);
}
}
System.Reflection.AssemblyName name = assembly.GetName();
return new ResultParameter("Version", name.Name, name.Version.ToString() + hash);
}
internal static IEnumerable GetPluginVersions(IEnumerable