From bbe2941d255318bffdd63887c22e32c8c8e48ad2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 12:06:48 +1100 Subject: [PATCH] New ext methods to check if Umbraco db is installed, fixes issue with SqlMainDomLock when doing unattended installs, changes how we determine if Umbraco db is installed --- .../Migrations/Install/DatabaseBuilder.cs | 29 +++------- .../Install/DatabaseSchemaCreator.cs | 2 +- .../Install/DatabaseSchemaResult.cs | 4 +- .../Persistence/UmbracoDatabaseExtensions.cs | 55 +++++++++++++++++++ src/Umbraco.Core/Runtime/CoreRuntime.cs | 10 ++-- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 29 ++++++++-- src/Umbraco.Web/Install/InstallHelper.cs | 2 +- .../Install/InstallSteps/NewInstallStep.cs | 2 +- src/Umbraco.Web/UmbracoApplication.cs | 2 + 9 files changed, 100 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs index d5d8bbab6f..4fad123548 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs @@ -89,26 +89,11 @@ namespace Umbraco.Core.Migrations.Install return DbConnectionExtensions.IsConnectionAvailable(connectionString, providerName); } - internal bool HasSomeNonDefaultUser() + internal bool IsUmbracoInstalled() { - using (var scope = _scopeProvider.CreateScope()) + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { - // look for the super user with default password - var sql = scope.Database.SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.Id == Constants.Security.SuperUserId && x.Password == "default"); - var result = scope.Database.ExecuteScalar(sql); - var has = result != 1; - if (has == false) - { - // found only 1 user == the default user with default password - // however this always exists on uCloud, also need to check if there are other users too - result = scope.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); - has = result != 1; - } - scope.Complete(); - return has; + return scope.Database.IsUmbracoInstalled(_logger); } } @@ -391,15 +376,15 @@ namespace Umbraco.Core.Migrations.Install private DatabaseSchemaResult ValidateSchema(IScope scope) { if (_databaseFactory.Initialized == false) - return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax); + return new DatabaseSchemaResult(); if (_databaseSchemaValidationResult != null) return _databaseSchemaValidationResult; - var database = scope.Database; - var dbSchema = new DatabaseSchemaCreator(database, _logger); - _databaseSchemaValidationResult = dbSchema.ValidateSchema(); + _databaseSchemaValidationResult = scope.Database.ValidateSchema(_logger); + scope.Complete(); + return _databaseSchemaValidationResult; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index eab7afe308..e15e58fde9 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -143,7 +143,7 @@ namespace Umbraco.Core.Migrations.Install internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) { - var result = new DatabaseSchemaResult(SqlSyntax); + var result = new DatabaseSchemaResult(); result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) .Select(x => new DbIndexDefinition(x))); diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs index f21216fde3..c0d8eff311 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Migrations.Install { @@ -12,7 +12,7 @@ namespace Umbraco.Core.Migrations.Install /// internal class DatabaseSchemaResult { - public DatabaseSchemaResult(ISqlSyntaxProvider sqlSyntax) + public DatabaseSchemaResult() { Errors = new List>(); TableDefinitions = new List(); diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseExtensions.cs index 249dd3dc73..e0f3581ab3 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Install; namespace Umbraco.Core.Persistence { @@ -10,5 +13,57 @@ namespace Umbraco.Core.Persistence if (asDatabase == null) throw new Exception("oops: database."); return asDatabase; } + + /// + /// Returns true if the database contains the specified table + /// + /// + /// + /// + public static bool HasTable(this IUmbracoDatabase database, string tableName) + { + try + { + return database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any(table => table.InvariantEquals(tableName)); + } + catch (Exception) + { + return false; // will occur if the database cannot connect + } + } + + /// + /// Returns true if the database contains no tables + /// + /// + /// + public static bool IsDatabaseEmpty(this IUmbracoDatabase database) + => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; + + /// + /// Returns the for the database + /// + /// + /// + /// + public static DatabaseSchemaResult ValidateSchema(this IUmbracoDatabase database, ILogger logger) + { + var dbSchema = new DatabaseSchemaCreator(database, logger); + var databaseSchemaValidationResult = dbSchema.ValidateSchema(); + return databaseSchemaValidationResult; + } + + /// + /// Returns true if Umbraco database tables are detected to be installed + /// + /// + /// + /// + public static bool IsUmbracoInstalled(this IUmbracoDatabase database, ILogger logger) + { + var databaseSchemaValidationResult = database.ValidateSchema(logger); + return databaseSchemaValidationResult.DetermineHasInstalledVersion(); + } + } } diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index aaa46843aa..de594df019 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -174,6 +174,7 @@ namespace Umbraco.Core.Runtime // determine our runtime level DetermineRuntimeLevel(databaseFactory, ProfilingLogger); + // get composers, and compose var composerTypes = ResolveComposerTypes(typeLoader); @@ -260,16 +261,17 @@ namespace Umbraco.Core.Runtime using (var database = databaseFactory.CreateDatabase()) { - var isDatabaseEmpty = databaseFactory.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; + var hasUmbracoTables = database.IsUmbracoInstalled(Logger); - // database is not empty, assume Umbraco is already installed - if (isDatabaseEmpty == false) return; + // database has umbraco tables, assume Umbraco is already installed + if (hasUmbracoTables) return; // all conditions fulfilled, do the install Logger.Info("Starting unattended install."); - database.BeginTransaction(); + try { + database.BeginTransaction(); var creator = new DatabaseSchemaCreator(database, Logger); creator.InitializeDatabaseSchema(); database.CompleteTransaction(); diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index e5d4fbba61..ef9882cee0 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -27,6 +27,7 @@ namespace Umbraco.Core.Runtime private readonly UmbracoDatabaseFactory _dbFactory; private bool _errorDuringAcquiring; private object _locker = new object(); + private bool _hasTable = false; public SqlMainDomLock(ILogger logger) { @@ -37,14 +38,14 @@ namespace Umbraco.Core.Runtime _dbFactory = new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, _logger, - new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); + new Lazy(() => new MapperCollection(Enumerable.Empty()))); } public async Task AcquireLockAsync(int millisecondsTimeout) { if (!_dbFactory.Configured) { - // if we aren't configured, then we're in an install state, in which case we have no choice but to assume we can acquire + // if we aren't configured then we're in an install state, in which case we have no choice but to assume we can acquire return true; } @@ -58,6 +59,14 @@ namespace Umbraco.Core.Runtime var tempId = Guid.NewGuid().ToString(); using var db = _dbFactory.CreateDatabase(); + + _hasTable = db.HasTable(Constants.DatabaseSchema.Tables.KeyValue); + if (!_hasTable) + { + // the Db does not contain the required table, we must be in an install state we have no choice but to assume we can acquire + return true; + } + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); try @@ -162,10 +171,22 @@ namespace Umbraco.Core.Runtime { _logger.Debug("Task canceled, exiting loop"); return; - } - + } using var db = _dbFactory.CreateDatabase(); + + if (!_hasTable) + { + // re-check if its still false, we don't want to re-query once we know its there since this + // loop needs to use minimal resources + _hasTable = db.HasTable(Constants.DatabaseSchema.Tables.KeyValue); + if (!_hasTable) + { + // the Db does not contain the required table, we just keep looping since we can't query the db + continue; + } + } + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); try { diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index a998a172fc..998e32d8ad 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -145,7 +145,7 @@ namespace Umbraco.Web.Install return true; } - return _databaseBuilder.HasSomeNonDefaultUser() == false; + return _databaseBuilder.IsUmbracoInstalled() == false; } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 151265f394..f9ae40b52b 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.Install.InstallSteps if (_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace() == false && databaseSettings != null) return false; if (_databaseBuilder.IsConnectionStringConfigured(databaseSettings) && _databaseBuilder.IsDatabaseConfigured) - return _databaseBuilder.HasSomeNonDefaultUser() == false; + return _databaseBuilder.IsUmbracoInstalled() == false; // 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 diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index f5667a5a85..f70651a43e 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -31,6 +31,8 @@ namespace Umbraco.Web // Determine if we should use the sql main dom or the default var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; + // TODO: Can we automatically and consistently determine we're running on Azure without this app setting? + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" ? (IMainDomLock)new SqlMainDomLock(logger) : new MainDomSemaphoreLock(logger);