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.Cache; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.PropertyEditors; using Umbraco.Core._Legacy.PackageActions; using File = System.IO.File; namespace Umbraco.Core.Plugins { /// Resolves and caches all plugin types, and instanciates them. /// /// /// This class should be used to resolve all plugin types, the TypeFinder should not be used directly!. /// 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 const string CacheKey = "umbraco-plugins.list"; private static object _instanceLocker = new object(); private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); private static PluginManager _instance; private static bool _hasInstance; private readonly IRuntimeCacheProvider _runtimeCache; private readonly ProfilingLogger _logger; private readonly string _tempFolder; private readonly HashSet _types = new HashSet(); private long _cachedAssembliesHash = -1; private long _currentAssembliesHash = -1; private IEnumerable _assemblies; /// /// Initializes a new instance of the class. /// /// A runtime cache. /// A logger /// A value indicating whether to detect changes. public PluginManager(IRuntimeCacheProvider runtimeCache, ProfilingLogger logger, bool detectChanges = true) { if (runtimeCache == null) throw new ArgumentNullException(nameof(runtimeCache)); if (logger == null) throw new ArgumentNullException(nameof(logger)); _runtimeCache = runtimeCache; _logger = logger; // create the temp folder if it doesn't exist _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); if (Directory.Exists(_tempFolder) == false) Directory.CreateDirectory(_tempFolder); var pluginListFile = GetPluginListFilePath(); // 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()) File.Delete(pluginListFile); if (detectChanges) { //first check if the cached hash is 0, if it is then we ne //do the check if they've changed RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0; //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 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 File.Delete(pluginListFile); // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; } } /// /// Gets the current plugin manager. /// /// /// Ensures that no matter what, only one plugin manager is created, and thus proper caching always takes place. /// The setter is generally only used for unit tests + when creating the master plugin manager in CoreBootManager. /// public static PluginManager Current { get { return LazyInitializer.EnsureInitialized(ref _instance, ref _hasInstance, ref _instanceLocker, () => { var appctx = ApplicationContext.Current; var cacheProvider = appctx == null ? new NullCacheProvider() : appctx.ApplicationCache.RuntimeCache; ProfilingLogger profilingLogger; if (appctx == null) { 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 { profilingLogger = appctx.ProfilingLogger; } return new PluginManager(cacheProvider, profilingLogger); }); } internal set { _hasInstance = true; _instance = value; } } #region Hash checking methods /// /// Gets a value indicating whether the assemblies in the /bin, app_code, global.asax, etc... have changed since they were last hashed. /// internal bool RequiresRescanning { get; } /// /// Gets the currently cached hash value of the scanned assemblies in the /bin folder. /// /// The cached hash value, or 0 if no cache is found. internal long CachedAssembliesHash { get { if (_cachedAssembliesHash != -1) return _cachedAssembliesHash; var filePath = GetPluginHashFilePath(); if (File.Exists(filePath) == false) return 0; var hash = File.ReadAllText(filePath, Encoding.UTF8); long val; if (long.TryParse(hash, out val) == false) return 0; _cachedAssembliesHash = val; return _cachedAssembliesHash; } } /// /// Gets the current assemblies hash based on creating a hash from the assemblies in the /bin folder. /// /// The current hash. 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) }, _logger ); 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 a combination of FileInfo objects. /// /// /// 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) /// /// A profiling logger. /// The hash. internal static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger) { var ffA = filesAndFolders.ToArray(); using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) { var hashCombiner = new HashCodeCombiner(); // get the file info's to check var fileInfos = ffA.Where(x => x.Item2 == false).ToArray(); var fileContents = ffA.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) .Where(x => File.Exists(x.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, ProfilingLogger logger) { using (logger.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; return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 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) == false) 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) var xml = _runtimeCache.GetCacheItem(CacheKey, () => XDocument.Load(filePath), new TimeSpan(0, 0, 5, 0)); 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 /// 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() { return Path.Combine(_tempFolder, $"umbraco-plugins.{NetworkHelper.FileSafeMachineName}.list"); } private string GetPluginHashFilePath() { return Path.Combine(_tempFolder, $"umbraco-plugins.{NetworkHelper.FileSafeMachineName}.hash"); } /// /// 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) == false) return false; try { var xml = XDocument.Load(filePath); var typeElement = xml.Root?.Elements() .FirstOrDefault(x => x.Name.LocalName == "baseType"); //now check if the typeElement is missing the resolutionType attribute return typeElement != null && 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 /// /// Gets or 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; } } #region Resolve Types private IEnumerable ResolveTypes( Func> finder, TypeResolutionKind resolutionType, bool cacheResult) { using (var readLock = new UpgradeableReadLock(Locker)) { var typesFound = new List(); using (_logger.DebugDuration( $"Starting resolution types of {typeof(T).FullName}", $"Completed resolution of types of {typeof(T).FullName}", // cannot contain typesFound.Count as it's evaluated before the find! 50)) { //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) { _logger.Logger.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 (RequiresRescanning == 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 is CachedPluginNotFoundInFileException) { _logger.Logger.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; _logger.Logger.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 { _logger.Logger.Debug("Loaded plugin types {0} with resolution {1} from persisted cache", () => typeof(T), () => resolutionType); } } } } else { _logger.Logger.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); _logger.Logger.Debug("Caching of typelist for type {0} and resolution {1} was successful = {2}", () => typeof(T), () => resolutionType, () => added); } } typesFound = typeList.GetTypes().ToList(); } return typesFound; } } /// /// Invokes the finder which scans the assemblies for the types and then loads the result into the type finder, /// then updates the cached type xml file. /// /// /// /// /// This method 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); } /// /// Resolves specified types. /// /// The type to find. /// The types. public IEnumerable ResolveTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) { return ResolveTypes( () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), TypeResolutionKind.FindAllTypes, cacheResult); } /// /// Resolves specified, attributed types. /// /// The type to find. /// The type of the attribute. /// The corresponding types. public IEnumerable ResolveTypesWithAttribute(bool cacheResult = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { return ResolveTypes( () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), TypeResolutionKind.FindTypesWithAttribute, cacheResult); } /// /// Resolves attributed types. /// /// The type of the attribute. /// The corresopnding types. public IEnumerable ResolveAttributedTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { return ResolveTypes( () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), TypeResolutionKind.FindAttributedTypes, cacheResult); } #endregion #region Private /// /// Gets the list of types. /// /// The list of types. /// For unit tests only. internal HashSet GetTypeLists() { return _types; } /// /// 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; } } /// /// Represents the error that occurs when a plugin was not found in the cache plugin list with the specified /// TypeResolutionKind. /// internal class CachedPluginNotFoundInFileException : Exception { } #endregion } internal static class PluginManagerExtensions { /// /// Resolves property editors (based on the resolved Iparameter editors - this saves a scan). /// public static IEnumerable ResolvePropertyEditors(this PluginManager mgr) { //return all proeprty editor types found except for the base property editor type return mgr.ResolveTypes() .Where(x => x.IsType()) .Except(new[] { typeof(PropertyEditor) }); } /// /// Resolves parameter editors (which includes property editors) /// internal static IEnumerable ResolveParameterEditors(this PluginManager mgr) { //return all paramter editor types found except for the base property editor type return mgr.ResolveTypes() .Except(new[] { typeof(ParameterEditor), typeof(PropertyEditor) }); } /// /// Resolves IApplicationStartupHandler objects. /// internal static IEnumerable ResolveApplicationStartupHandlers(this PluginManager mgr) { return mgr.ResolveTypes(); } /// /// Resolves ICacheRefresher objects. /// internal static IEnumerable ResolveCacheRefreshers(this PluginManager mgr) { return mgr.ResolveTypes(); } /// /// Resolves IPackageAction objects. /// internal static IEnumerable ResolvePackageActions(this PluginManager mgr) { return mgr.ResolveTypes(); } /// /// Resolves mapper types that have a MapperFor attribute defined. /// internal static IEnumerable ResolveAssignedMapperTypes(this PluginManager mgr) { return mgr.ResolveTypesWithAttribute(); } } }