Files
Umbraco-CMS/src/Umbraco.Infrastructure/RuntimeState.cs
Mole 2bf86acf38 V9: Place notifications in the same namespace (#10231)
* Gather all notifications in Umbraco.Cms.Core.Notifications

* Rename notifications to match convention

* Move and rename missed notifications

* Move the three remaining public notification into Umbraco.Cms.Core.Notifications
2021-05-11 14:33:49 +02:00

315 lines
13 KiB
C#

using System;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Semver;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Core
{
/// <summary>
/// Represents the state of the Umbraco runtime.
/// </summary>
public class RuntimeState : IRuntimeState
{
private readonly IOptions<GlobalSettings> _globalSettings;
private readonly IOptions<UnattendedSettings> _unattendedSettings;
private readonly IUmbracoVersion _umbracoVersion;
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly ILogger<RuntimeState> _logger;
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
private readonly IEventAggregator _eventAggregator;
/// <summary>
/// The initial <see cref="RuntimeState"/>
/// The initial <see cref="RuntimeState"/>
/// </summary>
public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot };
private RuntimeState()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState"/> class.
/// </summary>
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
IEventAggregator eventAggregator)
{
_globalSettings = globalSettings;
_unattendedSettings = unattendedSettings;
_umbracoVersion = umbracoVersion;
_databaseFactory = databaseFactory;
_logger = logger;
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
_eventAggregator = eventAggregator;
}
/// <inheritdoc />
public Version Version => _umbracoVersion.Version;
/// <inheritdoc />
public string VersionComment => _umbracoVersion.Comment;
/// <inheritdoc />
public SemVersion SemanticVersion => _umbracoVersion.SemanticVersion;
/// <inheritdoc />
public string CurrentMigrationState { get; private set; }
/// <inheritdoc />
public string FinalMigrationState { get; private set; }
/// <inheritdoc />
public RuntimeLevel Level { get; internal set; } = RuntimeLevel.Unknown;
/// <inheritdoc />
public RuntimeLevelReason Reason { get; internal set; } = RuntimeLevelReason.Unknown;
/// <inheritdoc />
public BootFailedException BootFailedException { get; internal set; }
/// <inheritdoc />
public void DetermineRuntimeLevel()
{
if (_databaseFactory.Configured == false)
{
// local version *does* match code version, but the database is not configured
// install - may happen with Deploy/Cloud/etc
_logger.LogDebug("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.LogDebug("Could not connect to database.");
if (_globalSettings.Value.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;
BootFailedException =new BootFailedException("A connection string is configured but Umbraco could not connect to the database.");
throw BootFailedException;
}
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.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco.");
Level = _unattendedSettings.Value.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())
{
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.LogWarning(e, "Could not check the upgrade state.");
// else it is bad enough that we want to throw
Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState;
BootFailedException = new BootFailedException("Could not check the upgrade state.", e);
throw BootFailedException;
}
}
public void Configure(RuntimeLevel level, RuntimeLevelReason reason)
{
Level = level;
Reason = reason;
}
public void DoUnattendedInstall()
{
// unattended install is not enabled
if (_unattendedSettings.Value.InstallUnattended == false) return;
// no connection string set
if (_databaseFactory.Configured == false) return;
var connect = false;
var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5;
for (var i = 0;;)
{
connect = _databaseFactory.CanConnect;
if (connect || ++i == tries) break;
_logger.LogDebug("Could not immediately connect to database, trying again.");
Thread.Sleep(1000);
}
// could not connect to the database
if (connect == false) return;
using (var database = _databaseFactory.CreateDatabase())
{
var hasUmbracoTables = database.IsUmbracoInstalled();
// database has umbraco tables, assume Umbraco is already installed
if (hasUmbracoTables) return;
// all conditions fulfilled, do the install
_logger.LogInformation("Starting unattended install.");
try
{
database.BeginTransaction();
var creator = _databaseSchemaCreatorFactory.Create(database);
creator.InitializeDatabaseSchema();
database.CompleteTransaction();
_logger.LogInformation("Unattended install completed.");
// Emit an event with EventAggregator that unattended install completed
// Then this event can be listened for and create an unattended user
_eventAggregator.Publish(new UnattendedInstallNotification());
}
catch (Exception ex)
{
_logger.LogInformation(ex, "Error during unattended install.");
database.AbortTransaction();
var innerException = new UnattendedInstallException(
"The database configuration failed with the following message: " + ex.Message
+ "\n Please check log file for additional information (can be found in '/App_Data/Logs/')");
BootFailedException = new BootFailedException(innerException.Message, innerException);
throw BootFailedException;
}
}
}
private bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger)
{
var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion));
var stateValueKey = upgrader.StateValueKey;
// no scope, no service - just directly accessing the database
using (var database = databaseFactory.CreateDatabase())
{
CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey);
FinalMigrationState = upgrader.Plan.FinalState;
}
logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? "<null>");
return CurrentMigrationState == FinalMigrationState;
}
private bool DoesUmbracoRequireUpgrade(IUmbracoDatabase database)
{
var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion));
var stateValueKey = upgrader.StateValueKey;
CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey);
FinalMigrationState = upgrader.Plan.FinalState;
_logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? "<null>");
return CurrentMigrationState != FinalMigrationState;
}
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 = _globalSettings.Value.InstallMissingDatabase ? 2 : 5;
for (var i = 0; ;)
{
canConnect = databaseFactory.CanConnect;
if (canConnect || ++i == tries) break;
_logger.LogDebug("Could not immediately connect to database, trying again.");
Thread.Sleep(1000);
}
return canConnect;
}
}
}