L2: Kopfloser Linux-Dienst - der Trading-Kern laeuft ohne Oberflaeche

Meilenstein: Trading, Worker, Accounting, Supervisor, REST und MCP laufen
als systemd-Dienst auf Linux. Nur noch IBKRTrader.App ist Windows-gebunden.

Neu IBKRTrader.Hosting: die komplette Host-Zusammenstellung, aus der
WinForms-Program.cs herausgezogen. Bewusst ein eigenes Projekt - laege sie
weiter in der Shell, muesste sie fuer den Daemon dupliziert werden, und
beide wuerden auseinanderlaufen. Ein Modul, das nur in einer Variante
registriert ist, faellt sonst erst im Betrieb auf. RunStartupChecks buendelt
die Startschritte in fester Reihenfolge (Zeitzone vor dem ersten Logeintrag,
Master-Key vor jeder Entschluesselung).

Neu IBKRTrader.Daemon: kopfloser Einstiegspunkt mit SIGTERM-Behandlung,
--check (Trockenlauf: Host bauen, Startpruefungen, keine Boersenverbindung -
fuer Deployment und CI) und --db-version.

AppPaths: Logs, Backups, settings.json und master.key lagen neben der
Binaerdatei. Unter /opt hat der Dienstbenutzer dort keinen Schreibzugriff -
der Dienst waere beim ersten Logeintrag gescheitert. Aufloesung jetzt:
Umgebungsvariable, sonst Binaerverzeichnis WENN beschreibbar (haelt Windows
und die Entwicklung unveraendert), sonst FHS. Der Schreibtest ist ein echter
Schreibversuch - Attributpruefung traegt nicht, weil unter Linux
Besitzer/Gruppe/Modus und unter Windows die ACL entscheiden.

Rechtepruefung fuer master.key/openrouter.key auf Unix: Windows-ACLs
uebertragen sich beim Kopieren auf einen Linux-Host nicht, eine key-Datei mit
644 waere dort fuer jeden lokalen Nutzer lesbar. Nur eine Warnung.

InvariantGlobalization ausdruecklich auf false. Der naheliegende Griff zum
schlanken Image waere hier ein Eigentor gewesen und haette genau die zwei
Punkte aus L1b lautlos rueckgaengig gemacht: ohne ICU faellt die Aufloesung
von Windows-Zeitzonen-IDs aus (Rueckfall auf UTC im Container) und
CultureInfo.GetCultureInfo("de-DE") liefert die invariante Kultur, womit die
PDF-Betraege wieder das Format wechseln.

systemd: Unit mit Type=notify plus UseSystemd() im Host - ohne sd_notify
haette systemctl start bis zum Timeout gehangen. Unit mit Absicherung
(ProtectSystem=strict, ReadWritePaths), deploy/README.md mit Voraussetzungen,
Verzeichnissen, Zeitzonen-Festlegung und dem, was der Daemon NICHT loest
(IB Gateway headless bleibt eine eigene Baustelle).

