2021-12-22 13:03:38 +01:00
using Microsoft.Extensions.DependencyInjection ;
2020-09-16 14:29:33 +02:00
using Microsoft.Extensions.Logging ;
2020-11-19 20:05:28 +00:00
using Microsoft.Extensions.Options ;
2021-06-10 08:06:17 -06:00
using Umbraco.Cms.Core ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Configuration ;
using Umbraco.Cms.Core.Configuration.Models ;
2022-11-29 11:22:57 +00:00
using Umbraco.Cms.Core.DependencyInjection ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Exceptions ;
2021-06-08 14:56:45 -06:00
using Umbraco.Cms.Core.Packaging ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Semver ;
using Umbraco.Cms.Core.Services ;
2021-02-12 12:40:08 +01:00
using Umbraco.Cms.Infrastructure.Migrations.Upgrade ;
2021-02-12 13:36:50 +01:00
using Umbraco.Cms.Infrastructure.Persistence ;
2016-09-01 19:06:08 +02:00
2022-07-01 08:48:05 +02:00
namespace Umbraco.Cms.Infrastructure.Runtime ;
/// <summary>
/// Represents the state of the Umbraco runtime.
/// </summary>
public class RuntimeState : IRuntimeState
2016-09-01 19:06:08 +02:00
{
2022-07-01 08:48:05 +02:00
internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations" ;
private readonly IOptions < GlobalSettings > _globalSettings = null ! ;
private readonly IOptions < UnattendedSettings > _unattendedSettings = null ! ;
private readonly IUmbracoVersion _umbracoVersion = null ! ;
private readonly IUmbracoDatabaseFactory _databaseFactory = null ! ;
private readonly ILogger < RuntimeState > _logger = null ! ;
private readonly PendingPackageMigrations _packageMigrationState = null ! ;
private readonly Dictionary < string , object > _startupState = new Dictionary < string , object > ( ) ;
private readonly IConflictingRouteService _conflictingRouteService = null ! ;
private readonly IEnumerable < IDatabaseProviderMetadata > _databaseProviderMetadata = null ! ;
private readonly IRuntimeModeValidationService _runtimeModeValidationService = null ! ;
/// <summary>
/// The initial <see cref="RuntimeState"/>
/// </summary>
public static RuntimeState Booting ( ) = > new RuntimeState ( ) { Level = RuntimeLevel . Boot } ;
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
/// </summary>
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 ,
PendingPackageMigrations packageMigrationState ,
IConflictingRouteService conflictingRouteService ,
IEnumerable < IDatabaseProviderMetadata > databaseProviderMetadata ,
IRuntimeModeValidationService runtimeModeValidationService )
{
_globalSettings = globalSettings ;
_unattendedSettings = unattendedSettings ;
_umbracoVersion = umbracoVersion ;
_databaseFactory = databaseFactory ;
_logger = logger ;
_packageMigrationState = packageMigrationState ;
_conflictingRouteService = conflictingRouteService ;
_databaseProviderMetadata = databaseProviderMetadata ;
_runtimeModeValidationService = runtimeModeValidationService ;
}
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
/// </summary>
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12.")]
public RuntimeState (
IOptions < GlobalSettings > globalSettings ,
IOptions < UnattendedSettings > unattendedSettings ,
IUmbracoVersion umbracoVersion ,
IUmbracoDatabaseFactory databaseFactory ,
ILogger < RuntimeState > logger ,
PendingPackageMigrations packageMigrationState ,
IConflictingRouteService conflictingRouteService ,
IEnumerable < IDatabaseProviderMetadata > databaseProviderMetadata )
: this (
globalSettings ,
unattendedSettings ,
umbracoVersion ,
databaseFactory ,
logger ,
packageMigrationState ,
conflictingRouteService ,
databaseProviderMetadata ,
StaticServiceProvider . Instance . GetRequiredService < IRuntimeModeValidationService > ( ) )
{ }
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12.")]
public RuntimeState (
IOptions < GlobalSettings > globalSettings ,
IOptions < UnattendedSettings > unattendedSettings ,
IUmbracoVersion umbracoVersion ,
IUmbracoDatabaseFactory databaseFactory ,
ILogger < RuntimeState > logger ,
PendingPackageMigrations packageMigrationState ,
IConflictingRouteService conflictingRouteService )
: this (
globalSettings ,
unattendedSettings ,
umbracoVersion ,
databaseFactory ,
logger ,
packageMigrationState ,
StaticServiceProvider . Instance . GetRequiredService < IConflictingRouteService > ( ) ,
StaticServiceProvider . Instance . GetServices < IDatabaseProviderMetadata > ( ) )
{ }
2021-06-10 08:06:17 -06:00
2016-09-01 19:06:08 +02:00
/// <summary>
2022-07-01 08:48:05 +02:00
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
2016-09-01 19:06:08 +02:00
/// </summary>
2022-07-01 08:48:05 +02:00
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12.")]
public RuntimeState (
IOptions < GlobalSettings > globalSettings ,
IOptions < UnattendedSettings > unattendedSettings ,
IUmbracoVersion umbracoVersion ,
IUmbracoDatabaseFactory databaseFactory ,
ILogger < RuntimeState > logger ,
PendingPackageMigrations packageMigrationState )
: this (
globalSettings ,
unattendedSettings ,
umbracoVersion ,
databaseFactory ,
logger ,
packageMigrationState ,
StaticServiceProvider . Instance . GetRequiredService < IConflictingRouteService > ( ) )
{ }
/// <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 IReadOnlyDictionary < string , object > StartupState = > _startupState ;
/// <inheritdoc />
public void DetermineRuntimeLevel ( )
2016-09-01 19:06:08 +02:00
{
2022-07-01 08:48:05 +02:00
if ( _databaseFactory . Configured = = false )
2022-01-27 10:32:56 +01:00
{
2022-07-01 08:48:05 +02:00
// local version *does* match code version, but the database is not configured
// install - may happen with Deploy/Cloud/etc
2023-06-07 21:47:05 +12:00
if ( _logger . IsEnabled ( Microsoft . Extensions . Logging . LogLevel . Debug ) )
{
_logger . LogDebug ( "Database is not configured, need to install Umbraco." ) ;
}
2022-07-01 08:48:05 +02:00
Level = RuntimeLevel . Install ;
Reason = RuntimeLevelReason . InstallNoDatabase ;
return ;
2022-01-27 10:32:56 +01:00
}
2022-07-01 08:48:05 +02:00
// Validate runtime mode
if ( _runtimeModeValidationService . Validate ( out var validationErrorMessage ) = = false )
2018-12-17 18:52:43 +01:00
{
2022-07-01 08:48:05 +02:00
_logger . LogError ( validationErrorMessage ) ;
2018-12-17 18:52:43 +01:00
2022-07-01 08:48:05 +02:00
Level = RuntimeLevel . BootFailed ;
Reason = RuntimeLevelReason . BootFailedOnException ;
BootFailedException = new BootFailedException ( validationErrorMessage ) ;
2021-12-22 13:03:38 +01:00
2022-07-01 08:48:05 +02:00
return ;
}
2021-12-22 13:03:38 +01:00
2022-07-01 08:48:05 +02:00
// Check if we have multiple controllers with the same name.
if ( _conflictingRouteService . HasConflictingRoutes ( out string controllerName ) )
{
var message = $"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}" ;
_logger . LogError ( message ) ;
2018-12-17 18:52:43 +01:00
2022-07-01 08:48:05 +02:00
Level = RuntimeLevel . BootFailed ;
Reason = RuntimeLevelReason . BootFailedOnException ;
BootFailedException = new BootFailedException ( message ) ;
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 :
2021-06-08 14:56:45 -06:00
{
2023-06-07 21:47:05 +12:00
// cannot connect to configured database, this is bad, fail
if ( _logger . IsEnabled ( Microsoft . Extensions . Logging . LogLevel . Debug ) )
{
_logger . LogDebug ( "Could not connect to database." ) ;
}
2021-06-08 14:56:45 -06:00
2023-03-30 10:19:35 +02:00
if ( _globalSettings . Value . InstallMissingDatabase | | _databaseProviderMetadata . CanForceCreateDatabase ( _databaseFactory ) )
2021-01-18 15:40:22 +01:00
{
2021-06-08 14:56:45 -06:00
// ok to install on a configured but missing database
2023-07-13 06:03:39 +02:00
Level = RuntimeLevel . BootFailed ;
2021-06-08 14:56:45 -06:00
Reason = RuntimeLevelReason . InstallMissingDatabase ;
2021-01-18 15:40:22 +01:00
return ;
}
2021-06-08 14:56:45 -06:00
// 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 ;
}
2022-07-01 08:48:05 +02:00
case UmbracoDatabaseState . NotInstalled :
2021-06-08 14:56:45 -06:00
{
// ok to install on an empty database
Level = RuntimeLevel . Install ;
Reason = RuntimeLevelReason . InstallEmptyDatabase ;
return ;
}
2022-07-01 08:48:05 +02:00
case UmbracoDatabaseState . NeedsUpgrade :
2021-06-08 14:56:45 -06:00
{
2023-06-07 21:47:05 +12:00
// 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
2021-06-08 14:56:45 -06:00
2023-06-07 21:47:05 +12:00
// 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
if ( _logger . IsEnabled ( Microsoft . Extensions . Logging . LogLevel . Debug ) )
{
_logger . LogDebug ( "Has not reached the final upgrade step, need to upgrade Umbraco." ) ;
}
2021-06-08 14:56:45 -06:00
Level = _unattendedSettings . Value . UpgradeUnattended ? RuntimeLevel . Run : RuntimeLevel . Upgrade ;
Reason = RuntimeLevelReason . UpgradeMigrations ;
}
break ;
2022-07-01 08:48:05 +02:00
case UmbracoDatabaseState . NeedsPackageMigration :
2021-06-08 14:56:45 -06:00
2022-07-01 08:48:05 +02:00
// no matter what the level is run for package migrations.
// they either run unattended, or only manually via the back office.
Level = RuntimeLevel . Run ;
2021-06-29 14:23:08 +02:00
2022-07-01 08:48:05 +02:00
if ( _unattendedSettings . Value . PackageMigrationsUnattended )
{
2023-06-07 21:47:05 +12:00
if ( _logger . IsEnabled ( Microsoft . Extensions . Logging . LogLevel . Debug ) )
{
_logger . LogDebug ( "Package migrations need to execute." ) ;
}
2022-07-01 08:48:05 +02:00
Reason = RuntimeLevelReason . UpgradePackageMigrations ;
}
else
{
_logger . LogInformation ( "Package migrations need to execute but unattended package migrations is disabled. They will need to be run from the back office." ) ;
Reason = RuntimeLevelReason . Run ;
}
2021-06-08 14:56:45 -06:00
2022-07-01 08:48:05 +02:00
break ;
case UmbracoDatabaseState . Ok :
default :
2021-06-08 14:56:45 -06:00
{
2021-06-29 14:23:08 +02:00
2021-01-18 15:40:22 +01:00
2021-06-08 14:56:45 -06:00
// the database version matches the code & files version, all clear, can run
Level = RuntimeLevel . Run ;
Reason = RuntimeLevelReason . Run ;
}
break ;
2021-01-18 15:40:22 +01:00
}
2022-07-01 08:48:05 +02:00
}
2018-12-17 18:52:43 +01:00
2022-07-01 08:48:05 +02:00
public void Configure ( RuntimeLevel level , RuntimeLevelReason reason , Exception ? bootFailedException = null )
{
Level = level ;
Reason = reason ;
2021-09-17 16:30:46 +02:00
2022-07-01 08:48:05 +02:00
if ( bootFailedException ! = null )
2021-01-18 15:40:22 +01:00
{
2022-07-01 08:48:05 +02:00
BootFailedException = new BootFailedException ( bootFailedException . Message , bootFailedException ) ;
2021-01-18 15:40:22 +01:00
}
2022-07-01 08:48:05 +02:00
}
2018-12-17 18:52:43 +01:00
2022-07-01 08:48:05 +02:00
private enum UmbracoDatabaseState
{
Ok ,
CannotConnect ,
NotInstalled ,
NeedsUpgrade ,
NeedsPackageMigration
}
private UmbracoDatabaseState GetUmbracoDatabaseState ( IUmbracoDatabaseFactory databaseFactory )
{
try
2021-01-18 15:40:22 +01:00
{
2022-07-01 08:48:05 +02:00
if ( ! TryDbConnect ( databaseFactory ) )
2018-12-17 18:52:43 +01:00
{
2022-07-01 08:48:05 +02:00
return UmbracoDatabaseState . CannotConnect ;
}
2021-01-18 15:40:22 +01:00
2022-07-01 08:48:05 +02:00
// no scope, no service - just directly accessing the database
using ( IUmbracoDatabase database = databaseFactory . CreateDatabase ( ) )
{
if ( ! database . IsUmbracoInstalled ( ) )
2021-01-18 15:40:22 +01:00
{
2022-07-01 08:48:05 +02:00
return UmbracoDatabaseState . NotInstalled ;
}
2021-01-18 15:40:22 +01:00
2022-07-01 08:48:05 +02:00
// Make ONE SQL call to determine Umbraco upgrade vs package migrations state.
// All will be prefixed with the same key.
IReadOnlyDictionary < string , string? > ? keyValues = database . GetFromKeyValueTable ( Constants . Conventions . Migrations . KeyValuePrefix ) ;
2021-06-08 14:56:45 -06:00
2022-07-01 08:48:05 +02:00
// This could need both an upgrade AND package migrations to execute but
// we will process them one at a time, first the upgrade, then the package migrations.
if ( DoesUmbracoRequireUpgrade ( keyValues ) )
{
return UmbracoDatabaseState . NeedsUpgrade ;
}
2021-06-08 14:56:45 -06:00
2022-07-01 08:48:05 +02:00
IReadOnlyList < string > packagesRequiringMigration = _packageMigrationState . GetPendingPackageMigrations ( keyValues ) ;
if ( packagesRequiringMigration . Count > 0 )
{
_startupState [ PendingPackageMigrationsStateKey ] = packagesRequiringMigration ;
2021-06-11 10:50:35 -06:00
2022-07-01 08:48:05 +02:00
return UmbracoDatabaseState . NeedsPackageMigration ;
2021-01-18 15:40:22 +01:00
}
2018-12-17 18:52:43 +01:00
}
2019-01-10 14:03:25 +01:00
2022-07-01 08:48:05 +02:00
return UmbracoDatabaseState . Ok ;
2021-01-18 15:40:22 +01:00
}
2022-07-01 08:48:05 +02:00
catch ( Exception e )
2021-01-18 15:40:22 +01:00
{
2022-07-01 08:48:05 +02:00
// can connect to the database so cannot check the upgrade state... oops
_logger . LogWarning ( e , "Could not check the upgrade state." ) ;
2021-01-18 15:40:22 +01:00
2022-07-01 08:48:05 +02:00
// 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 ;
}
}
2021-01-18 15:40:22 +01:00
2022-07-01 08:48:05 +02:00
private bool DoesUmbracoRequireUpgrade ( IReadOnlyDictionary < string , string? > ? keyValues )
{
var upgrader = new Upgrader ( new UmbracoPlan ( _umbracoVersion ) ) ;
var stateValueKey = upgrader . StateValueKey ;
2021-01-18 15:40:22 +01:00
2022-07-01 08:48:05 +02:00
if ( keyValues ? . TryGetValue ( stateValueKey , out var value ) ? ? false )
{
CurrentMigrationState = value ;
2021-01-18 15:40:22 +01:00
}
2022-07-01 08:48:05 +02:00
FinalMigrationState = upgrader . Plan . FinalState ;
2023-06-07 21:47:05 +12:00
if ( _logger . IsEnabled ( Microsoft . Extensions . Logging . LogLevel . Debug ) )
{
_logger . LogDebug ( "Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}" , FinalMigrationState , CurrentMigrationState ? ? "<null>" ) ;
}
2022-07-01 08:48:05 +02:00
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 ; ; )
2021-01-18 15:40:22 +01:00
{
2022-07-01 08:48:05 +02:00
canConnect = databaseFactory . CanConnect ;
if ( canConnect | | + + i = = tries )
2021-01-18 15:40:22 +01:00
{
2022-07-01 08:48:05 +02:00
break ;
2021-01-18 15:40:22 +01:00
}
2023-06-07 21:47:05 +12:00
if ( _logger . IsEnabled ( Microsoft . Extensions . Logging . LogLevel . Debug ) )
{
_logger . LogDebug ( "Could not immediately connect to database, trying again." ) ;
}
2022-07-01 08:48:05 +02:00
Thread . Sleep ( 1000 ) ;
2021-01-18 15:40:22 +01:00
}
2022-07-01 08:48:05 +02:00
return canConnect ;
2016-09-01 19:06:08 +02:00
}
2017-07-20 11:21:28 +02:00
}