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.Compilation;
using System.Xml.Linq;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using umbraco.interfaces;
using File = System.IO.File;
namespace Umbraco.Core
{
///
/// Used to resolve all plugin types and cache them and is also used to instantiate plugin types
///
///
///
/// This class should be used to resolve all plugin types, the TypeFinder should not be used directly!
///
/// This class can expose extension methods to resolve custom plugins
///
/// Before this class resolves any plugins it checks if the hash has changed for the DLLs in the /bin folder, if it hasn't
/// it will use the cached resolved plugins that it has already found which means that no assembly scanning is necessary. This leads
/// to much faster startup times.
///
internal class PluginManager
{
private readonly ApplicationContext _appContext;
///
/// Creates a new PluginManager with an ApplicationContext instance which ensures that the plugin xml
/// file is cached temporarily until app startup completes.
///
///
///
internal PluginManager(ApplicationContext appContext, bool detectBinChanges = true)
: this(detectBinChanges)
{
if (appContext == null) throw new ArgumentNullException("appContext");
_appContext = appContext;
}
///
/// Creates a new PluginManager
///
///
/// If true will detect changes in the /bin folder and therefor load plugins from the
/// cached plugins file if one is found. If false will never use the cache file for plugins
///
internal PluginManager(bool detectCodeChanges = true)
{
_tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache");
//create the folder if it doesn't exist
if (!Directory.Exists(_tempFolder))
{
Directory.CreateDirectory(_tempFolder);
}
//this is a check for legacy changes, before we didn't store the TypeResolutionKind in the file which was a mistake,
//so we need to detect if the old file is there without this attribute, if it is then we delete it
if (DetectLegacyPluginListFile())
{
var filePath = GetPluginListFilePath();
File.Delete(filePath);
}
if (detectCodeChanges)
{
//first check if the cached hash is 0, if it is then we ne
//do the check if they've changed
HaveAssembliesChanged = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0;
//if they have changed, we need to write the new file
if (HaveAssembliesChanged)
{
WriteCachePluginsHash();
}
}
else
{
//always set to true if we're not detecting (generally only for testing)
HaveAssembliesChanged = true;
}
}
static PluginManager _resolver;
static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim();
private readonly string _tempFolder;
private long _cachedAssembliesHash = -1;
private long _currentAssembliesHash = -1;
///
/// We will ensure that no matter what, only one of these is created, this is to ensure that caching always takes place
///
///
/// The setter is generally only used for unit tests
///
internal static PluginManager Current
{
get
{
using (var l = new UpgradeableReadLock(Lock))
{
if (_resolver == null)
{
l.UpgradeToWriteLock();
_resolver = ApplicationContext.Current == null
? new PluginManager()
: new PluginManager(ApplicationContext.Current);
}
return _resolver;
}
}
set { _resolver = value; }
}
#region Hash checking methods
///
/// Returns a bool if the assemblies in the /bin have changed since they were last hashed.
///
internal bool HaveAssembliesChanged { get; private set; }
///
/// Returns the currently cached hash value of the scanned assemblies in the /bin folder. Returns 0
/// if no cache is found.
///
///
internal long CachedAssembliesHash
{
get
{
if (_cachedAssembliesHash != -1)
return _cachedAssembliesHash;
var filePath = Path.Combine(_tempFolder, "umbraco-plugins.hash");
if (!File.Exists(filePath))
return 0;
var hash = File.ReadAllText(filePath, Encoding.UTF8);
Int64 val;
if (Int64.TryParse(hash, out val))
{
_cachedAssembliesHash = val;
return _cachedAssembliesHash;
}
//it could not parse for some reason so we'll return 0.
return 0;
}
}
///
/// Returns the current assemblies hash based on creating a hash from the assemblies in the /bin
///
///
internal long CurrentAssembliesHash
{
get
{
if (_currentAssembliesHash != -1)
return _currentAssembliesHash;
_currentAssembliesHash = GetAssembliesHash(
new FileSystemInfo[]
{
//add the bin folder and everything in it
new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)),
//add the app code folder and everything in it
new DirectoryInfo(IOHelper.MapPath("~/App_Code")),
//add the global.asax (the app domain also monitors this, if it changes will do a full restart)
new FileInfo(IOHelper.MapPath("~/global.asax")),
//add the trees.config
new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config"))
}
);
return _currentAssembliesHash;
}
}
///
/// Writes the assembly hash file
///
private void WriteCachePluginsHash()
{
var filePath = Path.Combine(_tempFolder, "umbraco-plugins.hash");
File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8);
}
///
/// Returns a unique hash for the combination of FileInfo objects passed in
///
///
///
internal static long GetAssembliesHash(IEnumerable filesAndFolders)
{
using (DisposableTimer.TraceDuration("Determining hash of code files on disk", "Hash determined"))
{
var hashCombiner = new HashCodeCombiner();
//add each unique folder to the hash
foreach (var i in filesAndFolders.DistinctBy(x => x.FullName))
{
hashCombiner.AddFileSystemItem(i);
}
return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode());
}
}
///
/// Converts the hash value of current plugins to long from string
///
///
///
internal static long ConvertPluginsHashFromHex(string val)
{
long outVal;
if (Int64.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal))
{
return outVal;
}
return 0;
}
///
/// Attempts to resolve the list of plugin + assemblies found in the runtime for the base type 'T' passed in.
/// If the cache file doesn't exist, fails to load, is corrupt or the type 'T' element is not found then
/// a false attempt is returned.
///
///
///
internal Attempt> TryGetCachedPluginsFromFile(TypeResolutionKind resolutionType)
{
var filePath = GetPluginListFilePath();
if (!File.Exists(filePath))
return Attempt>.False;
try
{
//we will load the xml document, if the app context exist, we will load it from the cache (which is only around for 5 minutes)
//while the app boots up, this should save some IO time on app startup when the app context is there (which is always unless in unit tests)
XDocument xml;
if (_appContext != null)
{
xml = _appContext.ApplicationCache.GetCacheItem("umbraco-plugins.list",
new TimeSpan(0, 0, 5, 0),
() => XDocument.Load(filePath));
}
else
{
xml = XDocument.Load(filePath);
}
if (xml.Root == null)
return Attempt>.False;
var typeElement = xml.Root.Elements()
.SingleOrDefault(x =>
x.Name.LocalName == "baseType"
&& ((string)x.Attribute("type")) == typeof(T).FullName
&& ((string)x.Attribute("resolutionType")) == resolutionType.ToString());
//return false but specify this exception type so we can detect it
if (typeElement == null)
return new Attempt>(new CachedPluginNotFoundInFile());
//return success
return new Attempt>(
true,
typeElement.Elements("add")
.Select(x => (string)x.Attribute("type")));
}
catch (Exception ex)
{
//if the file is corrupted, etc... return false
return new Attempt>(ex);
}
}
private string GetPluginListFilePath()
{
return Path.Combine(_tempFolder, "umbraco-plugins.list");
}
///
/// This will return true if the plugin list file is a legacy one
///
///
///
/// This method exists purely due to an error in 4.11. We were writing the plugin list file without the
/// type resolution kind which will have caused some problems. Now we detect this legacy file and if it is detected
/// we remove it so it can be recreated properly.
///
internal bool DetectLegacyPluginListFile()
{
var filePath = GetPluginListFilePath();
if (!File.Exists(filePath))
return false;
try
{
var xml = XDocument.Load(filePath);
if (xml.Root == null)
return false;
var typeElement = xml.Root.Elements()
.FirstOrDefault(x => x.Name.LocalName == "baseType");
if (typeElement == null)
return false;
//now check if the typeElement is missing the resolutionType attribute
return typeElement.Attributes().All(x => x.Name.LocalName != "resolutionType");
}
catch (Exception)
{
//if the file is corrupted, etc... return true so it is removed
return true;
}
}
///
/// Adds/Updates the type list for the base type 'T' in the cached file
///
///
///
///
///
/// THIS METHOD IS NOT THREAD SAFE
///
///
///
///
///
///
///
///
/// ]]>
///
internal void UpdateCachedPluginsFile(IEnumerable typesFound, TypeResolutionKind resolutionType)
{
var filePath = GetPluginListFilePath();
XDocument xml;
try
{
xml = XDocument.Load(filePath);
}
catch
{
//if there's an exception loading then this is somehow corrupt, we'll just replace it.
File.Delete(filePath);
//create the document and the root
xml = new XDocument(new XElement("plugins"));
}
if (xml.Root == null)
{
//if for some reason there is no root, create it
xml.Add(new XElement("plugins"));
}
//find the type 'T' element to add or update
var typeElement = xml.Root.Elements()
.SingleOrDefault(x =>
x.Name.LocalName == "baseType"
&& ((string)x.Attribute("type")) == typeof(T).FullName
&& ((string)x.Attribute("resolutionType")) == resolutionType.ToString());
if (typeElement == null)
{
//create the type element
typeElement = new XElement("baseType",
new XAttribute("type", typeof(T).FullName),
new XAttribute("resolutionType", resolutionType.ToString()));
//then add it to the root
xml.Root.Add(typeElement);
}
//now we have the type element, we need to clear any previous types as children and add/update it with new ones
typeElement.ReplaceNodes(typesFound.Select(x => new XElement("add", new XAttribute("type", x.AssemblyQualifiedName))));
//save the xml file
xml.Save(filePath);
}
#endregion
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly HashSet _types = new HashSet();
private IEnumerable _assemblies;
///
/// Returns all classes attributed with XsltExtensionAttribute attribute
///
///
internal IEnumerable ResolveCacheRefreshers()
{
return ResolveTypes();
}
///
/// Returns all available IPropertyEditorValueConverter
///
///
internal IEnumerable ResolvePropertyEditorValueConverters()
{
return ResolveTypes();
}
///
/// Returns all available IDataType in application
///
///
internal IEnumerable ResolveDataTypes()
{
return ResolveTypes();
}
///
/// Returns all available IMacroGuiRendering in application
///
///
internal IEnumerable ResolveMacroRenderings()
{
return ResolveTypes();
}
///
/// Returns all available IPackageAction in application
///
///
internal IEnumerable ResolvePackageActions()
{
return ResolveTypes();
}
///
/// Returns all available IAction in application
///
///
internal IEnumerable ResolveActions()
{
return ResolveTypes();
}
///
/// Returns all available IMacroPropertyTypes in application
///
///
internal IEnumerable ResolveMacroPropertyTypes()
{
return ResolveTypes();
}
///
/// Gets/sets which assemblies to scan when type finding, generally used for unit testing, if not explicitly set
/// this will search all assemblies known to have plugins and exclude ones known to not have them.
///
internal IEnumerable AssembliesToScan
{
get { return _assemblies ?? (_assemblies = TypeFinder.GetAssembliesWithKnownExclusions()); }
set { _assemblies = value; }
}
///
/// Used to resolve and create instances of the specified type based on the resolved/cached plugin types
///
///
/// set to true if an exception is to be thrown if there is an error during instantiation
///
internal IEnumerable FindAndCreateInstances(bool throwException = false)
{
var types = ResolveTypes();
return CreateInstances(types, throwException);
}
///
/// Used to create instances of the specified type based on the resolved/cached plugin types
///
///
///
/// set to true if an exception is to be thrown if there is an error during instantiation
///
internal IEnumerable CreateInstances(IEnumerable types, bool throwException = false)
{
//Have removed logging because it doesn't really need to be logged since the time taken is generally 0ms.
//we want to know if it fails ever, not how long it took if it is only 0.
var typesAsArray = types.ToArray();
//using (DisposableTimer.DebugDuration(
// String.Format("Starting instantiation of {0} objects of type {1}", typesAsArray.Length, typeof(T).FullName),
// String.Format("Completed instantiation of {0} objects of type {1}", typesAsArray.Length, typeof(T).FullName)))
//{
var instances = new List();
foreach (var t in typesAsArray)
{
try
{
var typeInstance = (T)Activator.CreateInstance(t);
instances.Add(typeInstance);
}
catch (Exception ex)
{
LogHelper.Error(String.Format("Error creating type {0}", t.FullName), ex);
if (throwException)
{
throw ex;
}
}
}
return instances;
//}
}
///
/// Used to create an instance of the specified type based on the resolved/cached plugin types
///
///
///
///
///
internal T CreateInstance(Type type, bool throwException = false)
{
var instances = CreateInstances(new[] { type }, throwException);
return instances.FirstOrDefault();
}
private IEnumerable ResolveTypes(
Func> finder,
TypeResolutionKind resolutionType,
bool cacheResult)
{
using (var readLock = new UpgradeableReadLock(_lock))
{
var typesFound = new List();
using (DisposableTimer.TraceDuration(
() => String.Format("Starting resolution types of {0}", typeof(T).FullName),
() => String.Format("Completed resolution of types of {0}, found {1}", typeof(T).FullName, typesFound.Count)))
{
//check if the TypeList already exists, if so return it, if not we'll create it
var typeList = _types.SingleOrDefault(x => x.IsTypeList(resolutionType));
//if we're not caching the result then proceed, or if the type list doesn't exist then proceed
if (!cacheResult || typeList == null)
{
//upgrade to a write lock since we're adding to the collection
readLock.UpgradeToWriteLock();
typeList = new TypeList(resolutionType);
//we first need to look into our cache file (this has nothing to do with the 'cacheResult' parameter which caches in memory).
//if assemblies have not changed and the cache file actually exists, then proceed to try to lookup by the cache file.
if (!HaveAssembliesChanged && File.Exists(GetPluginListFilePath()))
{
var fileCacheResult = TryGetCachedPluginsFromFile(resolutionType);
//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 (fileCacheResult.Error != null && fileCacheResult.Error is CachedPluginNotFoundInFile)
{
//we don't have a cache for this so proceed to look them up by scanning
LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder);
}
else
{
if (fileCacheResult.Success)
{
var successfullyLoadedFromCache = true;
//we have a previous cache for this so we don't need to scan we just load what has been found in the file
foreach (var t in fileCacheResult.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 then we have problems with that.
var type = BuildManager.GetType(t, true);
typeList.AddType(type);
}
catch (Exception ex)
{
//if there are any exceptions loading types, we have to exist, this should never happen so
//we will need to revert to scanning for types.
successfullyLoadedFromCache = false;
LogHelper.Error("Could not load a cached plugin type: " + t + " now reverting to re-scanning assemblies for the base type: " + typeof(T).FullName, ex);
break;
}
}
if (!successfullyLoadedFromCache)
{
//we need to manually load by scanning if loading from the file was not successful.
LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder);
}
else
{
LogHelper.Debug("Loaded plugin types " + typeof(T).FullName + " from persisted cache");
}
}
}
}
else
{
//we don't have a cache for this so proceed to look them up by scanning
LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder);
}
//only add the cache if we are to cache the results
if (cacheResult)
{
//add the type list to the collection
_types.Add(typeList);
}
}
typesFound = typeList.GetTypes().ToList();
}
return typesFound;
}
}
///
/// This method invokes the finder which scans the assemblies for the types and then loads the result into the type finder.
/// Once the results are loaded, we update the cached type xml file
///
///
///
///
///
/// THIS METHODS IS NOT THREAD SAFE
///
private void LoadViaScanningAndUpdateCacheFile(TypeList typeList, TypeResolutionKind resolutionKind, Func> finder)
{
//we don't have a cache for this so proceed to look them up by scanning
foreach (var t in finder())
{
typeList.AddType(t);
}
UpdateCachedPluginsFile(typeList.GetTypes(), resolutionKind);
}
///
/// Generic method to find the specified type and cache the result
///
///
///
internal IEnumerable ResolveTypes(bool cacheResult = true)
{
return ResolveTypes(
() => TypeFinder.FindClassesOfType(AssembliesToScan),
TypeResolutionKind.FindAllTypes,
cacheResult);
}
///
/// Generic method to find the specified type that has an attribute and cache the result
///
///
///
///
internal IEnumerable ResolveTypesWithAttribute(bool cacheResult = true)
where TAttribute : Attribute
{
return ResolveTypes(
() => TypeFinder.FindClassesOfTypeWithAttribute(AssembliesToScan),
TypeResolutionKind.FindTypesWithAttribute,
cacheResult);
}
///
/// Generic method to find any type that has the specified attribute
///
///
///
internal IEnumerable ResolveAttributedTypes(bool cacheResult = true)
where TAttribute : Attribute
{
return ResolveTypes(
() => TypeFinder.FindClassesWithAttribute(AssembliesToScan),
TypeResolutionKind.FindAttributedTypes,
cacheResult);
}
///
/// Used for unit tests
///
///
internal HashSet GetTypeLists()
{
return _types;
}
#region Private classes/Enums
///
/// The type of resolution being invoked
///
internal enum TypeResolutionKind
{
FindAllTypes,
FindAttributedTypes,
FindTypesWithAttribute
}
internal abstract class TypeList
{
public abstract void AddType(Type t);
public abstract bool IsTypeList(TypeResolutionKind resolutionType);
public abstract IEnumerable GetTypes();
}
internal class TypeList : TypeList
{
private readonly TypeResolutionKind _resolutionType;
public TypeList(TypeResolutionKind resolutionType)
{
_resolutionType = resolutionType;
}
private readonly List _types = new List();
public override void AddType(Type t)
{
//if the type is an attribute type we won't do the type check because typeof is going to be the
//attribute type whereas the 't' type is the object type found with the attribute.
if (_resolutionType == TypeResolutionKind.FindAttributedTypes || t.IsType())
{
_types.Add(t);
}
}
///
/// Returns true if the current TypeList is of the same type and of the same type
///
///
///
///
public override bool IsTypeList(TypeResolutionKind resolutionType)
{
return _resolutionType == resolutionType && (typeof(T)).IsType();
}
public override IEnumerable GetTypes()
{
return _types;
}
}
///
/// This class is used simply to determine that a plugin was not found in the cache plugin list with the specified
/// TypeResolutionKind.
///
internal class CachedPluginNotFoundInFile : Exception
{
}
#endregion
}
}