diff --git a/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs b/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs new file mode 100644 index 0000000000..422673a823 --- /dev/null +++ b/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Events +{ + public class UmbracoApplicationStarting : INotification + { + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + public UmbracoApplicationStarting(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + + /// + /// Gets the runtime level of execution. + /// + public RuntimeLevel RuntimeLevel { get; } + } +} diff --git a/src/Umbraco.Core/Events/UmbracoApplicationStopping.cs b/src/Umbraco.Core/Events/UmbracoApplicationStopping.cs new file mode 100644 index 0000000000..bef6f0de19 --- /dev/null +++ b/src/Umbraco.Core/Events/UmbracoApplicationStopping.cs @@ -0,0 +1,4 @@ +namespace Umbraco.Core.Events +{ + public class UmbracoApplicationStopping : INotification { } +} diff --git a/src/Umbraco.Core/Services/IRuntime.cs b/src/Umbraco.Core/Services/IRuntime.cs index 8a1be721d0..d1254c219f 100644 --- a/src/Umbraco.Core/Services/IRuntime.cs +++ b/src/Umbraco.Core/Services/IRuntime.cs @@ -1,22 +1,15 @@ -using System; +using Microsoft.Extensions.Hosting; namespace Umbraco.Core { /// /// Defines the Umbraco runtime. /// - public interface IRuntime + public interface IRuntime : IHostedService { /// /// Gets the runtime state. /// IRuntimeState State { get; } - - void Start(); - - /// - /// Terminates the runtime. - /// - void Terminate(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 300dedc1c6..24b2fae683 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 126d235ae0..e658405400 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -1,4 +1,4 @@ -using System; +using System; using Examine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -38,6 +38,7 @@ using Umbraco.Core.Templates; using Umbraco.Examine; using Umbraco.Infrastructure.Examine; using Umbraco.Infrastructure.Media; +using Umbraco.Infrastructure.Runtime; using Umbraco.Web; using Umbraco.Web.Actions; using Umbraco.Web.Cache; @@ -66,11 +67,12 @@ namespace Umbraco.Core.Runtime { // core's initial composer composes before all core composers [ComposeBefore(typeof(ICoreComposer))] - public class CoreInitialComposer : ComponentComposer + public class CoreInitialComposer : IComposer { - public override void Compose(IUmbracoBuilder builder) + /// + public void Compose(IUmbracoBuilder builder) { - base.Compose(builder); + builder.AddNotificationHandler(); // composers builder diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index cb02a90ebe..55fa3457ed 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -1,7 +1,10 @@ -using System; +using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -10,71 +13,102 @@ namespace Umbraco.Infrastructure.Runtime { public class CoreRuntime : IRuntime { - public IRuntimeState State { get; } - private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; private readonly ComponentCollection _components; private readonly IApplicationShutdownRegistry _applicationShutdownRegistry; private readonly IProfilingLogger _profilingLogger; private readonly IMainDom _mainDom; private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IEventAggregator _eventAggregator; + private readonly IHostingEnvironment _hostingEnvironment; public CoreRuntime( - ILogger logger, + ILoggerFactory loggerFactory, IRuntimeState state, ComponentCollection components, IApplicationShutdownRegistry applicationShutdownRegistry, IProfilingLogger profilingLogger, IMainDom mainDom, - IUmbracoDatabaseFactory databaseFactory) + IUmbracoDatabaseFactory databaseFactory, + IEventAggregator eventAggregator, + IHostingEnvironment hostingEnvironment) { State = state; - _logger = logger; + _loggerFactory = loggerFactory; _components = components; _applicationShutdownRegistry = applicationShutdownRegistry; _profilingLogger = profilingLogger; _mainDom = mainDom; _databaseFactory = databaseFactory; - } - + _eventAggregator = eventAggregator; + _hostingEnvironment = hostingEnvironment; - public void Start() + + _logger = _loggerFactory.CreateLogger(); + } + + /// + /// Gets the state of the Umbraco runtime. + /// + public IRuntimeState State { get; } + + /// + public async Task StartAsync(CancellationToken cancellationToken) { + StaticApplicationLogging.Initialize(_loggerFactory); + AppDomain.CurrentDomain.UnhandledException += (_, args) => { var exception = (Exception)args.ExceptionObject; var isTerminating = args.IsTerminating; // always true? var msg = "Unhandled exception in AppDomain"; - if (isTerminating) msg += " (terminating)"; + + if (isTerminating) + { + msg += " (terminating)"; + } + msg += "."; + _logger.LogError(exception, msg); }; + AppDomain.CurrentDomain.SetData("DataDirectory", _hostingEnvironment?.MapPathContentRoot(Core.Constants.SystemDirectories.Data)); + DetermineRuntimeLevel(); if (State.Level <= RuntimeLevel.BootFailed) + { throw new InvalidOperationException($"Cannot start the runtime if the runtime level is less than or equal to {RuntimeLevel.BootFailed}"); + } - var hostingEnvironmentLifetime = _applicationShutdownRegistry; + IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; if (hostingEnvironmentLifetime == null) - throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(Start)}"); + { + throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); + } // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); + await _eventAggregator.PublishAsync(new UmbracoApplicationStarting(State.Level), cancellationToken); + // create & initialize the components _components.Initialize(); } - public void Terminate() + public async Task StopAsync(CancellationToken cancellationToken) { _components.Terminate(); + await _eventAggregator.PublishAsync(new UmbracoApplicationStopping(), cancellationToken); + StaticApplicationLogging.Initialize(null); } private void AcquireMainDom() { - using (var timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) + using (DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { @@ -90,7 +124,7 @@ namespace Umbraco.Infrastructure.Runtime private void DetermineRuntimeLevel() { - using var timer = _profilingLogger.DebugDuration("Determining runtime level.", "Determined."); + using DisposableTimer timer = _profilingLogger.DebugDuration("Determining runtime level.", "Determined."); try { diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComponent.cs b/src/Umbraco.Infrastructure/Runtime/EssentialDirectoryCreator.cs similarity index 67% rename from src/Umbraco.Infrastructure/Runtime/CoreInitialComponent.cs rename to src/Umbraco.Infrastructure/Runtime/EssentialDirectoryCreator.cs index 73318208b6..5543662464 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComponent.cs +++ b/src/Umbraco.Infrastructure/Runtime/EssentialDirectoryCreator.cs @@ -1,25 +1,28 @@ -using Microsoft.Extensions.Options; -using Umbraco.Core.Composing; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.IO; -namespace Umbraco.Core.Runtime +namespace Umbraco.Infrastructure.Runtime { - public class CoreInitialComponent : IComponent + public class EssentialDirectoryCreator : INotificationHandler { private readonly IIOHelper _ioHelper; private readonly IHostingEnvironment _hostingEnvironment; private readonly GlobalSettings _globalSettings; - public CoreInitialComponent(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) + public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) { _ioHelper = ioHelper; _hostingEnvironment = hostingEnvironment; _globalSettings = globalSettings.Value; } - public void Initialize() + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) { // ensure we have some essential directories // every other component can then initialize safely @@ -28,9 +31,8 @@ namespace Umbraco.Core.Runtime _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); - } - public void Terminate() - { } + return Task.CompletedTask; + } } } diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 77ee27b8c4..1e3317262a 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -33,54 +33,6 @@ namespace Umbraco.Tests.Integration MyComposer.Reset(); } - /// - /// Calling AddUmbracoCore to configure the container - /// - [Test] - public async Task AddUmbracoCore() - { - var testHelper = new TestHelper(); - - var hostBuilder = new HostBuilder() - .UseUmbraco() - .ConfigureServices((hostContext, services) => - { - var webHostEnvironment = testHelper.GetWebHostEnvironment(); - services.AddSingleton(testHelper.DbProviderFactoryCreator); - services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); - - // Add it! - var typeLoader = services.AddTypeLoader( - GetType().Assembly, - webHostEnvironment, - testHelper.GetHostingEnvironment(), - testHelper.ConsoleLoggerFactory, - AppCaches.NoCache, - hostContext.Configuration, - testHelper.Profiler); - - var builder = new UmbracoBuilder(services, hostContext.Configuration, typeLoader, testHelper.ConsoleLoggerFactory); - builder.Services.AddUnique(AppCaches.NoCache); - builder.AddConfiguration(); - builder.AddUmbracoCore(); - }); - - var host = await hostBuilder.StartAsync(); - var app = new ApplicationBuilder(host.Services); - - // assert results - var runtimeState = app.ApplicationServices.GetRequiredService(); - var mainDom = app.ApplicationServices.GetRequiredService(); - - Assert.IsFalse(mainDom.IsMainDom); // We haven't "Started" the runtime yet - Assert.IsNull(runtimeState.BootFailedException); - Assert.IsFalse(MyComponent.IsInit); // We haven't "Started" the runtime yet - - await host.StopAsync(); - - Assert.IsFalse(MyComponent.IsTerminated); // we didn't "Start" the runtime so nothing was registered for shutdown - } - /// /// Calling AddUmbracoCore to configure the container and UseUmbracoCore to start the runtime /// diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 93769eaaed..c3c01ed977 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,4 +1,4 @@ - + using System; using System.Linq.Expressions; using System.Net.Http; @@ -119,15 +119,6 @@ namespace Umbraco.Tests.Integration.TestServerTest protected LinkGenerator LinkGenerator { get; private set; } protected WebApplicationFactory Factory { get; private set; } - [TearDown] - public override void TearDown() - { - base.TearDown(); - base.TerminateCoreRuntime(); - - Factory.Dispose(); - } - #region IStartup public override void ConfigureServices(IServiceCollection services) @@ -162,7 +153,5 @@ namespace Umbraco.Tests.Integration.TestServerTest } #endregion - - } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 43b2d236c7..f72139576e 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -71,9 +71,14 @@ namespace Umbraco.Tests.Integration.Testing foreach (var a in _testTeardown) a(); } + _testTeardown = null; FirstTestInFixture = false; FirstTestInSession = false; + + // Ensure CoreRuntime stopped (now it's a HostedService) + IHost host = Services.GetRequiredService(); + host.StopAsync().GetAwaiter().GetResult(); } [TearDown] @@ -100,8 +105,6 @@ namespace Umbraco.Tests.Integration.Testing var app = new ApplicationBuilder(host.Services); Configure(app); - - OnFixtureTearDown(() => host.Dispose()); } #region Generic Host Builder and Runtime @@ -135,6 +138,8 @@ namespace Umbraco.Tests.Integration.Testing /// public virtual IHostBuilder CreateHostBuilder() { + + var testOptions = TestOptionAttributeBase.GetTestOptions(); var hostBuilder = Host.CreateDefaultBuilder() // IMPORTANT: We Cannot use UseStartup, there's all sorts of threads about this with testing. Although this can work // if you want to setup your tests this way, it is a bit annoying to do that as the WebApplicationFactory will @@ -154,6 +159,12 @@ namespace Umbraco.Tests.Integration.Testing { services.AddTransient(_ => CreateLoggerFactory()); ConfigureServices(services); + + if (!testOptions.Boot) + { + // If boot is false, we don't want the CoreRuntime hosted service to start + services.AddUnique(Mock.Of()); + } }); return hostBuilder; } @@ -215,27 +226,9 @@ namespace Umbraco.Tests.Integration.Testing { UseTestLocalDb(app.ApplicationServices); - //get the currently set options - var testOptions = TestOptionAttributeBase.GetTestOptions(); - if (testOptions.Boot) - { - Services.GetRequiredService().EnsureBackOfficeSecurity(); - Services.GetRequiredService().EnsureUmbracoContext(); - app.UseUmbracoCore(); // Takes 200 ms - - OnTestTearDown(TerminateCoreRuntime); - } - } - - /// - /// Some IComponents hook onto static events (e.g. Published in ContentService) - /// If these fire after the components host has been shutdown, errors can occur. - /// If CoreRuntime.Start() is called We also need to de-register the events. - /// - protected void TerminateCoreRuntime() - { - Services.GetRequiredService().Terminate(); - StaticApplicationLogging.Initialize(null); + Services.GetRequiredService().EnsureBackOfficeSecurity(); + Services.GetRequiredService().EnsureUmbracoContext(); + app.UseUmbracoCore(); } #endregion diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 17c91d433a..4e528bafda 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -143,11 +143,13 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddUnique(factory => factory.GetRequiredService().CreateDatabase()); builder.Services.AddUnique(factory => factory.GetRequiredService().SqlContext); builder.Services.AddUnique(); - builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddHostedService(factory => factory.GetRequiredService()); + builder.AddComposers(); return builder; diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index caf4132664..a245b8121a 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,13 +1,11 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Serilog.Context; using Smidge; using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Core; -using Umbraco.Core.Hosting; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; @@ -23,9 +21,9 @@ namespace Umbraco.Extensions /// public static bool UmbracoCanBoot(this IApplicationBuilder app) { - var runtime = app.ApplicationServices.GetRequiredService(); + var state = app.ApplicationServices.GetRequiredService(); // can't continue if boot failed - return runtime.State.Level > RuntimeLevel.BootFailed; + return state.Level > RuntimeLevel.BootFailed; } /// @@ -39,25 +37,10 @@ namespace Umbraco.Extensions if (!app.UmbracoCanBoot()) return app; - var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - AppDomain.CurrentDomain.SetData("DataDirectory", hostingEnvironment?.MapPathContentRoot(Core.Constants.SystemDirectories.Data)); - - var runtime = app.ApplicationServices.GetRequiredService(); - - // Register a listener for application shutdown in order to terminate the runtime - var hostLifetime = app.ApplicationServices.GetRequiredService(); - var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime); - hostLifetime.RegisterObject(runtimeShutdown); - // Register our global threadabort enricher for logging var threadAbortEnricher = app.ApplicationServices.GetRequiredService(); LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context - StaticApplicationLogging.Initialize(app.ApplicationServices.GetRequiredService()); - - // Start the runtime! - runtime.Start(); - return app; } @@ -121,33 +104,6 @@ namespace Umbraco.Extensions return app; } - - /// - /// Ensures the runtime is shutdown when the application is shutting down - /// - private class CoreRuntimeShutdown : IRegisteredObject - { - public CoreRuntimeShutdown(IRuntime runtime, IApplicationShutdownRegistry hostLifetime) - { - _runtime = runtime; - _hostLifetime = hostLifetime; - } - - private bool _completed = false; - private readonly IRuntime _runtime; - private readonly IApplicationShutdownRegistry _hostLifetime; - - public void Stop(bool immediate) - { - if (!_completed) - { - _completed = true; - _runtime.Terminate(); - _hostLifetime.UnregisterObject(this); - } - - } - } } } diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index f10f4491d9..bdf5b40a02 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -46,7 +46,6 @@ namespace Umbraco.Web protected UmbracoApplicationBase() { - HostingSettings hostingSettings = null; GlobalSettings globalSettings = null; SecuritySettings securitySettings = null; @@ -60,8 +59,6 @@ namespace Umbraco.Web var backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, ioHelper, _loggerFactory.CreateLogger(), Options.Create(webRoutingSettings)); var profiler = GetWebProfiler(hostingEnvironment); - StaticApplicationLogging.Initialize(_loggerFactory); - Logger = NullLogger.Instance; } private IProfiler GetWebProfiler(IHostingEnvironment hostingEnvironment) @@ -87,7 +84,6 @@ namespace Umbraco.Web _loggerFactory = loggerFactory; Logger = logger; - StaticApplicationLogging.Initialize(_loggerFactory); } protected ILogger Logger { get; } @@ -189,7 +185,6 @@ namespace Umbraco.Web LogContext.Push(new HttpRequestIdEnricher(_factory.GetRequiredService())); _runtime = _factory.GetRequiredService(); - _runtime.Start(); } // called by ASP.NET (auto event wireup) once per app domain @@ -237,7 +232,6 @@ namespace Umbraco.Web { if (_runtime != null) { - _runtime.Terminate(); _runtime.DisposeIfDisposable(); _runtime = null;