using System; using System.Collections.Generic; using System.Reflection; using System.Threading; using System.Web; using Umbraco.Core.Cache; using Umbraco.Core.Components; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Services.Implement; using Umbraco.Core.Sync; namespace Umbraco.Core.Runtime { /// /// Represents the Core Umbraco runtime. /// /// Does not handle any of the web-related aspects of Umbraco (startup, etc). It /// should be possible to use this runtime in console apps. public class CoreRuntime : IRuntime { private Components.Components _components; private IFactory _factory; private RuntimeState _state; /// /// Gets the logger. /// protected ILogger Logger { get; private set; } /// /// Gets the profiling logger. /// protected IProfilingLogger ProfilingLogger { get; private set; } /// public IRuntimeState State => _state; /// public virtual IFactory Boot(IRegister register) { // create and register the essential services // ie the bare minimum required to boot // loggers var logger = GetLogger(); Logger = logger; var profiler = GetProfiler(); var profilingLogger = new ProfilingLogger(logger, profiler); ProfilingLogger = profilingLogger; // application environment ConfigureUnhandledException(); ConfigureAssemblyResolve(); ConfigureApplicationRootPath(); // application caches var appCaches = GetAppCaches(); var runtimeCache = appCaches.RuntimeCache; // database factory var databaseFactory = GetDatabaseFactory(); // type loader var globalSettings = Current.Config.Global(); var localTempStorage = globalSettings.LocalTempStorageLocation; var typeLoader = new TypeLoader(runtimeCache, localTempStorage, profilingLogger); // runtime state // beware! must use '() => _factory.GetInstance()' and NOT '_factory.GetInstance' // as the second one captures the current value (null) and therefore fails _state = new RuntimeState(logger, Current.Config.Umbraco(), Current.Config.Global(), new Lazy(() => _factory.GetInstance()), new Lazy(() => _factory.GetInstance())) { Level = RuntimeLevel.Boot }; // main dom var mainDom = new MainDom(logger); // create the composition var composition = new Composition(register, typeLoader, profilingLogger, _state); composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, _state); // register runtime-level services // there should be none, really - this is here "just in case" Compose(composition); // the boot loader boots using a container scope, so anything that is PerScope will // be disposed after the boot loader has booted, and anything else will remain. // note that this REQUIRES that perWebRequestScope has NOT been enabled yet, else // the container will fail to create a scope since there is no http context when // the application starts. // the boot loader is kept in the runtime for as long as Umbraco runs, and components // are NOT disposed - which is not a big deal as long as they remain lightweight // objects. using (var bootTimer = ProfilingLogger.TraceDuration( $"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()} on {NetworkHelper.MachineName}.", "Booted.", "Boot failed.")) { try { // throws if not full-trust new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand(); logger.Debug("Runtime: {Runtime}", GetType().FullName); AquireMainDom(mainDom); DetermineRuntimeLevel(databaseFactory); var componentTypes = ResolveComponentTypes(typeLoader); _components = new Components.Components(composition, componentTypes, profilingLogger); _components.Compose(); _factory = Current.Factory = composition.CreateFactory(); _components.Initialize(_factory); } catch (Exception e) { _state.Level = RuntimeLevel.BootFailed; var bfe = e as BootFailedException ?? new BootFailedException("Boot failed.", e); _state.BootFailedException = bfe; bootTimer.Fail(exception: bfe); // be sure to log the exception - even if we repeat ourselves // if something goes wrong above, we may end up with no factory // meaning nothing can get the runtime state, etc - so let's try // to make sure we have a factory if (_factory == null) { try { _factory = Current.Factory = composition.CreateFactory(); } catch { /* yea */ } } // throwing here can cause w3wp to hard-crash and we want to avoid it. // instead, we're logging the exception and setting level to BootFailed. // various parts of Umbraco such as UmbracoModule and UmbracoDefaultOwinStartup // understand this and will nullify themselves, while UmbracoModule will // throw a BootFailedException for every requests. } } return _factory; } protected virtual void ConfigureUnhandledException() { //take care of unhandled exceptions - there is nothing we can do to // prevent the launch process to go down but at least we can try // and log the exception AppDomain.CurrentDomain.UnhandledException += (_, args) => { var exception = (Exception)args.ExceptionObject; var isTerminating = args.IsTerminating; // always true? var msg = "Unhandled exception in AppDomain"; if (isTerminating) msg += " (terminating)"; msg += "."; Logger.Error(exception, msg); }; } protected virtual void ConfigureAssemblyResolve() { // When an assembly can't be resolved. In here we can do magic with the assembly name and try loading another. // This is used for loading a signed assembly of AutoMapper (v. 3.1+) without having to recompile old code. AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { // ensure the assembly is indeed AutoMapper and that the PublicKeyToken is null before trying to Load again // do NOT just replace this with 'return Assembly', as it will cause an infinite loop -> stack overflow if (args.Name.StartsWith("AutoMapper") && args.Name.EndsWith("PublicKeyToken=null")) return Assembly.Load(args.Name.Replace(", PublicKeyToken=null", ", PublicKeyToken=be96cd2c38ef1005")); return null; }; } protected virtual void ConfigureApplicationRootPath() { var path = GetApplicationRootPath(); if (string.IsNullOrWhiteSpace(path) == false) IOHelper.SetRootDirectory(path); } private void AquireMainDom(MainDom mainDom) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { mainDom.Acquire(); } catch { timer.Fail(); throw; } } } // internal for tests internal void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory) { using (var timer = ProfilingLogger.DebugDuration("Determining runtime level.", "Determined.")) { try { _state.Level = DetermineRuntimeLevel2(databaseFactory); ProfilingLogger.Debug("Runtime level: {RuntimeLevel}", _state.Level); if (_state.Level == RuntimeLevel.Upgrade) { ProfilingLogger.Debug("Configure database factory for upgrades."); databaseFactory.ConfigureForUpgrade(); } } catch { _state.Level = RuntimeLevel.BootFailed; timer.Fail(); throw; } } } private IEnumerable ResolveComponentTypes(TypeLoader typeLoader) { using (var timer = ProfilingLogger.TraceDuration("Resolving component types.", "Resolved.")) { try { return GetComponentTypes(typeLoader); } catch { timer.Fail(); throw; } } } /// public virtual void Terminate() { using (ProfilingLogger.DebugDuration("Terminating Umbraco.", "Terminated.")) { _components?.Terminate(); } } /// /// Composes the runtime. /// public virtual void Compose(Composition composition) { // nothing } private RuntimeLevel DetermineRuntimeLevel2(IUmbracoDatabaseFactory databaseFactory) { var localVersion = UmbracoVersion.LocalVersion; // the local, files, version var codeVersion = _state.SemanticVersion; // the executing code version var connect = false; if (localVersion == null) { // there is no local version, we are not installed Logger.Debug("No local version, need to install Umbraco."); return RuntimeLevel.Install; } if (localVersion < codeVersion) { // there *is* a local version, but it does not match the code version // need to upgrade Logger.Debug("Local version '{LocalVersion}' < code version '{CodeVersion}', need to upgrade Umbraco.", localVersion, codeVersion); } else if (localVersion > codeVersion) { Logger.Warn("Local version '{LocalVersion}' > code version '{CodeVersion}', downgrading is not supported.", localVersion, codeVersion); // in fact, this is bad enough that we want to throw throw new BootFailedException($"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); } else if (databaseFactory.Configured == false) { // local version *does* match code version, but the database is not configured // install (again? this is a weird situation...) Logger.Debug("Database is not configured, need to install Umbraco."); return RuntimeLevel.Install; } // else, keep going, // anything other than install wants a database - see if we can connect // (since this is an already existing database, assume localdb is ready) for (var i = 0; i < 5; i++) { connect = databaseFactory.CanConnect; if (connect) break; Logger.Debug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } if (connect == false) { // cannot connect to configured database, this is bad, fail Logger.Debug("Could not connect to database."); // in fact, this is bad enough that we want to throw throw new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); } // if we already know we want to upgrade, // still run EnsureUmbracoUpgradeState to get the states // (v7 will just get a null state, that's ok) // else // look for a matching migration entry - bypassing services entirely - they are not 'up' yet // fixme - in a LB scenario, ensure that the DB gets upgraded only once! bool noUpgrade; try { noUpgrade = EnsureUmbracoUpgradeState(databaseFactory); } catch (Exception e) { // can connect to the database but cannot check the upgrade state... oops Logger.Warn(e, "Could not check the upgrade state."); throw new BootFailedException("Could not check the upgrade state.", e); } if (noUpgrade) { // the database version matches the code & files version, all clear, can run return RuntimeLevel.Run; } // the db version does not match... but we do have a migration table // so, at least one valid table, so we quite probably are installed & need to upgrade // although the files version matches the code version, the database version does not // which means the local files have been upgraded but not the database - need to upgrade Logger.Debug("Has not reached the final upgrade step, need to upgrade Umbraco."); return RuntimeLevel.Upgrade; } protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory) { var umbracoPlan = new UmbracoPlan(); var stateValueKey = Upgrader.GetStateValueKey(umbracoPlan); // no scope, no service - just directly accessing the database using (var database = databaseFactory.CreateDatabase()) { _state.CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); _state.FinalMigrationState = umbracoPlan.FinalState; } Logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, _state.CurrentMigrationState ?? ""); return _state.CurrentMigrationState == _state.FinalMigrationState; } #region Getters // getters can be implemented by runtimes inheriting from CoreRuntime /// /// Gets all component types. /// protected virtual IEnumerable GetComponentTypes(TypeLoader typeLoader) => typeLoader.GetTypes(); /// /// Gets a logger. /// protected virtual ILogger GetLogger() => SerilogLogger.CreateWithDefaultConfiguration(); /// /// Gets a profiler. /// protected virtual IProfiler GetProfiler() => new LogProfiler(ProfilingLogger); /// /// Gets the application caches. /// protected virtual CacheHelper GetAppCaches() { // need the deep clone runtime cache provider to ensure entities are cached properly, ie // are cloned in and cloned out - no request-based cache here since no web-based context, // is overriden by the web runtime return new CacheHelper( new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()), new StaticCacheProvider(), NullCacheProvider.Instance, new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); } // by default, returns null, meaning that Umbraco should auto-detect the application root path. // override and return the absolute path to the Umbraco site/solution, if needed protected virtual string GetApplicationRootPath() => null; /// /// Gets the database factory. /// /// This is strictly internal, for tests only. protected internal virtual IUmbracoDatabaseFactory GetDatabaseFactory() => new UmbracoDatabaseFactory(Logger, new Lazy(() => _factory.GetInstance())); #endregion } }