Files
RichardandClaude Opus 5 9c7e19149c 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>
2026-08-06 23:09:33 +02:00

63 lines
2.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using FluentAssertions;
using IBKRTrader.Core.Configuration;
namespace IBKRTrader.Tests.Configuration;
/// <summary>
/// Die Ablageorte müssen auf beiden Plattformen bestimmbar und beschreibbar sein. Bisher lag alles
/// neben der Binärdatei unter <c>/opt</c> hat der Dienstbenutzer dort keinen Schreibzugriff, der
/// Dienst wäre beim ersten Logeintrag gescheitert.
/// </summary>
[Trait("cat", "unit")]
public class AppPathsTests
{
[Fact]
public void AlleVerzeichnisse_SindErmittelbarUndExistieren()
{
// Der Zugriff legt sie an ein Dienst darf nicht daran scheitern, dass ein Verzeichnis fehlt.
foreach (var dir in new[] { AppPaths.Config, AppPaths.Data, AppPaths.Logs })
{
dir.Should().NotBeNullOrWhiteSpace();
Directory.Exists(dir).Should().BeTrue($"AppPaths muss \"{dir}\" anlegen");
}
}
[Fact]
public void Verzeichnisse_SindTatsaechlichBeschreibbar()
{
// Die Auflösung prüft den Schreibzugriff durch einen echten Schreibversuch. Dieser Test
// haelt fest, dass das Ergebnis stimmt eine reine Attributpruefung waere hier wertlos,
// weil unter Linux Besitzer/Gruppe/Modus und unter Windows die ACL entscheiden.
foreach (var dir in new[] { AppPaths.Config, AppPaths.Data, AppPaths.Logs })
{
var probe = Path.Combine(dir, $"test-{Guid.NewGuid():N}.tmp");
File.WriteAllText(probe, "x");
File.Delete(probe);
}
}
[Fact]
public void ConfigFile_LiegtImKonfigurationsverzeichnis()
{
var path = AppPaths.ConfigFile("master.key");
Path.GetDirectoryName(path).Should().Be(AppPaths.Config.TrimEnd(Path.DirectorySeparatorChar));
Path.GetFileName(path).Should().Be("master.key");
}
[Fact]
public void DataPath_LiegtImDatenverzeichnis()
{
AppPaths.DataPath("Backups").Should().StartWith(AppPaths.Data);
}
[Fact]
public void Describe_NenntAlleDreiOrte()
{
// Wird beim Start geloggt im Betrieb muss sichtbar sein, wohin geschrieben wird.
var text = AppPaths.Describe();
text.Should().Contain("config=").And.Contain("data=").And.Contain("logs=");
}
}