using System; using System.Collections.Generic; using System.Threading; using System.Web; using Semver; 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 HashSet _applicationUrls = new HashSet(); private readonly Lazy _mainDom; private readonly Lazy _serverRegistrar; private RuntimeLevel _level = RuntimeLevel.Unknown; /// /// 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 => _level; internal set { _level = value; if (value == RuntimeLevel.Run) _runLevel.Set(); } } /// /// Ensures that the property has a value. /// /// internal void EnsureApplicationUrl(HttpRequestBase request = null) { // 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)); } private readonly ManualResetEventSlim _runLevel = new ManualResetEventSlim(false); /// /// Waits for the runtime level to become RuntimeLevel.Run. /// /// A timeout. /// True if the runtime level became RuntimeLevel.Run before the timeout, otherwise false. internal bool WaitForRunLevel(TimeSpan timeout) { return _runLevel.WaitHandle.WaitOne(timeout); } /// public BootFailedException BootFailedException { get; internal set; } /// /// Determines the runtime level. /// public void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) { 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."); Level = RuntimeLevel.Install; 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); } 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."); Level = RuntimeLevel.Install; 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) 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, logger); } 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 Level = RuntimeLevel.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; } protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) { var umbracoPlan = new UmbracoPlan(); var stateValueKey = Upgrader.GetStateValueKey(umbracoPlan); // no scope, no service - just directly accessing the database using (var database = databaseFactory.CreateDatabase()) { CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); FinalMigrationState = umbracoPlan.FinalState; } logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", CurrentMigrationState, FinalMigrationState ?? ""); return CurrentMigrationState == FinalMigrationState; } } }