diff --git a/src/fiskaltrust.Launcher.Common/fiskaltrust.Launcher.Common.csproj b/src/fiskaltrust.Launcher.Common/fiskaltrust.Launcher.Common.csproj index 7a3316a6..0645ad06 100644 --- a/src/fiskaltrust.Launcher.Common/fiskaltrust.Launcher.Common.csproj +++ b/src/fiskaltrust.Launcher.Common/fiskaltrust.Launcher.Common.csproj @@ -7,11 +7,11 @@ - - + + - + - + \ No newline at end of file diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index 9f80679d..37eb8915 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -157,12 +157,23 @@ public static async Task HandleAsync( ECDiffieHellman? clientEcdh = null; try { - clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value, useFallback: launcherConfiguration.UseLegacyDataProtection!.Value); - using var downloader = new ConfigurationDownloader(launcherConfiguration); - var exists = await downloader.DownloadConfigurationAsync(clientEcdh); - if (launcherConfiguration.UseOffline!.Value && !exists) + clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value); + } + catch (Exception e) + { + Log.Fatal(e, "Could not load client curve."); + } + + try + { + if (clientEcdh is not null) { - Log.Warning("Cashbox configuration was not downloaded because UseOffline is set."); + using var downloader = new ConfigurationDownloader(launcherConfiguration); + var exists = await downloader.DownloadConfigurationAsync(clientEcdh); + if (launcherConfiguration.UseOffline!.Value && !exists) + { + Log.Warning("Cashbox configuration was not downloaded because UseOffline is set."); + } } } catch (Exception e) @@ -192,7 +203,7 @@ public static async Task HandleAsync( try { cashboxConfiguration = CashBoxConfigurationExt.Deserialize(await File.ReadAllTextAsync(launcherConfiguration.CashboxConfigurationFile!)); - cashboxConfiguration.Decrypt(launcherConfiguration, clientEcdh); + if (clientEcdh is not null) { cashboxConfiguration.Decrypt(launcherConfiguration, clientEcdh); } } catch (Exception e) { @@ -224,6 +235,7 @@ public static async Task HandleAsync( Log.Debug("Cashbox Configuration File: {CashboxConfigurationFile}", launcherConfiguration.CashboxConfigurationFile); Log.Debug("Launcher Configuration: {@LauncherConfiguration}", launcherConfiguration.Redacted()); + Log.Debug("Launcher running as {ServiceType}", Enum.GetName(typeof(ServiceTypes), host.Services.GetRequiredService().Type)); var dataProtectionProvider = DataProtectionExtensions.Create(launcherConfiguration.AccessToken, useFallback: launcherConfiguration.UseLegacyDataProtection!.Value); @@ -236,12 +248,12 @@ public static async Task HandleAsync( Log.Warning(e, "Error decrypring launcher configuration file."); } - return await handler(options, new CommonProperties(launcherConfiguration, cashboxConfiguration, clientEcdh, dataProtectionProvider), specificOptions, host.Services.GetRequiredService()); + return await handler(options, new CommonProperties(launcherConfiguration, cashboxConfiguration, clientEcdh!, dataProtectionProvider), specificOptions, host.Services.GetRequiredService()); } private static async Task EnsureServiceDirectoryExists(LauncherConfiguration config) { - var serviceDirectory = config.ServiceFolder; + var serviceDirectory = config.ServiceFolder!; try { if (!Directory.Exists(serviceDirectory)) diff --git a/src/fiskaltrust.Launcher/Commands/RunCommand.cs b/src/fiskaltrust.Launcher/Commands/RunCommand.cs index 014a1400..7eb32b56 100644 --- a/src/fiskaltrust.Launcher/Commands/RunCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/RunCommand.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using System.CommandLine.Invocation; using fiskaltrust.Launcher.ProcessHost; using fiskaltrust.Launcher.Services; using Serilog; @@ -8,12 +7,6 @@ using fiskaltrust.Launcher.Extensions; using fiskaltrust.Launcher.Helpers; using Microsoft.AspNetCore.Server.Kestrel.Core; -using fiskaltrust.Launcher.Common.Configuration; -using fiskaltrust.storage.serialization.V0; -using System.Security.Cryptography; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; namespace fiskaltrust.Launcher.Commands diff --git a/src/fiskaltrust.Launcher/Extensions/LifetimeExtensions.cs b/src/fiskaltrust.Launcher/Extensions/LifetimeExtensions.cs index 2ae00daa..0e449510 100644 --- a/src/fiskaltrust.Launcher/Extensions/LifetimeExtensions.cs +++ b/src/fiskaltrust.Launcher/Extensions/LifetimeExtensions.cs @@ -1,10 +1,22 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; +using Microsoft.Extensions.Hosting.Systemd; using Microsoft.Extensions.Hosting.WindowsServices; using Microsoft.Extensions.Options; namespace fiskaltrust.Launcher.Extensions { + public record ServiceType(ServiceTypes Type); + + public enum ServiceTypes + { + WindowsService, + SystemdService, + ConsoleApplication + } + static class LifetimeExtensions { public static IHostBuilder UseCustomHostLifetime(this IHostBuilder builder) @@ -15,6 +27,7 @@ public static IHostBuilder UseCustomHostLifetime(this IHostBuilder builder) return builder.ConfigureServices(services => { + services.AddSingleton(new ServiceType(ServiceTypes.WindowsService)); var lifetime = services.FirstOrDefault(s => s.ImplementationType == typeof(WindowsServiceLifetime)); if (lifetime != null) @@ -28,10 +41,29 @@ public static IHostBuilder UseCustomHostLifetime(this IHostBuilder builder) #pragma warning restore CA1416 }); } + else if (SystemdHelpers.IsSystemdService()) + { + builder.UseSystemd(); + + return builder.ConfigureServices(services => + { + services + .AddSingleton(new ServiceType(ServiceTypes.SystemdService)) + .AddSingleton(); + + // #pragma warning disable CA1416 + // services.AddSingleton(); + // services.AddSingleton(sp => sp.GetRequiredService()); + // #pragma warning restore CA1416 + }); + } else { Console.OutputEncoding = Encoding.UTF8; - builder.ConfigureServices(services => services.AddSingleton()); + builder.ConfigureServices(services => services + .AddSingleton() + .AddSingleton(new ServiceType(ServiceTypes.ConsoleApplication))); + builder.UseConsoleLifetime(); return builder; } @@ -96,7 +128,7 @@ public CustomWindowsServiceLifetime( public void ServiceStartupCompleted() { - ApplicationLifetime.ApplicationStarted.Register(() => _started.Set()); + ApplicationLifetime.ApplicationStarted.Register(_started.Set); } public new async Task WaitForStartAsync(CancellationToken cancellationToken) @@ -133,4 +165,71 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } + + [SupportedOSPlatform("linux")] + public class CustomSystemDServiceLifetime : ILifetime, IHostLifetime, IDisposable + { + private readonly CancellationTokenSource _started = new(); + private readonly ISystemdNotifier _systemdNotifier; + public IHostApplicationLifetime ApplicationLifetime { get; init; } + + private CancellationTokenRegistration _applicationStartedRegistration; + private CancellationTokenRegistration _applicationStoppingRegistration; + private PosixSignalRegistration? _sigTermRegistration; + + public CustomSystemDServiceLifetime( + IHostApplicationLifetime applicationLifetime, + ISystemdNotifier systemdNotifier) + { + ApplicationLifetime = applicationLifetime; + _systemdNotifier = systemdNotifier; + } + + public void ServiceStartupCompleted() => _started.Cancel(); + + public Task WaitForStartAsync(CancellationToken cancellationToken) + { + _applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(OnApplicationStarted); + _applicationStoppingRegistration = ApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping); + + RegisterShutdownHandlers(); + + return Task.CompletedTask; + } + + private void OnApplicationStarted() + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(_started.Token, ApplicationLifetime.ApplicationStopping); + + cts.Token.Register(() => + { + _systemdNotifier.Notify(ServiceState.Stopping); + }); + } + + private void OnApplicationStopping() => _systemdNotifier.Notify(ServiceState.Stopping); + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private void RegisterShutdownHandlers() => _sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, HandlePosixSignal); + + private void HandlePosixSignal(PosixSignalContext context) + { + Debug.Assert(context.Signal == PosixSignal.SIGTERM); + + context.Cancel = true; + ApplicationLifetime.StopApplication(); + } + + private void UnregisterShutdownHandlers() => _sigTermRegistration?.Dispose(); + + public void Dispose() + { + _started.Cancel(); + + UnregisterShutdownHandlers(); + _applicationStartedRegistration.Dispose(); + _applicationStoppingRegistration.Dispose(); + } + } } \ No newline at end of file diff --git a/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs b/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs index 1e1cb9c3..4fbeef36 100644 --- a/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs +++ b/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs @@ -7,9 +7,9 @@ namespace fiskaltrust.Launcher.Helpers; public static class ProcessHelper { public static async Task<(int exitCode, string output)> RunProcess( - string fileName, - IEnumerable arguments, - LogEventLevel logLevel = LogEventLevel.Information) + string fileName, + IEnumerable arguments, + LogEventLevel? logLevel = LogEventLevel.Information) { var process = new Process { @@ -30,7 +30,7 @@ public static class ProcessHelper var stdOut = await process.StandardOutput.ReadToEndAsync(); if (!string.IsNullOrEmpty(stdOut)) { - Log.Write(logLevel, stdOut); + if (logLevel is not null) { Log.Write(logLevel.Value, stdOut); } } var stdErr = await process.StandardError.ReadToEndAsync(); diff --git a/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarch.cs b/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarch.cs index e1c0768a..64f521a0 100644 --- a/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarch.cs +++ b/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarch.cs @@ -45,11 +45,6 @@ public ProcessHostMonarch(ILogger logger, LauncherConfigurat _stopped = new TaskCompletionSource(); _started = new TaskCompletionSource(); - - // if (Debugger.IsAttached) - // { - // _process.StartInfo.Arguments += " --debugging"; - // } } private void Setup() @@ -61,11 +56,11 @@ private void Setup() UseShellExecute = false, FileName = _launcherExecutablePath.Path, CreateNoWindow = false, - Arguments = string.Join(" ", new string[] { + Arguments = string.Join(" ", [ "host", "--plebeian-configuration", $"\"{Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(new PlebeianConfiguration { PackageType = _packageType, PackageId = _packageConfiguration.Id }.Serialize()))}\"", "--launcher-configuration", $"\"{Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(_launcherConfiguration.Serialize()))}\"", - }), + ]), RedirectStandardInput = true, RedirectStandardError = true, RedirectStandardOutput = true @@ -75,6 +70,11 @@ private void Setup() _process.OutputDataReceived += ReceiveStdOut; _process.ErrorDataReceived += ReceiveStdOut; + + // if (Debugger.IsAttached && _packageType == PackageType.Helper) + // { + // _process.StartInfo.Arguments += " --debugging"; + // } } private void ReceiveStdOut(object sender, DataReceivedEventArgs e) diff --git a/src/fiskaltrust.Launcher/Program.cs b/src/fiskaltrust.Launcher/Program.cs index bd5ab77b..35141f1a 100644 --- a/src/fiskaltrust.Launcher/Program.cs +++ b/src/fiskaltrust.Launcher/Program.cs @@ -37,7 +37,7 @@ if (!args.Any()) { - args = new[] { runCommand.Name }; + args = [runCommand.Name]; } var subArguments = new SubArguments(args.SkipWhile(a => a != "--").Skip(1)); diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 97ccb82e..50ba5fb4 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -15,49 +15,53 @@ public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutab public override async Task InstallService(string commandArgs, string? displayName, bool delayedStart = false) { - if (!await IsSystemd()) + if (!await IdSystemdAvailable()) { + Log.Error("Systemd is not running on this machine. No service installation is possible."); + return -1; + } + + if (await IsSystemdServiceInstalled(_serviceName)) + { + Log.Error("Service is already installed and cannot be installed twice for one cashbox."); return -1; } Log.Information("Installing service via systemd."); var serviceFileContent = GetServiceFileContent(displayName ?? "Service installation of fiskaltrust launcher.", commandArgs); var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); await File.AppendAllLinesAsync(serviceFilePath, serviceFileContent).ConfigureAwait(false); - await ProcessHelper.RunProcess("systemctl", new[] { "daemon-reload" }); - Log.Information("Starting service."); - await ProcessHelper.RunProcess("systemctl", new[] { "start", _serviceName }); - Log.Information("Enable service."); - return (await ProcessHelper.RunProcess("systemctl", new[] { "enable", _serviceName, "-q" })).exitCode; + await ProcessHelper.RunProcess("systemctl", ["daemon-reload"]); + Log.Information("Starting systemd service."); + await ProcessHelper.RunProcess("systemctl", ["start", _serviceName]); + Log.Information("Enabling systemd service."); + return (await ProcessHelper.RunProcess("systemctl", ["enable", _serviceName, "-q"])).exitCode; } public override async Task UninstallService() { - if (!await IsSystemd()) + if (!await IdSystemdAvailable()) { + Log.Error("Systemd is not running on this machine. No service uninstallation is possible."); return -1; } - Log.Information("Stop service on systemd."); - await ProcessHelper.RunProcess("systemctl", new[] { "stop ", _serviceName }); - Log.Information("Disable service."); - await ProcessHelper.RunProcess("systemctl", new[] { "disable ", _serviceName, "-q" }); - Log.Information("Remove service."); - var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); - await ProcessHelper.RunProcess("rm", new[] { serviceFilePath }); - Log.Information("Reload daemon."); - await ProcessHelper.RunProcess("systemctl", new[] { "daemon-reload" }); - Log.Information("Reset failed."); - return (await ProcessHelper.RunProcess("systemctl", new[] { "reset-failed" })).exitCode; - } - private static async Task IsSystemd() - { - var (exitCode, output) = await ProcessHelper.RunProcess("ps", new[] { "--no-headers", "-o", "comm", "1" }); - if (exitCode != 0 && output.Contains("systemd")) + if (!await IsSystemdServiceInstalled(_serviceName)) { - Log.Error("Service installation works only for systemd setup."); - return false; + Log.Error("Service is not installed!"); + return -1; } - return true; + + Log.Information("Stoppig systemd service."); + await ProcessHelper.RunProcess("systemctl", ["stop ", _serviceName]); + Log.Information("Disabling systemd service."); + await ProcessHelper.RunProcess("systemctl", ["disable ", _serviceName, "-q"]); + Log.Information("Removing systemd service."); + var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); + await ProcessHelper.RunProcess("rm", [serviceFilePath]); + Log.Information("Reloading systemd daemon."); + await ProcessHelper.RunProcess("systemctl", ["daemon-reload"]); + Log.Information("Reseting state for failed systemd units."); + return (await ProcessHelper.RunProcess("systemctl", ["reset-failed"])).exitCode; } private string[] GetServiceFileContent(string serviceDescription, string commandArgs) @@ -65,18 +69,41 @@ private string[] GetServiceFileContent(string serviceDescription, string command var processPath = _launcherExecutablePath.Path; var command = $"{processPath} {commandArgs}"; - return new[] - { + + return [ "[Unit]", $"Description=\"{serviceDescription}\"", "", "[Service]", - "Type=simple", - $"ExecStart=\"{command}\"", + "Type=notify", + $"ExecStart={command}", + $"WorkingDirectory={Path.GetDirectoryName(_launcherExecutablePath.Path)}", "", "[Install]", "WantedBy = multi-user.target" - }; + ]; + } + + private static async Task IdSystemdAvailable() + { + var (exitCode, output) = await ProcessHelper.RunProcess("ps", ["--no-headers", "-o", "comm", "1"], logLevel: null); + + if (exitCode != 0 && output.Contains("systemd")) + { + Log.Error("Service installation works only for systemd setup."); + return false; + } + return true; + } + + private static async Task IsSystemdServiceInstalled(string serviceName) + { + var (exitCode, _) = await ProcessHelper.RunProcess("systemctl", [$"status {serviceName}"], logLevel: null); + if (exitCode == 4) + { + return false; + } + return true; } } } diff --git a/src/fiskaltrust.Launcher/fiskaltrust.Launcher.csproj b/src/fiskaltrust.Launcher/fiskaltrust.Launcher.csproj index b213e97a..8deb18d9 100644 --- a/src/fiskaltrust.Launcher/fiskaltrust.Launcher.csproj +++ b/src/fiskaltrust.Launcher/fiskaltrust.Launcher.csproj @@ -9,12 +9,12 @@ - $(DefineConstants);EnableSelfUpdate + $(DefineConstants);EnableSelfUpdate - - + + @@ -22,22 +22,24 @@ - + + - - - + + + - + - + - + - + \ No newline at end of file diff --git a/test/fiskaltrust.Launcher.IntegrationTest/fiskaltrust.Launcher.IntegrationTest.csproj b/test/fiskaltrust.Launcher.IntegrationTest/fiskaltrust.Launcher.IntegrationTest.csproj index 05cae015..48d35d7c 100644 --- a/test/fiskaltrust.Launcher.IntegrationTest/fiskaltrust.Launcher.IntegrationTest.csproj +++ b/test/fiskaltrust.Launcher.IntegrationTest/fiskaltrust.Launcher.IntegrationTest.csproj @@ -10,10 +10,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -25,15 +25,19 @@ - + - + - + - + \ No newline at end of file diff --git a/test/fiskaltrust.Launcher.UnitTest/fiskaltrust.Launcher.UnitTest.csproj b/test/fiskaltrust.Launcher.UnitTest/fiskaltrust.Launcher.UnitTest.csproj index 8eacef9e..8f1987a0 100644 --- a/test/fiskaltrust.Launcher.UnitTest/fiskaltrust.Launcher.UnitTest.csproj +++ b/test/fiskaltrust.Launcher.UnitTest/fiskaltrust.Launcher.UnitTest.csproj @@ -11,10 +11,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -25,15 +25,19 @@ - + - + - + - + \ No newline at end of file