using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using LightInject; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; namespace Umbraco.Core.Components { // note: this class is NOT thread-safe in any ways internal class BootLoader { private readonly IServiceContainer _container; private readonly ProfilingLogger _proflog; private IUmbracoComponent[] _components; private bool _booted; private const int LogThresholdMilliseconds = 100; /// /// Initializes a new instance of the class. /// /// The application container. public BootLoader(IServiceContainer container) { if (container == null) throw new ArgumentNullException(nameof(container)); _container = container; _proflog = container.GetInstance(); } private class EnableInfo { public bool Enabled; public int Weight = -1; } public void Boot(IEnumerable componentTypes, RuntimeLevel level) { if (_booted) throw new InvalidOperationException("Can not boot, has already booted."); var orderedComponentTypes = PrepareComponentTypes(componentTypes, level); InstanciateComponents(orderedComponentTypes); ComposeComponents(level); InitializeComponents(); // rejoice! _booted = true; } private IEnumerable PrepareComponentTypes(IEnumerable componentTypes, RuntimeLevel level) { using (_proflog.DebugDuration("Preparing component types.", "Prepared component types.")) { return PrepareComponentTypes2(componentTypes, level); } } private static IEnumerable PrepareComponentTypes2(IEnumerable componentTypes, RuntimeLevel level) { // create a list, remove those that cannot be enabled due to runtime level var componentTypeList = componentTypes .Where(x => { var attr = x.GetCustomAttribute(); return attr == null || level >= attr.MinLevel; }) .ToList(); // cannot remove that one - ever if (componentTypeList.Contains(typeof(UmbracoCoreComponent)) == false) componentTypeList.Add(typeof(UmbracoCoreComponent)); var enabled = new Dictionary(); // process the enable/disable attributes // these two attributes are *not* inherited and apply to *classes* only (not interfaces). // remote declarations (when a component enables/disables *another* component) // have priority over local declarations (when a component disables itself) so that // ppl can enable components that, by default, are disabled. // what happens in case of conflicting remote declarations is unspecified. more // precisely, the last declaration to be processed wins, but the order of the // declarations depends on the type finder and is unspecified. foreach (var componentType in componentTypeList) { foreach (var attr in componentType.GetCustomAttributes()) { var type = attr.EnabledType ?? componentType; EnableInfo enableInfo; if (enabled.TryGetValue(type, out enableInfo) == false) enableInfo = enabled[type] = new EnableInfo(); var weight = type == componentType ? 1 : 2; if (enableInfo.Weight > weight) continue; enableInfo.Enabled = true; enableInfo.Weight = weight; } foreach (var attr in componentType.GetCustomAttributes()) { var type = attr.DisabledType ?? componentType; if (type == typeof(UmbracoCoreComponent)) throw new InvalidOperationException("Cannot disable UmbracoCoreComponent."); EnableInfo enableInfo; if (enabled.TryGetValue(type, out enableInfo) == false) enableInfo = enabled[type] = new EnableInfo(); var weight = type == componentType ? 1 : 2; if (enableInfo.Weight > weight) continue; enableInfo.Enabled = false; enableInfo.Weight = weight; } } // remove components that end up being disabled foreach (var kvp in enabled.Where(x => x.Value.Enabled == false)) componentTypeList.Remove(kvp.Key); // sort the components according to their dependencies var items = new List>(); var temp = new List(); // reduce allocs foreach (var type in componentTypeList) { temp.Clear(); //// for tests //Console.WriteLine("Components & Dependencies:"); //Console.WriteLine(type.FullName); //foreach (var attribute in type.GetCustomAttributes()) // Console.WriteLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue ? (attribute.Weak.Value ? " (weak)" : " (strong)") : "")); //foreach (var i in type.GetInterfaces()) //{ // Console.WriteLine(" " + i.FullName); // foreach (var attribute in i.GetCustomAttributes()) // Console.WriteLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue ? (attribute.Weak.Value ? " (weak)" : " (strong)") : "")); //} //Console.WriteLine("/"); //Console.WriteLine(); // get attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only var attributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) .Concat(type.GetCustomAttributes()); // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. foreach (var attr in attributes) { // requiring an interface = require any enabled component implementing that interface // unless strong, and then require at least one enabled component implementing that interface if (attr.RequiredType.IsInterface) { var implems = componentTypeList.Where(x => attr.RequiredType.IsAssignableFrom(x)).ToList(); if (implems.Count > 0) temp.AddRange(implems); else if (attr.Weak == false) // if explicitely set to !weak, is strong, else is weak throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } // requiring a class = require that the component is enabled // unless weak, and then requires it if it is enabled else { if (componentTypeList.Contains(attr.RequiredType)) temp.Add(attr.RequiredType); else if (attr.Weak != true) // if not explicitely set to weak, is strong throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } var dependsOn = temp.Distinct().Select(x => x.FullName).ToArray(); items.Add(new TopologicalSorter.DependencyField(type.FullName, dependsOn, new Lazy(() => type))); } return TopologicalSorter.GetSortedItems(items); } private void InstanciateComponents(IEnumerable types) { using (_proflog.DebugDuration("Instanciating components.", "Instanciated components.")) { _components = types.Select(x => (IUmbracoComponent) Activator.CreateInstance(x)).ToArray(); } } private void ComposeComponents(RuntimeLevel level) { using (_proflog.DebugDuration($"Composing components. (log when >{LogThresholdMilliseconds}ms)", "Composed components.")) { var composition = new Composition(_container, level); foreach (var component in _components) { var componentType = component.GetType(); using (_proflog.DebugDuration($"Composing {componentType.FullName}.", $"Composed {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { component.Compose(composition); } } } } private void InitializeComponents() { // use a container scope to ensure that PerScope instances are disposed // components that require instances that should not survive should register them with PerScope lifetime using (_proflog.DebugDuration($"Initializing components. (log when >{LogThresholdMilliseconds}ms)", "Initialized components.")) using (_container.BeginScope()) { foreach (var component in _components) { var componentType = component.GetType(); var initializers = componentType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(x => x.Name == "Initialize" && x.IsGenericMethod == false); using (_proflog.DebugDuration($"Initializing {componentType.FullName}.", $"Initialised {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { foreach (var initializer in initializers) { var parameters = initializer.GetParameters() .Select(x => GetParameter(componentType, x.ParameterType)) .ToArray(); initializer.Invoke(component, parameters); } } } } } private object GetParameter(Type componentType, Type parameterType) { object param; try { param = _container.TryGetInstance(parameterType); } catch (Exception e) { throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}.", e); } if (param == null) throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}."); return param; } public void Terminate() { if (_booted == false) { _proflog.Logger.Warn("Cannot terminate, has not booted."); return; } using (_proflog.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) { foreach (var component in _components) { var componentType = component.GetType(); using (_proflog.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { component.Terminate(); } } } } } }