Fixes runtime state and boot failed middleware so that it actually runs. Separates out unattended install/upgrade into notification handlers.

This commit is contained in:
Shannon
2021-06-10 08:06:17 -06:00
parent c9ac62995d
commit 144014dc73
10 changed files with 303 additions and 128 deletions

View File

@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Core.Notifications
{
/// <summary>
/// Used to notify when the core runtime can do an unattended install.
/// </summary>
/// <remarks>
/// It is entirely up to the handler to determine if an unattended installation should occur and
/// to perform the logic.
/// </remarks>
public class RuntimeUnattendedInstallNotification : INotification
{
}
}

View File

@@ -0,0 +1,24 @@
namespace Umbraco.Cms.Core.Notifications
{
/// <summary>
/// Used to notify when the core runtime can do an unattended upgrade.
/// </summary>
/// <remarks>
/// It is entirely up to the handler to determine if an unattended upgrade should occur and
/// to perform the logic.
/// </remarks>
public class RuntimeUnattendedUpgradeNotification : INotification
{
/// <summary>
/// Gets/sets the result of the unattended upgrade
/// </summary>
public UpgradeResult UnattendedUpgradeResult { get; set; } = UpgradeResult.NotRequired;
public enum UpgradeResult
{
NotRequired = 0,
CoreUpgradeComplete = 100,
PackageMigrationComplete = 101
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Packaging;
@@ -22,6 +23,30 @@ namespace Umbraco.Cms.Core.Packaging
public IEnumerable<IContent> ContentInstalled { get; set; } = Enumerable.Empty<IContent>();
public IEnumerable<IMedia> MediaInstalled { get; set; } = Enumerable.Empty<IMedia>();
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("Content items installed: ");
sb.Append(ContentInstalled.Count());
sb.Append("Media items installed: ");
sb.Append(MediaInstalled.Count());
sb.Append("Dictionary items installed: ");
sb.Append(DictionaryItemsInstalled.Count());
sb.Append("Macros installed: ");
sb.Append(MacrosInstalled.Count());
sb.Append("Stylesheets installed: ");
sb.Append(StylesheetsInstalled.Count());
sb.Append("Templates installed: ");
sb.Append(TemplatesInstalled.Count());
sb.Append("Templates installed: ");
sb.Append("Document types installed: ");
sb.Append(DocumentTypesInstalled.Count());
sb.Append("Media types installed: ");
sb.Append(MediaTypesInstalled.Count());
sb.Append("Data types items installed: ");
sb.Append(DataTypesInstalled.Count());
return sb.ToString();
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Semver;
@@ -54,8 +54,6 @@ namespace Umbraco.Cms.Core.Services
/// </summary>
void DetermineRuntimeLevel();
void Configure(RuntimeLevel level, RuntimeLevelReason reason);
void DoUnattendedInstall();
void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception bootFailedException = null);
}
}

View File

@@ -16,6 +16,7 @@ using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -61,6 +62,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddUnique(factory => factory.GetRequiredService<IUmbracoDatabaseFactory>().SqlContext);
builder.Services.AddUnique<IRuntimeState, RuntimeState>();
builder.Services.AddUnique<IRuntime, CoreRuntime>();
builder.AddNotificationAsyncHandler<RuntimeUnattendedInstallNotification, UnattendedInstaller>();
builder.AddNotificationAsyncHandler<RuntimeUnattendedUpgradeNotification, UnattendedUpgrader>();
// composers
builder

View File

