diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs index 63767fbdc4..71faf42daf 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs @@ -446,11 +446,9 @@ namespace Umbraco.Core.Migrations.Install var schemaResult = ValidateSchema(); var hasInstalledVersion = schemaResult.DetermineHasInstalledVersion(); - //var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); - //var hasInstalledVersion = !installedSchemaVersion.Equals(new Version(0, 0, 0)); - //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing - if (string.IsNullOrEmpty(_globalSettings.ConfigurationStatus) && !hasInstalledVersion) + //If the determined version is "empty" its a new install - otherwise upgrade the existing + if (!hasInstalledVersion) { if (_runtime.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index de594df019..fdcf7f2289 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -336,7 +336,7 @@ namespace Umbraco.Core.Runtime { try { - _state.DetermineRuntimeLevel(databaseFactory, profilingLogger); + _state.DetermineRuntimeLevel(databaseFactory); profilingLogger.Debug("Runtime level: {RuntimeLevel} - {RuntimeLevelReason}", _state.Level, _state.Reason); diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index ef9882cee0..f41cbceb2c 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -401,7 +401,7 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - if (_dbFactory.Configured) + if (_dbFactory.Configured && _hasTable) { using var db = _dbFactory.CreateDatabase(); using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); diff --git a/src/Umbraco.Core/RuntimeOptions.cs b/src/Umbraco.Core/RuntimeOptions.cs index 413c022721..0d64d36849 100644 --- a/src/Umbraco.Core/RuntimeOptions.cs +++ b/src/Umbraco.Core/RuntimeOptions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Configuration; using System.Runtime.CompilerServices; using Umbraco.Core.Cache; @@ -42,14 +43,8 @@ namespace Umbraco.Core set => _installMissingDatabase = 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 Install level. If this options - /// is set to false, it enters the BootFailed level instead. - /// + [Obsolete("This setting is no longer used and will be removed in future versions. If a database connection string is configured and the database is empty Umbraco will be installed during the installation sequence.")] + [EditorBrowsable(EditorBrowsableState.Never)] public static bool InstallEmptyDatabase { get => _installEmptyDatabase ?? BoolSetting("Umbraco.Core.RuntimeState.InstallEmptyDatabase", true); diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 8a30a97e7b..4a10b48dd6 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -1,4 +1,7 @@ using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.Data.SqlServerCe; using System.Threading; using System.Web; using Semver; @@ -123,16 +126,15 @@ namespace Umbraco.Core /// /// Determines the runtime level. /// - public void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) + public void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory) { var localVersion = UmbracoVersion.LocalVersion; // the local, files, version var codeVersion = 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."); + _logger.Debug("No local version, need to install Umbraco."); Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallNoVersion; return; @@ -142,13 +144,13 @@ namespace Umbraco.Core { // 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); + _logger.Debug("Local version '{LocalVersion}' < code version '{CodeVersion}', need to upgrade Umbraco.", localVersion, codeVersion); Level = RuntimeLevel.Upgrade; Reason = RuntimeLevelReason.UpgradeOldVersion; } else if (localVersion > codeVersion) { - logger.Warn("Local version '{LocalVersion}' > code version '{CodeVersion}', downgrading is not supported.", 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 Reason = RuntimeLevelReason.BootFailedCannotDowngrade; @@ -158,108 +160,139 @@ namespace Umbraco.Core { // local version *does* match code version, but the database is not configured // install - may happen with Deploy/Cloud/etc - logger.Debug("Database is not configured, need to install Umbraco."); + _logger.Debug("Database is not configured, need to install Umbraco."); Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallNoDatabase; return; } - // 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 = RuntimeOptions.InstallMissingDatabase ? 2 : 5; - for (var i = 0;;) + // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... + + switch (GetUmbracoDatabaseState(databaseFactory)) { - connect = databaseFactory.CanConnect; - if (connect || ++i == tries) break; - logger.Debug("Could not immediately connect to database, trying again."); - Thread.Sleep(1000); + case UmbracoDatabaseState.CannotConnect: + { + // cannot connect to configured database, this is bad, fail + _logger.Debug("Could not connect to database."); + + if (RuntimeOptions.InstallMissingDatabase) + { + // ok to install on a configured but missing database + Level = RuntimeLevel.Install; + Reason = RuntimeLevelReason.InstallMissingDatabase; + return; + } + + // else it is bad enough that we want to throw + Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; + throw new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); + } + case UmbracoDatabaseState.NotInstalled: + { + // ok to install on an empty database + Level = RuntimeLevel.Install; + Reason = RuntimeLevelReason.InstallEmptyDatabase; + return; + } + case UmbracoDatabaseState.NeedsUpgrade: + { + // 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."); + Level = RuntimeLevel.Upgrade; + Reason = RuntimeLevelReason.UpgradeMigrations; + } + break; + case UmbracoDatabaseState.Ok: + default: + { + // if we already know we want to upgrade, exit here + if (Level == RuntimeLevel.Upgrade) + return; + + // the database version matches the code & files version, all clear, can run + Level = RuntimeLevel.Run; + Reason = RuntimeLevelReason.Run; + } + break; } + } - if (connect == false) - { - // cannot connect to configured database, this is bad, fail - logger.Debug("Could not connect to database."); + private enum UmbracoDatabaseState + { + Ok, + CannotConnect, + NotInstalled, + NeedsUpgrade + } - if (RuntimeOptions.InstallMissingDatabase) - { - // ok to install on a configured but missing database - Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallMissingDatabase; - return; - } - - // else it is bad enough that we want to throw - Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; - 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 - bool noUpgrade; + private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) + { try { - noUpgrade = EnsureUmbracoUpgradeState(databaseFactory, logger); + if (!TryDbConnect(databaseFactory)) + { + return UmbracoDatabaseState.CannotConnect; + } + + // no scope, no service - just directly accessing the database + using (var database = databaseFactory.CreateDatabase()) + { + if (!database.IsUmbracoInstalled(_logger)) + { + return UmbracoDatabaseState.NotInstalled; + } + + if (DoesUmbracoRequireUpgrade(database)) + { + return UmbracoDatabaseState.NeedsUpgrade; + } + } + + return UmbracoDatabaseState.Ok; } catch (Exception e) { - // can connect to the database but cannot check the upgrade state... oops - logger.Warn(e, "Could not check the upgrade state."); - - if (RuntimeOptions.InstallEmptyDatabase) - { - // ok to install on an empty database - Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallEmptyDatabase; - return; - } + // can connect to the database so cannot check the upgrade state... oops + _logger.Warn(e, "Could not check the upgrade state."); // else it is bad enough that we want to throw Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState; throw new BootFailedException("Could not check the upgrade state.", e); } - - // if we already know we want to upgrade, exit here - if (Level == RuntimeLevel.Upgrade) - return; - - if (noUpgrade) - { - // the database version matches the code & files version, all clear, can run - Level = RuntimeLevel.Run; - Reason = RuntimeLevelReason.Run; - return; - } - - // 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."); - Level = RuntimeLevel.Upgrade; - Reason = RuntimeLevelReason.UpgradeMigrations; } - protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) + private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) + { + // anything other than install wants a database - see if we can connect + // (since this is an already existing database, assume localdb is ready) + bool canConnect; + var tries = RuntimeOptions.InstallMissingDatabase ? 2 : 5; + for (var i = 0; ;) + { + canConnect = databaseFactory.CanConnect; + if (canConnect || ++i == tries) break; + _logger.Debug("Could not immediately connect to database, trying again."); + Thread.Sleep(1000); + } + + return canConnect; + } + + private bool DoesUmbracoRequireUpgrade(IUmbracoDatabase database) { var upgrader = new Upgrader(new UmbracoPlan()); var stateValueKey = upgrader.StateValueKey; - // no scope, no service - just directly accessing the database - using (var database = databaseFactory.CreateDatabase()) - { - CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); - FinalMigrationState = upgrader.Plan.FinalState; - } + CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); + FinalMigrationState = upgrader.Plan.FinalState; - logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); + _logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); - return CurrentMigrationState == FinalMigrationState; + return CurrentMigrationState != FinalMigrationState; } } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 10f251d2e1..aa445c04c6 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -12,6 +12,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Install.Models; @@ -124,32 +125,94 @@ namespace Umbraco.Web.Install.InstallSteps { get { - return RequiresExecution(null) - //the user UI - ? "user" - //the continue install UI - : "continueinstall"; + return ShowView() + // the user UI + ? "user" + // continue install UI + : "continueinstall"; } } + private InstallState GetInstallState() + { + var installState = InstallState.Unknown; + + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; + + var hasVersion = !_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace(); + if (hasVersion) + { + installState = InstallState.HasVersion; + } + + var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured; + if (hasConnString) + { + installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; + } + + var connStringConfigured = hasConnString ? _databaseBuilder.IsConnectionStringConfigured(databaseSettings) : false; + if (connStringConfigured) + { + installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; + } + + var canConnect = connStringConfigured ? DbConnectionExtensions.IsConnectionAvailable(databaseSettings.ConnectionString, databaseSettings.ProviderName) : false; + if (canConnect) + { + installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; + } + + var umbracoInstalled = canConnect ? _databaseBuilder.IsUmbracoInstalled() : false; + if (umbracoInstalled) + { + installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; + } + + var hasNonDefaultUser = umbracoInstalled ? _databaseBuilder.HasSomeNonDefaultUser() : false; + if (hasNonDefaultUser) + { + installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; + } + + return installState; + } + + private bool ShowView() + { + var installState = GetInstallState(); + + return installState.HasFlag(InstallState.Unknown) + || !installState.HasFlag(InstallState.UmbracoInstalled); + } + public override bool RequiresExecution(UserModel model) { - //now we have to check if this is really a new install, the db might be configured and might contain data - var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; + var installState = GetInstallState(); - //if there's already a version then there should def be a user but in some cases someone may have - // left a version number in there but cleared out their db conn string, in that case, it's really a new install. - if (_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace() == false && databaseSettings != null) return false; + if (installState.HasFlag(InstallState.Unknown)) + { + // In this one case when it's a brand new install and nothing has been configured, make sure the + // back office cookie is cleared so there's no old cookies lying around causing problems + _http.ExpireCookie(Current.Configs.Settings().Security.AuthCookieName); + } - // if Umbraco is already installed there's already users in the database, skip this step - if (_databaseBuilder.IsConnectionStringConfigured(databaseSettings) && _databaseBuilder.IsDatabaseConfigured && _databaseBuilder.IsUmbracoInstalled()) - return _databaseBuilder.HasSomeNonDefaultUser() == false; + return installState.HasFlag(InstallState.Unknown) + || !installState.HasFlag(InstallState.HasNonDefaultUser); + } - // In this one case when it's a brand new install and nothing has been configured, make sure the - // back office cookie is cleared so there's no old cookies lying around causing problems - _http.ExpireCookie(Current.Configs.Settings().Security.AuthCookieName); - - return true; + [Flags] + private enum InstallState : short + { + // This is an easy way to avoid 0 enum assignment and not worry about + // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags + Unknown = 1, + HasVersion = 1 << 1, + HasConnectionString = 1 << 2, + ConnectionStringConfigured = 1 << 3, + CanConnect = 1 << 4, + UmbracoInstalled = 1 << 5, + HasNonDefaultUser = 1 << 6 } } }