using System; using System.Data.Common; using System.Data.SqlClient; using System.Data.SqlServerCe; using System.Threading; using System.Web; using Semver; using Umbraco.Core.Collections; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Services.Implement; using Umbraco.Core.Sync; namespace Umbraco.Core { /// /// Represents the state of the Umbraco runtime. /// internal class RuntimeState : IRuntimeState { private readonly ILogger _logger; private readonly IUmbracoSettingsSection _settings; private readonly IGlobalSettings _globalSettings; private readonly ConcurrentHashSet _applicationUrls = new ConcurrentHashSet(); private readonly Lazy _mainDom; private readonly Lazy _serverRegistrar; /// /// Initializes a new instance of the class. /// public RuntimeState(ILogger logger, IUmbracoSettingsSection settings, IGlobalSettings globalSettings, Lazy mainDom, Lazy serverRegistrar) { _logger = logger; _settings = settings; _globalSettings = globalSettings; _mainDom = mainDom; _serverRegistrar = serverRegistrar; } /// /// Gets the server registrar. /// /// /// This is NOT exposed in the interface. /// private IServerRegistrar ServerRegistrar => _serverRegistrar.Value; /// /// Gets the application MainDom. /// /// /// This is NOT exposed in the interface as MainDom is internal. /// public IMainDom MainDom => _mainDom.Value; /// public Version Version => UmbracoVersion.Current; /// public string VersionComment => UmbracoVersion.Comment; /// public SemVersion SemanticVersion => UmbracoVersion.SemanticVersion; /// public bool Debug { get; } = GlobalSettings.DebugMode; /// public bool IsMainDom => MainDom.IsMainDom; /// public ServerRole ServerRole => ServerRegistrar.GetCurrentServerRole(); /// public Uri ApplicationUrl { get; private set; } /// public string ApplicationVirtualPath { get; } = HttpRuntime.AppDomainAppVirtualPath; /// public string CurrentMigrationState { get; internal set; } /// public string FinalMigrationState { get; internal set; } /// public RuntimeLevel Level { get; internal set; } = RuntimeLevel.Unknown; /// public RuntimeLevelReason Reason { get; internal set; } = RuntimeLevelReason.Unknown; /// /// Ensures that the property has a value. /// /// internal void EnsureApplicationUrl(HttpRequestBase request = null) { //Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 // see U4-10626 - in some cases we want to reset the application URL // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? var url = request == null ? null : ApplicationUrlHelper.GetApplicationUrlFromCurrentRequest(request, _globalSettings); var change = url != null && !_applicationUrls.Contains(url); if (change) { _logger.Info(typeof(ApplicationUrlHelper), "New url {Url} detected, re-discovering application url.", url); _applicationUrls.Add(url); } if (ApplicationUrl != null && !change) return; ApplicationUrl = new Uri(ApplicationUrlHelper.GetApplicationUrl(_logger, _globalSettings, _settings, ServerRegistrar, request)); } /// public BootFailedException BootFailedException { get; internal set; } /// /// Determines the runtime level. /// public void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory) { var localVersion = UmbracoVersion.LocalVersion; // the local, files, version var codeVersion = SemanticVersion; // the executing code version if (localVersion == null) { // there is no local version, we are not installed _logger.Debug("No local version, need to install Umbraco."); Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallNoVersion; return; } 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); Level = RuntimeLevel.Upgrade; Reason = RuntimeLevelReason.UpgradeOldVersion; } 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 Reason = RuntimeLevelReason.BootFailedCannotDowngrade; 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 - may happen with Deploy/Cloud/etc _logger.Debug("Database is not configured, need to install Umbraco."); Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallNoDatabase; return; } // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... switch (GetUmbracoDatabaseState(databaseFactory)) { 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 = RuntimeOptions.UpgradeUnattended ? RuntimeLevel.Run : 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; } } private enum UmbracoDatabaseState { Ok, CannotConnect, NotInstalled, NeedsUpgrade } private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) { try { 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 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); } } 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; CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); FinalMigrationState = upgrader.Plan.FinalState; _logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); return CurrentMigrationState != FinalMigrationState; } } }