@@ -0,0 +1,134 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
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.Services;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Infrastructure.Install
{
public class UnattendedInstaller : INotificationAsyncHandler<RuntimeUnattendedInstallNotification>
{
private readonly IOptions<UnattendedSettings> _unattendedSettings;
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
private readonly IEventAggregator _eventAggregator;
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly IOptions<GlobalSettings> _globalSettings;
private readonly ILogger<UnattendedInstaller> _logger;
private readonly IRuntimeState _runtimeState;
public UnattendedInstaller(
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
IEventAggregator eventAggregator,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoDatabaseFactory databaseFactory,
IOptions<GlobalSettings> globalSettings,
ILogger<UnattendedInstaller> logger,
IRuntimeState runtimeState)
{
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_unattendedSettings = unattendedSettings;
_databaseFactory = databaseFactory;
_globalSettings = globalSettings;
_logger = logger;
_runtimeState = runtimeState;
}
public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken)
{
// unattended install is not enabled
if (_unattendedSettings.Value.InstallUnattended == false)
{
return Task.CompletedTask;
}
// no connection string set
if (_databaseFactory.Configured == false)
{
return Task.CompletedTask;
}
var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5;
bool connect;
try
{
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);
}
}
catch (Exception ex)
{
_logger.LogInformation(ex, "Error during unattended install.");
var innerException = new UnattendedInstallException("Unattended installation failed.", ex);
_runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException);
return Task.CompletedTask;
}
// could not connect to the database
if (connect == false)
{
return Task.CompletedTask;
}
IUmbracoDatabase database = null;
try
{
using (database = _databaseFactory.CreateDatabase())
{
var hasUmbracoTables = database.IsUmbracoInstalled();
// database has umbraco tables, assume Umbraco is already installed
if (hasUmbracoTables)
{
return Task.CompletedTask;
}
// all conditions fulfilled, do the install
_logger.LogInformation("Starting unattended install.");
database.BeginTransaction();
DatabaseSchemaCreator 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."
+ "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')",
ex);
_runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException);
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Runtime;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Install
{
public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpgradeNotification>
{
private readonly IProfilingLogger _profilingLogger;
private readonly IUmbracoVersion _umbracoVersion;
private readonly DatabaseBuilder _databaseBuilder;
private readonly IRuntimeState _runtimeState;
public UnattendedUpgrader(IProfilingLogger profilingLogger, IUmbracoVersion umbracoVersion, DatabaseBuilder databaseBuilder, IRuntimeState runtimeState)
{
_profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger));
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
}
public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken)
{
if (_runtimeState.RunUnattendedBootLogic())
{
// TODO: Here is also where we would run package migrations!
var plan = new UmbracoPlan(_umbracoVersion);
using (_profilingLogger.TraceDuration<RuntimeState>("Starting unattended upgrade.", "Unattended upgrade completed."))
{
DatabaseBuilder.Result result = _databaseBuilder.UpgradeSchemaAndData(plan);
if (result.Success == false)
{
var innerException = new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message);
_runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException);
return Task.CompletedTask;
}
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete;
}
}
return Task.CompletedTask;
}
}
}

View File

@@ -1,7 +1,10 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations;
@@ -23,22 +26,33 @@ namespace Umbraco.Cms.Core.Packaging
throw new InvalidOperationException($"Nothing to execute, {nameof(FromEmbeddedResource)} has not been called.");
}
// lookup the embedded resource by convention
Type currentType = GetType();
Assembly currentAssembly = currentType.Assembly;
var fileName = $"{currentType.Namespace}.package.xml";
Stream stream = currentAssembly.GetManifestResourceStream(fileName);
if (stream == null)
try
{
throw new FileNotFoundException("Cannot find the embedded file.", fileName);
}
XDocument xml;
using (stream)
{
xml = XDocument.Load(stream);
}
// lookup the embedded resource by convention
Type currentType = GetType();
Assembly currentAssembly = currentType.Assembly;
var fileName = $"{currentType.Namespace}.package.xml";
Stream stream = currentAssembly.GetManifestResourceStream(fileName);
if (stream == null)
{
throw new FileNotFoundException("Cannot find the embedded file.", fileName);
}
XDocument xml;
using (stream)
{
xml = XDocument.Load(stream);
}
// TODO: Use the packaging service
InstallationSummary installationSummary = _packagingService.InstallCompiledPackageData(xml);
Logger.LogInformation($"Package migration executed. Summary: {installationSummary}");
}
catch (Exception ex)
{
Logger.LogError(ex, "Package migration failed.");
// TODO: We need to exit with a status
}
}
}
}

View File

