From cd6fca963eadbb74e9b6fa479af737e34e5b54ab Mon Sep 17 00:00:00 2001 From: forsthug <85173816+forsthug@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:33:07 +0100 Subject: [PATCH 01/48] script --- fiskaltrust.Launcher.sln | 7 +++- scripts/migration.cmd | 74 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 scripts/migration.cmd diff --git a/fiskaltrust.Launcher.sln b/fiskaltrust.Launcher.sln index 095cd005..9f411014 100644 --- a/fiskaltrust.Launcher.sln +++ b/fiskaltrust.Launcher.sln @@ -15,7 +15,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{84733EDD-4 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fiskaltrust.Launcher.IntegrationTest", "test\fiskaltrust.Launcher.IntegrationTest\fiskaltrust.Launcher.IntegrationTest.csproj", "{F90BE105-2B84-4CEC-8E9C-C8671E011F21}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fiskaltrust.Launcher.UnitTest", "test\fiskaltrust.Launcher.UnitTest\fiskaltrust.Launcher.UnitTest.csproj", "{68743F85-31DB-43BA-92AE-72FF634282D0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fiskaltrust.Launcher.UnitTest", "test\fiskaltrust.Launcher.UnitTest\fiskaltrust.Launcher.UnitTest.csproj", "{68743F85-31DB-43BA-92AE-72FF634282D0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{A8FEC919-37F1-426A-96F3-7A528AF684C9}" + ProjectSection(SolutionItems) = preProject + scripts\migration.cmd = scripts\migration.cmd + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/scripts/migration.cmd b/scripts/migration.cmd new file mode 100644 index 00000000..3cf50dd7 --- /dev/null +++ b/scripts/migration.cmd @@ -0,0 +1,74 @@ +@echo off +setlocal enableextensions enabledelayedexpansion +SET "ftServiceName=" +set "lim=;" +for /f "skip=1 tokens=1-6 delims=, " %%A in ('wmic service get name^, PathName^') do ( + echo.%%B | findstr %cd% 1> nul + if not errorlevel 1 ( + echo.%%B | findstr fiskaltrust.exe 1> nul + if not errorlevel 1 ( + if .!ftServiceName!==. ( + SET ftServiceName=%%A + ) ELSE ( + echo "More than one service is registered. This can not be migrated automatically." + exit /b 1 + ) + ) + ) +) +echo +if .!ftServiceName!==. ( + GOTO ResolveInitialState +) + +if .ftServiceName!==. ( + echo "No service installed" + exit /b 1 +) + +:ResolveInitialState +SC query %ftServiceName% | FIND "STATE" | FIND "RUNNING" >NUL +IF errorlevel 0 IF NOT errorlevel 1 GOTO StopService +SC query %ftServiceName% | FIND "STATE" | FIND "STOPPED" >NUL +IF errorlevel 0 IF NOT errorlevel 1 GOTO StopedService +SC query %ftServiceName% | FIND "STATE" | FIND "PAUSED" >NUL +IF errorlevel 0 IF NOT errorlevel 1 GOTO SystemOffline +echo Service State is changing, waiting for service to resolve its state before making changes +sc query %ftServiceName% | Find "STATE" +timeout /t 2 /nobreak >NUL +GOTO ResolveInitialState + +:StopService +echo Stopping %ftServiceName% +sc stop %ftServiceName% >NUL + +GOTO StopingService +:StopingServiceDelay +echo Waiting for %ftServiceName% to stop +timeout /t 2 /nobreak >NUL +:StopingService +SC query %ftServiceName% | FIND "STATE" | FIND "STOPPED" >NUL +IF errorlevel 1 GOTO StopingServiceDelay + +:StopedService +echo %ftServiceName% is stopped + +sc delete %ftServiceName% + +if exist .backup\ ( + echo "The Backup folder: '.backup' already exists. Rename this folder to not loose data." + exit /b 1 +) +mkdir .backup +set cpath=%cd% +FOR /R %cd% %%F in (*.dll) do ( + move %%F %cpath%\.backup +) +move %cpath%\fiskaltrust.exe %cpath%\.backup +move %cpath%\fiskaltrust.InstallLog %cpath%\.backup +move %cpath%\fiskaltrust.InstallState %cpath%\.backup +move %cpath%\install-service.cmd %cpath%\.backup +move %cpath%\test.cmd %cpath%\.backup +move %cpath%\uninstall-service.cmd %cpath%\.backup + +fiskaltrust.Launcher.exe install --service-name %ftServiceName% \ No newline at end of file From 64ccc1df83f4ed136f7e87bb04ddc93865f73ddd Mon Sep 17 00:00:00 2001 From: forsthug <85173816+forsthug@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:01:50 +0100 Subject: [PATCH 02/48] fix findstr problem --- scripts/migration.cmd | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/scripts/migration.cmd b/scripts/migration.cmd index 3cf50dd7..e915251e 100644 --- a/scripts/migration.cmd +++ b/scripts/migration.cmd @@ -1,18 +1,14 @@ @echo off setlocal enableextensions enabledelayedexpansion SET "ftServiceName=" -set "lim=;" +set _cmd="%cd%\fiskaltrust.exe" for /f "skip=1 tokens=1-6 delims=, " %%A in ('wmic service get name^, PathName^') do ( - echo.%%B | findstr %cd% 1> nul - if not errorlevel 1 ( - echo.%%B | findstr fiskaltrust.exe 1> nul - if not errorlevel 1 ( - if .!ftServiceName!==. ( - SET ftServiceName=%%A - ) ELSE ( - echo "More than one service is registered. This can not be migrated automatically." - exit /b 1 - ) + if %_cmd% == %%B ( + if .!ftServiceName!==. ( + SET ftServiceName=%%A + ) ELSE ( + echo "More than one service is registered. This can not be migrated automatically." + exit /b 1 ) ) ) From 7fce47673912203b4a223405308aaf83a05e4201 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Tue, 14 Nov 2023 11:35:19 +0100 Subject: [PATCH 03/48] small fixes --- scripts/migration.cmd | 81 +++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/scripts/migration.cmd b/scripts/migration.cmd index e915251e..7ee3840f 100644 --- a/scripts/migration.cmd +++ b/scripts/migration.cmd @@ -1,70 +1,77 @@ @echo off -setlocal enableextensions enabledelayedexpansion -SET "ftServiceName=" +setlocal enableextensions +cd /d "%~dp0%" +net.exe session 1>nul 2>nul || (echo This script requires elevated rights. & exit /b 1) set _cmd="%cd%\fiskaltrust.exe" for /f "skip=1 tokens=1-6 delims=, " %%A in ('wmic service get name^, PathName^') do ( if %_cmd% == %%B ( - if .!ftServiceName!==. ( - SET ftServiceName=%%A - ) ELSE ( - echo "More than one service is registered. This can not be migrated automatically." + if not defined ftServiceName ( + set ftServiceName=%%A + ) else ( + echo More than one service is registered. This can not be migrated automatically. + timeout 15 exit /b 1 ) ) ) echo -if .!ftServiceName!==. ( - GOTO ResolveInitialState +if exist .backup\ ( + echo The Backup folder: '.backup' already exists. Rename this folder to not loose data. + timeout 15 + exit /b 1 +) +if defined ftServiceName ( + goto ResolveInitialState ) -if .ftServiceName!==. ( - echo "No service installed" +if not defined ftServiceName ( + echo No service installed + timeout 15 exit /b 1 ) :ResolveInitialState -SC query %ftServiceName% | FIND "STATE" | FIND "RUNNING" >NUL -IF errorlevel 0 IF NOT errorlevel 1 GOTO StopService -SC query %ftServiceName% | FIND "STATE" | FIND "STOPPED" >NUL -IF errorlevel 0 IF NOT errorlevel 1 GOTO StopedService -SC query %ftServiceName% | FIND "STATE" | FIND "PAUSED" >NUL -IF errorlevel 0 IF NOT errorlevel 1 GOTO SystemOffline +sc query %ftServiceName% | find "STATE" | find "RUNNING" >NUL +if errorlevel 0 if not errorlevel 1 goto StopService +SC query %ftServiceName% | find "STATE" | find "STOPPED" >NUL +if errorlevel 0 if not errorlevel 1 goto StopedService +SC query %ftServiceName% | find "STATE" | find "PAUSED" >NUL +if errorlevel 0 if not errorlevel 1 goto SystemOffline echo Service State is changing, waiting for service to resolve its state before making changes -sc query %ftServiceName% | Find "STATE" +sc query %ftServiceName% | find "STATE" timeout /t 2 /nobreak >NUL -GOTO ResolveInitialState +goto ResolveInitialState :StopService echo Stopping %ftServiceName% sc stop %ftServiceName% >NUL -GOTO StopingService +goto StopingService + :StopingServiceDelay -echo Waiting for %ftServiceName% to stop timeout /t 2 /nobreak >NUL + :StopingService -SC query %ftServiceName% | FIND "STATE" | FIND "STOPPED" >NUL -IF errorlevel 1 GOTO StopingServiceDelay +echo Waiting for %ftServiceName% to stop +sc query %ftServiceName% | find "STATE" | find "STOPPED" >NUL +if errorlevel 1 goto StopingServiceDelay :StopedService echo %ftServiceName% is stopped sc delete %ftServiceName% -if exist .backup\ ( - echo "The Backup folder: '.backup' already exists. Rename this folder to not loose data." - exit /b 1 -) mkdir .backup -set cpath=%cd% -FOR /R %cd% %%F in (*.dll) do ( - move %%F %cpath%\.backup -) -move %cpath%\fiskaltrust.exe %cpath%\.backup -move %cpath%\fiskaltrust.InstallLog %cpath%\.backup -move %cpath%\fiskaltrust.InstallState %cpath%\.backup -move %cpath%\install-service.cmd %cpath%\.backup -move %cpath%\test.cmd %cpath%\.backup -move %cpath%\uninstall-service.cmd %cpath%\.backup -fiskaltrust.Launcher.exe install --service-name %ftServiceName% \ No newline at end of file +move *.dll .backup\ >nul +move fiskaltrust.exe .backup\ >nul +move fiskaltrust.InstallLog .backup\ >nul +move fiskaltrust.InstallState .backup\ >nul +move install-service.cmd .backup\ >nul +move test.cmd .backup\ >nul +move uninstall-service.cmd .backup\ >nul +copy fiskaltrust.exe.config .backup\ >nul + +fiskaltrust.Launcher.exe install --service-name %ftServiceName% + +timeout 15 \ No newline at end of file From 3993f59371018e6dcaaa4dd763560fb6ed436536 Mon Sep 17 00:00:00 2001 From: forsthug <85173816+forsthug@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:26:53 +0100 Subject: [PATCH 04/48] add systemoff method --- scripts/migration.cmd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/migration.cmd b/scripts/migration.cmd index 7ee3840f..6e712553 100644 --- a/scripts/migration.cmd +++ b/scripts/migration.cmd @@ -48,6 +48,10 @@ sc stop %ftServiceName% >NUL goto StopingService +:SystemOffline +echo System is offline +exit /b 1 + :StopingServiceDelay timeout /t 2 /nobreak >NUL From 0f44f821cdb6f90c9496ca0111b7e82e135120c2 Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:08:40 +0100 Subject: [PATCH 05/48] Update migration.cmd --- scripts/migration.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/migration.cmd b/scripts/migration.cmd index 6e712553..11912a11 100644 --- a/scripts/migration.cmd +++ b/scripts/migration.cmd @@ -78,4 +78,4 @@ copy fiskaltrust.exe.config .backup\ >nul fiskaltrust.Launcher.exe install --service-name %ftServiceName% -timeout 15 \ No newline at end of file +pause From dcceaa4f7dc7db1429732456498999326b7ba5f2 Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:25:27 +0100 Subject: [PATCH 06/48] Rename migration.cmd to migrate.cmd --- scripts/{migration.cmd => migrate.cmd} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{migration.cmd => migrate.cmd} (100%) diff --git a/scripts/migration.cmd b/scripts/migrate.cmd similarity index 100% rename from scripts/migration.cmd rename to scripts/migrate.cmd From c7489d38687a7c214a4ce140f5e1291f42a216ba Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:26:33 +0100 Subject: [PATCH 07/48] Update fiskaltrust.Launcher.sln --- fiskaltrust.Launcher.sln | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fiskaltrust.Launcher.sln b/fiskaltrust.Launcher.sln index 9f411014..988e0f10 100644 --- a/fiskaltrust.Launcher.sln +++ b/fiskaltrust.Launcher.sln @@ -17,11 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fiskaltrust.Launcher.Integr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fiskaltrust.Launcher.UnitTest", "test\fiskaltrust.Launcher.UnitTest\fiskaltrust.Launcher.UnitTest.csproj", "{68743F85-31DB-43BA-92AE-72FF634282D0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{A8FEC919-37F1-426A-96F3-7A528AF684C9}" - ProjectSection(SolutionItems) = preProject - scripts\migration.cmd = scripts\migration.cmd - EndProjectSection -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 5814f73364a2b95b1b518d39c05971f74a0ab257 Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:26:59 +0100 Subject: [PATCH 08/48] Update fiskaltrust.Launcher.sln --- fiskaltrust.Launcher.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiskaltrust.Launcher.sln b/fiskaltrust.Launcher.sln index 988e0f10..095cd005 100644 --- a/fiskaltrust.Launcher.sln +++ b/fiskaltrust.Launcher.sln @@ -15,7 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{84733EDD-4 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fiskaltrust.Launcher.IntegrationTest", "test\fiskaltrust.Launcher.IntegrationTest\fiskaltrust.Launcher.IntegrationTest.csproj", "{F90BE105-2B84-4CEC-8E9C-C8671E011F21}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fiskaltrust.Launcher.UnitTest", "test\fiskaltrust.Launcher.UnitTest\fiskaltrust.Launcher.UnitTest.csproj", "{68743F85-31DB-43BA-92AE-72FF634282D0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fiskaltrust.Launcher.UnitTest", "test\fiskaltrust.Launcher.UnitTest\fiskaltrust.Launcher.UnitTest.csproj", "{68743F85-31DB-43BA-92AE-72FF634282D0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From c8001d3d109af6b49a88456bc5ddedfd0b842253 Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:27:15 +0100 Subject: [PATCH 09/48] Rename scripts/migrate.cmd to scripts/windows/migrate.cmd --- scripts/{ => windows}/migrate.cmd | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{ => windows}/migrate.cmd (100%) diff --git a/scripts/migrate.cmd b/scripts/windows/migrate.cmd similarity index 100% rename from scripts/migrate.cmd rename to scripts/windows/migrate.cmd From ef72d7b450888e3bbe906f4c73d8faa9dcd85556 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Tue, 12 Dec 2023 23:13:15 +0100 Subject: [PATCH 10/48] Added proccesses: creating directory, changing directory owner and permissions --- .../ServiceInstallation/LinuxSystemD.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 8aa61fe3..521b299d 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -7,6 +7,8 @@ public class LinuxSystemD : ServiceInstaller { private static readonly string _servicePath = "/etc/systemd/system/"; private readonly string _serviceName = "fiskaltrustLauncher"; + private readonly string _serviceUser = "user"; + private readonly string _requiredDirectory = "/var/lib/fiskaltrust"; public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath) : base(launcherExecutablePath) { @@ -19,17 +21,29 @@ public override async Task InstallService(string commandArgs, string? displ { return -1; } + + // Creating a directory if does not exist + if (!Directory.Exists(_requiredDirectory)) + { + Directory.CreateDirectory(_requiredDirectory); + + // Change of directory owner + await RunProcess("chown", new[] { _serviceUser, _requiredDirectory }); + + // Changing directory permissions + await RunProcess("chmod", new[] { "700", _requiredDirectory }); + } + 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 File.WriteAllTextAsync(serviceFilePath, string.Join("\n", serviceFileContent)).ConfigureAwait(false); await RunProcess("systemctl", new[] { "daemon-reload" }); Log.Information("Starting service."); await RunProcess("systemctl", new[] { "start", _serviceName }); Log.Information("Enable service."); return (await RunProcess("systemctl", new[] { "enable", _serviceName, "-q" })).exitCode; } - public override async Task UninstallService() { if (!await IsSystemd()) From 1e2dc070242f41296a756c79cd4209aaa0b70d57 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Wed, 13 Dec 2023 09:49:49 +0100 Subject: [PATCH 11/48] Use of Environmental Variables --- .../ServiceInstallation/LinuxSystemD.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 521b299d..8b2c99bc 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -7,12 +7,14 @@ public class LinuxSystemD : ServiceInstaller { private static readonly string _servicePath = "/etc/systemd/system/"; private readonly string _serviceName = "fiskaltrustLauncher"; - private readonly string _serviceUser = "user"; + private readonly string _serviceUser; private readonly string _requiredDirectory = "/var/lib/fiskaltrust"; - - public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath) : base(launcherExecutablePath) + + public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath) + : base(launcherExecutablePath) { - _serviceName = serviceName ?? _serviceName; + _serviceName = serviceName ?? "fiskaltrustLauncher"; + _serviceUser = Environment.GetEnvironmentVariable("USER") ?? "defaultUser"; } public override async Task InstallService(string commandArgs, string? displayName, bool delayedStart = false) From dfa7e8d9fd318b18d9b26f7125f411d264fb0d8f Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Wed, 20 Dec 2023 23:11:06 +0100 Subject: [PATCH 12/48] small fix after review --- .../ServiceInstallation/LinuxSystemD.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 8b2c99bc..7d9fa8fe 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -1,4 +1,5 @@ -using fiskaltrust.Launcher.Helpers; +using fiskaltrust.Launcher.Common.Configuration; +using fiskaltrust.Launcher.Helpers; using Serilog; namespace fiskaltrust.Launcher.ServiceInstallation @@ -8,13 +9,14 @@ public class LinuxSystemD : ServiceInstaller private static readonly string _servicePath = "/etc/systemd/system/"; private readonly string _serviceName = "fiskaltrustLauncher"; private readonly string _serviceUser; - private readonly string _requiredDirectory = "/var/lib/fiskaltrust"; + private readonly string _requiredDirectory; - public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath) + public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath, LauncherConfiguration configuration) : base(launcherExecutablePath) { _serviceName = serviceName ?? "fiskaltrustLauncher"; - _serviceUser = Environment.GetEnvironmentVariable("USER") ?? "defaultUser"; + _serviceUser = Environment.GetEnvironmentVariable("USER"); + _requiredDirectory = configuration.ServiceFolder; } public override async Task InstallService(string commandArgs, string? displayName, bool delayedStart = false) From 0f30cf60931724b9b31224f67e68a401079efd58 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Wed, 20 Dec 2023 23:48:30 +0100 Subject: [PATCH 13/48] Added missing LauncherConfiguration argument to LinuxSystemD and serviceUser LogWarning --- src/fiskaltrust.Launcher/Commands/InstallCommand.cs | 2 +- src/fiskaltrust.Launcher/Commands/UninstallCommand.cs | 2 +- .../ServiceInstallation/LinuxSystemD.cs | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/InstallCommand.cs b/src/fiskaltrust.Launcher/Commands/InstallCommand.cs index 5b10bf1c..28eed3de 100644 --- a/src/fiskaltrust.Launcher/Commands/InstallCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/InstallCommand.cs @@ -61,7 +61,7 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro ServiceInstaller? installer = null; if (OperatingSystem.IsLinux()) { - installer = new LinuxSystemD(installOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", installServices.LauncherExecutablePath); + installer = new LinuxSystemD(installOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", installServices.LauncherExecutablePath, commonProperties.LauncherConfiguration); } if (OperatingSystem.IsWindows()) { diff --git a/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs b/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs index 53486bfb..1ac8b4b1 100644 --- a/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs @@ -41,7 +41,7 @@ public static async Task HandleAsync(CommonOptions _, CommonProperties comm ServiceInstaller? installer = null; if (OperatingSystem.IsLinux()) { - installer = new LinuxSystemD(uninstallOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", uninstallServices.LauncherExecutablePath); + installer = new LinuxSystemD(uninstallOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", uninstallServices.LauncherExecutablePath, commonProperties.LauncherConfiguration); } if (OperatingSystem.IsWindows()) { diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 7d9fa8fe..947ee81b 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -16,7 +16,13 @@ public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutab { _serviceName = serviceName ?? "fiskaltrustLauncher"; _serviceUser = Environment.GetEnvironmentVariable("USER"); - _requiredDirectory = configuration.ServiceFolder; + + if (string.IsNullOrEmpty(_serviceUser)) + { + Log.Warning("Service user name is not set. Owner of the service directory will not be changed."); + } + + _requiredDirectory = configuration.ServiceFolder ?? throw new ArgumentNullException(nameof(configuration.ServiceFolder), "Service directory path must be provided in configuration."); } public override async Task InstallService(string commandArgs, string? displayName, bool delayedStart = false) From d3b69bcc1349c2f83789781568a25dbc586800c3 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Thu, 21 Dec 2023 14:31:59 +0100 Subject: [PATCH 14/48] Added const --- src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 947ee81b..84e80f96 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -7,14 +7,15 @@ namespace fiskaltrust.Launcher.ServiceInstallation public class LinuxSystemD : ServiceInstaller { private static readonly string _servicePath = "/etc/systemd/system/"; - private readonly string _serviceName = "fiskaltrustLauncher"; + private const string DefaultServiceName = "fiskaltrustLauncher"; + private readonly string _serviceName; private readonly string _serviceUser; private readonly string _requiredDirectory; public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath, LauncherConfiguration configuration) : base(launcherExecutablePath) { - _serviceName = serviceName ?? "fiskaltrustLauncher"; + _serviceName = serviceName ?? DefaultServiceName; _serviceUser = Environment.GetEnvironmentVariable("USER"); if (string.IsNullOrEmpty(_serviceUser)) From 691e758ac467bfd999a17d5a7c2da2e76ab9d75a Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Wed, 27 Dec 2023 19:15:34 +0100 Subject: [PATCH 15/48] Refactor directory setup logic and service installation - Moved directory creation and permission setting from LinuxSystemD to CommonHandler to ensure directory setup at every launcher start - Simplified LinuxSystemD by removing directory setup code - Changed RunProcess method in ServiceInstaller from protected to public --- src/fiskaltrust.Launcher/Commands/Common.cs | 20 ++++++++ .../ServiceInstallation/LinuxSystemD.cs | 47 ++++--------------- .../ServiceInstallation/ServiceInstaller.cs | 2 +- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index 1fa6bf90..924417c9 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -10,6 +10,7 @@ using fiskaltrust.Launcher.Extensions; using fiskaltrust.Launcher.Helpers; using fiskaltrust.Launcher.Logging; +using fiskaltrust.Launcher.ServiceInstallation; using fiskaltrust.storage.serialization.V0; using Microsoft.AspNetCore.DataProtection; using Serilog; @@ -131,6 +132,7 @@ public static async Task HandleAsync( Log.Verbose("Merging launcher cli args."); launcherConfiguration.OverwriteWith(options.ArgsLauncherConfiguration); + await EnsureServiceDirectoryExists(launcherConfiguration); if (!launcherConfiguration.UseOffline!.Value && (launcherConfiguration.CashboxId is null || launcherConfiguration.AccessToken is null)) { @@ -228,7 +230,25 @@ public static async Task HandleAsync( 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; + if (!Directory.Exists(serviceDirectory)) + { + Directory.CreateDirectory(serviceDirectory); + var user = Environment.GetEnvironmentVariable("USER"); + if (!string.IsNullOrEmpty(user)) + { + await ServiceInstaller.RunProcess("chown", new[] { user, serviceDirectory }); + await ServiceInstaller.RunProcess("chmod", new[] { "774", serviceDirectory }); + } + else + { + Log.Warning("Service user name is not set. Owner of the service directory will not be changed."); + } + } + } public static async Task LoadCurve(Guid cashboxId, string accessToken, string serviceFolder, bool useOffline = false, bool dryRun = false, bool useFallback = false) { Log.Verbose("Loading Curve."); diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 84e80f96..445945f0 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -9,21 +9,11 @@ public class LinuxSystemD : ServiceInstaller private static readonly string _servicePath = "/etc/systemd/system/"; private const string DefaultServiceName = "fiskaltrustLauncher"; private readonly string _serviceName; - private readonly string _serviceUser; - private readonly string _requiredDirectory; public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath, LauncherConfiguration configuration) : base(launcherExecutablePath) { _serviceName = serviceName ?? DefaultServiceName; - _serviceUser = Environment.GetEnvironmentVariable("USER"); - - if (string.IsNullOrEmpty(_serviceUser)) - { - Log.Warning("Service user name is not set. Owner of the service directory will not be changed."); - } - - _requiredDirectory = configuration.ServiceFolder ?? throw new ArgumentNullException(nameof(configuration.ServiceFolder), "Service directory path must be provided in configuration."); } public override async Task InstallService(string commandArgs, string? displayName, bool delayedStart = false) @@ -33,18 +23,6 @@ public override async Task InstallService(string commandArgs, string? displ return -1; } - // Creating a directory if does not exist - if (!Directory.Exists(_requiredDirectory)) - { - Directory.CreateDirectory(_requiredDirectory); - - // Change of directory owner - await RunProcess("chown", new[] { _serviceUser, _requiredDirectory }); - - // Changing directory permissions - await RunProcess("chmod", new[] { "700", _requiredDirectory }); - } - Log.Information("Installing service via systemd."); var serviceFileContent = GetServiceFileContent(displayName ?? "Service installation of fiskaltrust launcher.", commandArgs); var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); @@ -55,40 +33,35 @@ public override async Task InstallService(string commandArgs, string? displ Log.Information("Enable service."); return (await RunProcess("systemctl", new[] { "enable", _serviceName, "-q" })).exitCode; } + public override async Task UninstallService() { if (!await IsSystemd()) { return -1; } - Log.Information("Stop service on systemd."); - await RunProcess("systemctl", new[] { "stop ", _serviceName }); - Log.Information("Disable service."); - await RunProcess("systemctl", new[] { "disable ", _serviceName, "-q" }); - Log.Information("Remove service."); + Log.Information("Stopping service on systemd."); + await RunProcess("systemctl", new[] { "stop", _serviceName }); + Log.Information("Disabling service."); + await RunProcess("systemctl", new[] { "disable", _serviceName, "-q" }); + Log.Information("Removing service."); var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); await RunProcess("rm", new[] { serviceFilePath }); - Log.Information("Reload daemon."); + Log.Information("Reloading daemon."); await RunProcess("systemctl", new[] { "daemon-reload" }); - Log.Information("Reset failed."); + Log.Information("Resetting failed state."); return (await RunProcess("systemctl", new[] { "reset-failed" })).exitCode; } private static async Task IsSystemd() { var (exitCode, output) = await RunProcess("ps", new[] { "--no-headers", "-o", "comm", "1" }); - if (exitCode != 0 && output.Contains("systemd")) - { - Log.Error("Service installation works only for systemd setup."); - return false; - } - return true; + return exitCode == 0 && output.Contains("systemd"); } private string[] GetServiceFileContent(string serviceDescription, string commandArgs) { var processPath = _launcherExecutablePath.Path; - var command = $"{processPath} {commandArgs}"; return new[] { @@ -104,4 +77,4 @@ private string[] GetServiceFileContent(string serviceDescription, string command }; } } -} +} \ No newline at end of file diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs b/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs index a10fddde..2d52b107 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs @@ -18,7 +18,7 @@ protected ServiceInstaller(LauncherExecutablePath launcherExecutablePath) public abstract Task UninstallService(); - protected static async Task<(int exitCode, string output)> RunProcess(string fileName, IEnumerable arguments) + public static async Task<(int exitCode, string output)> RunProcess(string fileName, IEnumerable arguments) { var process = new Process(); process.StartInfo.UseShellExecute = false; From 695bc0f03d04752b457c3fee8216db6679ad79a0 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Thu, 4 Jan 2024 09:45:26 +0100 Subject: [PATCH 16/48] Implemented ProcessHelper for process execution --- src/fiskaltrust.Launcher/Commands/Common.cs | 15 +++++- .../Helpers/ProcessHelper.cs | 49 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index 924417c9..e279fd17 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -230,6 +230,7 @@ public static async Task HandleAsync( 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; @@ -240,8 +241,17 @@ private static async Task EnsureServiceDirectoryExists(LauncherConfiguration con var user = Environment.GetEnvironmentVariable("USER"); if (!string.IsNullOrEmpty(user)) { - await ServiceInstaller.RunProcess("chown", new[] { user, serviceDirectory }); - await ServiceInstaller.RunProcess("chmod", new[] { "774", serviceDirectory }); + var chownResult = await ProcessHelper.RunProcess("chown", new[] { user, serviceDirectory }, LogEventLevel.Debug); + if (chownResult.exitCode != 0) + { + Log.Warning("Failed to change owner of the service directory."); + } + + var chmodResult = await ProcessHelper.RunProcess("chmod", new[] { "774", serviceDirectory }, LogEventLevel.Debug); + if (chmodResult.exitCode != 0) + { + Log.Warning("Failed to change permissions of the service directory."); + } } else { @@ -249,6 +259,7 @@ private static async Task EnsureServiceDirectoryExists(LauncherConfiguration con } } } + public static async Task LoadCurve(Guid cashboxId, string accessToken, string serviceFolder, bool useOffline = false, bool dryRun = false, bool useFallback = false) { Log.Verbose("Loading Curve."); diff --git a/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs b/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs new file mode 100644 index 00000000..1e1cb9c3 --- /dev/null +++ b/src/fiskaltrust.Launcher/Helpers/ProcessHelper.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using Serilog; +using Serilog.Events; + +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) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = string.Join(" ", arguments), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + process.Start(); + await process.WaitForExitAsync(); + + var stdOut = await process.StandardOutput.ReadToEndAsync(); + if (!string.IsNullOrEmpty(stdOut)) + { + Log.Write(logLevel, stdOut); + } + + var stdErr = await process.StandardError.ReadToEndAsync(); + if (!string.IsNullOrEmpty(stdErr)) + { + Log.Write(LogEventLevel.Warning, stdErr); + } + + if (process.ExitCode != 0) + { + Log.Warning($"Process {fileName} exited with code {process.ExitCode}"); + } + + return (process.ExitCode, stdOut); + } +} \ No newline at end of file From b6420fb6b4052ce743f53a81e5a8bf024dd44c1c Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Thu, 4 Jan 2024 14:09:25 +0100 Subject: [PATCH 17/48] undo changes to the current main --- .../Commands/InstallCommand.cs | 2 +- .../Commands/UninstallCommand.cs | 2 +- .../ServiceInstallation/LinuxSystemD.cs | 40 ++++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/InstallCommand.cs b/src/fiskaltrust.Launcher/Commands/InstallCommand.cs index 28eed3de..5b10bf1c 100644 --- a/src/fiskaltrust.Launcher/Commands/InstallCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/InstallCommand.cs @@ -61,7 +61,7 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro ServiceInstaller? installer = null; if (OperatingSystem.IsLinux()) { - installer = new LinuxSystemD(installOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", installServices.LauncherExecutablePath, commonProperties.LauncherConfiguration); + installer = new LinuxSystemD(installOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", installServices.LauncherExecutablePath); } if (OperatingSystem.IsWindows()) { diff --git a/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs b/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs index 1ac8b4b1..53486bfb 100644 --- a/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/UninstallCommand.cs @@ -41,7 +41,7 @@ public static async Task HandleAsync(CommonOptions _, CommonProperties comm ServiceInstaller? installer = null; if (OperatingSystem.IsLinux()) { - installer = new LinuxSystemD(uninstallOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", uninstallServices.LauncherExecutablePath, commonProperties.LauncherConfiguration); + installer = new LinuxSystemD(uninstallOptions.ServiceName ?? $"fiskaltrust-{commonProperties.LauncherConfiguration.CashboxId}", uninstallServices.LauncherExecutablePath); } if (OperatingSystem.IsWindows()) { diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 445945f0..8aa61fe3 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -1,5 +1,4 @@ -using fiskaltrust.Launcher.Common.Configuration; -using fiskaltrust.Launcher.Helpers; +using fiskaltrust.Launcher.Helpers; using Serilog; namespace fiskaltrust.Launcher.ServiceInstallation @@ -7,13 +6,11 @@ namespace fiskaltrust.Launcher.ServiceInstallation public class LinuxSystemD : ServiceInstaller { private static readonly string _servicePath = "/etc/systemd/system/"; - private const string DefaultServiceName = "fiskaltrustLauncher"; - private readonly string _serviceName; - - public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath, LauncherConfiguration configuration) - : base(launcherExecutablePath) + private readonly string _serviceName = "fiskaltrustLauncher"; + + public LinuxSystemD(string? serviceName, LauncherExecutablePath launcherExecutablePath) : base(launcherExecutablePath) { - _serviceName = serviceName ?? DefaultServiceName; + _serviceName = serviceName ?? _serviceName; } public override async Task InstallService(string commandArgs, string? displayName, bool delayedStart = false) @@ -22,11 +19,10 @@ public override async Task InstallService(string commandArgs, string? displ { 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.WriteAllTextAsync(serviceFilePath, string.Join("\n", serviceFileContent)).ConfigureAwait(false); + await File.AppendAllLinesAsync(serviceFilePath, serviceFileContent).ConfigureAwait(false); await RunProcess("systemctl", new[] { "daemon-reload" }); Log.Information("Starting service."); await RunProcess("systemctl", new[] { "start", _serviceName }); @@ -40,28 +36,34 @@ public override async Task UninstallService() { return -1; } - Log.Information("Stopping service on systemd."); - await RunProcess("systemctl", new[] { "stop", _serviceName }); - Log.Information("Disabling service."); - await RunProcess("systemctl", new[] { "disable", _serviceName, "-q" }); - Log.Information("Removing service."); + Log.Information("Stop service on systemd."); + await RunProcess("systemctl", new[] { "stop ", _serviceName }); + Log.Information("Disable service."); + await RunProcess("systemctl", new[] { "disable ", _serviceName, "-q" }); + Log.Information("Remove service."); var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); await RunProcess("rm", new[] { serviceFilePath }); - Log.Information("Reloading daemon."); + Log.Information("Reload daemon."); await RunProcess("systemctl", new[] { "daemon-reload" }); - Log.Information("Resetting failed state."); + Log.Information("Reset failed."); return (await RunProcess("systemctl", new[] { "reset-failed" })).exitCode; } private static async Task IsSystemd() { var (exitCode, output) = await RunProcess("ps", new[] { "--no-headers", "-o", "comm", "1" }); - return exitCode == 0 && output.Contains("systemd"); + if (exitCode != 0 && output.Contains("systemd")) + { + Log.Error("Service installation works only for systemd setup."); + return false; + } + return true; } private string[] GetServiceFileContent(string serviceDescription, string commandArgs) { var processPath = _launcherExecutablePath.Path; + var command = $"{processPath} {commandArgs}"; return new[] { @@ -77,4 +79,4 @@ private string[] GetServiceFileContent(string serviceDescription, string command }; } } -} \ No newline at end of file +} From c0104841e202ab0844b9c51535635d48cc68ee6c Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Thu, 4 Jan 2024 14:49:03 +0100 Subject: [PATCH 18/48] Replaced RunProcess with ProcessHelper --- .../ServiceInstallation/LinuxSystemD.cs | 18 ++++----- .../ServiceInstallation/ServiceInstaller.cs | 39 +------------------ .../ServiceInstallation/WindowsService.cs | 8 ++-- 3 files changed, 15 insertions(+), 50 deletions(-) diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs index 8aa61fe3..97ccb82e 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/LinuxSystemD.cs @@ -23,11 +23,11 @@ public override async Task InstallService(string commandArgs, string? displ 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 RunProcess("systemctl", new[] { "daemon-reload" }); + await ProcessHelper.RunProcess("systemctl", new[] { "daemon-reload" }); Log.Information("Starting service."); - await RunProcess("systemctl", new[] { "start", _serviceName }); + await ProcessHelper.RunProcess("systemctl", new[] { "start", _serviceName }); Log.Information("Enable service."); - return (await RunProcess("systemctl", new[] { "enable", _serviceName, "-q" })).exitCode; + return (await ProcessHelper.RunProcess("systemctl", new[] { "enable", _serviceName, "-q" })).exitCode; } public override async Task UninstallService() @@ -37,21 +37,21 @@ public override async Task UninstallService() return -1; } Log.Information("Stop service on systemd."); - await RunProcess("systemctl", new[] { "stop ", _serviceName }); + await ProcessHelper.RunProcess("systemctl", new[] { "stop ", _serviceName }); Log.Information("Disable service."); - await RunProcess("systemctl", new[] { "disable ", _serviceName, "-q" }); + await ProcessHelper.RunProcess("systemctl", new[] { "disable ", _serviceName, "-q" }); Log.Information("Remove service."); var serviceFilePath = Path.Combine(_servicePath, $"{_serviceName}.service"); - await RunProcess("rm", new[] { serviceFilePath }); + await ProcessHelper.RunProcess("rm", new[] { serviceFilePath }); Log.Information("Reload daemon."); - await RunProcess("systemctl", new[] { "daemon-reload" }); + await ProcessHelper.RunProcess("systemctl", new[] { "daemon-reload" }); Log.Information("Reset failed."); - return (await RunProcess("systemctl", new[] { "reset-failed" })).exitCode; + return (await ProcessHelper.RunProcess("systemctl", new[] { "reset-failed" })).exitCode; } private static async Task IsSystemd() { - var (exitCode, output) = await RunProcess("ps", new[] { "--no-headers", "-o", "comm", "1" }); + var (exitCode, output) = await ProcessHelper.RunProcess("ps", new[] { "--no-headers", "-o", "comm", "1" }); if (exitCode != 0 && output.Contains("systemd")) { Log.Error("Service installation works only for systemd setup."); diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs b/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs index 2d52b107..7a78d6f8 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/ServiceInstaller.cs @@ -17,42 +17,7 @@ protected ServiceInstaller(LauncherExecutablePath launcherExecutablePath) public abstract Task InstallService(string commandArgs, string? displayName, bool delayedStart = false); public abstract Task UninstallService(); - - public static async Task<(int exitCode, string output)> RunProcess(string fileName, IEnumerable arguments) - { - var process = new Process(); - process.StartInfo.UseShellExecute = false; - process.StartInfo.FileName = fileName; - process.StartInfo.CreateNoWindow = false; - - process.StartInfo.Arguments = string.Join(" ", arguments); - process.StartInfo.RedirectStandardError = true; - process.StartInfo.RedirectStandardOutput = true; - - process.Start(); - - await process.WaitForExitAsync(); - - var withEnrichedContext = (Action log) => - { - var enrichedContext = LogContext.PushProperty("EnrichedContext", $" {Path.GetFileName(fileName)}"); - log(); - enrichedContext.Dispose(); - }; - - var stdOut = await process.StandardOutput.ReadToEndAsync(); - if (!string.IsNullOrEmpty(stdOut)) - { - withEnrichedContext(() => Log.Information(stdOut)); - } - - var stdErr = await process.StandardError.ReadToEndAsync(); - if (!string.IsNullOrEmpty(stdErr)) - { - withEnrichedContext(() => Log.Error(stdErr)); - } - - return (process.ExitCode, stdOut); - } + + // The RunProcess method has been removed, as we now use ProcessHelper.RunProcess instead. } } diff --git a/src/fiskaltrust.Launcher/ServiceInstallation/WindowsService.cs b/src/fiskaltrust.Launcher/ServiceInstallation/WindowsService.cs index 4eaccff6..e3f6a93a 100644 --- a/src/fiskaltrust.Launcher/ServiceInstallation/WindowsService.cs +++ b/src/fiskaltrust.Launcher/ServiceInstallation/WindowsService.cs @@ -45,14 +45,14 @@ public override async Task InstallService(string commandArgs, string? displ } Log.Information("Installing service."); - if ((await RunProcess(@"C:\WINDOWS\system32\sc.exe", arguments)).exitCode != 0) + if ((await ProcessHelper.RunProcess(@"C:\WINDOWS\system32\sc.exe", arguments)).exitCode != 0) { Log.Information($"Could not install service \"{_serviceName}\"."); return 1; } Log.Information("Starting service."); - if ((await RunProcess(@"C:\WINDOWS\system32\sc.exe", new[] { "start", $"\"{_serviceName}\"" })).exitCode != 0) + if ((await ProcessHelper.RunProcess(@"C:\WINDOWS\system32\sc.exe", new[] { "start", $"\"{_serviceName}\"" })).exitCode != 0) { Log.Warning($"Could not start service \"{_serviceName}\"."); } @@ -80,13 +80,13 @@ public override async Task UninstallService() } Log.Information("Stopping service."); - if ((await RunProcess(@"C:\WINDOWS\system32\sc.exe", new[] { "stop", $"\"{_serviceName}\"" })).exitCode != 0) + if ((await ProcessHelper.RunProcess(@"C:\WINDOWS\system32\sc.exe", new[] { "stop", $"\"{_serviceName}\"" })).exitCode != 0) { Log.Warning($"Could not stop service \"{_serviceName}\"."); } Log.Information("Uninstalling service."); - if ((await RunProcess(@"C:\WINDOWS\system32\sc.exe", new[] { "delete", $"\"{_serviceName}\"" })).exitCode != 0) + if ((await ProcessHelper.RunProcess(@"C:\WINDOWS\system32\sc.exe", new[] { "delete", $"\"{_serviceName}\"" })).exitCode != 0) { Log.Warning($"Could not uninstall service \"{_serviceName}\"."); return 1; From afd9d0f190240701d74920ed0ecf94ed6bd0268f Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Thu, 4 Jan 2024 15:03:25 +0100 Subject: [PATCH 19/48] update error handling --- scripts/windows/migrate.cmd | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/windows/migrate.cmd b/scripts/windows/migrate.cmd index 11912a11..5dd51a03 100644 --- a/scripts/windows/migrate.cmd +++ b/scripts/windows/migrate.cmd @@ -2,14 +2,23 @@ setlocal enableextensions cd /d "%~dp0%" net.exe session 1>nul 2>nul || (echo This script requires elevated rights. & exit /b 1) + +if not exist fiskaltrust.exe ( + echo The file fiskaltrust.exe does not exist in the current folder. + echo See http://link.fiskaltrust.cloud/launcher/migration-script for more information on how to use the script. + pause + exit /b 1 +) + set _cmd="%cd%\fiskaltrust.exe" for /f "skip=1 tokens=1-6 delims=, " %%A in ('wmic service get name^, PathName^') do ( if %_cmd% == %%B ( if not defined ftServiceName ( set ftServiceName=%%A ) else ( - echo More than one service is registered. This can not be migrated automatically. - timeout 15 + echo More than one service is registered for fiskaltrust.exe. This installation can not be migrated automatically. + echo See http://link.fiskaltrust.cloud/launcher/migration-script for more information on how to use the script. + pause exit /b 1 ) ) @@ -17,7 +26,7 @@ for /f "skip=1 tokens=1-6 delims=, " %%A in ('wmic service get name^, PathName^' echo if exist .backup\ ( echo The Backup folder: '.backup' already exists. Rename this folder to not loose data. - timeout 15 + pause exit /b 1 ) if defined ftServiceName ( @@ -25,8 +34,9 @@ if defined ftServiceName ( ) if not defined ftServiceName ( - echo No service installed - timeout 15 + echo No installed service was found for fiskaltrust.exe. This installation can not be migrated automatically. + echo See http://link.fiskaltrust.cloud/launcher/migration-script for more information on how to use the script. + pause exit /b 1 ) From dae6ba30a0e1465d835689505e2f08682b47090f Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Thu, 4 Jan 2024 15:34:00 +0100 Subject: [PATCH 20/48] update README --- README.md | 91 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 30d65bb4..223f70cc 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,25 @@ Below, we illustrate a minimal sample configuration with the international SQLit Download the latest release from GitHub. We always recommend using the latest release to benefit from the newest improvements. Unzip the downloaded release. -Start the Launcher via the commandline: +You can also download the Launcher from the fiskaltrust Portal (only sandbox at the moment), the Launcher will come with a preconfigured `launcher.configuration.json` file. -```sh +The download will contain the `fiskaltrust.Launcher` executable and `test`, `install`, `uninstall` `.cmd` or `.sh` scripts and a `migrate.cmd` script on Windows. + +The `test.cmd` or `test.sh` script can be used to test the Launcher. +It will start the Launcher with `--log-level` parameter set to debug. + +The `install.cmd` or `install.sh` script can be used to install the Launcher as a service. + +The `uninstall.cmd` or `uninstall.sh` script can be used to uninstall the Launcher as a service. + +The `migrate.cmd` script can be used to from migrate the Launcher 1.3.x to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). + +Alternatively you can start the Launcher via the command line: + +```ps1 +# Will use the configuration file `launcher.configuration.json` in the current directory +fiskaltrust.Launcher.exe run +# Will use the cashbox id and access token from the cli parameters fiskaltrust.Launcher.exe run --cashbox-id --access-token --sandbox ``` @@ -46,16 +62,16 @@ To stop the Launcher press Ctrl + C. ### Installation -On debian based linux systems the Launcher can also be installed via `apt-get` . The executable will be installed at `/usr/bin/fiskaltrust.Launcher` and can be run like that `fiskaltrust.Launcher --help` . +On Debian based Linux systems the Launcher can also be installed via `apt-get` . The executable will be installed at `/usr/bin/fiskaltrust.Launcher` and can be run like that `fiskaltrust.Launcher --help` . ```bash curl -L http://downloads.fiskaltrust.cloud/apt-repo/KEY.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/fiskaltrust-archive-keyring.gpg > /dev/null -echo "deb [signed-by=/usr/share/keyrings/fiskaltrust-archive-keyring.gpg] http://downloads.fiskaltrust.cloud/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/fiskaltrust.list +echo "deb [signed-by=/usr/share/keyrings/fiskaltrust-archive-keyring.gpg] https://downloads.fiskaltrust.cloud/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/fiskaltrust.list sudo apt update sudo apt install fiskaltrust-middleware-launcher ``` -> When installed this way the self-update funtionality of the launcher is disabled and it has to be updated via `apt-get` . +> When installed this way the self-update functionality of the launcher is disabled and it has to be updated via `apt-get` . > > ```bash > sudo apt update && sudo apt install --only-upgrade fiskaltrust-middleware-launcher @@ -63,21 +79,45 @@ sudo apt install fiskaltrust-middleware-launcher ## Migration guide -> Caution: To switch from a launcher version 1.3.x to a version 2.0 is possible using the version Launcher 2.0- Public Preview 3 onwards. +Before switching from a 1.3.x Launcher to a Launcher 2.0, please update the Queues, SCUs and Helpers to the latest packages. + +Then download the new launcher from the Portal or the [GitHub release page](https://github.com/fiskaltrust/middleware-launcher/releases). -Before switching from a 1.3.x Launcher to a Launcher 2.0, please make sure that the packages configured are compatible. You can check with the [table of the supported Packages in the Alpha](#supported-packages-in-the-alpha). +Run the `uninstall-service.cmd` or `uninstall-service.sh` command to deinstall the old launcher. -Run the uninstall-service.cmd or sh command to deinstall the old launcher. +If you did not download the Launcher from the Portal manually create the [configuration file](#launcher-configuration), and make sure to include the `cashboxId` and `accessToken` and to set `sandbox` to true if needed. + +In the new launcher folder execute the `install.cmd` or `install.sh` script or run the following command `.\fiskaltrust.Launcher.exe install`. + +To check that the switch was successful, e.g. try sending receipt to the middleware using our Postman collection. + +### Automatic Migration using the Migration Script + +On Windows we provide a `migration.cmd` script that can be used to migrate the Launcher 1.3.x to the Launcher 2.0. + +To run this script unzip the downloaded Launcher 2.0 files into the folder containing the old Launcher 1.3. + +> _The folder should now contain at least the following files:_ +> ``` +> . +> ├─ fiskaltrust.Launcher.exe +> ├─ launcher.configuration.json +> ├─ migration.cmd +> └─ fiskaltrust.exe +> ``` -Create the [configuration file](#launcher-configuration), and make sure to include the cashboxId and access token. +And then run the `migration.cmd` script. -In the new launcher folder run the following command `.\fiskaltrust.Launcher.exe install --sandbox` . +The script will do the following: -To check that the switch is successful, try send receipt to the middleware using our Postman collection. +* Find the service of the old Launcher (`fiskaltrust.exe`) +* Stop and uninstall the service +* Install the new Launcher 2.0 as a service using the same service name as the old Launcher +* Backup the old Launcher 1.3 files to the `.backup` folder ## Launcher configuration -The Launcher 2.0 configuration is now read from a json file ( `launcher.configuration.json` in the working directory per default). The configuration has to be created mannually. +The Launcher 2.0 configuration is now read from a JSON file ( `launcher.configuration.json` in the working directory per default). The configuration has to be created manually. This file can be set via the `--launcher-configuration-file` cli argument. @@ -129,7 +169,7 @@ The `run` command of the fiskaltrust.Launcher is used to execute the launcher, p | `--merge-legacy-config-if-exists` | If set, merges legacy configuration if it exists. | `true` | | `--launcher-port ` | Specifies the port which the launcher will use for internal communication. A dynamic binding is used by default. | `0` | | `--use-offline` | Enables offline mode. | `false` | -| `--service-folder ` | Path to the service folder. | windows: `"C:/ProgramData/fiskaltrust"`
linux: `"/var/lib/fiskaltrust"`
macos: `"/Library/Application Support/fiskaltrust"` | +| `--service-folder ` | Path to the service folder. | Windows: `"C:/ProgramData/fiskaltrust"`
Linux: `"/var/lib/fiskaltrust"`
MacOS: `"/Library/Application Support/fiskaltrust"` | | `--configuration-url ` | URL to fetch the configuration from. | `"https://configuration[-sandbox].fiskaltrust.cloud"` | | `--packages-url ` | URL to fetch packages from. | `"https://packages-2-0[-sandbox].fiskaltrust.cloud"` | | `--package-cache ` | Cache directory for the packages. | `"/cache"` | @@ -199,23 +239,23 @@ The `doctor` command should give the following output when run successfully: ## Service -The Launcher 2.0 can be installed as a service on Windows and linux (when systemd is available) using the `install` command: +The Launcher 2.0 can be installed as a service on Windows and Linux (when `systemd` is available) using the `install` command: ```sh fiskaltrust.Launcher.exe install --cashbox-id --access-token --launcher-configuration-file ``` -## Selfupdate +## Self update -The Launcher 2.0 can update itsself automatically. For this the `launcherVersion` must be set in the [launcher configuration file](#launcher-configuration). +The Launcher 2.0 can update itself automatically. For this the `launcherVersion` must be set in the [launcher configuration file](#launcher-configuration). This can be set to a specific version (e.g. `"launcherVersion": "2.0.0-preview3"` updates to version `2.0.0-preview3` ). -Or this can be set to a [semver range](https://devhints.io/semver#ranges) (e.g. `"launcherVersion": ">= 2.0.0-preview3 < 2.0.0"` automatically updates to all preview versions greater or equal to `2.0.0-preview3` but does not update to non preview versions). +Or this can be set to a [SemVer Range](https://devhints.io/semver#ranges) (e.g. `"launcherVersion": ">= 2.0.0-preview3 < 2.0.0"` automatically updates to all preview versions greater or equal to `2.0.0-preview3` but does not update to non preview versions). ## Getting Started for developers -Clone this github repository and bild the project with Visual Studio. +Clone this GitHub repository and build the project with Visual Studio. When using VS Code, please ensure that the following command line parameters are passed to `dotnet build` to enable seamless debugging: `-p:PublishSingleFile=true -p:PublishReadyToRun=true` . @@ -223,36 +263,37 @@ When using VS Code, please ensure that the following command line parameters are **Q:** Are additional components required to be installed to be able to run the Launcher 2.0? -**A:** The Launcher 2.0 does not require any additionnal components to be installed. +**A:** The Launcher 2.0 does not require any additional components to be installed. --- **Q:** Which market can test the launcher 2.0? -**A:** Right now only the German and Italian market can test the launcher 2.0. It is possible for everyone to register to the German/Italian sandbox and test the launcher 2.0. Also, we are working on making the launcher available for all market. +**A:** Right now only the German and Italian market can test the launcher 2.0. It is possible for everyone to register to the German/Italian sandbox and test the launcher 2.0. Also, we are working on making the launcher available for all markets. --- **Q:** Is it possible to update the launcher version (e.g. from 1.3 to 2.0)? -**A:** It is possible to switch the launcher version from 1.3 to 2.0 using the version Launcher 2.0.0-rc.3 and later versions. +**A:** It is possible to switch the launcher version from 1.3 to 2.0 using the version Launcher `2.0.0-rc.3` and later versions. --- -**Q:** Can I use portsharing to run multiple Queues or SCUs on the same port (e.g. `rest://localhost:1500/queue1` and `rest://localhost:1500/queue2` ) +**Q:** Can I use port sharing to run multiple Queues or SCUs on the same port (e.g. `rest://localhost:1500/queue1` and `rest://localhost:1500/queue2` ) **A:** Yes this is possible by setting the launcher config parameter `useHttpSysBinding` to true. HttpSysBinding has some limitations: * It is only supported on windows -* It is not supported for grpc communication +* It is not supported for GRPC communication * The launcher may need to be run as an administrator -* No Tls certificates can be set +* No TLS certificates can be set ## Known Issues -* The Launcher has access problems when writing to the keyring on linux if run as a service. The launcher configuration parameter `useLegacyDataProtection` needs to be set to `true` as a workaround. ([#100](https://github.com/fiskaltrust/middleware-launcher/issues/100) +* The Launcher has access problems when writing to the keyring on Linux if run as a service. + The launcher configuration parameter `useLegacyDataProtection` needs to be set to `true` as a workaround. ([#100](https://github.com/fiskaltrust/middleware-launcher/issues/100) ## Contributing From b51163bea72120b17b13e55b62a9840a318a2528 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Thu, 4 Jan 2024 15:37:58 +0100 Subject: [PATCH 21/48] update docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 223f70cc..8ed2f0d3 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ To check that the switch was successful, e.g. try sending receipt to the middlew On Windows we provide a `migration.cmd` script that can be used to migrate the Launcher 1.3.x to the Launcher 2.0. +This script will migrate an existing service installation of the Launcher 1.3.x to the Launcher 2.0. + To run this script unzip the downloaded Launcher 2.0 files into the folder containing the old Launcher 1.3. > _The folder should now contain at least the following files:_ From ff31de6e1343939048e63e624f42ab2e51563362 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Thu, 4 Jan 2024 15:40:08 +0100 Subject: [PATCH 22/48] update docs --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8ed2f0d3..544d7ed0 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The `install.cmd` or `install.sh` script can be used to install the Launcher as The `uninstall.cmd` or `uninstall.sh` script can be used to uninstall the Launcher as a service. -The `migrate.cmd` script can be used to from migrate the Launcher 1.3.x to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). +The `migrate.cmd` script can be used to from migrate the Launcher 1.3 to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). Alternatively you can start the Launcher via the command line: @@ -79,7 +79,7 @@ sudo apt install fiskaltrust-middleware-launcher ## Migration guide -Before switching from a 1.3.x Launcher to a Launcher 2.0, please update the Queues, SCUs and Helpers to the latest packages. +Before switching from a 1.3 Launcher to a Launcher 2.0, please update the Queues, SCUs and Helpers to the latest packages. Then download the new launcher from the Portal or the [GitHub release page](https://github.com/fiskaltrust/middleware-launcher/releases). @@ -93,9 +93,9 @@ To check that the switch was successful, e.g. try sending receipt to the middlew ### Automatic Migration using the Migration Script -On Windows we provide a `migration.cmd` script that can be used to migrate the Launcher 1.3.x to the Launcher 2.0. +On Windows we provide a `migrate.cmd` script that can be used to migrate the Launcher 1.3 to the Launcher 2.0. -This script will migrate an existing service installation of the Launcher 1.3.x to the Launcher 2.0. +This script will migrate an existing service installation of the Launcher 1.3 to the Launcher 2.0. To run this script unzip the downloaded Launcher 2.0 files into the folder containing the old Launcher 1.3. @@ -104,11 +104,11 @@ To run this script unzip the downloaded Launcher 2.0 files into the folder conta > . > ├─ fiskaltrust.Launcher.exe > ├─ launcher.configuration.json -> ├─ migration.cmd +> ├─ migrate.cmd > └─ fiskaltrust.exe > ``` -And then run the `migration.cmd` script. +And then run the `migrate.cmd` script. The script will do the following: From ccc264ae7f463ddfe4f2fe17128f27ba4e15b272 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Thu, 4 Jan 2024 15:46:31 +0100 Subject: [PATCH 23/48] update docs --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 544d7ed0..2364bfdd 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ Below, we illustrate a minimal sample configuration with the international SQLit ## Getting Started -> warning: This beta version of the Launcher 2.0 is for test purpose only and should be used with our German sandbox. - Download the latest release from GitHub. We always recommend using the latest release to benefit from the newest improvements. Unzip the downloaded release. @@ -28,14 +26,11 @@ You can also download the Launcher from the fiskaltrust Portal (only sandbox at The download will contain the `fiskaltrust.Launcher` executable and `test`, `install`, `uninstall` `.cmd` or `.sh` scripts and a `migrate.cmd` script on Windows. -The `test.cmd` or `test.sh` script can be used to test the Launcher. -It will start the Launcher with `--log-level` parameter set to debug. - -The `install.cmd` or `install.sh` script can be used to install the Launcher as a service. - -The `uninstall.cmd` or `uninstall.sh` script can be used to uninstall the Launcher as a service. - -The `migrate.cmd` script can be used to from migrate the Launcher 1.3 to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). +* The `test.cmd` or `test.sh` script can be used to test the Launcher. + It will start the Launcher with `--log-level` parameter set to debug. +* The `install.cmd` or `install.sh` script can be used to install the Launcher as a service. +* The `uninstall.cmd` or `uninstall.sh` script can be used to uninstall the Launcher as a service. +* The `migrate.cmd` script can be used to from migrate the Launcher 1.3 to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). Alternatively you can start the Launcher via the command line: From da6f782ce5af9621882bf18ef8a6b5adf37b68a5 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Mon, 8 Jan 2024 21:46:57 +0100 Subject: [PATCH 24/48] Added exception handling for directory creation and conditional OS checks in directory setup --- src/fiskaltrust.Launcher/Commands/Common.cs | 48 ++++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index e279fd17..d5825887 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -1,5 +1,6 @@ using System.CommandLine; using System.CommandLine.Invocation; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json; using fiskaltrust.Launcher.Common.Configuration; @@ -234,29 +235,44 @@ public static async Task HandleAsync( private static async Task EnsureServiceDirectoryExists(LauncherConfiguration config) { var serviceDirectory = config.ServiceFolder; - if (!Directory.Exists(serviceDirectory)) + try { - Directory.CreateDirectory(serviceDirectory); - - var user = Environment.GetEnvironmentVariable("USER"); - if (!string.IsNullOrEmpty(user)) + if (!Directory.Exists(serviceDirectory)) { - var chownResult = await ProcessHelper.RunProcess("chown", new[] { user, serviceDirectory }, LogEventLevel.Debug); - if (chownResult.exitCode != 0) + Directory.CreateDirectory(serviceDirectory); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Log.Warning("Failed to change owner of the service directory."); + var user = Environment.GetEnvironmentVariable("USER"); + if (!string.IsNullOrEmpty(user)) + { + var chownResult = await ProcessHelper.RunProcess("chown", new[] { user, serviceDirectory }, LogEventLevel.Debug); + if (chownResult.exitCode != 0) + { + Log.Warning("Failed to change owner of the service directory."); + } + + var chmodResult = await ProcessHelper.RunProcess("chmod", new[] { "774", serviceDirectory }, LogEventLevel.Debug); + if (chmodResult.exitCode != 0) + { + Log.Warning("Failed to change permissions of the service directory."); + } + } + else + { + Log.Warning("Service user name is not set. Owner of the service directory will not be changed."); + } } - - var chmodResult = await ProcessHelper.RunProcess("chmod", new[] { "774", serviceDirectory }, LogEventLevel.Debug); - if (chmodResult.exitCode != 0) + else { - Log.Warning("Failed to change permissions of the service directory."); + Log.Debug("Changing owner and permissions is skipped on non-Unix operating systems."); } } - else - { - Log.Warning("Service user name is not set. Owner of the service directory will not be changed."); - } + } + catch (UnauthorizedAccessException) + { + Log.Error("Access to the path '{ServiceDirectory}' is denied. Please run the application with sufficient permissions.", serviceDirectory); + throw; } } From ff9f17ff0277ec8c05d43903df12b6de829242d7 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Tue, 9 Jan 2024 10:06:17 +0100 Subject: [PATCH 25/48] small logging improvement --- src/fiskaltrust.Launcher/Commands/Common.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index d5825887..b6c9d3a7 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -183,6 +183,7 @@ public static async Task HandleAsync( } catch (Exception e) { + // will exit with non-zero exit code later. Log.Fatal(e, "Could not read Cashbox configuration file."); } @@ -194,6 +195,7 @@ public static async Task HandleAsync( } catch (Exception e) { + // will exit with non-zero exit code later. Log.Fatal(e, "Could not parse Cashbox configuration."); } @@ -208,6 +210,9 @@ public static async Task HandleAsync( Log.Write(logEvent); } + // If any critical errors occured, we exit with a non-zero exit code. + // In many cases we don't want to immediately exit the application, + // but we want to log the error and continue and see what else is going on before we exit. if (collectionSink.Events.Where(e => e.Level == LogEventLevel.Fatal).Any()) { return 1; @@ -269,10 +274,10 @@ private static async Task EnsureServiceDirectoryExists(LauncherConfiguration con } } } - catch (UnauthorizedAccessException) + catch (UnauthorizedAccessException e) { - Log.Error("Access to the path '{ServiceDirectory}' is denied. Please run the application with sufficient permissions.", serviceDirectory); - throw; + // will exit with non-zero exit code later. + Log.Fatal(e, "Access to the path '{ServiceDirectory}' is denied. Please run the application with sufficient permissions.", serviceDirectory); } } From 2dfd1fbb040404d603b5075ce6f73b7423667ce0 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Tue, 9 Jan 2024 10:15:34 +0100 Subject: [PATCH 26/48] improve docs --- src/fiskaltrust.Launcher/Commands/Common.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index b6c9d3a7..b4c7a2bc 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -200,6 +200,7 @@ public static async Task HandleAsync( } Log.Logger = new LoggerConfiguration() + // Previous log messages will be logged here using this logger. .AddLoggingConfiguration(launcherConfiguration) .AddFileLoggingConfiguration(launcherConfiguration, new[] { "fiskaltrust.Launcher", launcherConfiguration.CashboxId?.ToString() }) .Enrich.FromLogContext() From 97a3b616ee83af7dc87e4d7b2241fe4f835c315a Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Tue, 9 Jan 2024 10:18:44 +0100 Subject: [PATCH 27/48] fix comment --- src/fiskaltrust.Launcher/Commands/Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index b4c7a2bc..e505b84d 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -199,8 +199,8 @@ public static async Task HandleAsync( Log.Fatal(e, "Could not parse Cashbox configuration."); } - Log.Logger = new LoggerConfiguration() // Previous log messages will be logged here using this logger. + Log.Logger = new LoggerConfiguration() .AddLoggingConfiguration(launcherConfiguration) .AddFileLoggingConfiguration(launcherConfiguration, new[] { "fiskaltrust.Launcher", launcherConfiguration.CashboxId?.ToString() }) .Enrich.FromLogContext() From cc8720d9c1e10183583fd5647d3f82750045cbdd Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Tue, 9 Jan 2024 10:19:01 +0100 Subject: [PATCH 28/48] add comments --- src/fiskaltrust.Launcher/Commands/Common.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index e505b84d..9dddcc0d 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -84,6 +84,7 @@ public static async Task HandleAsync( IHost host, Func> handler) where S : notnull { + // Log messages will be save here and logged later when we have the configuration options to create the logger. var collectionSink = new CollectionSink(); Log.Logger = new LoggerConfiguration() .WriteTo.Sink(collectionSink) From d17f302b97e697646dfadb8994a2a620d608f3bc Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:58:55 +0100 Subject: [PATCH 29/48] Update Common.cs --- src/fiskaltrust.Launcher/Commands/Common.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index 9dddcc0d..cbcc63d1 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -157,7 +157,7 @@ public static async Task HandleAsync( ECDiffieHellman? clientEcdh = null; try { - clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value); + clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value, launcherConfiguration.UseLegacyDataProtection!.Value); using var downloader = new ConfigurationDownloader(launcherConfiguration); var exists = await downloader.DownloadConfigurationAsync(clientEcdh); if (launcherConfiguration.UseOffline!.Value && !exists) @@ -321,4 +321,4 @@ public static async Task LoadCurve(Guid cashboxId, string acces } } } -} \ No newline at end of file +} From ac75aa4a434430cd9e73eddd37f0fd53fac3283d Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:00:07 +0100 Subject: [PATCH 30/48] Update Common.cs --- src/fiskaltrust.Launcher/Commands/Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fiskaltrust.Launcher/Commands/Common.cs b/src/fiskaltrust.Launcher/Commands/Common.cs index cbcc63d1..9f80679d 100644 --- a/src/fiskaltrust.Launcher/Commands/Common.cs +++ b/src/fiskaltrust.Launcher/Commands/Common.cs @@ -157,7 +157,7 @@ public static async Task HandleAsync( ECDiffieHellman? clientEcdh = null; try { - clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value, launcherConfiguration.UseLegacyDataProtection!.Value); + 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) From 8454e05a0265ad372053356ad23ac900fedd3341 Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:05:03 +0100 Subject: [PATCH 31/48] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2364bfdd..e0399228 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ To run this script unzip the downloaded Launcher 2.0 files into the folder conta > └─ fiskaltrust.exe > ``` -And then run the `migrate.cmd` script. +And then run the `migrate.cmd` script as an administrator. The script will do the following: From b10f0e6b94be7c932ff0266acb85a39da7e59efe Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Mon, 22 Jan 2024 08:31:34 +0100 Subject: [PATCH 32/48] dont crash on net.pipe urls --- .../ProcessHost/ProcessHostPlebeian.cs | 15 ++++++++++----- .../Services/HostingService.cs | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/fiskaltrust.Launcher/ProcessHost/ProcessHostPlebeian.cs b/src/fiskaltrust.Launcher/ProcessHost/ProcessHostPlebeian.cs index d1b16257..23191ac2 100644 --- a/src/fiskaltrust.Launcher/ProcessHost/ProcessHostPlebeian.cs +++ b/src/fiskaltrust.Launcher/ProcessHost/ProcessHostPlebeian.cs @@ -131,6 +131,10 @@ private async Task StartHosting(string[] uris) { var url = new Uri(uri); var hostingType = GetHostingType(url); + if (hostingType is null) + { + continue; + } Action? addEndpointsInner = hostingType switch { @@ -145,18 +149,18 @@ private async Task StartHosting(string[] uris) case PackageType.SCU: if (instanceInterface == typeof(IDESSCD)) { - await _hosting.HostService(url, hostingType, (IDESSCD)instance, addEndpoints); + await _hosting.HostService(url, hostingType.Value, (IDESSCD)instance, addEndpoints); } else if (instanceInterface == typeof(IITSSCD)) { - await _hosting.HostService(url, hostingType, (IITSSCD)instance, addEndpoints); + await _hosting.HostService(url, hostingType.Value, (IITSSCD)instance, addEndpoints); } break; case PackageType.Queue: - await _hosting.HostService(url, hostingType, (IPOS)instance, addEndpoints); + await _hosting.HostService(url, hostingType.Value, (IPOS)instance, addEndpoints); break; case PackageType.Helper: - await _hosting.HostService(url, hostingType, (IHelper)instance, addEndpoints); + await _hosting.HostService(url, hostingType.Value, (IHelper)instance, addEndpoints); break; default: throw new NotImplementedException(); @@ -201,13 +205,14 @@ private static (object, Action, Type) GetScu(IServiceProvider se throw new Exception("Could not resolve SCU with supported country. (Curently supported are DE and IT)"); } - private static HostingType GetHostingType(Uri url) + private static HostingType? GetHostingType(Uri url) { return url.Scheme.ToLowerInvariant() switch { "grpc" => HostingType.GRPC, "rest" => HostingType.REST, "http" or "https" or "net.tcp" => HostingType.SOAP, + "net.pipe" => null, _ => throw new NotImplementedException($"The hosting type for the URL {url} is currently not supported.") }; } diff --git a/src/fiskaltrust.Launcher/Services/HostingService.cs b/src/fiskaltrust.Launcher/Services/HostingService.cs index 7ebdb262..3ffab458 100644 --- a/src/fiskaltrust.Launcher/Services/HostingService.cs +++ b/src/fiskaltrust.Launcher/Services/HostingService.cs @@ -186,6 +186,9 @@ private WebApplication CreateSoapHost(WebApplicationBuilder builder, Uri uri, case "net.tcp": builder.AddServiceEndpoint(instance.GetType(), typeof(T), CreateNetTcpBinding(), uri, null); break; + case "net.pipe": + _logger.LogWarning("net.pipe url support will be added in an upcomming version of the launcher 2.0."); + break; default: throw new Exception(); }; From 85721d96d70d16de62796a288ff7d238db08add7 Mon Sep 17 00:00:00 2001 From: Pawel Karczewski <36538317+pawelvds@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:01:40 +0100 Subject: [PATCH 33/48] Domain Sockets and Named Pipes (#151) --- .../Configuration/Configuration.cs | 15 ++++- .../Commands/DoctorCommand.cs | 4 +- .../Commands/HostCommand.cs | 32 +++++----- .../Commands/RunCommand.cs | 22 ++++++- .../Factories/IpcConnectionFactory.cs | 64 +++++++++++++++++++ .../ProcessHost/ProcessHostMonarcStartup.cs | 33 ---------- src/fiskaltrust.Launcher/Program.cs | 2 - .../Services/HostingService.cs | 14 +++- .../Configuration/Configuration.cs | 26 ++++---- 9 files changed, 140 insertions(+), 72 deletions(-) create mode 100644 src/fiskaltrust.Launcher/Factories/IpcConnectionFactory.cs diff --git a/src/fiskaltrust.Launcher.Common/Configuration/Configuration.cs b/src/fiskaltrust.Launcher.Common/Configuration/Configuration.cs index 6f6f7232..9b2b746c 100644 --- a/src/fiskaltrust.Launcher.Common/Configuration/Configuration.cs +++ b/src/fiskaltrust.Launcher.Common/Configuration/Configuration.cs @@ -77,9 +77,18 @@ private T WithDefault(T value, Func defaultValue) [JsonPropertyName("accessToken")] public string? AccessToken { get => _accessToken; set => _accessToken = value; } - private int? _launcherPort; - [JsonPropertyName("launcherPort")] - public int? LauncherPort { get => WithDefault(_launcherPort, 0); set => _launcherPort = value; } + private string? _launcherServiceUri; + [JsonPropertyName("launcherServiceUri")] + public string? LauncherServiceUri + { + get => WithDefault( + _launcherServiceUri, + OperatingSystem.IsWindows() + ? $"fiskaltrust-{_cashboxId}" + : $"/tmp/fiskaltrust-{_cashboxId}.sock" + ); + set => _launcherServiceUri = value; + } private string? _serviceFolder; [JsonPropertyName("serviceFolder")] diff --git a/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs b/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs index 9007e414..4558a791 100644 --- a/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs @@ -156,7 +156,7 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro checkUp.Check("Setup monarch ProcessHostService", () => { - monarchBuilder.WebHost.ConfigureBinding(new Uri($"http://[::1]:{launcherConfiguration.LauncherPort}"), protocols: HttpProtocols.Http2); + monarchBuilder.WebHost.ConfigureBinding(new Uri($"http://[::1]:{launcherConfiguration.LauncherServiceUri}"), protocols: HttpProtocols.Http2); monarchBuilder.Services.AddCodeFirstGrpc(); }, throws: true); @@ -185,7 +185,7 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro Version = "1.0.0" }; - IProcessHostService? processHostService = checkUp.Check("Start plebeian processhostservice client", () => GrpcChannel.ForAddress($"http://localhost:{launcherConfiguration.LauncherPort}").CreateGrpcService()); + IProcessHostService? processHostService = checkUp.Check("Start plebeian processhostservice client", () => GrpcChannel.ForAddress($"http://localhost:{launcherConfiguration.LauncherServiceUri}").CreateGrpcService()); var plebeianBuilder = Host.CreateDefaultBuilder() .UseSerilog(new LoggerConfiguration().CreateLogger()) diff --git a/src/fiskaltrust.Launcher/Commands/HostCommand.cs b/src/fiskaltrust.Launcher/Commands/HostCommand.cs index d50a079a..42ff82a5 100644 --- a/src/fiskaltrust.Launcher/Commands/HostCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/HostCommand.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using System.CommandLine.Invocation; using fiskaltrust.Launcher.ProcessHost; using fiskaltrust.Launcher.Services; using fiskaltrust.storage.serialization.V0; @@ -15,12 +14,15 @@ using fiskaltrust.Launcher.Download; using fiskaltrust.Launcher.Constants; using System.Diagnostics; +using System.Net.Sockets; using fiskaltrust.Launcher.Common.Extensions; using fiskaltrust.Launcher.Common.Configuration; using fiskaltrust.Launcher.Configuration; using fiskaltrust.Launcher.Services.Interfaces; using fiskaltrust.ifPOS.v1.it; using fiskaltrust.Launcher.Helpers; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using fiskaltrust.Launcher.Factories; namespace fiskaltrust.Launcher.Commands { @@ -75,20 +77,18 @@ public static async Task HandleAsync(HostOptions hostOptions, HostServices } } - var launcherConfiguration = Common.Configuration.LauncherConfiguration.Deserialize(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(hostOptions.LauncherConfiguration))); - - var plebeianConfiguration = Configuration.PlebeianConfiguration.Deserialize(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(hostOptions.PlebeianConfiguration))); + var launcherConfiguration = LauncherConfiguration.Deserialize(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(hostOptions.LauncherConfiguration))); + var plebeianConfiguration = PlebeianConfiguration.Deserialize(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(hostOptions.PlebeianConfiguration))); var cashboxConfiguration = CashBoxConfigurationExt.Deserialize(await File.ReadAllTextAsync(launcherConfiguration.CashboxConfigurationFile!)); - - cashboxConfiguration.Decrypt(launcherConfiguration, await CommonHandler.LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseLegacyDataProtection!.Value)); + cashboxConfiguration.Decrypt(launcherConfiguration, await CommonHandler.LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!)); var packageConfiguration = (plebeianConfiguration.PackageType switch { PackageType.Queue => cashboxConfiguration.ftQueues, PackageType.SCU => cashboxConfiguration.ftSignaturCreationDevices, PackageType.Helper => cashboxConfiguration.helpers, - var unknown => throw new Exception($"Unknown PackageType {unknown}") + _ => throw new Exception($"Unknown PackageType {plebeianConfiguration.PackageType}") }).First(p => p.Id == plebeianConfiguration.PackageId); packageConfiguration.Configuration = ProcessPackageConfiguration(packageConfiguration.Configuration, launcherConfiguration, cashboxConfiguration); @@ -96,7 +96,8 @@ public static async Task HandleAsync(HostOptions hostOptions, HostServices IProcessHostService? processHostService = null; if (!hostOptions.NoProcessHostService) { - processHostService = GrpcChannel.ForAddress($"http://localhost:{launcherConfiguration.LauncherPort}").CreateGrpcService(); + var handler = new SocketsHttpHandler { ConnectCallback = new IpcConnectionFactory(launcherConfiguration).ConnectAsync }; + processHostService = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions { HttpHandler = handler }).CreateGrpcService(); } Log.Logger = new LoggerConfiguration() @@ -110,14 +111,14 @@ public static async Task HandleAsync(HostOptions hostOptions, HostServices .UseSerilog() .ConfigureServices(services => { + services.AddSingleton(_ => launcherConfiguration); + services.AddSingleton(_ => packageConfiguration); + services.AddSingleton(_ => plebeianConfiguration); + services.Configure(opts => { opts.ShutdownTimeout = TimeSpan.FromSeconds(30); - opts.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.StopHost; }); - services.AddSingleton(_ => launcherConfiguration); - services.AddSingleton(_ => packageConfiguration); - services.AddSingleton(_ => plebeianConfiguration); var pluginLoader = new PluginLoader(); services.AddSingleton(_ => pluginLoader); @@ -141,7 +142,7 @@ public static async Task HandleAsync(HostOptions hostOptions, HostServices var bootstrapper = pluginLoader .LoadComponent( downloader.GetPackagePath(packageConfiguration), - new[] { + [ typeof(IMiddlewareBootstrapper), typeof(IPOS), typeof(IDESSCD), @@ -153,11 +154,10 @@ public static async Task HandleAsync(HostOptions hostOptions, HostServices typeof(JournalResponse), typeof(IHelper), typeof(IServiceCollection), - typeof(Microsoft.Extensions.Logging.ILogger), + typeof(ILogger), typeof(ILoggerFactory), typeof(ILogger<>) - }); - + ]); bootstrapper.Id = packageConfiguration.Id; bootstrapper.Configuration = packageConfiguration.Configuration.ToDictionary(c => c.Key, c => (object?)c.Value.ToString()); diff --git a/src/fiskaltrust.Launcher/Commands/RunCommand.cs b/src/fiskaltrust.Launcher/Commands/RunCommand.cs index ddbff447..014a1400 100644 --- a/src/fiskaltrust.Launcher/Commands/RunCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/RunCommand.cs @@ -83,7 +83,27 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro services.AddSingleton(_ => runServices.LauncherExecutablePath); }); - builder.WebHost.ConfigureBinding(new Uri($"http://[::1]:{commonProperties.LauncherConfiguration.LauncherPort}"), protocols: HttpProtocols.Http2); + //Configure Kestrel for ProcessHostService + if (OperatingSystem.IsWindows()) + { + builder.WebHost.UseKestrel(serverOptions => + { + serverOptions.ListenNamedPipe(commonProperties.LauncherConfiguration.LauncherServiceUri!, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + } + else + { + builder.WebHost.UseKestrel(serverOptions => + { + serverOptions.ListenUnixSocket(commonProperties.LauncherConfiguration.LauncherServiceUri!, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + } builder.Services.AddCodeFirstGrpc(); diff --git a/src/fiskaltrust.Launcher/Factories/IpcConnectionFactory.cs b/src/fiskaltrust.Launcher/Factories/IpcConnectionFactory.cs new file mode 100644 index 00000000..47dc178c --- /dev/null +++ b/src/fiskaltrust.Launcher/Factories/IpcConnectionFactory.cs @@ -0,0 +1,64 @@ +using System.IO.Pipes; +using System.Net; +using System.Net.Sockets; +using System.Security.Principal; +using fiskaltrust.Launcher.Common.Configuration; + +namespace fiskaltrust.Launcher.Factories +{ + public interface IConnectionFactory + { + public ValueTask ConnectAsync(SocketsHttpConnectionContext _, CancellationToken cancellationToken); + } + + public class IpcConnectionFactory : IConnectionFactory + { + private IConnectionFactory _connectionFactory; + public IpcConnectionFactory(LauncherConfiguration configuration) + { + if (OperatingSystem.IsWindows()) + { + _connectionFactory = new NamedPipesConnectionFactory(configuration.LauncherServiceUri!); + } + else + { + _connectionFactory = new UnixDomainSocketsConnectionFactory(configuration.LauncherServiceUri!); + } + } + public ValueTask ConnectAsync(SocketsHttpConnectionContext _, CancellationToken cancellationToken) => _connectionFactory.ConnectAsync(_, cancellationToken); + } + + public class NamedPipesConnectionFactory : IConnectionFactory + { + private readonly string _uri; + + public NamedPipesConnectionFactory(string uri) + { + _uri = uri; + } + + public async ValueTask ConnectAsync(SocketsHttpConnectionContext _, CancellationToken cancellationToken) + { + var clientStream = new NamedPipeClientStream(".", _uri, PipeDirection.InOut, PipeOptions.WriteThrough | PipeOptions.Asynchronous, TokenImpersonationLevel.Anonymous); + await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false); + return clientStream; + } + } + + public class UnixDomainSocketsConnectionFactory : IConnectionFactory + { + private readonly EndPoint _endPoint; + + public UnixDomainSocketsConnectionFactory(string uri) + { + _endPoint = new UnixDomainSocketEndPoint(uri); + } + + public async ValueTask ConnectAsync(SocketsHttpConnectionContext _, CancellationToken cancellationToken) + { + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + await socket.ConnectAsync(_endPoint, cancellationToken).ConfigureAwait(false); + return new NetworkStream(socket, ownsSocket: true); + } + } +} \ No newline at end of file diff --git a/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarcStartup.cs b/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarcStartup.cs index 8efcd720..26175974 100644 --- a/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarcStartup.cs +++ b/src/fiskaltrust.Launcher/ProcessHost/ProcessHostMonarcStartup.cs @@ -6,7 +6,6 @@ using fiskaltrust.Launcher.Helpers; using fiskaltrust.storage.serialization.V0; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Hosting.WindowsServices; namespace fiskaltrust.Launcher.ProcessHost @@ -25,7 +24,6 @@ public class AlreadyLoggedException : Exception { } private readonly ILoggerFactory _loggerFactory; private readonly ILifetime _lifetime; private readonly LauncherExecutablePath _launcherExecutablePath; - private readonly TaskCompletionSource _kestrelReady; public ProcessHostMonarcStartup(ILoggerFactory loggerFactory, ILogger logger, Dictionary hosts, LauncherConfiguration launcherConfiguration, ftCashBoxConfiguration cashBoxConfiguration, PackageDownloader downloader, ILifetime lifetime, LauncherExecutablePath launcherExecutablePath, IHostApplicationLifetime hostApplicationLifetime, IServer server) { @@ -37,45 +35,14 @@ public ProcessHostMonarcStartup(ILoggerFactory loggerFactory, ILogger(); - - hostApplicationLifetime.ApplicationStarted.Register(() => - { - try - { - _kestrelReady.TrySetResult(new Uri(server.Features.Get()!.Addresses!.First())); - } - catch (Exception e) - { - _kestrelReady.TrySetException(e); - } - }); } - protected override async Task ExecuteAsync(CancellationToken cancellationToken) { _lifetime.ApplicationLifetime.ApplicationStopping.Register(() => _logger.LogInformation("Shutting down launcher.")); - cancellationToken.Register(() => _kestrelReady.TrySetCanceled()); StartupLogging(); - if (_launcherConfiguration.LauncherPort == 0) - { - try - { - var url = await _kestrelReady.Task.ConfigureAwait(false); - _launcherConfiguration.LauncherPort = url.Port; - _logger.LogInformation("ProcessHostService running on {url}", url); - } - catch (Exception e) - { - if (cancellationToken.IsCancellationRequested) { return; } - _logger.LogError(e, "Could not get Kestrel port."); - throw new AlreadyLoggedException(); - } - } - _downloader.CopyPackagesToCache(); try diff --git a/src/fiskaltrust.Launcher/Program.cs b/src/fiskaltrust.Launcher/Program.cs index 7efbafff..bd5ab77b 100644 --- a/src/fiskaltrust.Launcher/Program.cs +++ b/src/fiskaltrust.Launcher/Program.cs @@ -6,8 +6,6 @@ using fiskaltrust.Launcher.Extensions; using fiskaltrust.Launcher.Helpers; using System.CommandLine.NamingConventionBinder; -using fiskaltrust.Launcher.Common.Configuration; -using fiskaltrust.Launcher.Common.Constants; var runCommand = new RunCommand() { diff --git a/src/fiskaltrust.Launcher/Services/HostingService.cs b/src/fiskaltrust.Launcher/Services/HostingService.cs index 3ffab458..7380cc4d 100644 --- a/src/fiskaltrust.Launcher/Services/HostingService.cs +++ b/src/fiskaltrust.Launcher/Services/HostingService.cs @@ -54,11 +54,13 @@ public async Task HostService(Uri uri, HostingType hostingTyp { var builder = WebApplication.CreateBuilder(); + // Configure Serilog for logging builder.Host.UseSerilog((_, __, loggerConfiguration) => loggerConfiguration .AddLoggingConfiguration(_launcherConfiguration, aspLogging: true) .WriteTo.GrpcSink(_packageConfiguration, _processHostService)); + // Add HTTP logging if the log level is set to Debug or lower if (_launcherConfiguration.LogLevel <= LogLevel.Debug) { builder.Services.AddHttpLogging(options => @@ -71,9 +73,10 @@ public async Task HostService(Uri uri, HostingType hostingTyp HttpLoggingFields.ResponseStatusCode | HttpLoggingFields.ResponseBody); } - WebApplication app; + WebApplication app; + // Check if UseHttpSysBinding is enabled and log warnings if necessary if (_launcherConfiguration.UseHttpSysBinding!.Value) { const string message = $"The configuration parameter {{parametername}} will be ignored because {nameof(_launcherConfiguration.UseHttpSysBinding)} is enabled."; @@ -98,6 +101,7 @@ public async Task HostService(Uri uri, HostingType hostingTyp } } + // Create the appropriate host based on the hosting type switch (hostingType) { case HostingType.REST: @@ -117,6 +121,7 @@ public async Task HostService(Uri uri, HostingType hostingTyp throw new NotImplementedException(); } + // Use HTTP logging if the log level is set to Debug or lower if (_launcherConfiguration.LogLevel <= LogLevel.Debug) { app.UseHttpLogging(); @@ -130,6 +135,7 @@ public async Task HostService(Uri uri, HostingType hostingTyp private WebApplication CreateRestHost(WebApplicationBuilder builder, Uri uri, T instance, Action addEndpoints) { + // Configure JSON options builder.Services.Configure(options => { options.SerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString; @@ -140,6 +146,7 @@ private WebApplication CreateRestHost(WebApplicationBuilder builder, Uri uri, options.SerializerOptions.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; }); + // Configure Kestrel server binding builder.WebHost.ConfigureBinding(uri, listenOptions => ConfigureTls(listenOptions), isHttps: !string.IsNullOrEmpty(_launcherConfiguration.TlsCertificatePath) || !string.IsNullOrEmpty(_launcherConfiguration.TlsCertificateBase64), allowSynchronousIO: true, useHttpSys: _launcherConfiguration.UseHttpSysBinding!.Value); var app = builder.Build(); @@ -247,7 +254,10 @@ private WebApplication CreateGrpcHost(WebApplicationBuilder builder, Uri uri, builder.Services.AddSingleton(instance); var app = builder.Build(); - if (!OperatingSystem.IsWindows() || _launcherConfiguration.UseHttpSysBinding!.Value == false) { app.UsePathBase(uri.AbsolutePath); } + if (!OperatingSystem.IsWindows() || _launcherConfiguration.UseHttpSysBinding!.Value == false) + { + app.UsePathBase(uri.AbsolutePath); + } app.UseRouting(); #pragma warning disable ASP0014 diff --git a/test/fiskaltrust.Launcher.UnitTest/Configuration/Configuration.cs b/test/fiskaltrust.Launcher.UnitTest/Configuration/Configuration.cs index c6875ab6..a9883f3a 100644 --- a/test/fiskaltrust.Launcher.UnitTest/Configuration/Configuration.cs +++ b/test/fiskaltrust.Launcher.UnitTest/Configuration/Configuration.cs @@ -47,7 +47,7 @@ public void RandomConfiguration_SerializaAndDeserialize_ShouldPreserveNull() deserialized.Should().BeEquivalentTo(deserialized); } } - + [Fact] public void DifferentCaseInKeys_Deserialize_ShouldPreserveProperties() { @@ -56,46 +56,46 @@ public void DifferentCaseInKeys_Deserialize_ShouldPreserveProperties() ""LOGLEVEL"": ""Error"", ""LogLevel"": ""Warning"" }"; - + var deserialized = LauncherConfiguration.Deserialize(json); - + deserialized.LogLevel.Should().Be(LogLevel.Warning); } - + [Fact] public void LowerCaseKeys_Deserialize_ShouldPreserveProperties() { var json = @"{ ""loglevel"": ""Information"" }"; - + var deserialized = LauncherConfiguration.Deserialize(json); - + deserialized.LogLevel.Should().Be(LogLevel.Information); } - + [Fact] public void UpperCaseKeys_Deserialize_ShouldPreserveProperties() { var json = @"{ ""LOGLEVEL"": ""Error"" }"; - + var deserialized = LauncherConfiguration.Deserialize(json); - + deserialized.LogLevel.Should().Be(LogLevel.Error); } - + [Fact] public void MixedCaseKeys_Deserialize_ShouldPreserveProperties() { var json = @"{ ""logLevel"": ""Warning"" }"; - + var deserialized = LauncherConfiguration.Deserialize(json); - + deserialized.LogLevel.Should().Be(LogLevel.Warning); - } + } } } \ No newline at end of file From 2ef84414674bdc6664503595fb2f0cdf2e012a48 Mon Sep 17 00:00:00 2001 From: Paul Volavsek Date: Tue, 23 Jan 2024 15:02:07 +0100 Subject: [PATCH 34/48] fix doctor command --- .../Commands/DoctorCommand.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs b/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs index 4558a791..49801e07 100644 --- a/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs +++ b/src/fiskaltrust.Launcher/Commands/DoctorCommand.cs @@ -20,6 +20,7 @@ using fiskaltrust.ifPOS.v1.de; using fiskaltrust.ifPOS.v1; using Microsoft.AspNetCore.Server.Kestrel.Core; +using fiskaltrust.Launcher.Factories; namespace fiskaltrust.Launcher.Commands { @@ -156,7 +157,26 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro checkUp.Check("Setup monarch ProcessHostService", () => { - monarchBuilder.WebHost.ConfigureBinding(new Uri($"http://[::1]:{launcherConfiguration.LauncherServiceUri}"), protocols: HttpProtocols.Http2); + if (OperatingSystem.IsWindows()) + { + monarchBuilder.WebHost.UseKestrel(serverOptions => + { + serverOptions.ListenNamedPipe(commonProperties.LauncherConfiguration.LauncherServiceUri!, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + } + else + { + monarchBuilder.WebHost.UseKestrel(serverOptions => + { + serverOptions.ListenUnixSocket(commonProperties.LauncherConfiguration.LauncherServiceUri!, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + } monarchBuilder.Services.AddCodeFirstGrpc(); }, throws: true); @@ -185,7 +205,11 @@ public static async Task HandleAsync(CommonOptions commonOptions, CommonPro Version = "1.0.0" }; - IProcessHostService? processHostService = checkUp.Check("Start plebeian processhostservice client", () => GrpcChannel.ForAddress($"http://localhost:{launcherConfiguration.LauncherServiceUri}").CreateGrpcService()); + IProcessHostService? processHostService = checkUp.Check("Start plebeian processhostservice client", () => + { + var handler = new SocketsHttpHandler { ConnectCallback = new IpcConnectionFactory(launcherConfiguration).ConnectAsync }; + return GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions { HttpHandler = handler }).CreateGrpcService(); + }); var plebeianBuilder = Host.CreateDefaultBuilder() .UseSerilog(new LoggerConfiguration().CreateLogger()) From 651858182df2640f088a07ecf97d8eb0b9a56767 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Wed, 24 Jan 2024 23:01:58 +0100 Subject: [PATCH 35/48] Adapted README.md --- README.md | 307 ------------------------------------------------------ 1 file changed, 307 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index e0399228..00000000 --- a/README.md +++ /dev/null @@ -1,307 +0,0 @@ -# fiskaltrust Launcher - -The **fiskaltrust Launcher** is an application that hosts the packages of the **fiskaltrust Middleware**, a modular fiscalization and POS platform that can be embedded into POS systems to suffice international fiscalization regulations. - -> **Warning** -> This all-new fiskaltrust Launcher is currently in development. We plan to release a preview version to interested customers soon - please reach out to us in the [discussion section](https://github.com/fiskaltrust/middleware-launcher/discussions) if you want to participate. - -**You can track the ongoing development of the first release in the project's [backlog and board](https://github.com/orgs/fiskaltrust/projects/3/).** - -## Overview - -Middleware packages each provide specific fiscalization-, data source- and security device implementations. These package can be aggregated into a configuration container (_Cashbox_) in the fiskaltrust Portal. The Launcher then uses this configuration to decide which packages to download and run, and provides configurable hosted endpoints so that the POS software can communicate with them (e.g. gRPC or HTTP). - -Below, we illustrate a minimal sample configuration with the international SQLite _Queue_ package (with a configured HTTP endpoint) and a German _Signature Creation Unit_ (with a gRPC endpoint) that abstracts a Swissbit TSS. - -
- overview -
- -## Getting Started - -Download the latest release from GitHub. We always recommend using the latest release to benefit from the newest improvements. -Unzip the downloaded release. - -You can also download the Launcher from the fiskaltrust Portal (only sandbox at the moment), the Launcher will come with a preconfigured `launcher.configuration.json` file. - -The download will contain the `fiskaltrust.Launcher` executable and `test`, `install`, `uninstall` `.cmd` or `.sh` scripts and a `migrate.cmd` script on Windows. - -* The `test.cmd` or `test.sh` script can be used to test the Launcher. - It will start the Launcher with `--log-level` parameter set to debug. -* The `install.cmd` or `install.sh` script can be used to install the Launcher as a service. -* The `uninstall.cmd` or `uninstall.sh` script can be used to uninstall the Launcher as a service. -* The `migrate.cmd` script can be used to from migrate the Launcher 1.3 to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). - -Alternatively you can start the Launcher via the command line: - -```ps1 -# Will use the configuration file `launcher.configuration.json` in the current directory -fiskaltrust.Launcher.exe run -# Will use the cashbox id and access token from the cli parameters -fiskaltrust.Launcher.exe run --cashbox-id --access-token --sandbox -``` - -To stop the Launcher press Ctrl + C. - -> See help for other start parameters: -> ```sh -> fiskaltrust.Launcher.exe run --help -> ``` -> -> See help for other available commands: -> ```sh -> fiskaltrust.Launcher.exe --help -> ``` - -> See [CLI](#cli) for more information. - -### Installation - -On Debian based Linux systems the Launcher can also be installed via `apt-get` . The executable will be installed at `/usr/bin/fiskaltrust.Launcher` and can be run like that `fiskaltrust.Launcher --help` . - -```bash -curl -L http://downloads.fiskaltrust.cloud/apt-repo/KEY.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/fiskaltrust-archive-keyring.gpg > /dev/null -echo "deb [signed-by=/usr/share/keyrings/fiskaltrust-archive-keyring.gpg] https://downloads.fiskaltrust.cloud/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/fiskaltrust.list -sudo apt update -sudo apt install fiskaltrust-middleware-launcher -``` - -> When installed this way the self-update functionality of the launcher is disabled and it has to be updated via `apt-get` . -> -> ```bash -> sudo apt update && sudo apt install --only-upgrade fiskaltrust-middleware-launcher -> ``` - -## Migration guide - -Before switching from a 1.3 Launcher to a Launcher 2.0, please update the Queues, SCUs and Helpers to the latest packages. - -Then download the new launcher from the Portal or the [GitHub release page](https://github.com/fiskaltrust/middleware-launcher/releases). - -Run the `uninstall-service.cmd` or `uninstall-service.sh` command to deinstall the old launcher. - -If you did not download the Launcher from the Portal manually create the [configuration file](#launcher-configuration), and make sure to include the `cashboxId` and `accessToken` and to set `sandbox` to true if needed. - -In the new launcher folder execute the `install.cmd` or `install.sh` script or run the following command `.\fiskaltrust.Launcher.exe install`. - -To check that the switch was successful, e.g. try sending receipt to the middleware using our Postman collection. - -### Automatic Migration using the Migration Script - -On Windows we provide a `migrate.cmd` script that can be used to migrate the Launcher 1.3 to the Launcher 2.0. - -This script will migrate an existing service installation of the Launcher 1.3 to the Launcher 2.0. - -To run this script unzip the downloaded Launcher 2.0 files into the folder containing the old Launcher 1.3. - -> _The folder should now contain at least the following files:_ -> ``` -> . -> ├─ fiskaltrust.Launcher.exe -> ├─ launcher.configuration.json -> ├─ migrate.cmd -> └─ fiskaltrust.exe -> ``` - -And then run the `migrate.cmd` script as an administrator. - -The script will do the following: - -* Find the service of the old Launcher (`fiskaltrust.exe`) -* Stop and uninstall the service -* Install the new Launcher 2.0 as a service using the same service name as the old Launcher -* Backup the old Launcher 1.3 files to the `.backup` folder - -## Launcher configuration - -The Launcher 2.0 configuration is now read from a JSON file ( `launcher.configuration.json` in the working directory per default). The configuration has to be created manually. - -This file can be set via the `--launcher-configuration-file` cli argument. - -The configuration file should contain the following config keys: - -```jsonc -{ - - "ftCashBoxId": "", // string - "accessToken": "", // string - "launcherPort": "", // int (default: 0) - "serviceFolder": "", // string (default-windows: "C:/ProgramData/fiskaltrust", default-linux: "/var/lib/fiskaltrust", default-macos: "/Library/Application Support/fiskaltrust") - "sandbox": "", // bool (default: true) - "useOffline": "", // bool (default: false) - "launcherVersion": "", // string (default: null) - "logFolder": "", // string (default: "/logs") - "logLevel": "", // string (default: "Information") - "packageCache": "", // string (default: "/cache") - "packagesUrl": "", // string (default: "https://packages-2-0[-sandbox].fiskaltrust.cloud") - "helipadUrl": "", // string (default: "https://helipad[-sandbox].fiskaltrust.cloud") - "downloadRetry": "", // int (default: 1) - "sslValidation": "", // bool (default: false) - "proxy": "", // string (default: null) - "configurationUrl": "", // string (default: "https://configuration[-sandbox].fiskaltrust.cloud") - "downloadTimeoutSec": "", // int (default: 15) - "processHostPingPeriodSec": "", // int (default: 10) - "cashboxConfigurationFile": "", // string (default: "/service/Configuration-.json") - "useHttpSysBinding": "useHttpSysBinding", // bool (default: false) -} -``` - -All of these config keys can be overridden using the corresponding cli arguments. - -## CLI - -### `run` - -The `run` command of the fiskaltrust.Launcher is used to execute the launcher, providing users with various options to configure its behaviour and logging details. - -| Option | Description | Default | -|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| -| `--cashbox-id ` | Specifies the ID of the cashbox. | | -| `--access-token ` | Token used for authentication. | | -| `--sandbox` | Enables sandbox mode. | `false` | -| `--log-folder ` | Path to the folder where logs will be saved. | `"/logs"` | -| `--log-level ` | Determines the logging level. Accepts values like Critical, Debug, etc. | `"Information"` | -| `--launcher-configuration-file ` | Path to the launcher configuration file. | `"launcher.configuration.json"` | -| `--legacy-configuration-file ` | Path to the legacy configuration file. | `"fiskaltrust.exe.config"` | -| `--merge-legacy-config-if-exists` | If set, merges legacy configuration if it exists. | `true` | -| `--launcher-port ` | Specifies the port which the launcher will use for internal communication. A dynamic binding is used by default. | `0` | -| `--use-offline` | Enables offline mode. | `false` | -| `--service-folder ` | Path to the service folder. | Windows: `"C:/ProgramData/fiskaltrust"`
Linux: `"/var/lib/fiskaltrust"`
MacOS: `"/Library/Application Support/fiskaltrust"` | -| `--configuration-url ` | URL to fetch the configuration from. | `"https://configuration[-sandbox].fiskaltrust.cloud"` | -| `--packages-url ` | URL to fetch packages from. | `"https://packages-2-0[-sandbox].fiskaltrust.cloud"` | -| `--package-cache ` | Cache directory for the packages. | `"/cache"` | -| `--helipad-url ` | URL for the helipad. | `"https://helipad[-sandbox].fiskaltrust.cloud"` | -| `--download-timeout-sec ` | Timeout for downloads in seconds. | `15` | -| `--download-retry ` | Number of times to retry a failed download. | `1` | -| `--ssl-validation` | Validates SSL certificates. | `true` | -| `--proxy ` | Proxy server details. | | -| `--processhost-ping-period-sec ` | Ping period for the process host in seconds. | `10` | -| `--cashbox-configuration-file ` | Path to the cashbox configuration file. | `""/service/Configuration-.json"` | -| `--tls-certificate-path ` | Path to the TLS certificate. | | -| `--tls-certificate-base64 ` | Base64 encoded TLS certificate. | | -| `--tls-certificate-password ` | Password for the TLS certificate. | | -| `--use-http-sys-binding ` | Uses HTTP sys binding. | `false` | -| `--use-legacy-data-protection ` | Enables use of legacy data protection. | `false` | -| `-?` , `-h` , `--help` | Displays help and usage information. | | - -## `config` - -### `config get` - -The `config get` command of the fiskaltrust.Launcher can be used to get the current values of the Launcher configuration file. - -> **Usage:** -> -> `fiskaltrust.Launcher.exe config get` -> -> `fiskaltrust.Launcher.exe config --launcher-configuration-file get` To use an other location of the configuration file. - -### `config set` - -The `config set` command of the fiskaltrust.Launcher can be used to set configuration values in the Launcher configuration file. - -> **Usage:** -> -> `fiskaltrust.Launcher.exe config set -- ` -> -> E.g. `fiskaltrust.Launcher.exe config set --log-leve Debug` - - -## `doctor` - -The `doctor` command of the fiskaltrust.Launcher can be used to for troubleshooting launcher problems. It can be run with the same cli parameters as the `run` command. - -The `doctor` command should give the following output when run successfully: - -``` -[10:11:09 INF] ✅ Parse launcher configuration -[10:11:10 INF] ✅ Load ECDH Curve -[10:11:10 INF] ✅ Download cashbox configuration -[10:11:10 INF] ✅ Parse cashbox configuration in launcher configuration -[10:11:11 INF] ✅ Parse cashbox configuration -[10:11:11 INF] ✅ Decrypt cashbox configuration -[10:11:11 INF] ✅ Setup data protection -[10:11:11 INF] ✅ Decrypt launcher configuration -[10:11:11 INF] ✅ Setup monarch services -[10:11:11 INF] ✅ Setup monarch ProcessHostService -[10:11:11 INF] ✅ Build monarch WebApplication -[10:11:11 INF] ✅ Start monarch WebApplication -[10:11:11 INF] ✅ Start plebian processhostservice client -[10:11:11 INF] ✅ Setup plebian services -[10:11:11 INF] ✅ Build plebian Host -[10:11:11 INF] ✅ Start plebian Host -[10:11:11 INF] ✅ Shutdown launcher gracefully -[10:11:11 INF] Doctor found no issues. -``` - -## Service - -The Launcher 2.0 can be installed as a service on Windows and Linux (when `systemd` is available) using the `install` command: - -```sh -fiskaltrust.Launcher.exe install --cashbox-id --access-token --launcher-configuration-file -``` - -## Self update - -The Launcher 2.0 can update itself automatically. For this the `launcherVersion` must be set in the [launcher configuration file](#launcher-configuration). - -This can be set to a specific version (e.g. `"launcherVersion": "2.0.0-preview3"` updates to version `2.0.0-preview3` ). - -Or this can be set to a [SemVer Range](https://devhints.io/semver#ranges) (e.g. `"launcherVersion": ">= 2.0.0-preview3 < 2.0.0"` automatically updates to all preview versions greater or equal to `2.0.0-preview3` but does not update to non preview versions). - -## Getting Started for developers - -Clone this GitHub repository and build the project with Visual Studio. - -When using VS Code, please ensure that the following command line parameters are passed to `dotnet build` to enable seamless debugging: `-p:PublishSingleFile=true -p:PublishReadyToRun=true` . - -## FAQ - -**Q:** Are additional components required to be installed to be able to run the Launcher 2.0? - -**A:** The Launcher 2.0 does not require any additional components to be installed. - ---- - -**Q:** Which market can test the launcher 2.0? - -**A:** Right now only the German and Italian market can test the launcher 2.0. It is possible for everyone to register to the German/Italian sandbox and test the launcher 2.0. Also, we are working on making the launcher available for all markets. - ---- - -**Q:** Is it possible to update the launcher version (e.g. from 1.3 to 2.0)? - -**A:** It is possible to switch the launcher version from 1.3 to 2.0 using the version Launcher `2.0.0-rc.3` and later versions. - ---- - -**Q:** Can I use port sharing to run multiple Queues or SCUs on the same port (e.g. `rest://localhost:1500/queue1` and `rest://localhost:1500/queue2` ) - -**A:** Yes this is possible by setting the launcher config parameter `useHttpSysBinding` to true. - -HttpSysBinding has some limitations: - -* It is only supported on windows -* It is not supported for GRPC communication -* The launcher may need to be run as an administrator -* No TLS certificates can be set - -## Known Issues - -* The Launcher has access problems when writing to the keyring on Linux if run as a service. - The launcher configuration parameter `useLegacyDataProtection` needs to be set to `true` as a workaround. ([#100](https://github.com/fiskaltrust/middleware-launcher/issues/100) - -## Contributing - -We welcome all kinds of contributions and feedback, e.g. via issues or pull requests, and want to thank every future contributors in advance! - -Please check out the [contribution guidelines](CONTRIBUTING.md) for more detailed information about how to proceed. - -## License - -The fiskaltrust Middleware is released under the [EUPL 1.2](./LICENSE). - -As a Compliance-as-a-Service provider, the security and authenticity of the products installed on our users' endpoints is essential to us. To ensure that only peer-reviewed binaries are distributed by maintainers, fiskaltrust explicitly reserves the sole right to use the brand name "fiskaltrust Middleware" (and the brand names of related products and services) for the software provided here as open source - regardless of the spelling or abbreviation, as long as conclusions can be drawn about the original product name. - -The fiskaltrust Middleware (and related products and services) as contained in these repositories may therefore only be used in the form of binaries signed by fiskaltrust. From ae70435af2699533e0bdea257269b905cc7dd20d Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Wed, 24 Jan 2024 23:06:16 +0100 Subject: [PATCH 36/48] Adapted README.md with img base64 --- README.md | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..599bf40a --- /dev/null +++ b/README.md @@ -0,0 +1,311 @@ +# fiskaltrust Launcher + +The **fiskaltrust Launcher** is an application that hosts the packages of the **fiskaltrust Middleware**, a modular fiscalization and POS platform that can be embedded into POS systems to suffice international fiscalization regulations. + +> **Warning** +> This all-new fiskaltrust Launcher is currently in development. We plan to release a preview version to interested customers soon - please reach out to us in the [discussion section](https://github.com/fiskaltrust/middleware-launcher/discussions) if you want to participate. + +**You can track the ongoing development of the first release in the project's [backlog and board](https://github.com/orgs/fiskaltrust/projects/3/).** + +## Overview + +Middleware packages each provide specific fiscalization-, data source- and security device implementations. These package can be aggregated into a configuration container (_Cashbox_) in the fiskaltrust Portal. The Launcher then uses this configuration to decide which packages to download and run, and provides configurable hosted endpoints so that the POS software can communicate with them (e.g. gRPC or HTTP). + +Below, we illustrate a minimal sample configuration with the international SQLite _Queue_ package (with a configured HTTP endpoint) and a German _Signature Creation Unit_ (with a gRPC endpoint) that abstracts a Swissbit TSS. + +![Overview](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAk8AAAGzCAYAAAA2f/ORAAAHeXRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMkVsZWN0cm9uJTIyJTIwbW9kaWZpZWQlM0QlMjIyMDIxLTEwLTE5VDEwJTNBNDglM0EwMi44NjZaJTIyJTIwYWdlbnQlM0QlMjI1LjAlMjAoV2luZG93cyUyME5UJTIwMTAuMCUzQiUyMFdpbjY0JTNCJTIweDY0KSUyMEFwcGxlV2ViS2l0JTJGNTM3LjM2JTIwKEtIVE1MJTJDJTIwbGlrZSUyMEdlY2tvKSUyMGRyYXcuaW8lMkYxNS40LjAlMjBDaHJvbWUlMkY5MS4wLjQ0NzIuMTY0JTIwRWxlY3Ryb24lMkYxMy41LjAlMjBTYWZhcmklMkY1MzcuMzYlMjIlMjBldGFnJTNEJTIyQU9VazNjNmdZd2ZVd29QalptSUglMjIlMjB2ZXJzaW9uJTNEJTIyMTUuNC4wJTIyJTIwdHlwZSUzRCUyMmRldmljZSUyMiUzRSUzQ2RpYWdyYW0lMjBpZCUzRCUyMkI2SWM2REp3akZ1dnZ1akliTzJHJTIyJTIwbmFtZSUzRCUyMlBhZ2UtMSUyMiUzRTdWbGRrNkk0RlAwMVBwb2lDZER3MkRvNiUyQjlCVE96M1cxanhPUllpUTZVQllDS3U5djM0VENBb0daJTJCeGRkR3FyV2kwcTNIeVJjODVON3NVWlhtYUhqeVVwMGs4aXBueUduUGd3d3g5bUNJVUJWRmR0ZUcwTm5tOE1TY25pMXRRemJOamYxQmdkWTYxWlRLdEJReWtFbDZ3WUdpT1I1elNTQXhzcFM3RWZOdHNKUHB5MUlBbTFESnVJY052NmxjVXliYTJCNTV6c3YxR1dwTjNNMERFMUdla2FHME9Wa2xqc2V5YThtdUZsS1lSc1M5bGhTYm5HcnNPbDdiZSUyQlVIdDhzSkxtOHBvTyUyQlNxYm82ZjU1NiUyRjczejNwZjF0JTJCREolMkIlMkZ6R0hRRHZNWDRiVlpzWGxhJTJCZHBCVUlvNmo2a2V4Wm5oeFQ1bGttNEtFdW5hdmVKYzJWS1pjWFVIVlhFbmNtbFloR3FKaTBxVzRvVXVCUmVsTXVVaVZ6VUxNeWN0SlQxY1hBMDhZcVMwUlVWR1pmbXFtblFkY05oMk1icUNnWUY1ZjJMSkRZMHQ3VEdFZldNa1JobkpjZXdUZUtwZzhIc0xsaE5ET1FGS0dIdERsRUliSlJTTW9lVGNDaVZrb2JSajFRdmhzcXdyQ1Q2eE9PWjByeFlJbm10YVU3QjVmbElnJTJGWGROcmtuR3VFYmhpOWdLS1RxbGlycHNPcVZTcWcwRmVmaFJYZFQ2OUVVM3FFQWlSTUlwS1ZnRklwRTFGVkhWTkYzdjJqRlY4V3hVbzM5WDN6UE9PJTJGWFBOS3lQRDh2VkpiJTJGUW5YdU5kODFuSWlXNER3TWxvTzYlMkJyd1RQdDVWd00zZkIxd3BocyUyRndEZkZpQnpaNVYxWmJKZHkxTXJRWFhHZGtWN3FvRmQyVHI5TG1hZGJFZGNPMyUyRldldmpzZ0ZuWGpYUUtwSWNpSXJEcVZLVkV0bXdMb3VtZnQzODJ2RlVjZHMxZ0E1d0FFUkFuOW40RVhvZERQODNiU0ZiVyUyQnYxb2xuVlZkcHltdSUyQk45aGtIWGFjdDkxYmE4bSUyQmhyYVFzb2g5cWl3c1ZRNmFpa2xwWnJnUGZsVFcxc3JEbiUyRldKbFBmdzg0SXRKbFI1SkZyWGtMRmVRZGFtS00yU1pjSmJrcWh3cFJHaXBSVVMybEg4V0ZaTk1EQ28wa0V3SjdPbXNnYUpRS29wUERSN05rRkpvUmFrVXBOQVBsaDBTbmF5QlBkMXlrWWdLZks5RSUyRmcyTmtYJTJCQnowdWltSUJuRjdzZ0dERHRkZTdUWXpwd2dHdFRqVHlBblA0SDN5cld0OE5ZaTN2TjlkRlB1dVFSJTJGVGhkZWl2ZXBaREVjRDhSJTJGTWoxaHhzNHRBUEZzWXdCM2l5dHNnTkZDJTJCcE8yQ3hyMHZtZiUyQjBmaldRc1N2U1ROUGp3V2FJMjdFS21LMW5kMzdLQWRlOUZNJTJCZGhabmM1eTNKRzdRNkxTUkVWQXBqUlhNeGFsJTJCSzU2dEJ2eXVzZ1RkY1YlMkI0SVZlTUZjS0J0b3lUWjdzRG4wSkJxNUY1MWlhSE42TXpyRlk3OWQ2VGpqVnp1VUZZSmh2WSUyQlFEJTJCNVRDJTJCSjd1TXhiJTJGdkx2UHYzUWY3TmgwM3RkOXJnZzY3dXclMkJjOWViNk9oQkFUZyUyRmZVSUU3UE1Id3J0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) + +## Getting Started + +Download the latest release from GitHub. We always recommend using the latest release to benefit from the newest improvements. +Unzip the downloaded release. + +You can also download the Launcher from the fiskaltrust Portal (only sandbox at the moment), the Launcher will come with a preconfigured `launcher.configuration.json` file. + +The download will contain the `fiskaltrust.Launcher` executable and `test`, `install`, `uninstall` `.cmd` or `.sh` scripts and a `migrate.cmd` script on Windows. + +* The `test.cmd` or `test.sh` script can be used to test the Launcher. + It will start the Launcher with `--log-level` parameter set to debug. +* The `install.cmd` or `install.sh` script can be used to install the Launcher as a service. +* The `uninstall.cmd` or `uninstall.sh` script can be used to uninstall the Launcher as a service. +* The `migrate.cmd` script can be used to from migrate the Launcher 1.3 to the Launcher 2.0 (See [Migration Script](#automatic-migration-using-the-migration-script) for more information). + +Alternatively you can start the Launcher via the command line: + +```ps1 +# Will use the configuration file `launcher.configuration.json` in the current directory +fiskaltrust.Launcher.exe run +# Will use the cashbox id and access token from the cli parameters +fiskaltrust.Launcher.exe run --cashbox-id --access-token --sandbox +``` + +To stop the Launcher press Ctrl + C. + +> See help for other start parameters: +> ```sh +> fiskaltrust.Launcher.exe run --help +> ``` +> +> See help for other available commands: +> ```sh +> fiskaltrust.Launcher.exe --help +> ``` + +> See [CLI](#cli) for more information. + +### Installation + +On Debian based Linux systems the Launcher can also be installed via `apt-get` . The executable will be installed at `/usr/bin/fiskaltrust.Launcher` and can be run like that `fiskaltrust.Launcher --help` . + +```bash +curl -L http://downloads.fiskaltrust.cloud/apt-repo/KEY.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/fiskaltrust-archive-keyring.gpg > /dev/null +echo "deb [signed-by=/usr/share/keyrings/fiskaltrust-archive-keyring.gpg] https://downloads.fiskaltrust.cloud/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/fiskaltrust.list +sudo apt update +sudo apt install fiskaltrust-middleware-launcher +``` + +> When installed this way the self-update functionality of the launcher is disabled and it has to be updated via `apt-get` . +> +> ```bash +> sudo apt update && sudo apt install --only-upgrade fiskaltrust-middleware-launcher +> ``` + +## Migration guide + +Before switching from a 1.3 Launcher to a Launcher 2.0, please update the Queues, SCUs and Helpers to the latest packages. + +Then download the new launcher from the Portal or the [GitHub release page](https://github.com/fiskaltrust/middleware-launcher/releases). + +Run the `uninstall-service.cmd` or `uninstall-service.sh` command to deinstall the old launcher. + +If you did not download the Launcher from the Portal manually create the [configuration file](#launcher-configuration), and make sure to include the `cashboxId` and `accessToken` and to set `sandbox` to true if needed. + +In the new launcher folder execute the `install.cmd` or `install.sh` script or run the following command `.\fiskaltrust.Launcher.exe install`. + +To check that the switch was successful, e.g. try sending receipt to the middleware using our Postman collection. + +### Automatic Migration using the Migration Script + +On Windows we provide a `migrate.cmd` script that can be used to migrate the Launcher 1.3 to the Launcher 2.0. + +This script will migrate an existing service installation of the Launcher 1.3 to the Launcher 2.0. + +To run this script unzip the downloaded Launcher 2.0 files into the folder containing the old Launcher 1.3. + +> _The folder should now contain at least the following files:_ +> ``` +> . +> ├─ fiskaltrust.Launcher.exe +> ├─ launcher.configuration.json +> ├─ migrate.cmd +> └─ fiskaltrust.exe +> ``` + +And then run the `migrate.cmd` script as an administrator. + +The script will do the following: + +* Find the service of the old Launcher (`fiskaltrust.exe`) +* Stop and uninstall the service +* Install the new Launcher 2.0 as a service using the same service name as the old Launcher +* Backup the old Launcher 1.3 files to the `.backup` folder + +## Launcher configuration + +The Launcher 2.0 configuration is now read from a JSON file ( `launcher.configuration.json` in the working directory per default). The configuration has to be created manually. + +This file can be set via the `--launcher-configuration-file` cli argument. + +The configuration file should contain the following config keys: + +```jsonc +{ + + "ftCashBoxId": "", // string + "accessToken": "", // string + "launcherPort": "", // int (default: 0) + "serviceFolder": "", // string (default-windows: "C:/ProgramData/fiskaltrust", default-linux: "/var/lib/fiskaltrust", default-macos: "/Library/Application Support/fiskaltrust") + "sandbox": "", // bool (default: true) + "useOffline": "", // bool (default: false) + "launcherVersion": "", // string (default: null) + "logFolder": "", // string (default: "/logs") + "logLevel": "", // string (default: "Information") + "packageCache": "", // string (default: "/cache") + "packagesUrl": "", // string (default: "https://packages-2-0[-sandbox].fiskaltrust.cloud") + "helipadUrl": "", // string (default: "https://helipad[-sandbox].fiskaltrust.cloud") + "downloadRetry": "", // int (default: 1) + "sslValidation": "", // bool (default: false) + "proxy": "", // string (default: null) + "configurationUrl": "", // string (default: "https://configuration[-sandbox].fiskaltrust.cloud") + "downloadTimeoutSec": "", // int (default: 15) + "processHostPingPeriodSec": "", // int (default: 10) + "cashboxConfigurationFile": "", // string (default: "/service/Configuration-.json") + "useHttpSysBinding": "useHttpSysBinding", // bool (default: false) +} +``` + +All of these config keys can be overridden using the corresponding cli arguments. + +## CLI + +### `run` + +The `run` command of the fiskaltrust.Launcher is used to execute the launcher, providing users with various options to configure its behaviour and logging details. + +| Option | Description | Default | +|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| `--cashbox-id ` | Specifies the ID of the cashbox. | | +| `--access-token ` | Token used for authentication. | | +| `--sandbox` | Enables sandbox mode. | `false` | +| `--log-folder ` | Path to the folder where logs will be saved. | `"/logs"` | +| `--log-level ` | Determines the logging level. Accepts values like Critical, Debug, etc. | `"Information"` | +| `--launcher-configuration-file ` | Path to the launcher configuration file. | `"launcher.configuration.json"` | +| `--legacy-configuration-file ` | Path to the legacy configuration file. | `"fiskaltrust.exe.config"` | +| `--merge-legacy-config-if-exists` | If set, merges legacy configuration if it exists. | `true` | +| `--launcher-port ` | Specifies the port which the launcher will use for internal communication. A dynamic binding is used by default. | `0` | +| `--use-offline` | Enables offline mode. | `false` | +| `--service-folder ` | Path to the service folder. | Windows: `"C:/ProgramData/fiskaltrust"`
Linux: `"/var/lib/fiskaltrust"`
MacOS: `"/Library/Application Support/fiskaltrust"` | +| `--configuration-url ` | URL to fetch the configuration from. | `"https://configuration[-sandbox].fiskaltrust.cloud"` | +| `--packages-url ` | URL to fetch packages from. | `"https://packages-2-0[-sandbox].fiskaltrust.cloud"` | +| `--package-cache ` | Cache directory for the packages. | `"/cache"` | +| `--helipad-url ` | URL for the helipad. | `"https://helipad[-sandbox].fiskaltrust.cloud"` | +| `--download-timeout-sec ` | Timeout for downloads in seconds. | `15` | +| `--download-retry ` | Number of times to retry a failed download. | `1` | +| `--ssl-validation` | Validates SSL certificates. | `true` | +| `--proxy ` | Proxy server details. | | +| `--processhost-ping-period-sec ` | Ping period for the process host in seconds. | `10` | +| `--cashbox-configuration-file ` | Path to the cashbox configuration file. | `""/service/Configuration-.json"` | +| `--tls-certificate-path ` | Path to the TLS certificate. | | +| `--tls-certificate-base64 ` | Base64 encoded TLS certificate. | | +| `--tls-certificate-password ` | Password for the TLS certificate. | | +| `--use-http-sys-binding ` | Uses HTTP sys binding. | `false` | +| `--use-legacy-data-protection ` | Enables use of legacy data protection. | `false` | +| `-?` , `-h` , `--help` | Displays help and usage information. | | + +## `config` + +### `config get` + +The `config get` command of the fiskaltrust.Launcher can be used to get the current values of the Launcher configuration file. + +> **Usage:** +> +> `fiskaltrust.Launcher.exe config get` +> +> `fiskaltrust.Launcher.exe config --launcher-configuration-file get` To use an other location of the configuration file. + +### `config set` + +The `config set` command of the fiskaltrust.Launcher can be used to set configuration values in the Launcher configuration file. + +> **Usage:** +> +> `fiskaltrust.Launcher.exe config set -- ` +> +> E.g. `fiskaltrust.Launcher.exe config set --log-leve Debug` + + +## `doctor` + +The `doctor` command of the fiskaltrust.Launcher can be used to for troubleshooting launcher problems. It can be run with the same cli parameters as the `run` command. + +The `doctor` command should give the following output when run successfully: + +``` +[10:11:09 INF] ✅ Parse launcher configuration +[10:11:10 INF] ✅ Load ECDH Curve +[10:11:10 INF] ✅ Download cashbox configuration +[10:11:10 INF] ✅ Parse cashbox configuration in launcher configuration +[10:11:11 INF] ✅ Parse cashbox configuration +[10:11:11 INF] ✅ Decrypt cashbox configuration +[10:11:11 INF] ✅ Setup data protection +[10:11:11 INF] ✅ Decrypt launcher configuration +[10:11:11 INF] ✅ Setup monarch services +[10:11:11 INF] ✅ Setup monarch ProcessHostService +[10:11:11 INF] ✅ Build monarch WebApplication +[10:11:11 INF] ✅ Start monarch WebApplication +[10:11:11 INF] ✅ Start plebian processhostservice client +[10:11:11 INF] ✅ Setup plebian services +[10:11:11 INF] ✅ Build plebian Host +[10:11:11 INF] ✅ Start plebian Host +[10:11:11 INF] ✅ Shutdown launcher gracefully +[10:11:11 INF] Doctor found no issues. +``` + +## Service + +The Launcher 2.0 can be installed as a service on Windows and Linux (when `systemd` is available) using the `install` command: + +```sh +fiskaltrust.Launcher.exe install --cashbox-id --access-token --launcher-configuration-file +``` + +## Self update + +The Launcher 2.0 can update itself automatically. For this the `launcherVersion` must be set in the [launcher configuration file](#launcher-configuration). + +This can be set to a specific version (e.g. `"launcherVersion": "2.0.0-preview3"` updates to version `2.0.0-preview3` ). + +Or this can be set to a [SemVer Range](https://devhints.io/semver#ranges) (e.g. `"launcherVersion": ">= 2.0.0-preview3 < 2.0.0"` automatically updates to all preview versions greater or equal to `2.0.0-preview3` but does not update to non preview versions). + +## Getting Started for developers + +Clone this GitHub repository and build the project with Visual Studio. + +When using VS Code, please ensure that the following command line parameters are passed to `dotnet build` to enable seamless debugging: `-p:PublishSingleFile=true -p:PublishReadyToRun=true` . + +## FAQ + +**Q:** Are additional components required to be installed to be able to run the Launcher 2.0? + +**A:** The Launcher 2.0 does not require any additional components to be installed. + +--- + +**Q:** Which market can test the launcher 2.0? + +**A:** Right now only the German and Italian market can test the launcher 2.0. It is possible for everyone to register to the German/Italian sandbox and test the launcher 2.0. Also, we are working on making the launcher available for all markets. + +--- + +**Q:** Is it possible to update the launcher version (e.g. from 1.3 to 2.0)? + +**A:** It is possible to switch the launcher version from 1.3 to 2.0 using the version Launcher `2.0.0-rc.3` and later versions. + +--- + +**Q:** Can I use port sharing to run multiple Queues or SCUs on the same port (e.g. `rest://localhost:1500/queue1` and `rest://localhost:1500/queue2` ) + +**A:** Yes this is possible by setting the launcher config parameter `useHttpSysBinding` to true. + +HttpSysBinding has some limitations: + +* It is only supported on windows +* It is not supported for GRPC communication +* The launcher may need to be run as an administrator +* No TLS certificates can be set + +## Known Issues + +* The Launcher has access problems when writing to the keyring on Linux if run as a service. + The launcher configuration parameter `useLegacyDataProtection` needs to be set to `true` as a workaround. ([#100](https://github.com/fiskaltrust/middleware-launcher/issues/100) + + + + From 2acb1648334880a640caef63017508f7d6959cd4 Mon Sep 17 00:00:00 2001 From: forsthug <85173816+forsthug@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:05:46 +0100 Subject: [PATCH 37/48] #139 - Fix Launcher not starting on Systemd & multiple installations (#154) --- .../fiskaltrust.Launcher.Common.csproj | 8 +- src/fiskaltrust.Launcher/Commands/Common.cs | 28 +++-- .../Commands/RunCommand.cs | 7 -- .../Extensions/LifetimeExtensions.cs | 103 +++++++++++++++++- .../Helpers/ProcessHelper.cs | 8 +- .../ProcessHost/ProcessHostMonarch.cs | 14 +-- src/fiskaltrust.Launcher/Program.cs | 2 +- .../ServiceInstallation/LinuxSystemD.cs | 89 +++++++++------ .../fiskaltrust.Launcher.csproj | 24 ++-- ...iskaltrust.Launcher.IntegrationTest.csproj | 20 ++-- .../fiskaltrust.Launcher.UnitTest.csproj | 20 ++-- 11 files changed, 232 insertions(+), 91 deletions(-) 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 From 614d72bd18418e2414d72636c028bd5492e0b425 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Thu, 25 Jan 2024 12:25:47 +0100 Subject: [PATCH 38/48] Added README.md to the package archive in the build process --- azure-pipelines/templates/stages/build.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index 35dd6ead..d1df6e9a 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -83,17 +83,24 @@ stages: - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText + $packageDir = "$(Build.ArtifactStagingDirectory)/package-$(target)" + New-Item -ItemType Directory -Path $packageDir + + Copy-Item -Path $(Build.ArtifactStagingDirectory)/raw-$(target)/* -Destination $packageDir + Copy-Item -Path README.md -Destination $packageDir + + $packagePath = "$packageDir/fiskaltrust.Launcher-$version.zip" if("$(vmImage)" -eq "windows-latest") { - Compress-Archive -Path $(Build.ArtifactStagingDirectory)/raw-$(target)/* -DestinationPath $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip + Compress-Archive -Path $packageDir/* -DestinationPath $packagePath } else { - bash -c "cd $(Build.ArtifactStagingDirectory)/raw-$(target)/`nzip -r $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip ./" + bash -c "cd $packageDir && zip -r $packagePath ./" } - $hash = Get-FileHash $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip -Algorithm SHA256 + $hash = Get-FileHash $packagePath -Algorithm SHA256 $hashbytes = $hash.Hash -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} $hashstring = [System.Convert]::ToBase64String($hashbytes) - $hashstring | Set-Content $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip.hash - displayName: Pagkage executables + $hashstring | Set-Content $packageDir/fiskaltrust.Launcher-$version.zip.hash + displayName: Package executables and README.md - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText From 7505e8662813926461a0b43e5a6a01239bf432ce Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Thu, 25 Jan 2024 12:55:15 +0100 Subject: [PATCH 39/48] Fixed pipeline error by adding existence check for package directory before creation --- azure-pipelines/templates/stages/build.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index d1df6e9a..a498c353 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -84,24 +84,23 @@ stages: $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText $packageDir = "$(Build.ArtifactStagingDirectory)/package-$(target)" - New-Item -ItemType Directory -Path $packageDir + + if (-Not (Test-Path -Path $packageDir)) { + New-Item -ItemType Directory -Path $packageDir + } Copy-Item -Path $(Build.ArtifactStagingDirectory)/raw-$(target)/* -Destination $packageDir Copy-Item -Path README.md -Destination $packageDir $packagePath = "$packageDir/fiskaltrust.Launcher-$version.zip" - if("$(vmImage)" -eq "windows-latest") { - Compress-Archive -Path $packageDir/* -DestinationPath $packagePath - } else { - bash -c "cd $packageDir && zip -r $packagePath ./" - } + Compress-Archive -Path $packageDir/* -DestinationPath $packagePath $hash = Get-FileHash $packagePath -Algorithm SHA256 $hashbytes = $hash.Hash -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} $hashstring = [System.Convert]::ToBase64String($hashbytes) $hashstring | Set-Content $packageDir/fiskaltrust.Launcher-$version.zip.hash - displayName: Package executables and README.md - + displayName: Package executables and README.md + - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText From 91a0717523bddb9398abcd6bb9777ca622e5ddad Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Fri, 26 Jan 2024 14:51:06 +0100 Subject: [PATCH 40/48] Restored build.yml from master --- azure-pipelines/templates/stages/build.yml | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index a498c353..35dd6ead 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -83,24 +83,18 @@ stages: - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText - $packageDir = "$(Build.ArtifactStagingDirectory)/package-$(target)" - - if (-Not (Test-Path -Path $packageDir)) { - New-Item -ItemType Directory -Path $packageDir + if("$(vmImage)" -eq "windows-latest") { + Compress-Archive -Path $(Build.ArtifactStagingDirectory)/raw-$(target)/* -DestinationPath $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip + } else { + bash -c "cd $(Build.ArtifactStagingDirectory)/raw-$(target)/`nzip -r $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip ./" } - Copy-Item -Path $(Build.ArtifactStagingDirectory)/raw-$(target)/* -Destination $packageDir - Copy-Item -Path README.md -Destination $packageDir - - $packagePath = "$packageDir/fiskaltrust.Launcher-$version.zip" - Compress-Archive -Path $packageDir/* -DestinationPath $packagePath - - $hash = Get-FileHash $packagePath -Algorithm SHA256 + $hash = Get-FileHash $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip -Algorithm SHA256 $hashbytes = $hash.Hash -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} $hashstring = [System.Convert]::ToBase64String($hashbytes) - $hashstring | Set-Content $packageDir/fiskaltrust.Launcher-$version.zip.hash - displayName: Package executables and README.md - + $hashstring | Set-Content $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip.hash + displayName: Pagkage executables + - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText From 62ac404a79d009ea9db7ba7fd88e854c62e4bd49 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Fri, 26 Jan 2024 15:00:56 +0100 Subject: [PATCH 41/48] Improved/fixed addition of README.md to script artifacts --- azure-pipelines/templates/stages/build.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index 35dd6ead..3b45fc9e 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -96,18 +96,22 @@ stages: displayName: Pagkage executables - pwsh: | - $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText + Copy-Item -Path ./README.md -Destination $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md + displayName: "Copy README.md to scripts artifacts" + - pwsh: | + $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText $scriptTargetPath = "$(Build.ArtifactStagingDirectory)/scripts-$(target)/fiskaltrust.Launcher.Scripts-$version.zip" if("$(vmImage)" -eq "windows-latest") { - Compress-Archive -Path ./scripts/$(scriptFolder)/* -DestinationPath $scriptTargetPath + Compress-Archive -Path ./scripts/$(scriptFolder)/*, $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md -DestinationPath $scriptTargetPath } else { bash -c "chmod +x ./scripts/$(scriptFolder)/*" bash -c "cd ./scripts/$(scriptFolder)/`nzip -r $scriptTargetPath ./" + bash -c "cd $(Build.ArtifactStagingDirectory)/scripts-$(target)/`nzip -r $scriptTargetPath README.md" } - displayName: Package scripts - + displayName: Package scripts with README.md + - publish: $(Build.ArtifactStagingDirectory)/package-$(target) artifact: package-$(target) From 14cb06af55986efd3e5bab77fb56950013e1b6f7 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Fri, 26 Jan 2024 15:16:58 +0100 Subject: [PATCH 42/48] Added tags/comments to unwanted sections in README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 599bf40a..254f4572 100644 --- a/README.md +++ b/README.md @@ -290,16 +290,16 @@ HttpSysBinding has some limitations: * The Launcher has access problems when writing to the keyring on Linux if run as a service. The launcher configuration parameter `useLegacyDataProtection` needs to be set to `true` as a workaround. ([#100](https://github.com/fiskaltrust/middleware-launcher/issues/100) - ## Contributing We welcome all kinds of contributions and feedback, e.g. via issues or pull requests, and want to thank every future contributors in advance! Please check out the [contribution guidelines](CONTRIBUTING.md) for more detailed information about how to proceed. ---> + - ## License The fiskaltrust Middleware is released under the [EUPL 1.2](./LICENSE). @@ -308,4 +308,4 @@ As a Compliance-as-a-Service provider, the security and authenticity of the prod The fiskaltrust Middleware (and related products and services) as contained in these repositories may therefore only be used in the form of binaries signed by fiskaltrust. ---> + \ No newline at end of file From 2e6415ea65efd5e36943efc23f2f932cba673516 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Fri, 26 Jan 2024 15:18:01 +0100 Subject: [PATCH 43/48] added a script that removed the 'CONTRIBUTING' and 'LICENSE' sections from the README.md file during the artifact packaging process --- azure-pipelines/templates/stages/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index 3b45fc9e..eea04fda 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -93,11 +93,17 @@ stages: $hashbytes = $hash.Hash -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} $hashstring = [System.Convert]::ToBase64String($hashbytes) $hashstring | Set-Content $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip.hash - displayName: Pagkage executables + displayName: Package executables - pwsh: | Copy-Item -Path ./README.md -Destination $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md displayName: "Copy README.md to scripts artifacts" + + - pwsh: | + $readmeContent = Get-Content $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md -Raw + $updatedContent = $readmeContent -replace '.*?\s*', '' + $updatedContent | Set-Content $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md + displayName: "Update README.md" - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText From 610d9061b02b3984377b6f19424603a8f2ec69c9 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Fri, 26 Jan 2024 15:39:47 +0100 Subject: [PATCH 44/48] Fixed part of script which removes Contributing and License section from README.md --- azure-pipelines/templates/stages/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index eea04fda..4ba67720 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -98,13 +98,13 @@ stages: - pwsh: | Copy-Item -Path ./README.md -Destination $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md displayName: "Copy README.md to scripts artifacts" - + - pwsh: | $readmeContent = Get-Content $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md -Raw - $updatedContent = $readmeContent -replace '.*?\s*', '' + $updatedContent = $readmeContent -replace '(?s).*?\s*', '' $updatedContent | Set-Content $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md displayName: "Update README.md" - + - pwsh: | $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText $scriptTargetPath = "$(Build.ArtifactStagingDirectory)/scripts-$(target)/fiskaltrust.Launcher.Scripts-$version.zip" From 7eed831e7a2d4f462d986fcdf4922089404ffd85 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Mon, 29 Jan 2024 10:17:28 +0100 Subject: [PATCH 45/48] Updated build pipeline to include README.md in script package artifacts --- azure-pipelines/templates/stages/build.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index 4ba67720..258d20b0 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -93,7 +93,7 @@ stages: $hashbytes = $hash.Hash -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} $hashstring = [System.Convert]::ToBase64String($hashbytes) $hashstring | Set-Content $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip.hash - displayName: Package executables + displayName: "Package executables" - pwsh: | Copy-Item -Path ./README.md -Destination $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md @@ -104,20 +104,22 @@ stages: $updatedContent = $readmeContent -replace '(?s).*?\s*', '' $updatedContent | Set-Content $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md displayName: "Update README.md" - + - pwsh: | + $scriptFolderPath = "$(Build.ArtifactStagingDirectory)/scripts-$(target)" $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText - $scriptTargetPath = "$(Build.ArtifactStagingDirectory)/scripts-$(target)/fiskaltrust.Launcher.Scripts-$version.zip" + $scriptTargetPath = "$scriptFolderPath/fiskaltrust.Launcher.Scripts-$version.zip" + + Copy-Item -Path ./scripts/$(scriptFolder)/* -Destination $scriptFolderPath if("$(vmImage)" -eq "windows-latest") { - Compress-Archive -Path ./scripts/$(scriptFolder)/*, $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md -DestinationPath $scriptTargetPath + Compress-Archive -Path $scriptFolderPath/* -DestinationPath $scriptTargetPath } else { - bash -c "chmod +x ./scripts/$(scriptFolder)/*" - bash -c "cd ./scripts/$(scriptFolder)/`nzip -r $scriptTargetPath ./" - bash -c "cd $(Build.ArtifactStagingDirectory)/scripts-$(target)/`nzip -r $scriptTargetPath README.md" + bash -c "chmod +x $scriptFolderPath/*" + bash -c "cd $scriptFolderPath && zip -r $scriptTargetPath ./" } - displayName: Package scripts with README.md - + displayName: "Package scripts with README.md" + - publish: $(Build.ArtifactStagingDirectory)/package-$(target) artifact: package-$(target) From a283c7f7382fe04240608581258596101b6354c5 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Mon, 29 Jan 2024 10:25:46 +0100 Subject: [PATCH 46/48] Added step to remove README.md from scripts artifacts --- azure-pipelines/templates/stages/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index 258d20b0..64394c20 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -120,6 +120,10 @@ stages: } displayName: "Package scripts with README.md" + - pwsh: | + Remove-Item -Path $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md + displayName: "Remove README.md from scripts artifacts" + - publish: $(Build.ArtifactStagingDirectory)/package-$(target) artifact: package-$(target) From 7d078fd5b89500b4763bbc5f140f83b7195242d7 Mon Sep 17 00:00:00 2001 From: PawelKarczewski Date: Mon, 29 Jan 2024 11:07:50 +0100 Subject: [PATCH 47/48] Quick improvement --- azure-pipelines/templates/stages/build.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/azure-pipelines/templates/stages/build.yml b/azure-pipelines/templates/stages/build.yml index 64394c20..29aaa901 100644 --- a/azure-pipelines/templates/stages/build.yml +++ b/azure-pipelines/templates/stages/build.yml @@ -93,7 +93,7 @@ stages: $hashbytes = $hash.Hash -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} $hashstring = [System.Convert]::ToBase64String($hashbytes) $hashstring | Set-Content $(Build.ArtifactStagingDirectory)/package-$(target)/fiskaltrust.Launcher-$version.zip.hash - displayName: "Package executables" + displayName: Package executables - pwsh: | Copy-Item -Path ./README.md -Destination $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md @@ -106,24 +106,22 @@ stages: displayName: "Update README.md" - pwsh: | - $scriptFolderPath = "$(Build.ArtifactStagingDirectory)/scripts-$(target)" $version = (Select-Xml -Path ./Directory.Build.props -XPath 'Project/PropertyGroup/Version').Node.InnerText - $scriptTargetPath = "$scriptFolderPath/fiskaltrust.Launcher.Scripts-$version.zip" - - Copy-Item -Path ./scripts/$(scriptFolder)/* -Destination $scriptFolderPath + $scriptTargetPath = "$(Build.ArtifactStagingDirectory)/scripts-$(target)/fiskaltrust.Launcher.Scripts-$version.zip" + Copy-Item -Path ./scripts/$(scriptFolder)/* -Destination $(Build.ArtifactStagingDirectory)/scripts-$(target) if("$(vmImage)" -eq "windows-latest") { - Compress-Archive -Path $scriptFolderPath/* -DestinationPath $scriptTargetPath + Compress-Archive -Path $(Build.ArtifactStagingDirectory)/scripts-$(target)/* -DestinationPath $scriptTargetPath } else { - bash -c "chmod +x $scriptFolderPath/*" - bash -c "cd $scriptFolderPath && zip -r $scriptTargetPath ./" + bash -c "chmod +x $(Build.ArtifactStagingDirectory)/scripts-$(target)/*" + bash -c "cd $(Build.ArtifactStagingDirectory)/scripts-$(target) && zip -r $scriptTargetPath ./" } displayName: "Package scripts with README.md" - pwsh: | - Remove-Item -Path $(Build.ArtifactStagingDirectory)/scripts-$(target)/README.md - displayName: "Remove README.md from scripts artifacts" - + Get-ChildItem -Path $(Build.ArtifactStagingDirectory)/scripts-$(target)/* -Exclude *.zip | Remove-Item + displayName: "Clean up scripts directory" + - publish: $(Build.ArtifactStagingDirectory)/package-$(target) artifact: package-$(target) From 72828de9838a8112f02473632525b1f0a63477d2 Mon Sep 17 00:00:00 2001 From: Paul Volavsek <24523184+volllly@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:27:20 +0100 Subject: [PATCH 48/48] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 254f4572..90ad7508 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Middleware packages each provide specific fiscalization-, data source- and secur Below, we illustrate a minimal sample configuration with the international SQLite _Queue_ package (with a configured HTTP endpoint) and a German _Signature Creation Unit_ (with a gRPC endpoint) that abstracts a Swissbit TSS. -![Overview](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAk8AAAGzCAYAAAA2f/ORAAAHeXRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMkVsZWN0cm9uJTIyJTIwbW9kaWZpZWQlM0QlMjIyMDIxLTEwLTE5VDEwJTNBNDglM0EwMi44NjZaJTIyJTIwYWdlbnQlM0QlMjI1LjAlMjAoV2luZG93cyUyME5UJTIwMTAuMCUzQiUyMFdpbjY0JTNCJTIweDY0KSUyMEFwcGxlV2ViS2l0JTJGNTM3LjM2JTIwKEtIVE1MJTJDJTIwbGlrZSUyMEdlY2tvKSUyMGRyYXcuaW8lMkYxNS40LjAlMjBDaHJvbWUlMkY5MS4wLjQ0NzIuMTY0JTIwRWxlY3Ryb24lMkYxMy41LjAlMjBTYWZhcmklMkY1MzcuMzYlMjIlMjBldGFnJTNEJTIyQU9VazNjNmdZd2ZVd29QalptSUglMjIlMjB2ZXJzaW9uJTNEJTIyMTUuNC4wJTIyJTIwdHlwZSUzRCUyMmRldmljZSUyMiUzRSUzQ2RpYWdyYW0lMjBpZCUzRCUyMkI2SWM2REp3akZ1dnZ1akliTzJHJTIyJTIwbmFtZSUzRCUyMlBhZ2UtMSUyMiUzRTdWbGRrNkk0RlAwMVBwb2lDZER3MkRvNiUyQjlCVE96M1cxanhPUllpUTZVQllDS3U5djM0VENBb0daJTJCeGRkR3FyV2kwcTNIeVJjODVON3NVWlhtYUhqeVVwMGs4aXBueUduUGd3d3g5bUNJVUJWRmR0ZUcwTm5tOE1TY25pMXRRemJOamYxQmdkWTYxWlRLdEJReWtFbDZ3WUdpT1I1elNTQXhzcFM3RWZOdHNKUHB5MUlBbTFESnVJY052NmxjVXliYTJCNTV6c3YxR1dwTjNNMERFMUdla2FHME9Wa2xqc2V5YThtdUZsS1lSc1M5bGhTYm5HcnNPbDdiZSUyQlVIdDhzSkxtOHBvTyUyQlNxYm82ZjU1NiUyRjczejNwZjF0JTJCREolMkIlMkZ6R0hRRHZNWDRiVlpzWGxhJTJCZHBCVUlvNmo2a2V4Wm5oeFQ1bGttNEtFdW5hdmVKYzJWS1pjWFVIVlhFbmNtbFloR3FKaTBxVzRvVXVCUmVsTXVVaVZ6VUxNeWN0SlQxY1hBMDhZcVMwUlVWR1pmbXFtblFkY05oMk1icUNnWUY1ZjJMSkRZMHQ3VEdFZldNa1JobkpjZXdUZUtwZzhIc0xsaE5ET1FGS0dIdERsRUliSlJTTW9lVGNDaVZrb2JSajFRdmhzcXdyQ1Q2eE9PWjByeFlJbm10YVU3QjVmbElnJTJGWGROcmtuR3VFYmhpOWdLS1RxbGlycHNPcVZTcWcwRmVmaFJYZFQ2OUVVM3FFQWlSTUlwS1ZnRklwRTFGVkhWTkYzdjJqRlY4V3hVbzM5WDN6UE9PJTJGWFBOS3lQRDh2VkpiJTJGUW5YdU5kODFuSWlXNER3TWxvTzYlMkJyd1RQdDVWd00zZkIxd3BocyUyRndEZkZpQnpaNVYxWmJKZHkxTXJRWFhHZGtWN3FvRmQyVHI5TG1hZGJFZGNPMyUyRldldmpzZ0ZuWGpYUUtwSWNpSXJEcVZLVkV0bXdMb3VtZnQzODJ2RlVjZHMxZ0E1d0FFUkFuOW40RVhvZERQODNiU0ZiVyUyQnYxb2xuVlZkcHltdSUyQk45aGtIWGFjdDkxYmE4bSUyQmhyYVFzb2g5cWl3c1ZRNmFpa2xwWnJnUGZsVFcxc3JEbiUyRldKbFBmdzg0SXRKbFI1SkZyWGtMRmVRZGFtS00yU1pjSmJrcWh3cFJHaXBSVVMybEg4V0ZaTk1EQ28wa0V3SjdPbXNnYUpRS29wUERSN05rRkpvUmFrVXBOQVBsaDBTbmF5QlBkMXlrWWdLZks5RSUyRmcyTmtYJTJCQnowdWltSUJuRjdzZ0dERHRkZTdUWXpwd2dHdFRqVHlBblA0SDN5cld0OE5ZaTN2TjlkRlB1dVFSJTJGVGhkZWl2ZXBaREVjRDhSJTJGTWoxaHhzNHRBUEZzWXdCM2l5dHNnTkZDJTJCcE8yQ3hyMHZtZiUyQjBmaldRc1N2U1ROUGp3V2FJMjdFS21LMW5kMzdLQWRlOUZNJTJCZGhabmM1eTNKRzdRNkxTUkVWQXBqUlhNeGFsJTJCSzU2dEJ2eXVzZ1RkY1YlMkI0SVZlTUZjS0J0b3lUWjdzRG4wSkJxNUY1MWlhSE42TXpyRlk3OWQ2VGpqVnp1VUZZSmh2WSUyQlFEJTJCNVRDJTJCSjd1TXhiJTJGdkx2UHYzUWY3TmgwM3RkOXJnZzY3dXclMkJjOWViNk9oQkFUZyUyRmZVSUU3UE1Id3J0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) +![Overview](https://raw.githubusercontent.com/fiskaltrust/middleware-launcher/main/doc/images/overview.png) ## Getting Started @@ -308,4 +308,4 @@ As a Compliance-as-a-Service provider, the security and authenticity of the prod The fiskaltrust Middleware (and related products and services) as contained in these repositories may therefore only be used in the form of binaries signed by fiskaltrust. - \ No newline at end of file +