Files
Umbraco-CMS/src/Umbraco.Core/PluginManager.cs
2017-10-06 11:51:27 +11:00

996 lines
43 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Web;
using System.Web.Compilation;
using Umbraco.Core.Cache;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Profiling;
using Umbraco.Core.PropertyEditors;
using umbraco.interfaces;
using Umbraco.Core.Configuration;
using File = System.IO.File;
namespace Umbraco.Core
{
/// <summary>
/// Provides methods to find and instanciate types.
/// </summary>
/// <remarks>
/// <para>This class should be used to resolve all types, the <see cref="TypeFinder"/> class should never be used directly.</para>
/// <para>In most cases this class is not used directly but through extension methods that retrieve specific types.</para>
/// <para>This class caches the types it knows to avoid excessive assembly scanning and shorten startup times, relying
/// on a hash of the DLLs in the ~/bin folder to check for cache expiration.</para>
/// </remarks>
public class PluginManager
{
private const string CacheKey = "umbraco-plugins.list";
private static PluginManager _current;
private static bool _hasCurrent;
private static object _currentLock = new object();
private readonly IServiceProvider _serviceProvider;
private readonly IRuntimeCacheProvider _runtimeCache;
private readonly ProfilingLogger _logger;
private readonly string _tempFolder;
private readonly object _typesLock = new object();
private readonly Dictionary<TypeListKey, TypeList> _types = new Dictionary<TypeListKey, TypeList>();
private string _cachedAssembliesHash = null;
private string _currentAssembliesHash = null;
private IEnumerable<Assembly> _assemblies;
private bool _reportedChange;
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
/// <param name="serviceProvider">A mechanism for retrieving service objects.</param>
/// <param name="runtimeCache">The application runtime cache.</param>
/// <param name="logger">A profiling logger.</param>
/// <param name="detectChanges">Whether to detect changes using hashes.</param>
internal PluginManager(IServiceProvider serviceProvider, IRuntimeCacheProvider runtimeCache, ProfilingLogger logger, bool detectChanges = true)
{
if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");
if (runtimeCache == null) throw new ArgumentNullException("runtimeCache");
if (logger == null) throw new ArgumentNullException("logger");
_serviceProvider = serviceProvider;
_runtimeCache = runtimeCache;
_logger = logger;
// the temp folder where the cache file lives
_tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache");
if (Directory.Exists(_tempFolder) == false)
Directory.CreateDirectory(_tempFolder);
var pluginListFile = GetPluginListFilePath();
if (detectChanges)
{
//first check if the cached hash is string.Empty, if it is then we need
//do the check if they've changed
RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == string.Empty;
//if they have changed, we need to write the new file
if (RequiresRescanning)
{
// if the hash has changed, clear out the persisted list no matter what, this will force
// rescanning of all plugin types including lazy ones.
// http://issues.umbraco.org/issue/U4-4789
if(File.Exists(pluginListFile))
File.Delete(pluginListFile);
WriteCachePluginsHash();
}
}
else
{
// if the hash has changed, clear out the persisted list no matter what, this will force
// rescanning of all plugin types including lazy ones.
// http://issues.umbraco.org/issue/U4-4789
if (File.Exists(pluginListFile))
File.Delete(pluginListFile);
// always set to true if we're not detecting (generally only for testing)
RequiresRescanning = true;
}
}
/// <summary>
/// Gets or sets the set of assemblies to scan.
/// </summary>
/// <remarks>
/// <para>If not explicitely set, defaults to all assemblies except those that are know to not have any of the
/// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies
/// for example.</para>
/// <para>This is for unit tests.</para>
/// </remarks>
internal IEnumerable<Assembly> AssembliesToScan
{
get { return _assemblies ?? (_assemblies = TypeFinder.GetAssembliesWithKnownExclusions()); }
set { _assemblies = value; }
}
/// <summary>
/// Gets the type lists.
/// </summary>
/// <remarks>For unit tests.</remarks>
internal IEnumerable<TypeList> TypeLists
{
get { return _types.Values; }
}
/// <summary>
/// Sets a type list.
/// </summary>
/// <remarks>For unit tests.</remarks>
internal void AddTypeList(TypeList typeList)
{
_types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList;
}
/// <summary>
/// Gets or sets the singleton instance.
/// </summary>
/// <remarks>The setter exists for unit tests.</remarks>
public static PluginManager Current
{
get
{
return LazyInitializer.EnsureInitialized(ref _current, ref _hasCurrent, ref _currentLock, () =>
{
IRuntimeCacheProvider runtimeCache;
ProfilingLogger profilingLogger;
if (ApplicationContext.Current == null)
{
runtimeCache = new NullCacheProvider();
var logger = LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger();
var profiler = ProfilerResolver.HasCurrent ? ProfilerResolver.Current.Profiler : new LogProfiler(logger);
profilingLogger = new ProfilingLogger(logger, profiler);
}
else
{
runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache;
profilingLogger = ApplicationContext.Current.ProfilingLogger;
}
return new PluginManager(new ActivatorServiceProvider(), runtimeCache, profilingLogger);
});
}
set
{
_hasCurrent = true;
_current = value;
}
}
#region Hashing
/// <summary>
/// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed.
/// </summary>
internal bool RequiresRescanning { get; private set; }
/// <summary>
/// Gets the currently cached hash value of the scanned assemblies.
/// </summary>
/// <value>The cached hash value, or string.Empty if no cache is found.</value>
internal string CachedAssembliesHash
{
get
{
if (_cachedAssembliesHash != null)
return _cachedAssembliesHash;
var filePath = GetPluginHashFilePath();
if (File.Exists(filePath) == false) return string.Empty;
var hash = File.ReadAllText(filePath, Encoding.UTF8);
_cachedAssembliesHash = hash;
return _cachedAssembliesHash;
}
}
/// <summary>
/// Gets the current assemblies hash based on creating a hash from the assemblies in various places.
/// </summary>
/// <value>The current hash.</value>
internal string CurrentAssembliesHash
{
get
{
if (_currentAssembliesHash != null)
return _currentAssembliesHash;
_currentAssembliesHash = GetFileHash(new List<Tuple<FileSystemInfo, bool>>
{
// the bin folder and everything in it
new Tuple<FileSystemInfo, bool>(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false),
// the app code folder and everything in it
new Tuple<FileSystemInfo, bool>(new DirectoryInfo(IOHelper.MapPath("~/App_Code")), false),
// global.asax (the app domain also monitors this, if it changes will do a full restart)
new Tuple<FileSystemInfo, bool>(new FileInfo(IOHelper.MapPath("~/global.asax")), false),
// trees.config - use the contents to create the hash since this gets resaved on every app startup!
new Tuple<FileSystemInfo, bool>(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true)
}, _logger);
return _currentAssembliesHash;
}
}
/// <summary>
/// Writes the assembly hash file.
/// </summary>
private void WriteCachePluginsHash()
{
var filePath = GetPluginHashFilePath();
File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8);
}
/// <summary>
/// Returns a unique hash for a combination of FileInfo objects.
/// </summary>
/// <param name="filesAndFolders">A collection of files.</param>
/// <param name="logger">A profiling logger.</param>
/// <returns>The hash.</returns>
/// <remarks>Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the
/// file properties (false) or the file contents (true).</remarks>
internal static string GetFileHash(IEnumerable<Tuple<FileSystemInfo, bool>> filesAndFolders, ProfilingLogger logger)
{
using (logger.TraceDuration<PluginManager>("Determining hash of code files on disk", "Hash determined"))
{
// get the distinct file infos to hash
var uniqInfos = new HashSet<string>();
var uniqContent = new HashSet<string>();
using (var generator = new HashGenerator())
{
foreach (var fileOrFolder in filesAndFolders)
{
var info = fileOrFolder.Item1;
if (fileOrFolder.Item2)
{
// add each unique file's contents to the hash
// normalize the content for cr/lf and case-sensitivity
if (uniqContent.Add(info.FullName))
{
if (File.Exists(info.FullName) == false) continue;
var content = RemoveCrLf(File.ReadAllText(info.FullName));
generator.AddCaseInsensitiveString(content);
}
}
else
{
// add each unique folder/file to the hash
if (uniqInfos.Add(info.FullName))
{
generator.AddFileSystemItem(info);
}
}
}
return generator.GenerateHash();
}
}
}
// fast! (yes, according to benchmarks)
private static string RemoveCrLf(string s)
{
var buffer = new char[s.Length];
var count = 0;
// ReSharper disable once ForCanBeConvertedToForeach - no!
for (var i = 0; i < s.Length; i++)
{
if (s[i] != '\r' && s[i] != '\n')
buffer[count++] = s[i];
}
return new string(buffer, 0, count);
}
/// <summary>
/// Returns a unique hash for a combination of FileInfo objects.
/// </summary>
/// <param name="filesAndFolders">A collection of files.</param>
/// <param name="logger">A profiling logger.</param>
/// <returns>The hash.</returns>
internal static string GetFileHash(IEnumerable<FileSystemInfo> filesAndFolders, ProfilingLogger logger)
{
using (logger.TraceDuration<PluginManager>("Determining hash of code files on disk", "Hash determined"))
{
using (var generator = new HashGenerator())
{
// get the distinct file infos to hash
var uniqInfos = new HashSet<string>();
foreach (var fileOrFolder in filesAndFolders)
{
if (uniqInfos.Contains(fileOrFolder.FullName)) continue;
uniqInfos.Add(fileOrFolder.FullName);
generator.AddFileSystemItem(fileOrFolder);
}
return generator.GenerateHash();
}
}
}
#endregion
#region Cache
private const int ListFileOpenReadTimeout = 4000; // milliseconds
private const int ListFileOpenWriteTimeout = 2000; // milliseconds
/// <summary>
/// Attemps to retrieve the list of types from the cache.
/// </summary>
/// <remarks>Fails if the cache is missing or corrupt in any way.</remarks>
internal Attempt<IEnumerable<string>> TryGetCached(Type baseType, Type attributeType)
{
var cache = _runtimeCache.GetCacheItem<Dictionary<Tuple<string, string>, IEnumerable<string>>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4));
IEnumerable<string> types;
cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out types);
return types == null
? Attempt<IEnumerable<string>>.Fail()
: Attempt.Succeed(types);
}
internal Dictionary<Tuple<string, string>, IEnumerable<string>> ReadCacheSafe()
{
try
{
return ReadCache();
}
catch
{
try
{
var filePath = GetPluginListFilePath();
File.Delete(filePath);
}
catch
{
// on-purpose, does not matter
}
}
return new Dictionary<Tuple<string, string>, IEnumerable<string>>();
}
internal Dictionary<Tuple<string, string>, IEnumerable<string>> ReadCache()
{
var cache = new Dictionary<Tuple<string, string>, IEnumerable<string>>();
var filePath = GetPluginListFilePath();
if (File.Exists(filePath) == false)
return cache;
using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout))
using (var reader = new StreamReader(stream))
{
while (true)
{
var baseType = reader.ReadLine();
if (baseType == null) return cache; // exit
if (baseType.StartsWith("<")) break; // old xml
var attributeType = reader.ReadLine();
if (attributeType == null) break;
var types = new List<string>();
while (true)
{
var type = reader.ReadLine();
if (type == null)
{
types = null; // break 2 levels
break;
}
if (type == string.Empty)
{
cache[Tuple.Create(baseType, attributeType)] = types;
break;
}
types.Add(type);
}
if (types == null) break;
}
}
cache.Clear();
return cache;
}
/// <summary>
/// Removes cache files and internal cache.
/// </summary>
/// <remarks>Generally only used for resetting cache, for example during the install process.</remarks>
public void ClearPluginCache()
{
var path = GetPluginListFilePath();
if (File.Exists(path))
File.Delete(path);
path = GetPluginHashFilePath();
if (File.Exists(path))
File.Delete(path);
_runtimeCache.ClearCacheItem(CacheKey);
}
private string GetPluginListFilePath()
{
switch (GlobalSettings.LocalTempStorageLocation)
{
case LocalTempStorage.AspNetTemp:
return Path.Combine(HttpRuntime.CodegenDir, "umbraco-plugins.list");
case LocalTempStorage.EnvironmentTemp:
var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1();
var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoPlugins",
//include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back
// to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not
// utilizing an old path
appDomainHash);
return Path.Combine(cachePath, "umbraco-plugins.list");
case LocalTempStorage.Default:
default:
return Path.Combine(_tempFolder, "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".list");
}
}
private string GetPluginHashFilePath()
{
var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".hash";
return Path.Combine(_tempFolder, filename);
}
internal void WriteCache()
{
// be absolutely sure
if (Directory.Exists(_tempFolder) == false)
Directory.CreateDirectory(_tempFolder);
var filePath = GetPluginListFilePath();
using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout))
using (var writer = new StreamWriter(stream))
{
foreach (var typeList in _types.Values)
{
writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName);
writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName);
foreach (var type in typeList.Types)
writer.WriteLine(type.AssemblyQualifiedName);
writer.WriteLine();
}
}
}
internal void UpdateCache()
{
// note
// at the moment we write the cache to disk every time we update it. ideally we defer the writing
// since all the updates are going to happen in a row when Umbraco starts. that being said, the
// file is small enough, so it is not a priority.
WriteCache();
}
private static Stream GetFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, int timeoutMilliseconds)
{
const int pauseMilliseconds = 250;
var attempts = timeoutMilliseconds / pauseMilliseconds;
while (true)
{
try
{
return new FileStream(path, fileMode, fileAccess, fileShare);
}
catch
{
if (--attempts == 0)
throw;
LogHelper.Debug<PluginManager>(string.Format("Attempted to get filestream for file {0} failed, {1} attempts left, pausing for {2} milliseconds", path, attempts, pauseMilliseconds));
Thread.Sleep(pauseMilliseconds);
}
}
}
#endregion
#region Create Instances
/// <summary>
/// Resolves and creates instances.
/// </summary>
/// <typeparam name="T">The type to use for resolution.</typeparam>
/// <param name="throwException">Indicates whether to throw if an instance cannot be created.</param>
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>The created instances.</returns>
/// <remarks>
/// <para>By default <paramref name="throwException"/> is false and instances that cannot be created are just skipped.</para>
/// <para>By default <paramref name="cache"/> is true and cache is used for type resolution.</para>
/// <para>By default <paramref name="specificAssemblies"/> is null and <see cref="AssembliesToScan"/> is used.</para>
/// <para>Caching is disabled when using specific assemblies.</para>
/// </remarks>
internal IEnumerable<T> FindAndCreateInstances<T>(bool throwException = false, bool cache = true, IEnumerable<Assembly> specificAssemblies = null)
{
var types = ResolveTypes<T>(cache, specificAssemblies);
return CreateInstances<T>(types, throwException);
}
/// <summary>
/// Creates instances of the specified types.
/// </summary>
/// <typeparam name="T">The base type for all instances.</typeparam>
/// <param name="types">The instance types.</param>
/// <param name="throwException">Indicates whether to throw if an instance cannot be created.</param>
/// <returns>The created instances.</returns>
/// <remarks>By default <paramref name="throwException"/> is false and instances that cannot be created are just skipped.</remarks>
internal IEnumerable<T> CreateInstances<T>(IEnumerable<Type> types, bool throwException = false)
{
return _serviceProvider.CreateInstances<T>(types, _logger.Logger, throwException);
}
/// <summary>
/// Creates an instance of the specified type.
/// </summary>
/// <typeparam name="T">The base type of the instance.</typeparam>
/// <param name="type">The type of the instance.</param>
/// <param name="throwException"></param>
/// <returns>The created instance.</returns>
internal T CreateInstance<T>(Type type, bool throwException = false)
{
var instances = CreateInstances<T>(new[] { type }, throwException);
return instances.FirstOrDefault();
}
#endregion
#region Resolve Types
/// <summary>
/// Resolves class types inheriting from or implementing the specified type
/// </summary>
/// <typeparam name="T">The type to inherit from or implement.</typeparam>
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>All class types inheriting from or implementing the specified type.</returns>
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable<Type> ResolveTypes<T>(bool cache = true, IEnumerable<Assembly> specificAssemblies = null)
{
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
// if not caching, or not IDiscoverable, directly resolve types
if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false)
{
return ResolveTypesInternal(
typeof (T), null,
() => TypeFinder.FindClassesOfType<T>(specificAssemblies ?? AssembliesToScan),
cache);
}
// if caching and IDiscoverable
// filter the cached discovered types (and cache the result)
var discovered = ResolveTypesInternal(
typeof (IDiscoverable), null,
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
true);
return ResolveTypesInternal(
typeof (T), null,
() => discovered
.Where(x => typeof (T).IsAssignableFrom(x)),
true);
}
/// <summary>
/// Resolves class types inheriting from or implementing the specified type and marked with the specified attribute.
/// </summary>
/// <typeparam name="T">The type to inherit from or implement.</typeparam>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>All class types inheriting from or implementing the specified type and marked with the specified attribute.</returns>
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable<Type> ResolveTypesWithAttribute<T, TAttribute>(bool cache = true, IEnumerable<Assembly> specificAssemblies = null)
where TAttribute : Attribute
{
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
// if not caching, or not IDiscoverable, directly resolve types
if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false)
{
return ResolveTypesInternal(
typeof (T), typeof (TAttribute),
() => TypeFinder.FindClassesOfTypeWithAttribute<T, TAttribute>(specificAssemblies ?? AssembliesToScan),
cache);
}
// if caching and IDiscoverable
// filter the cached discovered types (and cache the result)
var discovered = ResolveTypesInternal(
typeof (IDiscoverable), null,
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
true);
return ResolveTypesInternal(
typeof (T), typeof (TAttribute),
() => discovered
.Where(x => typeof(T).IsAssignableFrom(x))
.Where(x => x.GetCustomAttributes<TAttribute>(false).Any()),
true);
}
/// <summary>
/// Resolves class types marked with the specified attribute.
/// </summary>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>All class types marked with the specified attribute.</returns>
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable<Type> ResolveAttributedTypes<TAttribute>(bool cache = true, IEnumerable<Assembly> specificAssemblies = null)
where TAttribute : Attribute
{
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
return ResolveTypesInternal(
typeof (object), typeof (TAttribute),
() => TypeFinder.FindClassesWithAttribute<TAttribute>(specificAssemblies ?? AssembliesToScan),
cache);
}
private IEnumerable<Type> ResolveTypesInternal(
Type baseType, Type attributeType,
Func<IEnumerable<Type>> finder,
bool cache)
{
// using an upgradeable lock makes little sense here as only one thread can enter the upgradeable
// lock at a time, and we don't have non-upgradeable readers, and quite probably the plugin
// manager is mostly not going to be used in any kind of massively multi-threaded scenario - so,
// a plain lock is enough
var name = ResolvedName(baseType, attributeType);
lock (_typesLock)
using (_logger.TraceDuration<PluginManager>(
"Resolving " + name,
"Resolved " + name)) // cannot contain typesFound.Count as it's evaluated before the find
{
// resolve within a lock & timer
return ResolveTypesInternalLocked(baseType, attributeType, finder, cache);
}
}
private static string ResolvedName(Type baseType, Type attributeType)
{
var s = attributeType == null ? string.Empty : ("[" + attributeType + "]");
s += baseType;
return s;
}
private IEnumerable<Type> ResolveTypesInternalLocked(
Type baseType, Type attributeType,
Func<IEnumerable<Type>> finder,
bool cache)
{
// check if the TypeList already exists, if so return it, if not we'll create it
var listKey = new TypeListKey(baseType, attributeType);
TypeList typeList = null;
if (cache)
_types.TryGetValue(listKey, out typeList); // else null
// if caching and found, return
if (typeList != null)
{
// need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505
_logger.Logger.Debug<PluginManager>("Resolving {0}: found a cached type list.", () => ResolvedName(baseType, attributeType));
return typeList.Types;
}
// else proceed,
typeList = new TypeList(baseType, attributeType);
var scan = RequiresRescanning || File.Exists(GetPluginListFilePath()) == false;
if (scan)
{
// either we have to rescan, or we could not find the cache file:
// report (only once) and scan and update the cache file - this variable is purely used to check if we need to log
if (_reportedChange == false)
{
_logger.Logger.Debug<PluginManager>("Assembly changes detected, need to rescan everything.");
_reportedChange = true;
}
}
if (scan == false)
{
// if we don't have to scan, try the cache
var cacheResult = TryGetCached(baseType, attributeType);
// here we need to identify if the CachedPluginNotFoundInFile was the exception, if it was then we need to re-scan
// in some cases the plugin will not have been scanned for on application startup, but the assemblies haven't changed
// so in this instance there will never be a result.
if (cacheResult.Exception is CachedPluginNotFoundInFileException || cacheResult.Success == false)
{
_logger.Logger.Debug<PluginManager>("Resolving {0}: failed to load from cache file, must scan assemblies.", () => ResolvedName(baseType, attributeType));
scan = true;
}
else
{
// successfully retrieved types from the file cache: load
foreach (var type in cacheResult.Result)
{
try
{
// we use the build manager to ensure we get all types loaded, this is slightly slower than
// Type.GetType but if the types in the assembly aren't loaded yet it would fail whereas
// BuildManager will load them - this is how eg MVC loads types, etc - no need to make it
// more complicated
typeList.Add(BuildManager.GetType(type, true));
}
catch (Exception ex)
{
// in case of any exception, we have to exit, and revert to scanning
_logger.Logger.Error<PluginManager>("Resolving " + ResolvedName(baseType, attributeType) + ": failed to load cache file type " + type + ", reverting to scanning assemblies.", ex);
scan = true;
break;
}
}
if (scan == false)
{
_logger.Logger.Debug<PluginManager>("Resolving {0}: loaded types from cache file.", () => ResolvedName(baseType, attributeType));
}
}
}
if (scan)
{
// either we had to scan, or we could not resolve the types from the cache file - scan now
_logger.Logger.Debug<PluginManager>("Resolving {0}: scanning assemblies.", () => ResolvedName(baseType, attributeType));
foreach (var t in finder())
typeList.Add(t);
}
// if we are to cache the results, do so
if (cache)
{
var added = _types.ContainsKey(listKey) == false;
if (added)
{
_types[listKey] = typeList;
//if we are scanning then update the cache file
if (scan)
{
UpdateCache();
}
}
_logger.Logger.Debug<PluginManager>("Resolved {0}, caching ({1}).", () => ResolvedName(baseType, attributeType), () => added.ToString().ToLowerInvariant());
}
else
{
_logger.Logger.Debug<PluginManager>("Resolved {0}.", () => ResolvedName(baseType, attributeType));
}
return typeList.Types;
}
#endregion
#region Nested classes and stuff
/// <summary>
/// Groups a type and a resolution kind into a key.
/// </summary>
private struct TypeListKey
{
// ReSharper disable MemberCanBePrivate.Local
public readonly Type BaseType;
public readonly Type AttributeType;
// ReSharper restore MemberCanBePrivate.Local
public TypeListKey(Type baseType, Type attributeType)
{
BaseType = baseType ?? typeof (object);
AttributeType = attributeType;
}
public override bool Equals(object obj)
{
if (obj == null || obj is TypeListKey == false) return false;
var o = (TypeListKey)obj;
return BaseType == o.BaseType && AttributeType == o.AttributeType;
}
public override int GetHashCode()
{
// in case AttributeType is null we need something else, using typeof (TypeListKey)
// which does not really "mean" anything, it's just a value...
var hash = 5381;
hash = ((hash << 5) + hash) ^ BaseType.GetHashCode();
hash = ((hash << 5) + hash) ^ (AttributeType ?? typeof (TypeListKey)).GetHashCode();
return hash;
}
}
/// <summary>
/// Represents a list of types obtained by looking for types inheriting/implementing a
/// specified type, and/or marked with a specified attribute type.
/// </summary>
internal class TypeList
{
private readonly HashSet<Type> _types = new HashSet<Type>();
public TypeList(Type baseType, Type attributeType)
{
BaseType = baseType;
AttributeType = attributeType;
}
public Type BaseType { get; private set; }
public Type AttributeType { get; private set; }
/// <summary>
/// Adds a type.
/// </summary>
public void Add(Type type)
{
if (BaseType.IsAssignableFrom(type) == false)
throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", "type");
_types.Add(type);
}
/// <summary>
/// Gets the types.
/// </summary>
public IEnumerable<Type> Types
{
get { return _types; }
}
}
/// <summary>
/// Represents the error that occurs when a plugin was not found in the cache plugin
/// list with the specified TypeResolutionKind.
/// </summary>
internal class CachedPluginNotFoundInFileException : Exception
{ }
#endregion
}
internal static class PluginManagerExtensions
{
/// <summary>
/// Gets all classes inheriting from PropertyEditor.
/// </summary>
/// <remarks>
/// <para>Excludes the actual PropertyEditor base type.</para>
/// </remarks>
public static IEnumerable<Type> ResolvePropertyEditors(this PluginManager mgr)
{
// look for IParameterEditor (fast, IDiscoverable) then filter
var propertyEditor = typeof (PropertyEditor);
return mgr.ResolveTypes<IParameterEditor>()
.Where(x => propertyEditor.IsAssignableFrom(x) && x != propertyEditor);
}
/// <summary>
/// Gets all classes implementing IParameterEditor.
/// </summary>
/// <remarks>
/// <para>Includes property editors.</para>
/// <para>Excludes the actual ParameterEditor and PropertyEditor base types.</para>
/// </remarks>
public static IEnumerable<Type> ResolveParameterEditors(this PluginManager mgr)
{
var propertyEditor = typeof (PropertyEditor);
var parameterEditor = typeof (ParameterEditor);
return mgr.ResolveTypes<IParameterEditor>()
.Where(x => x != propertyEditor && x != parameterEditor);
}
/// <summary>
/// Gets all classes implementing IApplicationStartupHandler.
/// </summary>
[Obsolete("IApplicationStartupHandler is obsolete.")]
public static IEnumerable<Type> ResolveApplicationStartupHandlers(this PluginManager mgr)
{
return mgr.ResolveTypes<IApplicationStartupHandler>();
}
/// <summary>
/// Gets all classes implementing ICacheRefresher.
/// </summary>
public static IEnumerable<Type> ResolveCacheRefreshers(this PluginManager mgr)
{
return mgr.ResolveTypes<ICacheRefresher>();
}
/// <summary>
/// Gets all classes implementing IPropertyEditorValueConverter.
/// </summary>
[Obsolete("IPropertyEditorValueConverter is obsolete.")]
public static IEnumerable<Type> ResolvePropertyEditorValueConverters(this PluginManager mgr)
{
return mgr.ResolveTypes<IPropertyEditorValueConverter>();
}
/// <summary>
/// Gets all classes implementing IDataType.
/// </summary>
[Obsolete("IDataType is obsolete.")]
public static IEnumerable<Type> ResolveDataTypes(this PluginManager mgr)
{
return mgr.ResolveTypes<IDataType>();
}
/// <summary>
/// Gets all classes implementing IMacroGuiRendering.
/// </summary>
[Obsolete("IMacroGuiRendering is obsolete.")]
public static IEnumerable<Type> ResolveMacroRenderings(this PluginManager mgr)
{
return mgr.ResolveTypes<IMacroGuiRendering>();
}
/// <summary>
/// Gets all classes implementing IPackageAction.
/// </summary>
public static IEnumerable<Type> ResolvePackageActions(this PluginManager mgr)
{
return mgr.ResolveTypes<IPackageAction>();
}
/// <summary>
/// Gets all classes implementing IAction.
/// </summary>
public static IEnumerable<Type> ResolveActions(this PluginManager mgr)
{
return mgr.ResolveTypes<IAction>();
}
/// <summary>
/// Gets all classes inheriting from BaseMapper and marked with the MapperForAttribute.
/// </summary>
public static IEnumerable<Type> ResolveAssignedMapperTypes(this PluginManager mgr)
{
return mgr.ResolveTypesWithAttribute<BaseMapper, MapperForAttribute>();
}
/// <summary>
/// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute.
/// </summary>
public static IEnumerable<Type> ResolveSqlSyntaxProviders(this PluginManager mgr)
{
return mgr.ResolveTypesWithAttribute<ISqlSyntaxProvider, SqlSyntaxProviderAttribute>();
}
}
}