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>
34 KiB
KONZEPT: Deploymentcenter-Integration
Stand: 2026-08-14 · Deploymentcenter-Version 2.5.1 · Quelle:
J:\Softwareprojekte\Deploymentcenter\docs,GET /api/updateservice/v1/changelog?since=2.4Zielprojekt: IBKRTrader (App + Daemon), Projekt-SlugibkrtraderSDK-Zielversion:Deploymentcenter.Client2.5.1 (DeploymentcenterSdk.Versionmitführen, damit künftige Anbindungen über?since=2.5.1direkt sehen, was sich seither geändert hat)
Nachtrag 2026-08-14: Die ursprüngliche Fassung dieses Dokuments (Stand DC 2.4) enthielt sieben Befunde/Bedenken in §8, die dem Deploymentcenter-Entwickler mitgeteilt wurden. Alle sieben sind mit 2.5.0/2.5.1 behoben — Einzelheiten weiterhin in §8, jetzt mit Fix-Vermerk. Zwei Neuerungen aus derselben Runde gehen über die ursprünglichen Befunde hinaus und sind in §5.1, §7.2 und §2.2 eingearbeitet:
exitCurrentAppist jetzt als harter Kill dokumentiert, undBuildInfo.targetsliegt im NuGet-Paket selbst.
Das Deploymentcenter (https://dc.mhdf.de) bündelt fünf Dienste, die IBKRTrader bisher
alle nicht hat: Lizenz, Watchdog, Error-Reporting, Bugtracker und
UpdateService inkl. Erstinstallation. Dieses Dokument beschreibt, in welcher
Reihenfolge sie angebunden werden, was dabei am Projekt vorher aufgeräumt werden muss
und wo die vom Deploymentcenter vorgeschlagenen Muster für ein handelndes System
nicht ohne Weiteres passen.
0. Ausgangslage
| Zustand heute | |
|---|---|
| DC-Anbindung | keine — kein Treffer für Deploymentcenter, dc.mhdf.de oder DC_TOKEN im Quelltext |
| Version | Zeichenkette "1.0.0" in App/Program.cs:34 und Daemon/Program.cs:43, keine <Version>, keine Directory.Build.props |
| Globale Fehlerbehandlung | keine — weder AppDomain.UnhandledException noch TaskScheduler.UnobservedTaskException |
| Auslieferung | dotnet publish von Hand, deploy/README.md, systemd-Unit |
| CI | Gitea Actions, Matrix ubuntu-latest / windows-latest, Checkout nur von IBKRTrader |
Referenzintegration ist Predictalytics (src/Predictalytics.Hosting/Dc*.cs,
docs/BETRIEB-Deploymentcenter.md). Sie liegt auf SDK-Stand 2.2 und ist damit
unvollständig — siehe §8, Punkt D2. Übernommen wird
das Muster, nicht der Code eins zu eins.
Der Einbauort ist IBKRTrader.Hosting: den Host teilen sich Shell und Daemon
(AppHostBuilder), damit gilt jede Anbindung automatisch für beide Betriebsformen.
IBKRTrader.Core bleibt frei davon — der Core soll keine Netzabhängigkeit zu einem
Verwaltungsserver bekommen.
1. Voraussetzungen im Deploymentcenter (vor jedem Code)
Ohne diese Schritte läuft nichts von dem, was danach kommt:
- Projekt
ibkrtraderanlegen (WebUI → Projekte). Slug wird von Lizenz, UpdateService, Fehler-Stream und Bugtracker gemeinsam benutzt. - Offline-Gnadenfrist festlegen (
default_cache_ttl_hours). Vorgabe 168 h. Für einen Handelsdienst, der auch bei gestörter Internetverbindung weiterlaufen soll, ist das die entscheidende Stellschraube — siehe §5. - Lizenz für
ibkrtraderausstellen. Ohne sie kommt die Anwendung nach dem ersten Release nicht mehr an ihre Updates (Zugangsschutz seit 2.4). - Master-Token mit
watchdog:ping,bugtracker:report— für die Anwendung. Zweites Token mitupdateservice:publish— für die Release-Pipeline, gehört nicht in die Anwendung. - Installationskonto (Rolle
installer) für die Erstinstallation. - Evaluator-Cron prüfen.
* * * * * php cli/tick.php --quiet. Ohne ihn bleibt ein abgestürzter Trading-Daemon im Dashboard dauerhaft grün — der Watchdog wäre dekorativ. Derselbe Job erzeugt auch den Release-Zugangsschutz.
2. Build-Grundlage
2.1 Directory.Build.props mit <Version>
<Project>
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
pack-and-deploy liest die Version aus der Hauptassembly und bricht bei einer
Abweichung zu --version ab. Der Grund ist unangenehm genug dafür: Wird 1.0.1
als 1.0.2 veröffentlicht, aktualisiert die gesamte Installationsbasis, meldet
danach weiterhin 1.0.1, hält das Release erneut für neu — und aktualisiert bei
jedem Start wieder.
Gleichzeitig entfallen die beiden hartkodierten "1.0.0" in den Program.cs;
sie werden durch BuildInfo.Version ersetzt.
2.2 SDK-Anbindung — entschieden, in zwei Stufen
Deploymentcenter.Client (netstandard2.0;net8.0) wird von net10.0 problemlos
konsumiert. Seit SDK 2.5.0 liegt BuildInfo.targets im Paket selbst
(build/-Ordner, von NuGet automatisch importiert) — der frühere <Import> per
Cross-Repo-Pfad ist damit nicht mehr nötig. Das entscheidet die Frage
grundsätzlich zugunsten eines echten NuGet-Bezugs. Geprüft (2026-08-14): Weder
https://dc.mhdf.de/nuget/ noch die Gitea-Package-Registry unter
http://192.168.178.10:8418/api/packages/Richard/nuget/ führen aktuell ein
Paket Deploymentcenter.Client — es gibt also noch keine Quelle, gegen die eine
PackageReference auflösen könnte.
Deshalb zweistufig:
| Stufe | Weg | Zustand |
|---|---|---|
| Jetzt (umgesetzt) | Cross-Repo-ProjectReference (Muster Predictalytics), Deploymentcenter.BuildInfo.targets weiterhin per <Import> eingebunden — der pfadbasierte Import funktioniert laut Leitfaden unverändert weiter, auch wenn er nicht mehr nötig wäre |
funktioniert sofort, keine neue Infrastruktur, keine Push-Entscheidung nötig |
| Folgeschritt (offen) | dotnet pack im Deploymentcenter-Repo, Push nach .../api/packages/Richard/nuget/, NuGet.config um Quelle gitea mit packageSourceMapping auf Deploymentcenter.* ergänzen, <Import> durch <PackageReference Include="Deploymentcenter.Client" Version="2.5.1" /> ersetzen |
braucht einen Push in ein fremdes Repo (Deploymentcenter) und Package-Registry-Zugangsdaten — bewusst nicht Teil dieses Durchlaufs |
Der Cross-Repo-Bezug bricht die Gitea-CI von IBKRTrader (der Runner checkt nur dieses eine Repo aus) — siehe P5. Das ist mit der Wahl von Stufe 1 bewusst in Kauf genommen, weil eine funktionierende lokale Integration wichtiger ist als eine grüne CI, die eine noch nicht existierende Paketquelle voraussetzt.
2.3 BuildInfo
Deploymentcenter.BuildInfo.targets in die csproj von IBKRTrader.Hosting
importieren. Die Klasse entsteht in $(RootNamespace), also IBKRTrader.Hosting.BuildInfo
— erreichbar ohne using. Liefert Version, GitCommit, GitCommitShort,
BuildDateUtc, Channel, Summary.
Erledigt mit 2.5.1 (vormals §8 D4):
BuildDateUtctrug die volle Uhrzeit,WriteOnlyWhenDifferentgriff dadurch nie, und jedes importierende Projekt wurde bei jedem Build neu übersetzt. Seit 2.5.1 ist das Feld tagesgenau — höchstens ein Neubau pro Tag. Vorbedingung: die Targets-Datei im Schwester-Repo muss auf 2.5.1 oder neuer stehen (git -C ../Deploymentcenter log -1 --oneline -- client-dotnet/Deploymentcenter.Client/Deploymentcenter.BuildInfo.targets).
3. Konfiguration und gemeinsame Bausteine
3.1 DcConfig (kompiliert, nicht einstellbar)
namespace IBKRTrader.Hosting.Deploymentcenter;
public static class DcConfig
{
public const string BaseUrl = "https://dc.mhdf.de"; // bewusst KEINE Einstellung
public const string ProductSlug = "ibkrtrader";
public const string MonitorGroup = "Trading";
public static string AppVersion => BuildInfo.Version;
public static string GitCommitShort => BuildInfo.GitCommitShort;
}
Die Basis-URL entscheidet, wohin die Lizenzprüfung geht und woher ausführbarer Code kommt. Einstellbar wäre sie eine Einladung, die Anwendung auf einen gefälschten Update-Server zu zeigen.
3.2 Neuer Abschnitt in AppSettings
Ergänzt src/IBKRTrader.Core/Settings/AppSettings.cs um
DeploymentcenterSettings (gleiches Muster wie LoggingSettings, mit
Category/DisplayName/Description, damit es in der Settings-Ansicht auftaucht):
| Feld | Vorgabe | Bedeutung |
|---|---|---|
ApiToken |
(leer) | Authorization: Bearer. Leer = kein Heartbeat, keine Fehlermeldung. [PasswordPropertyText] |
HeartbeatEnabled |
true |
Dead-Man's-Switch |
MonitorSource |
(Hostname) | Monitorname im Dashboard |
MonitorInstance |
default |
mehrere Instanzen (EU/US) desselben Dienstes |
HeartbeatIntervalSeconds |
60 |
warning nach 2×, down nach 4× |
ReportErrors |
true |
Error/Fatal an den Fehler-Stream |
CheckUpdatesOnStart |
true |
prüft nur, installiert nie von selbst |
UpdateChannel |
prod |
prod, beta, dev |
LicenseKey |
(leer) | nur Rückfall; Regelfall ist der verschlüsselte Cache |
ApiToken und LicenseKey sind schutzbedürftig und laufen über
SecretProtection (AES-256-GCM at rest), wie die übrigen Geheimnisse des Projekts.
3.3 DcApiClient
Schlanker JSON-Client für Heartbeat und Fehler-Stream (Lizenz und Update bringen ihren eigenen mit). Übernimmt von Predictalytics:
DcApiExceptionmit stabilemerror.codeaus dem Umschlag{"status":"error","error":{"code":"…"}}— reagiert wird auf den Code, nie auf den Text.IsPermanentfür401/403/unauthorized: danach wird nicht weiter versucht.- Zeitgrenze 10 s. Ein nicht erreichbares Deploymentcenter darf den Handel niemals beeinflussen — jeder Aufruf ist best effort.
4. Watchdog
Neuer Worker DcHeartbeatWorker in IBKRTrader.Hosting, registriert wie die
übrigen (IWorker + AddHostedService), damit er in der Worker-Ansicht
auftaucht und manuell auslösbar ist.
POST /api/watchdog/v1/ping alle n Sekunden mit source, instance,
interval, os, version = BuildInfo.Version, group = "Trading".
4.1 checks — was IBKRTrader wirklich über sich weiß
Ein Heartbeat beweist nur, dass ein Timer läuft. Der klassische Fehlerfall dieses Projekts ist ein anderer: der Prozess lebt, aber die TWS-Verbindung ist seit einer Stunde tot und es kommen keine Kurse mehr.
| Check | Quelle | ok wenn |
|---|---|---|
db |
EF/MySqlConnector, SELECT 1, gedeckelt auf 5 s |
Verbindung steht |
ibkr_gateway |
IBKRGatewayService |
verbunden oder UseTwsApi = false (NullBroker ist ein gewollter Zustand, kein Fehler) |
market_data |
IBKRMarketDataRepository |
letzter Tick jünger als x min — nur wenn TWS aktiv |
worker_engine |
WorkerEngine |
kein Worker im Fehlerzustand |
Bewusst kein Check auf „Trading aktiv": TradingEnabled = false ist ein
gewollter Betriebszustand und würde den Monitor sonst dauerhaft auf warning
halten. Er wandert als Metrik mit.
4.2 metrics
uptime_sec, trading_enabled (0/1), open_positions, orders_today,
account_value, workers_running, memory_mb, decision_journal_lag_sec.
Serverseitig 14 Tage Verlauf mit Abweichung zum 7-Tage-Mittel desselben Monitors — damit lässt sich „heute plötzlich 40 Orders statt 3" erkennen, ohne im Deploymentcenter Schwellwerte für jedes Projekt zu pflegen.
4.3 Sauberes Beenden
Beim geordneten Herunterfahren (SIGTERM/systemd, Fenster schließen) geht ein Ping
mit status: "stopped" raus. Ohne ihn folgt wenige Minuten nach jedem
planmäßigen Neustart ein Fehlalarm. Der Aufruf gehört in StopAsync
vor das Beenden der übrigen Dienste und braucht eine eigene, kurze Zeitgrenze
(3 s), damit ein hängender Monitoring-Server das Herunterfahren nicht blockiert.
4.4 Hierarchie
Läuft der Daemon auf einem eigenen Host, gehört im WebUI unter WatchDog → System-Hierarchie die Übergeordnete Entität auf diesen Host gesetzt. Sonst erzeugt ein Maschinenausfall zwei Meldungen für ein Problem.
5. Lizenz
LicenseGuard in IBKRTrader.Hosting, aufgerufen aus RunStartupChecks —
nach der Zeitzone (Logausgaben) und vor dem Start der Worker.
- Produkt-Slug
ibkrtrader, Hardware-ID v2 (2:win:…/2:lin:…). LicenseClient.DefaultAppVersion = BuildInfo.Versioneinmalig beim Start, sonst trägt jede Installation in der Aktivierungsliste dieselbe1.0.0.- Revalidierung zur Laufzeit alle 12 h; Warnung, wenn die Gnadenfrist in unter 48 h abläuft.
LicenseConfig.SetStorageDirectory(AppPaths.ConfigFile("license"))— ausdrücklich setzen. Die Vorgabe des SDK löst unter Linux über$XDG_CONFIG_HOMEbzw.$HOMEauf; ein systemd-Dienst mitUser=ibkrtraderund ohne Heimatverzeichnis landet im Rückfall aufAppContext.BaseDirectory/license, also unter/opt— dort ist nichts schreibbar, und es gäbe keinen Offline-Cache. MitAppPathsfügt sich die Ablage in die bereits vorhandene Verzeichnisauflösung des Projekts ein. Erledigt/entschärft mit 2.5.1 (vormals §8 D4): der Rückfall ohne$HOMEzeigt jetzt serverseitig auf/var/lib/<slug>statt ins Installationsverzeichnis — unser eigenes explizites Setzen bleibt trotzdem die robustere Wahl und ist unabhängig vom SDK-Rückfall. ⚠️ Breaking Change 2.5.1:SetStorageDirectoryhängt jetzt selbst<slug>/licensean (vorher wurde der Pfad roh benutzt). Der Aufruf wird zuLicenseConfig.SetStorageDirectory(AppPaths.Config)— ohne das eigene"license"-Suffix, sonst entstünde…/license/ibkrtrader/license.
5.1 Kopflose Schalter
Konvention aus dem Lizenz-Leitfaden, im Daemon umzusetzen (das SDK bringt keine CLI mit):
IBKRTrader.Daemon --license-status
IBKRTrader.Daemon --license-set-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
--license-deactivate nicht verdrahten: es verlangt den shared_key des
Servers, der gehört nicht in eine ausgelieferte Anwendung. Freigeben läuft über
das WebUI (Hardware-Liste → „Freigeben").
Geändert mit 2.5.0: der
update-agentnimmt den Lizenzschlüssel nicht mehr als--license-key-Argument entgegen —pszeigt Prozessargumente jedem Nutzer der Maschine, dieselbe Begründung wie beim Crontab-Verzicht in UPGRADE §5. Der Weg ist jetzt die UmgebungsvariableDC_LICENSE_KEY(LaunchUpdateAgentsetzt sie selbst, wennlicenseKeyübergeben wird — kein eigener Code nötig). Unsere--license-set-key-Implementierung ruft alsoLicenseClient.ValidateAsyncdirekt auf und schreibt nicht--license-keyin einen eigenen Aufruf des Agenten.Ebenfalls neu:
EnsureLicensedAsync()aufLicenseClient— nimmt den zwischengespeicherten Schlüssel, fragt bei Bedarf überILicensePromptnach, prüft, fragt bei Ablehnung erneut.allowPrompt:falselehnt ohne Cache ab, statt im Daemon auf eine Eingabe zu warten, die nie kommt — das ist für den Daemon die richtige Einstiegsmethode, nicht das manuelle Zusammensetzen ausTryGetCachedKey+ValidateAsync.
5.2 Reaktion — hier weicht IBKRTrader bewusst vom Leitfaden ab
Die DC-Doku empfiehlt bei einem endgültigen Urteil Environment.Exit(1).
Für einen Dienst mit offenen Positionen ist das die falsche Reaktion. Ein
abgelaufener Vertrag ist kein Grund, ein Portfolio unbeaufsichtigt zu lassen —
Stop-Loss- und Exit-Logik müssen weiterlaufen, sonst richtet die Lizenzdurchsetzung
mehr Schaden an als der unlizenzierte Betrieb.
Vorschlag — drei Stufen statt zwei:
| Ergebnis | Zustände | Reaktion |
|---|---|---|
IsValid |
valid, valid_offline |
Normalbetrieb |
IsTransient |
server_unavailable, cache_expired |
kein Urteil, nur keine Auskunft. Warnen, weiterlaufen, beim nächsten Durchlauf erneut fragen |
| Urteil | revoked, expired, not_found, activation_limit, suspended, clock_rollback |
Sperrbetrieb: TradingEnabled hart auf false, keine neuen Einstiege, Risiko-/Exit-Worker und Buchhaltung laufen weiter, Meldung an Watchdog (status: "warning") und Fehler-Stream. Beendet wird erst, wenn keine Position mehr offen ist — oder auf ausdrückliche Anweisung |
Bei der Desktop-Shell darf es beim Start weiterhin ein Lizenzfenster geben; der Daemon darf niemals auf eine Eingabe warten.
6. Error-Reporting
Zwei Wege in POST /api/errors/v1/report, beide in IBKRTrader.Hosting:
- Senke am
LoggingService.LoggingService.EntryWrittenexistiert bereits und ist genau dafür gedacht (die Oberfläche hängt schon dran). EinDcErrorSinkfiltert aufAppLogLevel.Errorund meldet mitlevel: "error",build: BuildInfo.Version,file/lineaus dem Stacktrace,contextmitGitCommitShortundCorrelationId/SignalId, wenn vorhanden. DieSignalIdmitzuschicken ist der eigentliche Gewinn: eine Fehlermeldung lässt sich damit im Deploymentcenter direkt gegen das Entscheidungsjournal halten. - Globale Handler —
AppDomain.UnhandledException(level: "fatal", blockierend gesendet, der Prozess ist gleich weg) undTaskScheduler.UnobservedTaskException. Beide fehlen im Projekt heute vollständig; sie müssen sowieso her.
6.1 Bremse
Verpflichtend, sonst verbrennt ein Fehler in einer Handelsschleife das serverseitige Rate-Limit binnen Sekunden:
- höchstens 20 Meldungen pro Minute,
- derselbe Fehler höchstens alle 5 Minuten,
- bei
401/403dauerhaft abschalten, - bei
429Intervall verdoppeln.
Betriebsbedingtes Rauschen gehört nicht in diese Bremse, sondern in die Ignore-Regeln des WebUI: dort wird weitergezählt, aber nicht gemeldet — und eine Alarmschwelle schlägt an, wenn ein bekannter Fehler plötzlich hundertmal so oft auftritt.
Nicht melden: alles, was Kontodaten, Positionen oder Schlüsselmaterial im Text führen könnte. Der Fehler-Stream ist eine externe Ablage. Vor dem Senden wird der Text durch dieselbe Maskierung geschickt, die schon für die Logs gilt.
7. UpdateService, Erstinstallation und Release-Pipeline
7.1 Prüfen, nicht installieren
DcUpdateService.CheckAsync beim Start (GET /api/updateservice/v1/check).
Mit Zugangsdaten, sonst gibt es seit 2.4 nur noch 401:
var key = settings.Deploymentcenter.LicenseKey is { Length: > 0 } k
? k
: LicenseClient.TryGetCachedKey(DcConfig.ProductSlug);
var check = await new UpdateClient().CheckForUpdateAsync(
baseUrl: DcConfig.BaseUrl,
projectId: DcConfig.ProductSlug,
currentVersion: BuildInfo.Version,
channel: settings.Deploymentcenter.UpdateChannel,
credentials: ReleaseCredentials.FromLicenseKey(key),
cancellationToken: ct);
if (check.Unauthorized) { /* Lizenzproblem, KEIN Netzwerkfehler — eigene Meldung */ }
Gefunden wird nur, installiert wird nichts von selbst. Ein Trading-Dienst, der sich unangekündigt neu startet, ist keine akzeptable Vorgabe.
- Kein Update → Info ins Log.
- Update verfügbar → Warnung + Hinweis in der Shell.
- Kritisch → zusätzlich Dialog (Shell) bzw. Watchdog-Event (Daemon).
7.2 Anwenden
Über UpdateClient.LaunchUpdateAgent(..., licenseKey: key); der Agent bekommt
--restart, --wait-for-pid, --platform von selbst, den Lizenzschlüssel seit
2.5.0 nicht mehr als --license-key-Argument, sondern über die
Umgebungsvariable DC_LICENSE_KEY (siehe §5.1). Vorher geht ein
status: "stopped" an den Watchdog.
Vorbedingung, die die Anwendung selbst prüfen muss: kein offener Auftrag, keine laufende Order. Ein Update, das den Prozess zwischen Order-Absendung und Statusrückmeldung abräumt, hinterlässt eine Order bei IBKR ohne Gegenstück im Journal. Das Update wird also erst nach dem Handelsschluss bzw. bei leerem Auftragsbuch angeboten.
Verschärft mit 2.5.0:
exitCurrentApp: trueist jetzt ausdrücklich als harter Kill dokumentiert — laufendefinally-Blöcke,IHostApplicationLifetimeund Destruktoren kommen nicht mehr zum Zug. Für IBKRTrader heißt das: Solange die Vorbedingung oben nicht sicher geprüft ist (offenes Auftragsbuch, laufende Buchungstransaktion), mussexitCurrentApp: falsegesetzt werden. Der Agent wartet über--wait-for-pidohnehin auf das Prozessende; das eigene Herunterfahren läuft dann überhost.StopAsync(...)wie inApp/Program.csbereits vorhanden — nicht überLaunchUpdateAgentselbst. ZusätzlichwaitTimeoutSecondsheraufsetzen, falls das geordnete Beenden (EF-Flush, Kestrel, Log-Flush, DB-Backup-Worker) länger als die Vorgabe von 60 s dauern kann — läuft die Zeit ab, bricht der Agent ohne Änderung ab.
Für den Linux-Dienst siehe §8 D3 — dafür gibt es im Deploymentcenter derzeit keinen fertigen Weg.
7.3 Was ausgeliefert wird — Ausschließen vs. Schützen
Das ist die Stelle, an der Updates in anderen Projekten schon Konfigurationen zerstört haben. Für IBKRTrader gilt:
| Datei | Liste | Begründung |
|---|---|---|
settings.example.json |
(im Paket) | Vorlage mit Platzhaltern — Erstinstallation braucht sie |
settings.json |
preservePatterns |
enthält DB-Zugang, DC-Token, Flex-Token. Muss ein Update überleben |
appsettings.Local.json |
preservePatterns |
Verbindungszeichenfolge des Zielsystems |
master.key, openrouter.key |
excludePatterns |
darf nie ins Paket |
Logs/**, Backups/** |
excludePatterns |
Laufzeitdaten |
*.pdb, *.xml |
excludePatterns |
Build-Artefakte |
update-agent* |
bewusst entscheiden | mitliefern = aktualisiert sich selbst mit |
pack-and-deploy warnt von sich aus bei Dateien, die nach Zugangsdaten aussehen.
Die Warnung ist ernst zu nehmen: Release-Pakete liegen zwar hinter einem
Zugangsschutz, aber jeder lizenzierte Kunde kann sie auspacken.
7.4 setup.json
Beschreibt der Erstinstallation, was abzufragen ist; wird ins Paket kopiert. Entwurf für IBKRTrader:
{
"schema": 1,
"displayName": "IBKRTrader",
"targets": [
{ "id": "app", "file": "settings.json", "location": "install", "format": "json" }
],
"fields": [
{ "key": "Database:Host", "label": "MySQL-Host", "type": "string" },
{ "key": "Database:Database", "label": "Datenbank", "type": "string" },
{ "key": "Database:User", "label": "Benutzer", "type": "string" },
{ "key": "Database:Password", "label": "Passwort", "type": "secret" },
{ "key": "IBKR:Host", "label": "TWS/Gateway", "default": "127.0.0.1" },
{ "key": "IBKR:Port", "label": "Port (Paper 4002 / Live 4001)", "type": "int", "default": "4002" },
{ "key": "Trading:ApplicationTimeZoneId", "label": "Betriebszeitzone", "default": "Europe/Berlin" },
{ "key": "Deploymentcenter:BaseUrl", "source": "detect:baseurl", "type": "url" },
{ "key": "Deploymentcenter:ApiToken", "label": "Token dieser Installation",
"source": "provision", "scopes": ["watchdog:ping", "bugtracker:report"] },
{ "key": "Deploymentcenter:MonitorSource", "source": "detect:hostname" }
]
}
Zwei Punkte dazu:
location: "install"ist nur unter Windows richtig — und auch dort nur, solange nicht nachC:\Program Filesinstalliert wird. Siehe §8 P2; solangeAppPathsnicht korrigiert ist, passensetup.jsonund die tatsächliche Leseposition der Anwendung unter Linux nicht zusammen. Dieser Punkt blockiert die Erstinstallation und gehört vor §7.4.- Der Installer schreibt Klartext.
SecretProtectionmuss also Klartext unverändert durchreichen und beim ersten Speichern verschlüsseln — die Weiche ist zu prüfen und im Test ausdrücklich als aktiv genutzt zu beschreiben, damit sie nicht eines Tages als tot entfernt wird.
7.5 Release-Pipeline
scripts/release.ps1 und scripts/release.sh sind per curl von
/docs/release-template/ geholt (wie release.md empfiehlt, statt aus dem
lokalen Schwester-Repo kopiert — damit ist sichergestellt, dass die kanonische
Fassung im Projekt liegt) und unverändert gelassen. Nur
scripts/release.config.json ist ausgefüllt:
{
"project": "ibkrtrader",
"csproj": "src/IBKRTrader.Daemon/IBKRTrader.Daemon.csproj",
"runtimes": ["win-x64", "linux-x64"],
"selfContained": true,
"setupJson": "setup.json"
}
Entschieden: nur der Daemon, keine Zwei-Produkte-Aufteilung. IBKRTrader hat
zwei Einstiegspunkte (App = Avalonia-Shell, Daemon = kopflos). Die Vorlage
nimmt aber genau eine csproj für alle Runtimes in der Liste — sie ruft
dotnet publish <csproj> -r <rid> in einer Schleife auf, es gibt keine
Möglichkeit, App für win-x64 und Daemon für linux-x64 in demselben Lauf
zu veröffentlichen, ohne das (laut Anleitung unveränderte) Skript anzufassen.
Ursprünglich in diesem Dokument vorgeschlagen war genau das ("Windows = App,
Linux = Daemon") — das ist mit der Vorlage so nicht umsetzbar und wurde beim
Umsetzen korrigiert.
IBKRTrader.Daemon ist der bewusste Kandidat: laut README „plattformneutral"
(net10.0 ohne Plattformbindung), läuft kopflos auf beiden Zielsystemen und
ist ohnehin die für den Serverbetrieb vorgesehene Form (deploy/README.md).
Eine automatisierte Auslieferung der Avalonia-Shell (App) als
Windows-Desktop-Distribution ist damit nicht Teil dieses Durchlaufs — dafür
bräuchte es entweder einen zweiten Produkt-Slug (ibkrtrader-app, doppelte
Lizenz-/Update-Pflege) oder ein angepasstes Release-Skript. Zurückgestellt, bis
tatsächlich Bedarf an einer automatisierten Desktop-Auslieferung besteht — bis
dahin bleibt dotnet run --project src/IBKRTrader.App bzw. eine manuelle
dotnet publish der übliche Weg für die Shell.
Zugangsdaten kommen aus DC_FTP_HOST / DC_FTP_USER / DC_FTP_PASS / DC_TOKEN,
.dc-tools/ und artifacts/ sind in der .gitignore.
packager.config.json (excludePatterns/preservePatterns) ist nicht Teil
dieses Durchlaufs. Sie liegt "neben dem Werkzeug", also unter .dc-tools/ —
das entsteht erst, wenn pack-and-deploy beim ersten echten Lauf von
release.ps1/release.sh selbst heruntergeladen wird (Prüfsummen-geprüft).
Ein erster Release-Lauf braucht DC_FTP_*/DC_TOKEN und ist damit eine
credentialierte, vom Betreiber auszulösende Aktion. Sicherheitsnetz bis
dahin: Der Packager bricht seit 2.5.0 ohnehin hart ab, wenn er im
Publish-Verzeichnis nach Zugangsdaten aussehende Dateien findet (Name und
Inhalt) — die in §7.3 beschriebenen Dateien (settings.json,
appsettings.Local.json) sind ohnehin gitignored und landen nur dann im
Publish-Output, wenn sie lokal im Projektverzeichnis liegen (<None Update Condition="Exists(...)"> in den .csproj-Dateien kopiert sie dann mit). Bricht
release.ps1 mit einem Hinweis auf eine vermutete Zugangsdatei ab, ist das der
Packager, der genau das abfängt — kein Fehlschlag, sondern die Absicherung, die
2.5.0 dafür eingeführt hat.
8. Befunde und offene Punkte
Status 2026-08-14: Alle sieben ursprünglich an den Deploymentcenter-Entwickler gemeldeten Befunde (D1–D7) sind mit den Versionen 2.5.0/2.5.1 behoben — bestätigt über
GET /api/updateservice/v1/changelog?since=2.4. Der Abschnitt bleibt als Nachweis stehen, jeder Punkt trägt jetzt einen Fix-Vermerk. Die Projekt-Befunde (P1–P6) sind davon unabhängig und weiterhin offen — sie werden in diesem Durchlauf abgearbeitet.
Am Projekt (P) — muss vor der Integration bereinigt werden
P1 · Echte Zugangsdaten als Vorgabewerte im Quelltext.
src/IBKRTrader.Core/Settings/AppSettings.cs:14-35 trägt Host, Datenbankname,
Benutzer und Passwort einer produktiven MariaDB als Vorgabe. Die Datei ist
versioniert, die Werte stehen damit auch in der Git-Historie — settings.json
im .gitignore hilft dagegen nicht. Das Passwort ist als kompromittiert zu
behandeln und zu wechseln; die Vorgaben gehören auf Platzhalter. Zusätzlich
brisant, weil die Vorlage künftig im Release-Paket ausgeliefert wird.
Verschärft mit 2.5.0: der Packager bricht jetzt hart ab, wenn er Zugangsdaten im Publish-Verzeichnis erkennt (Dateiname und Inhalt, u. a. gefülltes
Password=) — vorher nur eine Warnung, die auf der Preserve-Liste sogar unterdrückt wurde. Damit ist P1 nicht mehr „sollte bereinigt werden", sondern release-blockierend: Ohne Fix lässt sichsettings.example.jsongar nicht mehr veröffentlichen.
P2 · AppPaths fällt unter Windows auf einen Linux-Pfad zurück.
Configuration/AppPaths.cs:57-71: ist das Binärverzeichnis nicht beschreibbar,
gilt der FHS-Rückfall /etc/ibkrtrader. Unter Windows löst .NET das zu
C:\etc\ibkrtrader auf. Genau dieser Fall tritt ein, sobald der DC-Installer
seinen Windows-Vorschlag Programme\<slug> benutzt — dann liegen settings.json,
master.key und Logs/ in C:\etc\. Nötig: plattformabhängiger Rückfall
(%ProgramData%\IBKRTrader bzw. %APPDATA%).
P3 · Keine globalen Ausnahmebehandler. Ohne
AppDomain.UnhandledException und TaskScheduler.UnobservedTaskException gibt es
keine fatal-Meldungen — und heute auch keinen Logeintrag, wenn der Prozess
wegbricht. Unabhängig vom Deploymentcenter überfällig.
P4 · Version an zwei Stellen hartkodiert. App/Program.cs:34 und
Daemon/Program.cs:43. Ohne Directory.Build.props bricht pack-and-deploy beim
ersten Release ab — zu Recht.
P5 · CI kennt das Schwester-Repo nicht. .gitea/workflows/build.yml checkt
nur IBKRTrader aus. Eine ProjectReference nach ..\..\..\..\Deploymentcenter\…
macht den Build auf beiden Matrix-Zielen rot. Siehe die Entscheidung in §2.2.
P6 · Konfiguration liegt im Installationsverzeichnis. Unter Windows schreibt
AppPaths neben die Binärdatei. Das ist mit preservePatterns beherrschbar
(§7.3), muss aber beim ersten Release stimmen — ein Update, das settings.json
überschreibt, nimmt einer laufenden Installation Datenbank, Token und Flex-Zugang
gleichzeitig.
Am Deploymentcenter (D) — gemeldet, Stand vor 2.5.0
D1 · Der Lizenzschlüssel als Update-Zugang war nirgends vollständig erklärt.
✅ Behoben (2.5.1, Doku). Laut Changelog explizit als „Lückenschluss" genannt:
„Woher die Anwendung den Lizenzschlüssel für den Update-Zugang nimmt
(TryGetCachedKey)". Für uns bleibt zusätzlich EnsureLicensedAsync() (neu in
2.5.0) relevant — siehe §5.1.
D2 · Predictalytics wurde vom Zugangsschutz ausgesperrt.
✅ Bestätigt und behoben (2.5.1). Der Changelog bestätigt den Verdacht wörtlich:
„bei Predictalytics ist genau das passiert". Fix: Zugangsschutz ist jetzt je
Produkt abschaltbar (WebUI → UpdateService → Zugangsschutz) — für Installationsbasen
ohne SDK 2.4 lässt sich das Produkt ausnehmen, mit Schlüssel neu ausliefern, danach
wieder einschalten. Für IBKRTrader heißt das: genau diese Falle von Anfang an
vermeiden, indem der erste ausgelieferte Build bereits licenseKey mitgibt (§7.5,
§9 Reihenfolge).
D3 · Für einen systemd-Dienst gab es keinen dokumentierten Update-Weg.
✅ Behoben (2.5.1, Doku). Laut Changelog ergänzt: „wie ein laufender
systemd-Dienst aktualisiert wird". Bleibt in §7.2/§8-P laufend zu beobachten, ob
die neue Beschreibung mit unserem tatsächlichen Vorgehen (--target-dir,
Dienstbenutzer-Rechte) übereinstimmt — bei der Umsetzung von Aufgabe „Update-Prüfung
mit ReleaseCredentials" gegenprüfen.
D4 · LicenseConfig.GetStorageDirectory/SetStorageDirectory war für Dienste
unglücklich. ✅ Behoben, mit Breaking Change (2.5.1). Zwei Korrekturen:
(a) Rückfall ohne $HOME zeigt jetzt auf /var/lib/<slug> statt ins
Installationsverzeichnis. (b) SetStorageDirectory hängt jetzt selbst
<slug>/license an, statt den Pfad roh zu verwenden — das ändert unseren
Aufruf, siehe §5 (AppPaths.Config statt AppPaths.ConfigFile("license")).
D5 · BuildInfo.targets verhinderte inkrementelle Builds.
✅ Behoben (2.5.1). BuildDateUtc ist jetzt tagesgenau statt mit voller Uhrzeit,
WriteOnlyWhenDifferent greift wieder. Siehe §2.3.
D6 · Die Empfehlung „bei einem Urteil beenden" passte für handelnde Systeme nicht.
✅ Behoben (2.5.1, Doku) — als Lückenschluss genannt: „was statt
Environment.Exit(1) zu tun ist, wenn Beenden die gefährlichere Option wäre".
Unser Sperrbetrieb-Ansatz (§5.2) bleibt die konkrete Umsetzung für IBKRTrader.
Zusätzlich, über D6 hinausgehend: 2.5.0 dokumentiert exitCurrentApp: true
jetzt als harten Kill ohne finally/Lifecycle-Hooks — praktisch relevanter für uns
als die Lizenz-Frage allein, siehe §7.2.
D7 · Kleinigkeiten (README-Verweis auf AGENT_RELEASE_GUIDE.md, Rate-Limits
nicht durchgängig beziffert). ✅ Behoben (2.5.1, Doku) — Changelog nennt
„die Drosselungsgrenzen aller Endpunkte" als ergänzt; docs/README.md führt jetzt
/docs/release.md und den Changelog-Endpunkt im Index.
9. Reihenfolge
| Schritt | Inhalt | Abhängig von | Stand |
|---|---|---|---|
| 0 | P1 Zugangsdaten wechseln, Vorgaben auf Platzhalter | — | ⬜ dieser Durchlauf |
| 1 | P2 AppPaths-Rückfall plattformabhängig, P4 Directory.Build.props |
— | ⬜ dieser Durchlauf |
| 2 | SDK-Bezug Stufe 1: Cross-Repo-ProjectReference (§2.2) |
— | ⬜ dieser Durchlauf |
| 3 | DcConfig, DeploymentcenterSettings, DcApiClient, BuildInfo |
1, 2 | ⬜ dieser Durchlauf |
| 4 | P3 globale Handler + Fehler-Stream | 3 | ⬜ dieser Durchlauf |
| 5 | Watchdog-Heartbeat inkl. stopped |
3 | ⬜ dieser Durchlauf |
| 6 | Lizenz mit Sperrbetrieb statt Abbruch (§5.2), EnsureLicensedAsync |
3 | ⬜ dieser Durchlauf |
| 7 | Update-Prüfung mit ReleaseCredentials, exitCurrentApp:false bei offenem Zustand |
6 (braucht den Schlüssel) | ⬜ dieser Durchlauf |
| 8 | setup.json, preserve/exclude, Release-Pipeline (Vorlage per curl von /docs/release-template/) |
1–7 | ⬜ dieser Durchlauf |
| 9 | Bugtracker-Baustein nach AGENTS.md / .agents/rules |
Projekt-Slug angelegt | ⬜ separat (Projekt ibkrtrader muss im WebUI erst angelegt werden) |
| 10 | Folgeschritt, nicht Teil dieses Durchlaufs: Deploymentcenter.Client als NuGet-Paket in die Gitea-Registry pushen, <Import> durch PackageReference ersetzen, Gitea-CI (P5) auf den Paketbezug umstellen |
2 | ⬜ offen — siehe §2.2 |
Schritt 7 und 8 hängen zusammen: der erste ausgelieferte Build muss die
Zugangsdaten schon mitbringen. Für ein Produkt, das noch nie veröffentlicht hat,
entsteht /releases/ibkrtrader/ mit dem ersten Upload und wird vom nächsten
tick.php-Lauf geschützt — es gibt kein Zeitfenster, das SDK danach nachzurüsten.
Wird Schritt 7 übersprungen, fällt die Tür hinter dem ersten Release zu (genau der
Fall, der laut Changelog bei Predictalytics bereits eingetreten ist, siehe D2).
Schritt 9 (Bugtracker) setzt voraus, dass das Projekt ibkrtrader im
Deploymentcenter-WebUI angelegt ist und ein Token mit bugtracker:report
vorliegt — beides serverseitige Handarbeit, die dieser Durchlauf nicht auslösen
kann. Wird zurückgestellt.