@@ -30,7 +30,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly IEventAggregator _eventAggregator;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly DatabaseBuilder _databaseBuilder;
private readonly IUmbracoVersion _umbracoVersion;
private CancellationToken _cancellationToken;
@@ -47,7 +46,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime
IUmbracoDatabaseFactory databaseFactory,
IEventAggregator eventAggregator,
IHostingEnvironment hostingEnvironment,
DatabaseBuilder databaseBuilder,
IUmbracoVersion umbracoVersion)
{
State = state;
@@ -59,7 +57,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime
_databaseFactory = databaseFactory;
_eventAggregator = eventAggregator;
_hostingEnvironment = hostingEnvironment;
_databaseBuilder = databaseBuilder;
_umbracoVersion = umbracoVersion;
_logger = _loggerFactory.CreateLogger<CoreRuntime>();
}
@@ -104,7 +101,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime
// acquire the main domain - if this fails then anything that should be registered with MainDom will not operate
AcquireMainDom();
DoUnattendedInstall();
// notify for unattended install
await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification());
DetermineRuntimeLevel();
if (!State.UmbracoCanBoot())
@@ -119,11 +117,10 @@ namespace Umbraco.Cms.Infrastructure.Runtime
}
// if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade
if (State.RunUnattendedBootLogic())
var unattendedUpgradeNotification = new RuntimeUnattendedUpgradeNotification();
await _eventAggregator.PublishAsync(unattendedUpgradeNotification);
if ((int)unattendedUpgradeNotification.UnattendedUpgradeResult >= 100)
{
// do the upgrade
DoUnattendedUpgrade();
// upgrade is done, set reason to Run
DetermineRuntimeLevel();
}
@@ -134,25 +131,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime
await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level), cancellationToken);
}
private void DoUnattendedUpgrade()
{
// TODO: Here is also where we would run package migrations!
var plan = new UmbracoPlan(_umbracoVersion);
using (_profilingLogger.TraceDuration<RuntimeState>("Starting unattended upgrade.", "Unattended upgrade completed."))
{
var result = _databaseBuilder.UpgradeSchemaAndData(plan);
if (result.Success == false)
throw new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message);
}
}
private void DoUnattendedInstall()
{
State.DoUnattendedInstall();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_components.Terminate();
@@ -178,6 +156,12 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private void DetermineRuntimeLevel()
{
if (State.BootFailedException != null)
{
// there's already been an exception so cannot boot and no need to check
return;
}
using DisposableTimer timer = _profilingLogger.DebugDuration<CoreRuntime>("Determining runtime level.", "Determined.");
try

View File

@@ -4,20 +4,19 @@ using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
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.Packaging;
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
namespace Umbraco.Cms.Infrastructure.Runtime
{
/// <summary>
/// Represents the state of the Umbraco runtime.
/// </summary>
@@ -28,8 +27,6 @@ namespace Umbraco.Cms.Core
private readonly IUmbracoVersion _umbracoVersion;
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly ILogger<RuntimeState> _logger;
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
private readonly IEventAggregator _eventAggregator;
private readonly PackageMigrationPlanCollection _packageMigrationPlans;
/// <summary>
@@ -51,8 +48,6 @@ namespace Umbraco.Cms.Core
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
IEventAggregator eventAggregator,
PackageMigrationPlanCollection packageMigrationPlans)
{
_globalSettings = globalSettings;
@@ -60,8 +55,6 @@ namespace Umbraco.Cms.Core
_umbracoVersion = umbracoVersion;
_databaseFactory = databaseFactory;
_logger = logger;
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
_eventAggregator = eventAggregator;
_packageMigrationPlans = packageMigrationPlans;
}
@@ -227,83 +220,14 @@ namespace Umbraco.Cms.Core
}
}
public void Configure(RuntimeLevel level, RuntimeLevelReason reason)
public void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception bootFailedException = null)
{
Level = level;
Reason = reason;
}
public void DoUnattendedInstall()
{
// unattended install is not enabled
if (_unattendedSettings.Value.InstallUnattended == false)
if (bootFailedException != null)
{
return;
}
// no connection string set
if (_databaseFactory.Configured == false)
{
return;
}
var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5;
bool connect;
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;
}
BootFailedException = new BootFailedException(bootFailedException.Message, bootFailedException);
}
}
@@ -328,7 +252,7 @@ namespace Umbraco.Cms.Core
var result = new List<string>(packageMigrationPlans.Count);
foreach(PackageMigrationPlan plan in packageMigrationPlans)
foreach (PackageMigrationPlan plan in packageMigrationPlans)
{
string currentMigrationState = null;
var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name;