diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index 25938e7b94..02ec81a8e1 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -7,7 +7,6 @@ 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; @@ -33,8 +32,6 @@ namespace Umbraco.Core { private const string CacheKey = "umbraco-plugins.list"; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - private static PluginManager _current; private static bool _hasCurrent; private static object _currentLock = new object(); @@ -43,22 +40,22 @@ namespace Umbraco.Core private readonly IRuntimeCacheProvider _runtimeCache; private readonly ProfilingLogger _logger; private readonly string _tempFolder; - private readonly HashSet _types = new HashSet(); + + private readonly object _typesLock = new object(); + private readonly Dictionary _types = new Dictionary(); private long _cachedAssembliesHash = -1; private long _currentAssembliesHash = -1; private IEnumerable _assemblies; - private HashSet _extensions; + private bool _reportedChange; /// /// Initializes a new instance of the class. - /// Creates a new PluginManager with an ApplicationContext instance which ensures that the plugin xml - /// file is cached temporarily until app startup completes. fixme? /// /// A mechanism for retrieving service objects. /// The application runtime cache. /// A profiling logger. - /// fixme + /// Whether to detect changes using hashes. internal PluginManager(IServiceProvider serviceProvider, IRuntimeCacheProvider runtimeCache, ProfilingLogger logger, bool detectChanges = true) { if (serviceProvider == null) throw new ArgumentNullException("serviceProvider"); @@ -76,13 +73,6 @@ namespace Umbraco.Core var pluginListFile = GetPluginListFilePath(); - //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()) - { - File.Delete(pluginListFile); - } - if (detectChanges) { //first check if the cached hash is 0, if it is then we ne @@ -91,7 +81,7 @@ namespace Umbraco.Core //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 + // 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); @@ -101,13 +91,12 @@ namespace Umbraco.Core } else { - - //if the hash has changed, clear out the persisted list no matter what, this will force + // 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) + // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; } } @@ -127,6 +116,24 @@ namespace Umbraco.Core set { _assemblies = value; } } + /// + /// Gets the type lists. + /// + /// For unit tests. + internal IEnumerable TypeLists + { + get { return _types.Values; } + } + + /// + /// Sets a type list. + /// + /// For unit tests. + internal void AddTypeList(TypeList typeList) + { + _types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList; + } + /// /// Gets or sets the singleton instance. /// @@ -163,19 +170,17 @@ namespace Umbraco.Core } } - #region Hash checking methods - + #region Hashing /// - /// Returns a bool if the assemblies in the /bin, app_code, global.asax, etc... have changed since they were last hashed. + /// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed. /// internal bool RequiresRescanning { get; private set; } /// - /// Returns the currently cached hash value of the scanned assemblies in the /bin folder. Returns 0 - /// if no cache is found. + /// Gets the currently cached hash value of the scanned assemblies. /// - /// + /// The cached hash value, or 0 if no cache is found. internal long CachedAssembliesHash { get @@ -184,24 +189,22 @@ namespace Umbraco.Core return _cachedAssembliesHash; var filePath = GetPluginHashFilePath(); - if (!File.Exists(filePath)) - return 0; + if (File.Exists(filePath) == false) 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; + + long val; + if (long.TryParse(hash, out val) == false) return 0; + + _cachedAssembliesHash = val; + return _cachedAssembliesHash; } } /// - /// Returns the current assemblies hash based on creating a hash from the assemblies in the /bin + /// Gets the current assemblies hash based on creating a hash from the assemblies in various places. /// - /// + /// The current hash. internal long CurrentAssembliesHash { get @@ -209,26 +212,24 @@ namespace Umbraco.Core 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), + _currentAssembliesHash = GetFileHash(new List> + { + // the bin folder and everything in it + new Tuple(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false), + // the app code folder and everything in it + new Tuple(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(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(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true) + }, _logger); - //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 + /// Writes the assembly hash file. /// private void WriteCachePluginsHash() { @@ -237,130 +238,174 @@ namespace Umbraco.Core } /// - /// Returns a unique hash for the combination of FileInfo objects passed in + /// 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 collection of files. + /// A profiling logger. + /// The hash. + /// 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). internal static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger) { 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 = filesAndFolders.Where(x => x.Item2 == false).ToArray(); - var fileContents = filesAndFolders.Except(fileInfos); + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + var uniqContent = new HashSet(); - //add each unique folder/file to the hash - foreach (var i in fileInfos.Select(x => x.Item1).DistinctBy(x => x.FullName)) + foreach (var fileOrFolder in filesAndFolders) { - 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 info = fileOrFolder.Item1; + if (fileOrFolder.Item2) { - var content = File.ReadAllText(i.FullName).Replace("\r\n", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty); + // add each unique file's contents to the hash + // normalize the content for cr/lf and case-sensitivity + + if (uniqContent.Contains(info.FullName)) continue; + uniqContent.Add(info.FullName); + if (File.Exists(info.FullName) == false) continue; + var content = RemoveCrLf(File.ReadAllText(info.FullName)); hashCombiner.AddCaseInsensitiveString(content); } + else + { + // add each unique folder/file to the hash + if (uniqInfos.Contains(info.FullName)) continue; + uniqInfos.Add(info.FullName); + hashCombiner.AddFileSystemItem(info); + } } - return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode()); + return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); } } + // 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); + } + + /// + /// Returns a unique hash for a combination of FileInfo objects. + /// + /// A collection of files. + /// A profiling logger. + /// The hash. 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)) + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + + foreach (var fileOrFolder in filesAndFolders) { - hashCombiner.AddFileSystemItem(i); + if (uniqInfos.Contains(fileOrFolder.FullName)) continue; + uniqInfos.Add(fileOrFolder.FullName); + hashCombiner.AddFileSystemItem(fileOrFolder); } - return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode()); + + return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); } } /// - /// Converts the hash value of current plugins to long from string + /// Converts a string hash value into an Int64. /// - /// - /// - internal static long ConvertPluginsHashFromHex(string val) + internal static long ConvertHashToInt64(string val) { long outVal; - if (Int64.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal)) - { - return outVal; - } - return 0; + return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 0; } + #endregion + + #region Cache + /// - /// 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. + /// Attemps to retrieve the list of types from the cache. /// - /// - /// - internal Attempt> TryGetCachedPluginsFromFile(TypeResolutionKind resolutionType) + /// Fails if the cache is missing or corrupt in any way. + internal Attempt> TryGetCached(Type baseType, Type attributeType) { + var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCache, TimeSpan.FromMinutes(4)); + + IEnumerable types; + cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out types); + return types == null + ? Attempt>.Fail() + : Attempt.Succeed(types); + } + + internal Dictionary, IEnumerable> ReadCache() + { + var cache = new Dictionary, IEnumerable>(); + var filePath = GetPluginListFilePath(); - if (!File.Exists(filePath)) - return Attempt>.Fail(); + if (File.Exists(filePath) == false) + return cache; - try + using (var stream = File.OpenRead(filePath)) + using (var reader = new StreamReader(stream)) { - //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)); + while (true) + { + var baseType = reader.ReadLine(); + if (baseType == null) return cache; // exit + if (baseType.StartsWith("<")) break; // old xml - if (xml.Root == null) - return Attempt>.Fail(); + var attributeType = reader.ReadLine(); + if (attributeType == null) break; - var typeElement = xml.Root.Elements() - .FirstOrDefault(x => - x.Name.LocalName == "baseType" - && ((string)x.Attribute("type")) == typeof(T).FullName - && ((string)x.Attribute("resolutionType")) == resolutionType.ToString()); + var types = new List(); + 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); + } - //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); + if (types == null) break; + } } + + cache.Clear(); + return cache; } /// - /// Removes cache files and internal cache as well + /// Removes cache files and internal cache. /// - /// - /// Generally only used for resetting cache, for example during the install process - /// + /// 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); @@ -370,112 +415,41 @@ namespace Umbraco.Core private string GetPluginListFilePath() { - return Path.Combine(_tempFolder, string.Format("umbraco-plugins.{0}.list", NetworkHelper.FileSafeMachineName)); + var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".list"; + return Path.Combine(_tempFolder, filename); } private string GetPluginHashFilePath() { - return Path.Combine(_tempFolder, string.Format("umbraco-plugins.{0}.hash", NetworkHelper.FileSafeMachineName)); + var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".hash"; + return Path.Combine(_tempFolder, filename); } - /// - /// 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() + internal void WriteCache() { var filePath = GetPluginListFilePath(); - if (!File.Exists(filePath)) - return false; - try + using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite)) + using (var writer = new StreamWriter(stream)) { - 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; + 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(); + } } } - /// - /// 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) + internal void UpdateCache() { - 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); + // 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(); } #endregion @@ -487,18 +461,18 @@ namespace Umbraco.Core /// /// The type to use for resolution. /// Indicates whether to throw if an instance cannot be created. - /// Indicates whether to use cache for type resolution. + /// Indicates whether to use cache for type resolution. /// A set of assemblies for type resolution. /// The created instances. /// /// By default is false and instances that cannot be created are just skipped. - /// By default is true and cache is used for type resolution. + /// By default is true and cache is used for type resolution. /// By default is null and is used. - //fixme if we specify assemblies we should not cache? + /// Caching is disabled when using specific assemblies. /// - internal IEnumerable FindAndCreateInstances(bool throwException = false, bool cacheResult = true, IEnumerable specificAssemblies = null) + internal IEnumerable FindAndCreateInstances(bool throwException = false, bool cache = true, IEnumerable specificAssemblies = null) { - var types = ResolveTypes(cacheResult, specificAssemblies); + var types = ResolveTypes(cache, specificAssemblies); return CreateInstances(types, throwException); } @@ -532,278 +506,311 @@ namespace Umbraco.Core #region Resolve Types - private IEnumerable ResolveTypesInternal( - Func> finder, - TypeResolutionKind resolutionType, - bool cacheResult) - { - using (var readLock = new UpgradeableReadLock(Locker)) - { - var typesFound = new List(); - - using (_logger.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.FirstOrDefault(x => x.IsList(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 != null && 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.Add(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.Types.ToList(); - } - - return typesFound; - } - } - - #endregion - /// - /// 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 + /// Resolves class types inheriting from or implementing the specified type /// - /// - /// - /// - /// - /// THIS METHODS IS NOT THREAD SAFE - /// - private void LoadViaScanningAndUpdateCacheFile(TypeList typeList, TypeResolutionKind resolutionKind, Func> finder) + /// The type to inherit from or implement. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type. + /// Caching is disabled when using specific assemblies. + public IEnumerable ResolveTypes(bool cache = true, IEnumerable specificAssemblies = null) { - //we don't have a cache for this so proceed to look them up by scanning - foreach (var t in finder()) - { - typeList.Add(t); - } - UpdateCachedPluginsFile(typeList.Types, resolutionKind); - } + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - #region Public Methods - - /// - /// Generic method to find the specified type and cache the result - /// - /// - /// - public IEnumerable ResolveTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) - { - if (specificAssemblies != null || cacheResult == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + // if not caching, or not IDiscoverable, directly resolve types + if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { - var extensions = ResolveTypesInternal( + return ResolveTypesInternal( + typeof (T), null, () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), - TypeResolutionKind.FindAllTypes, - cacheResult); - - return extensions.Where(x => typeof(T).IsAssignableFrom(x)); + cache); } - else - { - //Use the cache if all assemblies - var extensions = _extensions ?? (_extensions = new HashSet(ResolveTypesInternal( - () => TypeFinder.FindClassesOfType(AssembliesToScan), - TypeResolutionKind.FindAllTypes, true))); + // if caching and IDiscoverable + // filter the cached discovered types (and cache the result) - return extensions.Where(x => typeof(T).IsAssignableFrom(x)); - } + var discovered = ResolveTypesInternal( + typeof (IDiscoverable), null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + true); + + return ResolveTypesInternal( + typeof (T), null, + () => discovered + .Where(x => typeof (T).IsAssignableFrom(x)), + true); } /// - /// Generic method to find the specified type that has an attribute and cache the result + /// Resolves class types inheriting from or implementing the specified type and marked with the specified attribute. /// - /// - /// - /// - public IEnumerable ResolveTypesWithAttribute(bool cacheResult = true, IEnumerable specificAssemblies = null) + /// The type to inherit from or implement. + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type and marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable ResolveTypesWithAttribute(bool cache = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { - return ResolveTypes(specificAssemblies: specificAssemblies) - .Where(x => x.GetCustomAttributes(false).Any()); + // 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(specificAssemblies ?? AssembliesToScan), + cache); + } + + // if caching and IDiscoverable + // filter the cached discovered types (and cache the result) + + var discovered = ResolveTypesInternal( + typeof (IDiscoverable), null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + true); + + return ResolveTypesInternal( + typeof (T), typeof (TAttribute), + () => discovered + .Where(x => typeof(T).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttributes(false).Any()), + true); } /// /// Resolves class types marked with the specified attribute. /// /// The type of the attribute. - /// Indicates whether to use cache for type resolution. + /// Indicates whether to use cache for type resolution. /// A set of assemblies for type resolution. /// All class types marked with the specified attribute. - public IEnumerable ResolveAttributedTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) + /// Caching is disabled when using specific assemblies. + public IEnumerable ResolveAttributedTypes(bool cache = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { - return ResolveTypesInternal( + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + return ResolveTypesInternal( + typeof (object), typeof (TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), - TypeResolutionKind.FindAttributedTypes, - cacheResult); + cache); + } + + private IEnumerable ResolveTypesInternal( + Type baseType, Type attributeType, + Func> 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( + "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 ResolveTypesInternalLocked( + Type baseType, Type attributeType, + Func> 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("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 + if (_reportedChange == false) + { + _logger.Logger.Debug("Assemblies 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("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("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("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("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; + UpdateCache(); + } + + _logger.Logger.Debug("Resolved {0}, caching ({1}).", () => ResolvedName(baseType, attributeType), () => added.ToString().ToLowerInvariant()); + } + else + { + _logger.Logger.Debug("Resolved {0}.", () => ResolvedName(baseType, attributeType)); + } + + return typeList.Types; } #endregion - /// - /// Used for unit tests - /// - /// - internal HashSet GetTypeLists() - { - return _types; - } - - - #region Nested classes and stuff /// - /// The type of resolution being invoked + /// Groups a type and a resolution kind into a key. /// - internal enum TypeResolutionKind + private struct TypeListKey { - FindAllTypes, - FindAttributedTypes, - FindTypesWithAttribute + // 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; + } } /// /// Represents a list of types obtained by looking for types inheriting/implementing a /// specified type, and/or marked with a specified attribute type. /// - internal abstract class TypeList + internal class TypeList { + private readonly HashSet _types = new HashSet(); + + public TypeList(Type baseType, Type attributeType) + { + BaseType = baseType; + AttributeType = attributeType; + } + + public Type BaseType { get; private set; } + public Type AttributeType { get; private set; } + /// /// Adds a type. /// - public abstract void Add(Type t); + 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); + } /// /// Gets the types. /// - public abstract IEnumerable Types { get; } - - /// - /// Gets a value indicating whether this instance is a type list for a specified type and resolution type. - /// - public abstract bool IsList(TypeResolutionKind resolutionType); - } - - /// - /// Represents a list of types obtained by looking for types inheriting/implementing a - /// specified type, and/or marked with a specified attribute type. - /// - internal class TypeList : TypeList - { - private readonly TypeResolutionKind _resolutionType; - private readonly HashSet _types = new HashSet(); - - /// - /// Initializes a new instance of the class. - /// - public TypeList(TypeResolutionKind resolutionType) - { - _resolutionType = resolutionType; - } - - /// - public override void Add(Type type) - { - // only add the type if it inherits/implements T - // skip the check for FindAttributedTypes as in this case T is the attribute type - if (_resolutionType == TypeResolutionKind.FindAttributedTypes || typeof(T).IsAssignableFrom(type)) - _types.Add(type); - } - - /// - public override IEnumerable Types + public IEnumerable Types { get { return _types; } } - - /// - public override bool IsList(TypeResolutionKind resolutionType) - { - return _resolutionType == resolutionType && typeof (T) == typeof (TLookup); - } } /// diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index f970cf225b..26abad0f2e 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -1,32 +1,24 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Security; using System.Text; -using System.Threading; using System.Web; using System.Web.Compilation; -using System.Web.Hosting; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; namespace Umbraco.Core { - /// /// A utility class to find all classes of a certain type by reflection in the current bin folder /// of the web application. /// public static class TypeFinder { - private static volatile HashSet _localFilteredAssemblyCache = null; + private static volatile HashSet _localFilteredAssemblyCache; private static readonly object LocalFilteredAssemblyCacheLocker = new object(); /// @@ -63,7 +55,7 @@ namespace Umbraco.Core } catch (InvalidOperationException e) { - if (!(e.InnerException is SecurityException)) + if ((e.InnerException is SecurityException) == false) throw; } @@ -99,7 +91,7 @@ namespace Umbraco.Core } //if for some reason they are still no assemblies, then use the AppDomain to load in already loaded assemblies. - if (!assemblies.Any()) + if (assemblies.Any() == false) { foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) { @@ -111,12 +103,12 @@ namespace Umbraco.Core var fileExtensions = new[] { ".cs", ".vb" }; //only vb and cs files are supported var appCodeFolder = new DirectoryInfo(IOHelper.MapPath(IOHelper.ResolveUrl("~/App_code"))); //check if the folder exists and if there are any files in it with the supported file extensions - if (appCodeFolder.Exists && (fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any()))) + if (appCodeFolder.Exists && fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any())) { try { var appCodeAssembly = Assembly.Load("App_Code"); - if (!assemblies.Contains(appCodeAssembly)) // BuildManager will find App_Code already + if (assemblies.Contains(appCodeAssembly) == false) // BuildManager will find App_Code already assemblies.Add(appCodeAssembly); } catch (FileNotFoundException ex) @@ -128,7 +120,7 @@ namespace Umbraco.Core } catch (InvalidOperationException e) { - if (!(e.InnerException is SecurityException)) + if (e.InnerException is SecurityException == false) throw; } @@ -145,23 +137,16 @@ namespace Umbraco.Core internal static HashSet GetAssembliesWithKnownExclusions( IEnumerable excludeFromResults = null) { - if (_localFilteredAssemblyCache == null) + lock (LocalFilteredAssemblyCacheLocker) { - lock (LocalFilteredAssemblyCacheLocker) - { - //double check - if (_localFilteredAssemblyCache == null) - { - _localFilteredAssemblyCache = new HashSet(); - var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); - foreach (var a in assemblies) - { - _localFilteredAssemblyCache.Add(a); - } - } - } + // double check + if (_localFilteredAssemblyCache != null) + return _localFilteredAssemblyCache; + + var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); + _localFilteredAssemblyCache = new HashSet(assemblies); + return _localFilteredAssemblyCache; } - return _localFilteredAssemblyCache; } /// @@ -180,9 +165,9 @@ namespace Umbraco.Core exclusionFilter = new string[] { }; return GetAllAssemblies() - .Where(x => !excludeFromResults.Contains(x) - && !x.GlobalAssemblyCache - && !exclusionFilter.Any(f => x.FullName.StartsWith(f))); + .Where(x => excludeFromResults.Contains(x) == false + && x.GlobalAssemblyCache == false + && exclusionFilter.Any(f => x.FullName.StartsWith(f)) == false); } /// @@ -298,7 +283,7 @@ namespace Umbraco.Core { if (assemblies == null) throw new ArgumentNullException("assemblies"); - return GetClasses(assignTypeFrom, assemblies, onlyConcreteClasses, + return GetClassesWithBaseType(assignTypeFrom, assemblies, onlyConcreteClasses, //the additional filter will ensure that any found types also have the attribute applied. t => t.GetCustomAttributes(false).Any()); } @@ -324,7 +309,7 @@ namespace Umbraco.Core { if (assemblies == null) throw new ArgumentNullException("assemblies"); - return GetClasses(typeof(T), assemblies, onlyConcreteClasses); + return GetClassesWithBaseType(typeof(T), assemblies, onlyConcreteClasses); } /// @@ -363,105 +348,9 @@ namespace Umbraco.Core IEnumerable assemblies, bool onlyConcreteClasses) { - if (assemblies == null) throw new ArgumentNullException("assemblies"); - - if (TypeHelper.IsTypeAssignableFrom(attributeType) == false) - throw new ArgumentException("The type specified: " + attributeType + " is not an Attribute type"); - - var foundAttributedTypes = new HashSet(); - - var assemblyList = assemblies.ToArray(); - - //find all assembly references that are referencing the attribute type's assembly since we - //should only be scanning those assemblies because any other assembly will definitely not - //contain a class that has this attribute. - var referencedAssemblies = TypeHelper.GetReferencedAssemblies(attributeType, assemblyList); - - //get a list of non-referenced assemblies (we'll use this when we recurse below) - var otherAssemblies = assemblyList.Where(x => referencedAssemblies.Contains(x) == false).ToArray(); - - //loop through the referenced assemblies - foreach (var a in referencedAssemblies) - { - //get all types in this assembly - var allTypes = GetTypesWithFormattedException(a) - .ToArray(); - - var attributedTypes = new Type[] { }; - try - { - //now filter the types based on the onlyConcreteClasses flag, not interfaces, not static classes but have - //the specified attribute - attributedTypes = allTypes - .Where(t => (TypeHelper.IsNonStaticClass(t) - && (onlyConcreteClasses == false || t.IsAbstract == false)) - //the type must have this attribute - && t.GetCustomAttributes(attributeType, false).Any()) - .ToArray(); - } - catch (TypeLoadException ex) - { - LogHelper.Error(typeof(TypeFinder), string.Format("Could not query types on {0} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", a), ex); - continue; - } - - //add the types to our list to return - foreach (var t in attributedTypes) - { - foundAttributedTypes.Add(t); - } - - //get all attributes of the type being searched for - var allAttributeTypes = allTypes.Where(attributeType.IsAssignableFrom); - - //now we need to include types that may be inheriting from sub classes of the attribute type being searched for - //so we will search in assemblies that reference those types too. - foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)) - { - - //So that we are not scanning too much, we need to group the sub types: - // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. - // * We should also not search for sub types if the type is sealed since you cannot inherit from a sealed class - // * We should not search for sub types if the type is static since you cannot inherit from them. - var subTypeList = subTypesInAssembly - .Where(t => t.IsSealed == false && TypeHelper.IsStaticClass(t) == false) - .ToArray(); - - var baseClassAttempt = TypeHelper.GetLowestBaseType(subTypeList); - - //if there's a base class amongst the types then we'll only search for that type. - //otherwise we'll have to search for all of them. - var subTypesToSearch = new HashSet(); - if (baseClassAttempt.Success) - { - subTypesToSearch.Add(baseClassAttempt.Result); - } - else - { - foreach (var t in subTypeList) - { - subTypesToSearch.Add(t); - } - } - - foreach (var typeToSearch in subTypesToSearch) - { - //recursively find the types inheriting from this sub type in the other non-scanned assemblies. - var foundTypes = FindClassesWithAttribute(typeToSearch, otherAssemblies, onlyConcreteClasses); - - foreach (var f in foundTypes) - { - foundAttributedTypes.Add(f); - } - } - - } - } - - return foundAttributedTypes; + return GetClassesWithAttribute(attributeType, assemblies, onlyConcreteClasses); } - /// /// Finds the classes with attribute. /// @@ -485,122 +374,129 @@ namespace Umbraco.Core return FindClassesWithAttribute(GetAssembliesWithKnownExclusions()); } - #region Private methods + private static IEnumerable GetClassesWithAttribute( + Type attributeType, + IEnumerable assemblies, + bool onlyConcreteClasses) + { + if (typeof(Attribute).IsAssignableFrom(attributeType) == false) + throw new ArgumentException("Type " + attributeType + " is not an Attribute type."); + + var candidateAssemblies = new HashSet(assemblies); + var attributeAssemblyIsCandidate = candidateAssemblies.Contains(attributeType.Assembly); + candidateAssemblies.Remove(attributeType.Assembly); + var types = new List(); + + var stack = new Stack(); + stack.Push(attributeType.Assembly); + + while (stack.Count > 0) + { + var assembly = stack.Pop(); + + Type[] assemblyTypes = null; + if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) + { + // get all assembly types that can be assigned to baseType + try + { + assemblyTypes = GetTypesWithFormattedException(assembly) + .ToArray(); // in try block + } + catch (TypeLoadException ex) + { + LogHelper.Error(typeof(TypeFinder), string.Format("Could not query types on {0} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly), ex); + continue; + } + + types.AddRange(assemblyTypes.Where(x => + x.IsClass // only classes + && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed + && x.IsNestedPrivate == false // exclude nested private + && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract + && x.GetCustomAttribute() == null // exclude hidden + && x.GetCustomAttributes(attributeType, false).Any())); // marked with the attribute + } + + if (assembly != attributeType.Assembly && assemblyTypes.Where(attributeType.IsAssignableFrom).Any() == false) + continue; + + foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) + { + candidateAssemblies.Remove(referencing); + stack.Push(referencing); + } + } + + return types; + } + /// /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type /// deriving from the base type. /// - /// + /// /// /// /// An additional filter to apply for what types will actually be included in the return value /// - private static IEnumerable GetClasses( - Type assignTypeFrom, + private static IEnumerable GetClassesWithBaseType( + Type baseType, IEnumerable assemblies, bool onlyConcreteClasses, Func additionalFilter = null) { - //the default filter will always return true. - if (additionalFilter == null) + var candidateAssemblies = new HashSet(assemblies); + var baseTypeAssemblyIsCandidate = candidateAssemblies.Contains(baseType.Assembly); + candidateAssemblies.Remove(baseType.Assembly); + var types = new List(); + + var stack = new Stack(); + stack.Push(baseType.Assembly); + + while (stack.Count > 0) { - additionalFilter = type => true; - } + var assembly = stack.Pop(); - var foundAssignableTypes = new HashSet(); - - var assemblyList = assemblies.ToArray(); - - //find all assembly references that are referencing the current type's assembly since we - //should only be scanning those assemblies because any other assembly will definitely not - //contain sub type's of the one we're currently looking for - var referencedAssemblies = TypeHelper.GetReferencedAssemblies(assignTypeFrom, assemblyList); - - //get a list of non-referenced assemblies (we'll use this when we recurse below) - var otherAssemblies = assemblyList.Where(x => referencedAssemblies.Contains(x) == false).ToArray(); - - //loop through the referenced assemblies - foreach (var a in referencedAssemblies) - { - //get all types in the assembly that are sub types of the current type - var allSubTypes = GetTypesWithFormattedException(a) - .Where(assignTypeFrom.IsAssignableFrom) - .ToArray(); - - var filteredTypes = new Type[] { }; - try + // get all assembly types that can be assigned to baseType + Type[] assemblyTypes = null; + if (assembly != baseType.Assembly || baseTypeAssemblyIsCandidate) { - //now filter the types based on the onlyConcreteClasses flag, not interfaces, not static classes - filteredTypes = allSubTypes - .Where(t => (TypeHelper.IsNonStaticClass(t) - //Do not include nested private classes - since we are in full trust now this will find those too! - && t.IsNestedPrivate == false - && (onlyConcreteClasses == false || t.IsAbstract == false) - //Do not include classes that are flagged to hide from the type finder - && t.GetCustomAttribute() == null - && additionalFilter(t))) - .ToArray(); + try + { + assemblyTypes = GetTypesWithFormattedException(assembly) + .Where(baseType.IsAssignableFrom) + .ToArray(); // in try block + } + catch (TypeLoadException ex) + { + LogHelper.Error(typeof(TypeFinder), string.Format("Could not query types on {0} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly), ex); + continue; + } + + types.AddRange(assemblyTypes.Where(x => + x.IsClass // only classes + && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed + && x.IsNestedPrivate == false // exclude nested private + && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract + && x.GetCustomAttribute() == null // exclude hidden + && (additionalFilter == null || additionalFilter(x)))); // filter } - catch (TypeLoadException ex) - { - LogHelper.Error(typeof(TypeFinder), string.Format("Could not query types on {0} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", a), ex); + + if (assembly != baseType.Assembly && assemblyTypes.All(x => x.IsSealed)) continue; - } - //add the types to our list to return - foreach (var t in filteredTypes) + foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) { - foundAssignableTypes.Add(t); + candidateAssemblies.Remove(referencing); + stack.Push(referencing); } - - //now we need to include types that may be inheriting from sub classes of the type being searched for - //so we will search in assemblies that reference those types too. - foreach (var subTypesInAssembly in allSubTypes.GroupBy(x => x.Assembly)) - { - - //So that we are not scanning too much, we need to group the sub types: - // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. - // * We should also not search for sub types if the type is sealed since you cannot inherit from a sealed class - // * We should not search for sub types if the type is static since you cannot inherit from them. - var subTypeList = subTypesInAssembly - .Where(t => t.IsSealed == false && TypeHelper.IsStaticClass(t) == false) - .ToArray(); - - var baseClassAttempt = TypeHelper.GetLowestBaseType(subTypeList); - - //if there's a base class amongst the types then we'll only search for that type. - //otherwise we'll have to search for all of them. - var subTypesToSearch = new HashSet(); - if (baseClassAttempt.Success) - { - subTypesToSearch.Add(baseClassAttempt.Result); - } - else - { - foreach (var t in subTypeList) - { - subTypesToSearch.Add(t); - } - } - - foreach (var typeToSearch in subTypesToSearch) - { - //recursively find the types inheriting from this sub type in the other non-scanned assemblies. - var foundTypes = GetClasses(typeToSearch, otherAssemblies, onlyConcreteClasses, additionalFilter); - - foreach (var f in foundTypes) - { - foundAssignableTypes.Add(f); - } - } - - } - } - return foundAssignableTypes; + + return types; } internal static IEnumerable GetTypesWithFormattedException(Assembly a) @@ -666,7 +562,6 @@ namespace Umbraco.Core #endregion - public static Type GetTypeByName(string typeName) { var type = BuildManager.GetType(typeName, false); @@ -684,6 +579,5 @@ namespace Umbraco.Core .Select(x => x.GetType(typeName)) .FirstOrDefault(x => x != null); } - } } diff --git a/src/Umbraco.Core/TypeHelper.cs b/src/Umbraco.Core/TypeHelper.cs index f0b3a82961..b63205ae3d 100644 --- a/src/Umbraco.Core/TypeHelper.cs +++ b/src/Umbraco.Core/TypeHelper.cs @@ -30,44 +30,43 @@ namespace Umbraco.Core /// /// Find all assembly references that are referencing the assignTypeFrom Type's assembly found in the assemblyList /// - /// - /// + /// The referenced assembly. + /// A list of assemblies. /// /// /// If the assembly of the assignTypeFrom Type is in the App_Code assembly, then we return nothing since things cannot /// reference that assembly, same with the global.asax assembly. /// - public static Assembly[] GetReferencedAssemblies(Type assignTypeFrom, IEnumerable assemblies) + public static Assembly[] GetReferencingAssemblies(Assembly assembly, IEnumerable assemblies) { - //check if it is the app_code assembly. - //check if it is App_global.asax assembly - if (assignTypeFrom.Assembly.IsAppCodeAssembly() || assignTypeFrom.Assembly.IsGlobalAsaxAssembly()) + // check if it is the app_code assembly. + // check if it is App_global.asax assembly + if (assembly.IsAppCodeAssembly() || assembly.IsGlobalAsaxAssembly()) { return Enumerable.Empty().ToArray(); } - //find all assembly references that are referencing the current type's assembly since we - //should only be scanning those assemblies because any other assembly will definitely not - //contain sub type's of the one we're currently looking for - return assemblies - .Where(assembly => - assembly == assignTypeFrom.Assembly - || HasReferenceToAssemblyWithName(assembly, assignTypeFrom.Assembly.GetName().Name)) - .ToArray(); + // find all assembly references that are referencing the current type's assembly since we + // should only be scanning those assemblies because any other assembly will definitely not + // contain sub type's of the one we're currently looking for + var name = assembly.GetName().Name; + return assemblies.Where(x => x == assembly || HasReference(x, name)).ToArray(); } /// /// checks if the assembly has a reference with the same name as the expected assembly name. /// /// - /// + /// /// - private static bool HasReferenceToAssemblyWithName(Assembly assembly, string expectedAssemblyName) - { - return assembly - .GetReferencedAssemblies() - .Select(a => a.Name) - .Contains(expectedAssemblyName, StringComparer.Ordinal); + public static bool HasReference(Assembly assembly, string name) + { + // ReSharper disable once LoopCanBeConvertedToQuery - no! + foreach (var a in assembly.GetReferencedAssemblies()) + { + if (string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)) return true; + } + return false; } /// diff --git a/src/Umbraco.Tests/CodeFirst/TypeInheritanceTest.cs b/src/Umbraco.Tests/CodeFirst/TypeInheritanceTest.cs index 7808b04eaa..7d2942f1c9 100644 --- a/src/Umbraco.Tests/CodeFirst/TypeInheritanceTest.cs +++ b/src/Umbraco.Tests/CodeFirst/TypeInheritanceTest.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.CodeFirst } }; - + } [Test] @@ -84,10 +84,7 @@ namespace Umbraco.Tests.CodeFirst var foundTypes = _pluginManager.ResolveContentTypeBaseTypes(); Assert.That(foundTypes.Count(), Is.EqualTo(15)); - Assert.AreEqual(1, - _pluginManager.GetTypeLists() - .Count(x => x.IsList(PluginManager.TypeResolutionKind.FindAllTypes))); + Assert.AreEqual(1, _pluginManager.TypeLists.Count(x => x.BaseType == typeof (ContentTypeBase) && x.AttributeType == null)); } - } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 5ea3267a37..66a6e9da47 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -35,14 +35,14 @@ namespace Umbraco.Tests.Plugins public void Initialize() { //this ensures its reset - _manager = new PluginManager(new ActivatorServiceProvider(), new NullCacheProvider(), + _manager = new PluginManager(new ActivatorServiceProvider(), new NullCacheProvider(), new ProfilingLogger(Mock.Of(), Mock.Of())); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver //TODO: Should probably update this so it only searches this assembly and add custom types to be found _manager.AssembliesToScan = new[] { - this.GetType().Assembly, + this.GetType().Assembly, typeof(ApplicationStartupHandler).Assembly, typeof(SqlCEHelper).Assembly, typeof(CMSNode).Assembly, @@ -161,7 +161,7 @@ namespace Umbraco.Tests.Plugins public void Detect_Legacy_Plugin_File_List() { var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); - + var filePath= Path.Combine(tempFolder, string.Format("umbraco-plugins.{0}.list", NetworkHelper.FileSafeMachineName)); File.WriteAllText(filePath, @" @@ -170,12 +170,11 @@ namespace Umbraco.Tests.Plugins "); - - Assert.IsTrue(_manager.DetectLegacyPluginListFile()); + + Assert.IsEmpty(_manager.ReadCache()); // uber-legacy cannot be read File.Delete(filePath); - //now create a valid one File.WriteAllText(filePath, @" @@ -183,19 +182,32 @@ namespace Umbraco.Tests.Plugins "); - Assert.IsFalse(_manager.DetectLegacyPluginListFile()); + Assert.IsEmpty(_manager.ReadCache()); // legacy cannot be read + + File.Delete(filePath); + + File.WriteAllText(filePath, @"IContentFinder + +MyContentFinder +AnotherContentFinder + +"); + + Assert.IsNotNull(_manager.ReadCache()); // works } [Test] public void Create_Cached_Plugin_File() { - var types = new[] { typeof(PluginManager), typeof(PluginManagerTests), typeof(UmbracoContext) }; + var types = new[] { typeof (PluginManager), typeof (PluginManagerTests), typeof (UmbracoContext) }; - //yes this is silly, none of these types inherit from string, but this is just to test the xml file format - _manager.UpdateCachedPluginsFile(types, PluginManager.TypeResolutionKind.FindAllTypes); + var typeList1 = new PluginManager.TypeList(typeof (object), null); + foreach (var type in types) typeList1.Add(type); + _manager.AddTypeList(typeList1); + _manager.WriteCache(); - var plugins = _manager.TryGetCachedPluginsFromFile(PluginManager.TypeResolutionKind.FindAllTypes); - var diffType = _manager.TryGetCachedPluginsFromFile(PluginManager.TypeResolutionKind.FindAttributedTypes); + var plugins = _manager.TryGetCached(typeof (object), null); + var diffType = _manager.TryGetCached(typeof (object), typeof (ObsoleteAttribute)); Assert.IsTrue(plugins.Success); //this will be false since there is no cache of that type resolution kind @@ -211,7 +223,7 @@ namespace Umbraco.Tests.Plugins public void PluginHash_From_String() { var s = "hello my name is someone".GetHashCode().ToString("x", CultureInfo.InvariantCulture); - var output = PluginManager.ConvertPluginsHashFromHex(s); + var output = PluginManager.ConvertHashToInt64(s); Assert.AreNotEqual(0, output); } @@ -241,7 +253,7 @@ namespace Umbraco.Tests.Plugins var list1 = new[] { f1, f2, f3, f4, f5, f6 }; var list2 = new[] { f1, f3, f5 }; var list3 = new[] { f1, f3, f5, f7 }; - + //Act var hash1 = PluginManager.GetFileHash(list1, new ProfilingLogger(Mock.Of(), Mock.Of())); var hash2 = PluginManager.GetFileHash(list2, new ProfilingLogger(Mock.Of(), Mock.Of())); @@ -260,9 +272,7 @@ namespace Umbraco.Tests.Plugins { var foundTypes1 = _manager.ResolveFindMeTypes(); var foundTypes2 = _manager.ResolveFindMeTypes(); - Assert.AreEqual(1, - _manager.GetTypeLists() - .Count(x => x.IsList(PluginManager.TypeResolutionKind.FindAllTypes))); + Assert.AreEqual(1, _manager.TypeLists.Count(x => x.BaseType == typeof(IFindMe) && x.AttributeType == null)); } [Test] @@ -345,20 +355,20 @@ namespace Umbraco.Tests.Plugins { var types = new HashSet(); - var propEditors = new PluginManager.TypeList(PluginManager.TypeResolutionKind.FindAllTypes); + var propEditors = new PluginManager.TypeList(typeof (PropertyEditor), null); propEditors.Add(typeof(LabelPropertyEditor)); types.Add(propEditors); - var found = types.SingleOrDefault(x => x.IsList(PluginManager.TypeResolutionKind.FindAllTypes)); + var found = types.SingleOrDefault(x => x.BaseType == typeof (PropertyEditor) && x.AttributeType == null); Assert.IsNotNull(found); //This should not find a type list of this type - var shouldNotFind = types.SingleOrDefault(x => x.IsList(PluginManager.TypeResolutionKind.FindAllTypes)); + var shouldNotFind = types.SingleOrDefault(x => x.BaseType == typeof (IParameterEditor) && x.AttributeType == null); Assert.IsNull(shouldNotFind); } - + [XsltExtension("Blah.Blah")] public class MyXsltExtension { diff --git a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs index 50ab053668..35c14860d7 100644 --- a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -26,11 +26,11 @@ using Umbraco.Web.BaseRest; namespace Umbraco.Tests.Plugins { - + /// /// Tests for typefinder /// - [TestFixture] + [TestFixture] public class TypeFinderTests { /// @@ -43,7 +43,7 @@ namespace Umbraco.Tests.Plugins { _assemblies = new[] { - this.GetType().Assembly, + this.GetType().Assembly, typeof(ApplicationStartupHandler).Assembly, typeof(SqlCEHelper).Assembly, typeof(CMSNode).Assembly, @@ -75,7 +75,7 @@ namespace Umbraco.Tests.Plugins [Test] public void Find_Classes_Of_Type() { - var typesFound = TypeFinder.FindClassesOfType(_assemblies); + var typesFound = TypeFinder.FindClassesOfType(_assemblies); var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); @@ -118,7 +118,7 @@ namespace Umbraco.Tests.Plugins } } } - + } [Ignore] @@ -149,7 +149,7 @@ namespace Umbraco.Tests.Plugins } } } - + } public class MyTag : ITag @@ -161,7 +161,7 @@ namespace Umbraco.Tests.Plugins public class MySuperTag : MyTag { - + } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] @@ -203,7 +203,7 @@ namespace Umbraco.Tests.Plugins /// This is a modified version of: http://www.dominicpettifer.co.uk/Blog/44/how-to-get-a-reference-to-all-assemblies-in-the--bin-folder /// /// - /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been + /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been /// loaded in the CLR, not all assemblies. /// See these threads: /// http://issues.umbraco.org/issue/U5-198 @@ -329,8 +329,8 @@ namespace Umbraco.Tests.Plugins } catch (SecurityException) { - //we will just ignore this because this will fail - //in medium trust for system assemblies, we get an exception but we just want to continue until we get to + //we will just ignore this because this will fail + //in medium trust for system assemblies, we get an exception but we just want to continue until we get to //an assembly that is ok. } } @@ -347,9 +347,9 @@ namespace Umbraco.Tests.Plugins } catch (SecurityException) { - //we will just ignore this because if we are trying to do a call to: + //we will just ignore this because if we are trying to do a call to: // AssemblyName.ReferenceMatchesDefinition(a.GetName(), assemblyName))) - //in medium trust for system assemblies, we get an exception but we just want to continue until we get to + //in medium trust for system assemblies, we get an exception but we just want to continue until we get to //an assembly that is ok. } } @@ -361,7 +361,7 @@ namespace Umbraco.Tests.Plugins } /// - /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan + /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan /// and exluding the ones passed in and excluding the exclusion list filter, the results of this are /// cached for perforance reasons. /// @@ -429,7 +429,7 @@ namespace Umbraco.Tests.Plugins "RouteDebugger,", "SqlCE4Umbraco,", "umbraco.datalayer,", - "umbraco.interfaces,", + "umbraco.interfaces,", "umbraco.providers,", "Umbraco.Web.UI,", "umbraco.webservices", @@ -631,5 +631,5 @@ namespace Umbraco.Tests.Plugins } } - + } \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 642bad4a82..56e6a43a05 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -60,7 +60,7 @@ namespace Umbraco.Tests.TestHelpers public override void TearDown() { base.TearDown(); - + // reset settings SettingsForTests.Reset(); UmbracoContext.Current = null; @@ -117,7 +117,7 @@ namespace Umbraco.Tests.TestHelpers } /// - /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan + /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan /// all of the assemblies. Inheritors can override this if plugin manager resetting is required, generally needs /// to be set to true if the SetupPluginManager has been overridden. /// diff --git a/src/umbraco.businesslogic/PluginManagerExtensions.cs b/src/umbraco.businesslogic/PluginManagerExtensions.cs index 7a61414315..a7f60d3c27 100644 --- a/src/umbraco.businesslogic/PluginManagerExtensions.cs +++ b/src/umbraco.businesslogic/PluginManagerExtensions.cs @@ -18,7 +18,7 @@ namespace umbraco.businesslogic internal static IEnumerable ResolveApplications(this PluginManager resolver) { //don't cache the result of this because it is only used once during app startup, caching will just add a bit more mem overhead for no reason - return resolver.ResolveTypesWithAttribute(cacheResult:false); + return resolver.ResolveTypesWithAttribute(cache:false); } /// @@ -29,7 +29,7 @@ namespace umbraco.businesslogic internal static IEnumerable ResolveAttributedTrees(this PluginManager resolver) { //don't cache the result of this because it is only used once during app startup, caching will just add a bit more mem overhead for no reason - return resolver.ResolveTypesWithAttribute(cacheResult:false); + return resolver.ResolveTypesWithAttribute(cache:false); } }