From 25b10096a8482ce40d76fd2966a6c12ecc77ed7c Mon Sep 17 00:00:00 2001 From: Jasmin Date: Fri, 2 Feb 2024 14:13:47 +0100 Subject: [PATCH] SIANXSVC-1186: Terminate application after writing metadata (#20) * SIANXSVC-1186: Update documentation for dotnet-e5e for service initialization * SIANXSVC-1186: Exit application after writing metadata Closes SIANXSVC-1186 --- README.md | 18 ++++++++++++++++++ .../Integration/StartupTests.cs | 5 +++++ .../TestHelpers/TerminatorMock.cs | 10 ++++++++++ .../TestHelpers/TestHostBuilder.cs | 7 ++++++- .../Termination/EnvironmentTerminator.cs | 6 ++++++ .../Abstractions/Termination/ITerminator.cs | 10 ++++++++++ src/Anexia.E5E/Anexia.E5E.csproj | 5 +++++ src/Anexia.E5E/Hosting/E5EHostWrapper.cs | 14 ++++++++------ 8 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/Anexia.E5E.Tests/TestHelpers/TerminatorMock.cs create mode 100644 src/Anexia.E5E/Abstractions/Termination/EnvironmentTerminator.cs create mode 100644 src/Anexia.E5E/Abstractions/Termination/ITerminator.cs diff --git a/README.md b/README.md index 2dc36c8..e4d3536 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,24 @@ await host.RunAsync(); Further examples can be found in the [examples folder](./examples). +### Note on startup initialization + +Due to the fact that the communication with e5e relies on the `IHost` to run, any initialization code should +**not** be done before starting the host. It **must** be done after starting the host, for example by splitting +up the `RunAsync` call like in the example below. + +```csharp +using var host = /* ...omitted for brevity */ + +// Don't do this or bad things are going to happen. +await SomeExtremelyLongStartupTask(); + +// Instead do this: +await host.StartAsync(); +await SomeExtremelyLongStartupTask(); +await host.WaitForShutdownAsync(); +``` + # Supported versions | | Supported | diff --git a/src/Anexia.E5E.Tests/Integration/StartupTests.cs b/src/Anexia.E5E.Tests/Integration/StartupTests.cs index 2980390..c52cc30 100644 --- a/src/Anexia.E5E.Tests/Integration/StartupTests.cs +++ b/src/Anexia.E5E.Tests/Integration/StartupTests.cs @@ -2,10 +2,13 @@ using System.Threading; using System.Threading.Tasks; +using Anexia.E5E.Abstractions.Termination; using Anexia.E5E.Runtime; using Anexia.E5E.Serialization; using Anexia.E5E.Tests.TestHelpers; +using Microsoft.Extensions.DependencyInjection; + using Xunit; using Xunit.Abstractions; @@ -36,5 +39,7 @@ public async Task TerminatesAutomatically() cts.Token.Register(() => Assert.Fail("Shutdown took longer than three seconds"), true); await Host.WaitForShutdownAsync(cts.Token); + var terminator = Host.Inner.Services.GetRequiredService() as TerminatorMock; + Assert.True(terminator?.Called); } } diff --git a/src/Anexia.E5E.Tests/TestHelpers/TerminatorMock.cs b/src/Anexia.E5E.Tests/TestHelpers/TerminatorMock.cs new file mode 100644 index 0000000..542bfba --- /dev/null +++ b/src/Anexia.E5E.Tests/TestHelpers/TerminatorMock.cs @@ -0,0 +1,10 @@ +using Anexia.E5E.Abstractions.Termination; + +namespace Anexia.E5E.Tests.TestHelpers; + +public class TerminatorMock : ITerminator +{ + public void Exit() { Called = true; } + + public bool Called { get; private set; } +} diff --git a/src/Anexia.E5E.Tests/TestHelpers/TestHostBuilder.cs b/src/Anexia.E5E.Tests/TestHelpers/TestHostBuilder.cs index a2926b3..2303506 100644 --- a/src/Anexia.E5E.Tests/TestHelpers/TestHostBuilder.cs +++ b/src/Anexia.E5E.Tests/TestHelpers/TestHostBuilder.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Anexia.E5E.Abstractions; +using Anexia.E5E.Abstractions.Termination; using Anexia.E5E.Extensions; using Anexia.E5E.Functions; using Anexia.E5E.Runtime; @@ -53,7 +54,11 @@ public TestHostBuilder ConfigureEndpoints(Action configur public Task StartAsync(int maximumLifetimeMs = 5000) { // In order to override the default implementation, we need to call this just before we build our host. - _hb.ConfigureServices(services => services.AddSingleton()); + _hb.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + }); _host = _hb.Build(); // Shutdown the host automatically after five seconds, there's no test that should run that long. diff --git a/src/Anexia.E5E/Abstractions/Termination/EnvironmentTerminator.cs b/src/Anexia.E5E/Abstractions/Termination/EnvironmentTerminator.cs new file mode 100644 index 0000000..a30eb7f --- /dev/null +++ b/src/Anexia.E5E/Abstractions/Termination/EnvironmentTerminator.cs @@ -0,0 +1,6 @@ +namespace Anexia.E5E.Abstractions.Termination; + +internal sealed class EnvironmentTerminator : ITerminator +{ + public void Exit() => Environment.Exit(0); +} diff --git a/src/Anexia.E5E/Abstractions/Termination/ITerminator.cs b/src/Anexia.E5E/Abstractions/Termination/ITerminator.cs new file mode 100644 index 0000000..fcfffdd --- /dev/null +++ b/src/Anexia.E5E/Abstractions/Termination/ITerminator.cs @@ -0,0 +1,10 @@ +namespace Anexia.E5E.Abstractions.Termination; + +/// +/// Because the usage of would also crash our test processes, it is abstracted. +/// By default, the is used as implementation. +/// +internal interface ITerminator +{ + void Exit(); +} diff --git a/src/Anexia.E5E/Anexia.E5E.csproj b/src/Anexia.E5E/Anexia.E5E.csproj index 5fb5ace..a3a0fa7 100644 --- a/src/Anexia.E5E/Anexia.E5E.csproj +++ b/src/Anexia.E5E/Anexia.E5E.csproj @@ -55,4 +55,9 @@ + + + + + diff --git a/src/Anexia.E5E/Hosting/E5EHostWrapper.cs b/src/Anexia.E5E/Hosting/E5EHostWrapper.cs index 8f6d6e6..37f8154 100644 --- a/src/Anexia.E5E/Hosting/E5EHostWrapper.cs +++ b/src/Anexia.E5E/Hosting/E5EHostWrapper.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Anexia.E5E.Abstractions; +using Anexia.E5E.Abstractions.Termination; using Anexia.E5E.Runtime; using Anexia.E5E.Serialization; @@ -12,6 +13,7 @@ namespace Anexia.E5E.Hosting; internal sealed class E5EHostWrapper : IHost { private readonly IConsoleAbstraction _console; + private readonly ITerminator _terminator; private readonly IHost _host; private readonly E5ERuntimeOptions _options; @@ -20,6 +22,7 @@ public E5EHostWrapper(IHost host) _host = host; _options = Services.GetRequiredService(); _console = Services.GetRequiredService(); + _terminator = Services.GetService() ?? new EnvironmentTerminator(); } public void Dispose() @@ -35,21 +38,20 @@ public async Task StartAsync(CancellationToken cancellationToken = default) return; } - string metadata = ""; #if NET8_0_OR_GREATER - metadata = JsonSerializer.Serialize(new E5ERuntimeMetadata(), + var metadata = JsonSerializer.Serialize(new E5ERuntimeMetadata(), E5ESerializationContext.Default.E5ERuntimeMetadata); #else - metadata = JsonSerializer.Serialize(new E5ERuntimeMetadata(), E5EJsonSerializerOptions.Default); + var metadata = JsonSerializer.Serialize(new E5ERuntimeMetadata(), E5EJsonSerializerOptions.Default); #endif _console.Open(); await _console.WriteToStdoutAsync(metadata).ConfigureAwait(false); _console.Close(); - // If we wrote the metadata, circumvent the default host mechanism as used by Run/RunAsync extensions - // and just stop the application. - _host.Services.GetRequiredService().StopApplication(); + // After we wrote the metadata, close the application immediately(!). Any startup tasks that occur after the startup + // (e.g. a long initialization task) won't be executed and the metadata is returned to e5e. + _terminator.Exit(); } public Task StopAsync(CancellationToken cancellationToken = default)