From 31a6f2d67c40f78e6f65ff905f7a7b35d5ad4bd4 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 23 Jan 2019 21:22:49 +0100 Subject: [PATCH] Add runtime hooks for Deploy --- .../Migrations/Install/DatabaseBuilder.cs | 64 ++++++---- .../Persistence/UmbracoDatabaseFactory.cs | 8 +- src/Umbraco.Core/Runtime/CoreRuntime.cs | 10 +- src/Umbraco.Core/RuntimeOptions.cs | 114 ++++++++++++++++++ src/Umbraco.Core/RuntimeState.cs | 8 +- src/Umbraco.Core/RuntimeStateOptions.cs | 40 ------ src/Umbraco.Core/Umbraco.Core.csproj | 2 +- 7 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 src/Umbraco.Core/RuntimeOptions.cs delete mode 100644 src/Umbraco.Core/RuntimeStateOptions.cs diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs index 4104ce947c..e73fc7453f 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs @@ -283,48 +283,60 @@ namespace Umbraco.Core.Migrations.Install if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullOrEmptyException(nameof(connectionString)); if (string.IsNullOrWhiteSpace(providerName)) throw new ArgumentNullOrEmptyException(nameof(providerName)); - // set the connection string for the new datalayer - var connectionStringSettings = new ConnectionStringSettings(Constants.System.UmbracoConnectionName, connectionString, providerName); + var fileSource = "web.config"; + var fileName = IOHelper.MapPath(Path.Combine(SystemDirectories.Root, fileSource)); - var fileName = IOHelper.MapPath($"{SystemDirectories.Root}/web.config"); var xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); - if (xml.Root == null) throw new Exception("Invalid web.config file."); - var connectionStrings = xml.Root.DescendantsAndSelf("connectionStrings").FirstOrDefault(); - if (connectionStrings == null) throw new Exception("Invalid web.config file."); + if (xml.Root == null) throw new Exception($"Invalid {fileSource} file (no root)."); - // honour configSource, if its set, change the xml file we are saving the configuration - // to the one set in the configSource attribute - if (connectionStrings.Attribute("configSource") != null) + var connectionStrings = xml.Root.DescendantsAndSelf("connectionStrings").FirstOrDefault(); + if (connectionStrings == null) throw new Exception($"Invalid {fileSource} file (no connection strings)."); + + // handle configSource + var configSourceAttribute = connectionStrings.Attribute("configSource"); + if (configSourceAttribute != null) { - var source = connectionStrings.Attribute("configSource").Value; - var configFile = IOHelper.MapPath($"{SystemDirectories.Root}/{source}"); - logger.Info("Storing ConnectionString in {ConfigFile}", configFile); - if (File.Exists(configFile)) - { - xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); - fileName = configFile; - } + fileSource = configSourceAttribute.Value; + fileName = IOHelper.MapPath(Path.Combine(SystemDirectories.Root, fileSource)); + + if (!File.Exists(fileName)) + throw new Exception($"Invalid configSource \"{fileSource}\" (no such file)."); + + xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); + if (xml.Root == null) throw new Exception($"Invalid {fileSource} file (no root)."); + connectionStrings = xml.Root.DescendantsAndSelf("connectionStrings").FirstOrDefault(); - if (connectionStrings == null) throw new Exception("Invalid web.config file."); + if (connectionStrings == null) throw new Exception($"Invalid {fileSource} file (no connection strings)."); } - // update connectionString if it exists, or else create a new connectionString - var setting = connectionStrings.Descendants("add").FirstOrDefault(s => s.Attribute("name").Value == Constants.System.UmbracoConnectionName); + // create or update connection string + var setting = connectionStrings.Descendants("add").FirstOrDefault(s => s.Attribute("name")?.Value == Constants.System.UmbracoConnectionName); if (setting == null) { connectionStrings.Add(new XElement("add", new XAttribute("name", Constants.System.UmbracoConnectionName), - new XAttribute("connectionString", connectionStringSettings), + new XAttribute("connectionString", connectionString), new XAttribute("providerName", providerName))); } else { - setting.Attribute("connectionString").Value = connectionString; - setting.Attribute("providerName").Value = providerName; + AddOrUpdateAttribute(setting, "connectionString", connectionString); + AddOrUpdateAttribute(setting, "providerName", providerName); } + // save + logger.Info("Saving connection string to {ConfigFile}.", fileSource); xml.Save(fileName, SaveOptions.DisableFormatting); - logger.Info("Configured a new ConnectionString using the '{ProviderName}' provider.", providerName); + logger.Info("Saved connection string to {ConfigFile}.", fileSource); + } + + private static void AddOrUpdateAttribute(XElement element, string name, string value) + { + var attribute = element.Attribute(name); + if (attribute == null) + element.Add(new XAttribute(name, value)); + else + attribute.Value = value; } internal bool IsConnectionStringConfigured(ConnectionStringSettings databaseSettings) @@ -422,7 +434,7 @@ namespace Umbraco.Core.Migrations.Install _logger.Info("Database configuration status: Started"); var database = scope.Database; - + var message = string.Empty; var schemaResult = ValidateSchema(); @@ -482,7 +494,7 @@ namespace Umbraco.Core.Migrations.Install } _logger.Info("Database upgrade started"); - + // upgrade var upgrader = new UmbracoUpgrader(); upgrader.Execute(_scopeProvider, _migrationBuilder, _keyValueService, _logger, _postMigrations); diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs index cd6da569c5..aec49b8eb5 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs @@ -59,14 +59,18 @@ namespace Umbraco.Core.Persistence /// Used by the other ctor and in tests. public UmbracoDatabaseFactory(string connectionStringName, ILogger logger, Lazy mappers) { - if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); + if (string.IsNullOrWhiteSpace(connectionStringName)) + throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var settings = ConfigurationManager.ConnectionStrings[connectionStringName]; if (settings == null) + { + logger.Debug("Missing connection string, defer configuration."); return; // not configured + } // could as well be // so need to test the values too @@ -74,7 +78,7 @@ namespace Umbraco.Core.Persistence var providerName = settings.ProviderName; if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) { - logger.Debug("Missing connection string or provider name, defer configuration."); + logger.Debug("Empty connection string or provider name, defer configuration."); return; // not configured } diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 13e6aae149..793eb751dd 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -100,6 +100,9 @@ namespace Umbraco.Core.Runtime // throws if not full-trust new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand(); + // run handlers + RuntimeOptions.DoRuntimeBoot(ProfilingLogger); + // application caches var appCaches = GetAppCaches(); @@ -131,12 +134,17 @@ namespace Umbraco.Core.Runtime composition = new Composition(register, typeLoader, ProfilingLogger, _state, configs); composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, mainDom, appCaches, databaseFactory, typeLoader, _state); + // run handlers + RuntimeOptions.DoRuntimeEssentials(composition, appCaches, typeLoader, databaseFactory); + // register runtime-level services // there should be none, really - this is here "just in case" Compose(composition); - // acquire the main domain, determine our runtime level + // acquire the main domain AcquireMainDom(mainDom); + + // determine our runtime level DetermineRuntimeLevel(databaseFactory, ProfilingLogger); // get composers, and compose diff --git a/src/Umbraco.Core/RuntimeOptions.cs b/src/Umbraco.Core/RuntimeOptions.cs new file mode 100644 index 0000000000..1f89ee6314 --- /dev/null +++ b/src/Umbraco.Core/RuntimeOptions.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Runtime.CompilerServices; +using Umbraco.Core.Cache; +using Umbraco.Core.Components; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core +{ + /// + /// Provides static options for the runtime. + /// + /// + /// These options can be configured in PreApplicationStart or via appSettings. + /// + public static class RuntimeOptions + { + private static List> _onBoot; + private static List> _onEssentials; + private static bool? _installMissingDatabase; + private static bool? _installEmptyDatabase; + + // reads a boolean appSetting + private static bool BoolSetting(string key, bool missing) => ConfigurationManager.AppSettings[key]?.InvariantEquals("true") ?? missing; + + /// + /// Gets a value indicating whether the runtime should enter Install level when the database is missing. + /// + /// + /// By default, when a database connection string is configured but it is not possible to + /// connect to the database, the runtime enters the BootFailed level. If this options is set to true, + /// it enters the Install level instead. + /// It is then up to the implementor, that is setting this value, to take over the installation + /// sequence. + /// + public static bool InstallMissingDatabase + { + get => _installEmptyDatabase ?? BoolSetting("Umbraco.Core.RuntimeState.InstallMissingDatabase", false); + set => _installEmptyDatabase = value; + } + + /// + /// Gets a value indicating whether the runtime should enter Install level when the database is empty. + /// + /// + /// By default, when a database connection string is configured and it is possible to connect to + /// the database, but the database is empty, the runtime enters the BootFailed level. If this options + /// is set to true, it enters the Install level instead. + /// It is then up to the implementor, that is setting this value, to take over the installation + /// sequence. + /// + public static bool InstallEmptyDatabase + { + get => _installMissingDatabase ?? BoolSetting("Umbraco.Core.RuntimeState.InstallEmptyDatabase", false); + set => _installMissingDatabase = value; + } + + /// + /// Executes the RuntimeBoot handlers. + /// + internal static void DoRuntimeBoot(IProfilingLogger logger) + { + if (_onBoot == null) + return; + + foreach (var action in _onBoot) + action(logger); + } + + /// + /// Executes the RuntimeEssentials handlers. + /// + internal static void DoRuntimeEssentials(Composition composition, AppCaches appCaches, TypeLoader typeLoader, IUmbracoDatabaseFactory databaseFactory) + { + if (_onEssentials== null) + return; + + foreach (var action in _onEssentials) + action(composition, appCaches, typeLoader, databaseFactory); + } + + /// + /// Registers a RuntimeBoot handler. + /// + /// + /// A RuntimeBoot handler runs when the runtime boots, right after the + /// loggers have been created, but before anything else. + /// + public static void OnRuntimeBoot(Action action) + { + if (_onBoot == null) + _onBoot = new List>(); + _onBoot.Add(action); + } + + /// + /// Registers a RuntimeEssentials handler. + /// + /// + /// A RuntimeEssentials handler runs after the runtime has created a few + /// essential things (AppCaches, a TypeLoader, and a database factory) but + /// before anything else. + /// + public static void OnRuntimeEssentials(Action action) + { + if (_onEssentials == null) + _onEssentials = new List>(); + _onEssentials.Add(action); + } + } +} diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 85e8c7370d..b21c02fdb9 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -169,7 +169,7 @@ namespace Umbraco.Core else if (databaseFactory.Configured == false) { // local version *does* match code version, but the database is not configured - // install (again? this is a weird situation...) + // install - may happen with Deploy/Cloud/etc logger.Debug("Database is not configured, need to install Umbraco."); Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallNoDatabase; @@ -179,7 +179,7 @@ namespace Umbraco.Core // 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) - var tries = RuntimeStateOptions.InstallMissingDatabase ? 2 : 5; + var tries = RuntimeOptions.InstallMissingDatabase ? 2 : 5; for (var i = 0;;) { connect = databaseFactory.CanConnect; @@ -193,7 +193,7 @@ namespace Umbraco.Core // cannot connect to configured database, this is bad, fail logger.Debug("Could not connect to database."); - if (RuntimeStateOptions.InstallMissingDatabase) + if (RuntimeOptions.InstallMissingDatabase) { // ok to install on a configured but missing database Level = RuntimeLevel.Install; @@ -222,7 +222,7 @@ namespace Umbraco.Core // can connect to the database but cannot check the upgrade state... oops logger.Warn(e, "Could not check the upgrade state."); - if (RuntimeStateOptions.InstallEmptyDatabase) + if (RuntimeOptions.InstallEmptyDatabase) { // ok to install on an empty database Level = RuntimeLevel.Install; diff --git a/src/Umbraco.Core/RuntimeStateOptions.cs b/src/Umbraco.Core/RuntimeStateOptions.cs deleted file mode 100644 index 9262a8a990..0000000000 --- a/src/Umbraco.Core/RuntimeStateOptions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Configuration; - -namespace Umbraco.Core -{ - /// - /// Allows configuration of the in PreApplicationStart or in appSettings - /// - public static class RuntimeStateOptions - { - // configured statically or via app settings - private static bool BoolSetting(string key, bool missing) => ConfigurationManager.AppSettings[key]?.InvariantEquals("true") ?? missing; - - /// - /// If true the RuntimeState will continue the installation sequence when a database is missing - /// - /// - /// In this case it will be up to the implementor that is setting this value to true to take over the bootup/installation sequence - /// - public static bool InstallMissingDatabase - { - get => _installEmptyDatabase ?? BoolSetting("Umbraco.Core.RuntimeState.InstallMissingDatabase", false); - set => _installEmptyDatabase = value; - } - - /// - /// If true the RuntimeState will continue the installation sequence when a database is available but is empty - /// - /// - /// In this case it will be up to the implementor that is setting this value to true to take over the bootup/installation sequence - /// - public static bool InstallEmptyDatabase - { - get => _installMissingDatabase ?? BoolSetting("Umbraco.Core.RuntimeState.InstallEmptyDatabase", false); - set => _installMissingDatabase = value; - } - - private static bool? _installMissingDatabase; - private static bool? _installEmptyDatabase; - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 9a52cb8dc5..1a978650f0 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -530,7 +530,7 @@ - +