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;
}
}
}