Verifiziert: 193 Tests gruen (+5), Build 0 Fehler/0 Warnungen, --smoke-ui
konstruiert alle 7 Fenster, --check laeuft durch, und publish -r linux-x64
liefert 11 MB mit ELF-Launcher, ohne eine einzige Windows-Abhaengigkeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-06 23:09:33 +02:00
co-authored by Claude Opus 5
parent 8b9b993d1d
commit 9c7e19149c
19 changed files with 819 additions and 222 deletions
+29
View File
@@ -0,0 +1,29 @@
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Daemon;
/// <summary>
/// Diagnose: öffnet die Datenbank aus <c>settings.json</c> und gibt die Serverversion aus.
/// Gebraucht für das EF-<c>ServerVersion</c>-Pinning; läuft ohne Host und ohne Oberfläche.
/// </summary>
internal static class DbVersion
{
public static int Print()
{
var settings = new SettingsService();
settings.Load();
try
{
using var c = new MySqlConnector.MySqlConnection(settings.Settings.Database.BuildConnectionString());
c.Open();
Console.WriteLine($"ServerVersion: {c.ServerVersion}");
return 0;
}
catch (Exception ex)
{
Console.WriteLine($"FEHLER: {ex.GetType().Name}: {ex.Message}");
return 1;
}
}
}
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Kopfloser Betrieb: Trading-Kern, Worker, Accounting, Supervisor, REST und MCP ohne
Oberfläche. Das ist der Einstiegspunkt für den Linux-Dienst (systemd). -->
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>IBKRTrader.Daemon</AssemblyName>
<RootNamespace>IBKRTrader.Daemon</RootNamespace>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<!-- BEWUSST NICHT InvariantGlobalization: das Image braucht ICU (libicu / icu-data-full).
Zwei Dinge hängen daran und würden sonst lautlos falsch werden:
1. AppTimeZone löst Windows-Zeitzonen-IDs über TimeZoneInfo.TryConvertWindowsIdToIanaId
auf. Ohne ICU schlägt das fehl – eine Instanz mit einer Altkonfiguration
("W. Europe Standard Time") würde auf die Systemzeitzone zurückfallen, also je nach
Container auf UTC. Genau die Verschiebung, die wir in L1b beseitigt haben.
2. Der PDF-Export formatiert Beträge fest gegen de-DE. Ohne ICU liefert
CultureInfo.GetCultureInfo("de-DE") die invariante Kultur, aus "1.234,56" würde
wieder "1,234.56" – in einem Dokument, das als prüfbare Aufstellung gilt. -->
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\IBKRTrader.Hosting\IBKRTrader.Hosting.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="appsettings.Local.json" Condition="Exists('appsettings.Local.json')" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+79
View File
@@ -0,0 +1,79 @@
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Modularity;
using IBKRTrader.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace IBKRTrader.Daemon;
/// <summary>
/// Kopfloser Einstiegspunkt: Trading-Kern, Worker, Accounting, Supervisor, REST-API und MCP-Light
/// laufen ohne Oberfläche. Das ist die Betriebsform auf Linux (systemd).
///
/// <para>Der Host kommt unverändert aus <see cref="AppHostBuilder"/> – dieselbe Zusammenstellung,
/// die auch die Desktop-Shell startet. Hier fehlt lediglich alles, was ein Fenster braucht.</para>
/// </summary>
internal static class Program
{
private static async Task<int> Main(string[] args)
{
// Diagnose: Serverversion ausgeben (für das EF-ServerVersion-Pinning). Kein Host nötig.
if (HasFlag(args, "--db-version"))
return DbVersion.Print();
// Trockenlauf: Host bauen, Startprüfungen fahren, Dienste NICHT starten. Für Deployment
// und CI – prüft Konfiguration, DB-Verbindungszeichenfolge, Master-Key und Ablageorte,
// ohne eine einzige Verbindung zur Börse aufzubauen.
var checkOnly = HasFlag(args, "--check");
var modules = AppHostBuilder.CreateModules();
using var host = AppHostBuilder.Build(modules);
var logger = host.Services.GetRequiredService<LoggingService>();
AppHostBuilder.RunStartupChecks(host.Services);
if (checkOnly)
{
logger.Info("Core", "Prüflauf (--check) erfolgreich: Host baubar, Startprüfungen bestanden.");
Console.WriteLine("=== Prüflauf OK ===");
return 0;
}
logger.Info("Core", "=== IBKRTrader (kopflos) startet ===");
logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version} | {RuntimeDescription()}");
await host.StartAsync().ConfigureAwait(false);
// Module nach dem Host starten – so laufen sie nie gegen einen leeren Zustand an.
foreach (var module in modules)
{
try { await module.StartAsync(default).ConfigureAwait(false); }
catch (Exception ex) { logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
}
logger.Info("Core", "IBKRTrader bereit. Beenden mit Strg+C bzw. SIGTERM.");
// WaitForShutdown behandelt SIGTERM und SIGINT – genau das, was systemd beim Stoppen sendet.
await host.WaitForShutdownAsync().ConfigureAwait(false);
await StopModulesAsync(modules, logger).ConfigureAwait(false);
logger.Info("Core", "IBKRTrader beendet.");
return 0;
}
private static async Task StopModulesAsync(IReadOnlyList<IModule> modules, LoggingService logger)
{
foreach (var module in modules)
{
try { await module.StopAsync(default).ConfigureAwait(false); }
catch (Exception ex) { logger.Warn(module.Name, $"{module.Name}: Stopp fehlgeschlagen: {ex.Message}"); }
}
}
private static bool HasFlag(string[] args, string flag) =>
args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
private static string RuntimeDescription() =>
$"{System.Runtime.InteropServices.RuntimeInformation.OSDescription.Trim()} " +
$"({System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture})";
}
+11
View File
@@ -0,0 +1,11 @@
{
"Database": {
"MySqlConnectionString": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Warning"
}
}
}