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.Persistence.Mappers; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.SqlSyntax; 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. /// public class PluginManager { private readonly ApplicationContext _appContext; private const string CacheKey = "umbraco-plugins.list"; /// /// 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 /// public 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 = GetPluginHashFilePath(); 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 = GetFileHash( new List> { //add the bin folder and everything in it new Tuple(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false), //add the app code folder and everything in it new Tuple(new DirectoryInfo(IOHelper.MapPath("~/App_Code")), false), //add the global.asax (the app domain also monitors this, if it changes will do a full restart) new Tuple(new FileInfo(IOHelper.MapPath("~/global.asax")), false), //add the trees.config - use the contents to create the has since this gets resaved on every app startup! new Tuple(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true) } ); return _currentAssembliesHash; } } /// /// Writes the assembly hash file /// private void WriteCachePluginsHash() { var filePath = GetPluginHashFilePath(); File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8); } /// /// Returns a unique hash for the combination of FileInfo objects passed in /// /// /// A collection of files and whether or not to use their file contents to determine the hash or the file's properties /// (true will make a hash based on it's contents) /// /// internal static long GetFileHash(IEnumerable> filesAndFolders) { using (DisposableTimer.TraceDuration("Determining hash of code files on disk", "Hash determined")) { var hashCombiner = new HashCodeCombiner(); //get the file info's to check var fileInfos = filesAndFolders.Where(x => x.Item2 == false).ToArray(); var fileContents = filesAndFolders.Except(fileInfos); //add each unique folder/file to the hash foreach (var i in fileInfos.Select(x => x.Item1).DistinctBy(x => x.FullName)) { hashCombiner.AddFileSystemItem(i); } //add each unique file's contents to the hash foreach (var i in fileContents.Select(x => x.Item1).DistinctBy(x => x.FullName)) { if (File.Exists(i.FullName)) { var content = File.ReadAllText(i.FullName).Replace("\r\n", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty); hashCombiner.AddCaseInsensitiveString(content); } } return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode()); } } internal static long GetFileHash(IEnumerable filesAndFolders) { using (DisposableTimer.TraceDuration("Determining hash of code files on disk", "Hash determined")) { var hashCombiner = new HashCodeCombiner(); //add each unique folder/file 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>.Fail(); 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(CacheKey, new TimeSpan(0, 0, 5, 0), () => XDocument.Load(filePath)); } else { xml = XDocument.Load(filePath); } if (xml.Root == null) return Attempt>.Fail(); 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 Attempt>.Fail(new CachedPluginNotFoundInFileException()); //return success return Attempt.Succeed(typeElement.Elements("add") .Select(x => (string)x.Attribute("type"))); } catch (Exception ex) { //if the file is corrupted, etc... return false return Attempt>.Fail(ex); } } /// /// Removes cache files and internal cache as well /// /// /// Generally only used for resetting cache, for example during the install process /// internal void ClearPluginCache() { var path = GetPluginListFilePath(); if (File.Exists(path)) File.Delete(path); path = GetPluginHashFilePath(); if (File.Exists(path)) File.Delete(path); if (_appContext != null) { _appContext.ApplicationCache.ClearCacheItem(CacheKey); } } private string GetPluginListFilePath() { return Path.Combine(_tempFolder, string.Format("umbraco-plugins.{0}.list", NetworkHelper.FileSafeMachineName)); } private string GetPluginHashFilePath() { return Path.Combine(_tempFolder, string.Format("umbraco-plugins.{0}.hash", NetworkHelper.FileSafeMachineName)); } /// /// 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 static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); private readonly HashSet _types = new HashSet(); private IEnumerable _assemblies; /// /// Returns all available IApplicationStartupHandler objects /// /// internal IEnumerable ResolveApplicationStartupHandlers() { return ResolveTypes(); } /// /// 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 mapper types that have a MapperFor attribute defined /// /// internal IEnumerable ResolveAssignedMapperTypes() { return ResolveTypesWithAttribute(); } /// /// Returns all SqlSyntaxProviders with the SqlSyntaxProviderAttribute /// /// internal IEnumerable ResolveSqlSyntaxProviders() { return ResolveTypesWithAttribute(); } /// /// 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(Locker)) { 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)); //need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 if (cacheResult && typeList != null) { LogHelper.Debug("Existing typeList found for {0} with resolution type {1}", () => typeof(T), () => resolutionType); } //if we're not caching the result then proceed, or if the type list doesn't exist then proceed if (cacheResult == false || 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 == false && 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.Exception != null && fileCacheResult.Exception is CachedPluginNotFoundInFileException) { LogHelper.Debug("Tried to find typelist for type {0} and resolution {1} in file cache but the type was not found so loading types by assembly scan ", () => typeof(T), () => resolutionType); //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 == false) { //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 {0} with resolution {1} from persisted cache", () => typeof(T), () => resolutionType); } } } } else { LogHelper.Debug("Assembly changes detected, loading types {0} for resolution {1} by assembly scan", () => typeof(T), () => resolutionType); //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 var added = _types.Add(typeList); LogHelper.Debug("Caching of typelist for type {0} and resolution {1} was successful = {2}", () => typeof(T), () => resolutionType, () => added); } } 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); } #region Public Methods /// /// Generic method to find the specified type and cache the result /// /// /// public IEnumerable ResolveTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) { return ResolveTypes( () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), TypeResolutionKind.FindAllTypes, cacheResult); } /// /// Generic method to find the specified type that has an attribute and cache the result /// /// /// /// public IEnumerable ResolveTypesWithAttribute(bool cacheResult = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { return ResolveTypes( () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), TypeResolutionKind.FindTypesWithAttribute, cacheResult); } /// /// Generic method to find any type that has the specified attribute /// /// /// public IEnumerable ResolveAttributedTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { return ResolveTypes( () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), TypeResolutionKind.FindAttributedTypes, cacheResult); } #endregion /// /// 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 lookup type /// /// /// /// public override bool IsTypeList(TypeResolutionKind resolutionType) { return _resolutionType == resolutionType && (typeof(T)) == typeof(TLookup); } 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 CachedPluginNotFoundInFileException : Exception { } #endregion } }