Deploymentcenter-Integration (Schritte 0-8 des Konzepts)

Bindet Lizenz, Watchdog, Fehler-Stream, UpdateService und Erstinstallation an
das Deploymentcenter 2.5.1 an. Einbauort ist IBKRTrader.Hosting - den Host
teilen sich Shell und Daemon.

Projekt-Befunde aus dem Konzept vorab bereinigt:
  P1  Echte DB-Zugangsdaten als Vorgabewerte in AppSettings -> Platzhalter.
      Das alte Passwort steht weiterhin in der Git-Historie und ist als
      kompromittiert zu behandeln (Rotation ist Nutzer-Aktion).
  P2  AppPaths fiel unter Windows auf /etc/ibkrtrader zurueck, was .NET zu
      C:\etc\ibkrtrader aufloest. Jetzt %ProgramData%\IBKRTrader.
  P3  Globale Ausnahmebehandler (AppDomain / TaskScheduler) - vorher gab es
      keinen Logeintrag, wenn der Prozess unbehandelt wegbrach.
  P4  Version einmal zentral in Directory.Build.props statt zweimal hartkodiert.

Bewusste Abweichungen vom DC-Leitfaden, beide fuer ein handelndes System:
  - Lizenz-Urteil fuehrt zum Sperrbetrieb (TradingEnabled=false) statt zu
    Environment.Exit(1). Keine neuen Einstiege, aber Risiko-, Exit- und
    Buchhaltungslogik laufen weiter.
  - exitCurrentApp bleibt immer false; der Aufrufer beendet geordnet.

Das SDK haengt als Cross-Repo-ProjectReference am Schwester-Repo
Deploymentcenter (Interim, siehe Konzept 2.2). Damit ist P5 offen: die
Gitea-CI checkt das Schwester-Repo nicht aus und wird rot, bis der Bezug auf
ein NuGet-Paket umgestellt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-22 10:44:56 +02:00
co-authored by Claude Opus 5
parent 87194bfc48
commit 0a72ea7e28
22 changed files with 2281 additions and 15 deletions
+49 -1
View File
@@ -1,6 +1,9 @@
using Deploymentcenter.Client;
using IBKRTrader.Core.Configuration;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Modularity;
using IBKRTrader.Hosting;
using IBKRTrader.Hosting.Deploymentcenter;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -21,6 +24,16 @@ internal static class Program
if (HasFlag(args, "--db-version"))
return DbVersion.Print();
// Lizenz-Konvention aus dem Deploymentcenter-Leitfaden: das SDK bringt keine eigene CLI mit,
// diese beiden Schalter sind hier die vom Leitfaden empfohlene Umsetzung. Kein Host nötig -
// beide arbeiten nur mit dem lokalen Lizenz-Cache bzw. einer einzelnen Validierung.
if (HasFlag(args, "--license-status"))
return PrintLicenseStatus();
var setKeyIndex = Array.IndexOf(args, "--license-set-key");
if (setKeyIndex >= 0 && setKeyIndex + 1 < args.Length)
return await SetLicenseKeyAsync(args[setKeyIndex + 1]).ConfigureAwait(false);
// 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.
@@ -40,7 +53,7 @@ internal static class Program
}
logger.Info("Core", "=== IBKRTrader (kopflos) startet ===");
logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version} | {RuntimeDescription()}");
logger.Info("Core", $"Version: {IBKRTrader.Hosting.BuildInfo.Summary} | .NET {Environment.Version} | {RuntimeDescription()}");
await host.StartAsync().ConfigureAwait(false);
@@ -73,6 +86,41 @@ internal static class Program
private static bool HasFlag(string[] args, string flag) =>
args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
// ─── Lizenz-CLI (kein Host nötig) ──────────────────────────────────────────
private static int PrintLicenseStatus()
{
LicenseConfig.SetStorageDirectory(AppPaths.Config);
var hw = HardwareId.GetHardwareId(DcConfig.ProductSlug);
var state = LicenseClient.TryGetCachedState(DcConfig.ProductSlug);
Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
Console.WriteLine($"Schlüssel: {LicenseClient.TryGetCachedKey(DcConfig.ProductSlug) ?? "keiner"}");
Console.WriteLine($"Cache: {(state == null ? "leer" : state.Status)}");
if (state is { CacheExpiresAt: > 0 })
{
var until = DateTimeOffset.FromUnixTimeSeconds(state.CacheExpiresAt);
Console.WriteLine($"Offline noch bis: {until:u}");
}
return 0;
}
private static async Task<int> SetLicenseKeyAsync(string key)
{
LicenseConfig.SetStorageDirectory(AppPaths.Config);
// ValidateAsync legt den Schluessel bei Erfolg selbst im verschluesselten Cache ab - eine
// eigene Ablage braucht es dafuer nicht.
var client = new LicenseClient();
var result = await client.ValidateAsync(DcConfig.ProductSlug, key, DcConfig.BaseUrl, DcConfig.AppVersion)
.ConfigureAwait(false);
Console.WriteLine(result.Message);
return result.IsValid ? 0 : 1;
}
private static string RuntimeDescription() =>
$"{System.Runtime.InteropServices.RuntimeInformation.OSDescription.Trim()} " +
$"({System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture})";