Files
PolyTraderSharp/docs/pruefplaene/PRUEFPLAN-Linux-Betrieb.md
T
RichardandClaude Opus 5 e14dd25979
CI / Build & Tests (Linux) (push) Waiting to run
CI / Plattformneutralität & Hygiene (push) Waiting to run
Pruefplan fuer den ersten Linux-Lauf (A4a)
docs/pruefplaene/PRUEFPLAN-Linux-Betrieb.md: Schritt-fuer-Schritt-Protokoll zum
Abarbeiten auf einer Test-VM, jeder Schritt mit erwartetem Ergebnis und der
Angabe, was zurueckzumelden ist.

Zwei Fallen vorab, die beim Vorbereiten aufgefallen sind:

1. Der Linux-Publish enthaelt appsettings.Local.json mit den echten
   MySQL-Zugangsdaten. Ungeprueft auf die VM kopiert, laeuft der Test gegen die
   PRODUKTIVdatenbank - und der MarketSyncService schreibt dort als
   Hintergrunddienst hinein. Der Plan legt deshalb zuerst eine eigene
   Testdatenbank samt eigenem Benutzer an; das Umstellen der Verbindung ist
   ausdruecklich als wichtigster Schritt markiert.
2. Es gibt keine automatischen Migrationen beim Start (nachgeprueft: kein
   Migrate()/EnsureCreated() im Code). Das Schema muss vorher stehen - fuenf
   DbContexts, jeder mit eigenen Migrationen, ueber die Umgebungsvariable
   POLYTRADER_MYSQL.

Inhaltliche Schwerpunkte des Plans:
- Die Hypothese, dass --headless OHNE GUI-Bibliotheken auskommt, weil
  BuildAvaloniaApp() dort nie aufgerufen wird und Skia damit nie geladen wird.
  Der Plan prueft das bewusst zuerst ohne die Pakete - das Ergebnis entscheidet,
  was spaeter ins Server-Grundimage muss.
- Die WorkingDirectory-Falle aus D-11: server_settings.xml relativ zum
  Arbeitsverzeichnis, master.key relativ zur Programmdatei. Bei Abweichung legt
  ServerSettings.Load() kommentarlos eine neue Datei an. Wird per find ueber
  frisch geaenderte Dateien nachgewiesen.
- SIGTERM-Shutdown gegen TimeoutStopSec=45 gemessen, Restart=on-failure
  per pkill -9 gegengetestet.
- T2 (TerminalLogger stempelt DateTime.Now statt AppTimeZone) wird auf einer
  UTC-VM erstmals nachweisbar statt nur behauptet.
- Logrotate mit copytruncate, weil die App die Logdatei offen haelt.
- Master-Key als Umgebungsvariable im Dienst: erster echter Lauf von
  FilePermissions unter Linux-Rechten.

Am Ende eine Liste der zehn zurueckzumeldenden Ausgaben und die Befehle zum
Aufraeumen der VM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 22:33:00 +02:00

412 lines
14 KiB
Markdown

# Prüfplan: Linux-Betrieb (Roadmap A4a)
**Erstellt: 23.08.2026** · Für den ersten Lauf auf einer Test-VM.
Roadmap-Bezug: [A4a](../ROADMAP.md)
> **Warum das wichtig ist:** PolyTrader ist **noch nie auf Linux gelaufen**. Verifiziert ist
> bisher nur, dass sich die Anwendung für `linux-x64` übersetzen und publishen lässt — das ist
> etwas völlig anderes. Dieser Durchgang klärt, ob sie dort auch *startet*, *läuft* und sich
> *sauber beenden* lässt.
**So arbeitest du damit:** Schritte der Reihe nach abarbeiten. Bei jedem Schritt steht, was
erwartet wird und **was du mir melden sollst**. Wenn etwas abweicht: abbrechen, Ausgabe schicken —
nicht selbst reparieren, sonst wissen wir am Ende nicht, was der Ausgangszustand war.
---
## ⚠️ Vorab: zwei Dinge, die schiefgehen können
### 1. Nicht gegen die Produktivdatenbank testen
Der Publish enthält **`appsettings.Local.json` mit deinen echten MySQL-Zugangsdaten**. Startest du
die Anwendung so auf der VM, verbindet sie sich mit deiner **produktiven Datenbank** — und der
`MarketSyncService` läuft als Hintergrunddienst und **schreibt** dort hinein. Zwei Instanzen auf
derselben Datenbank sind unsauber.
**Deshalb: eigene Testdatenbank** (Schritt 1.3). Kein Weltuntergang, wenn es passiert — es gibt
keine automatischen Migrationen beim Start, und ohne Lizenz laufen die Trading-Module gar nicht.
Aber sauber ist anders.
### 2. Zugangsdaten gehen über die Leitung
`appsettings.Local.json` ist eine Secret-Datei. Beim Kopieren auf die VM landet sie dort im
Klartext. Für eine Test-VM in deinem Netz vertretbar — aber setz dort die **Testdatenbank**-Daten
ein, nicht die produktiven (siehe 1.3), dann ist das Risiko gegenstandslos.
---
## Teil 1 — Vorbereitung auf dem Windows-Rechner
### 1.1 Publish erstellen
```bash
dotnet publish src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj -c Release -r linux-x64 --self-contained false -o ./publish-linux
```
**Erwartet:** 0 Fehler, ~41 MB, darin `PolyTrader.App.Avalonia` (ohne Endung),
`libSkiaSharp.so`, `libHarfBuzzSharp.so`, `appsettings.json`, `setup.json`.
> `--self-contained false` ist Absicht: Das ist der Weg, den auch der Deploymentcenter-Packager
> geht. Wir testen die echte Auslieferungsform, nicht eine bequemere. Dafür braucht die VM die
> .NET-10-Runtime (Schritt 2.2).
### 1.2 Testdatenbank anlegen
Auf deinem MySQL-Server:
```sql
CREATE DATABASE polytrader_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
Und einen eigenen Benutzer dafür — nicht den produktiven wiederverwenden:
```sql
CREATE USER 'polytrader_test'@'%' IDENTIFIED BY '<eigenes-passwort>';
GRANT ALL PRIVILEGES ON polytrader_test.* TO 'polytrader_test'@'%';
FLUSH PRIVILEGES;
```
### 1.3 Schema in die Testdatenbank migrieren
Es gibt **keine automatischen Migrationen beim Start** — das Schema muss vorher stehen. Die
Design-Time-Factories lesen die Verbindung aus `POLYTRADER_MYSQL`. Fünf DbContexts, jeder mit
eigenen Migrationen:
```bash
export POLYTRADER_MYSQL="Server=<db-host>;Port=3306;Database=polytrader_test;User ID=polytrader_test;Password=<passwort>;"
dotnet ef database update --project src/PolyTrader.Core
dotnet ef database update --project src/PolyTrader.Modules.CopyTrading
dotnet ef database update --project src/PolyTrader.Modules.ResolutionFarming
dotnet ef database update --project src/PolyTrader.Modules.Supervisor
dotnet ef database update --project src/PolyTrader.Modules.Accounting
```
Falls `dotnet ef` fehlt: `dotnet tool install --global dotnet-ef`
**Erwartet:** Fünfmal „Done." **Melde mir:** ob alle fünf durchlaufen — und bei Fehlern die
vollständige Meldung. Prüfen mit:
```sql
USE polytrader_test; SHOW TABLES;
```
Erwartet: Tabellen mit den Präfixen `core_`, `ct_`, `rf_`, `sup_`, `acc_`.
### 1.4 Zugangsdaten für die VM eintragen
In `./publish-linux/appsettings.Local.json` die Verbindung auf die **Testdatenbank** ändern:
```json
{
"Database": {
"MySqlConnectionString": "Server=<db-host>;Port=3306;Database=polytrader_test;User ID=polytrader_test;Password=<passwort>;"
}
}
```
⚠️ **Dieser Schritt ist der wichtigste des ganzen Prüfplans.** Wird er vergessen, läuft der Test
gegen die Produktivdatenbank.
### 1.5 systemd-Unit dazulegen und übertragen
Die Unit liegt im Repo, **nicht** im Publish — sie muss mit:
```bash
cp deploy/polytrader.service ./publish-linux/
scp -r ./publish-linux <user>@<vm-host>:/tmp/polytrader
```
---
## Teil 2 — VM vorbereiten
### 2.1 Bestandsaufnahme
```bash
cat /etc/os-release; uname -m; timedatectl | head -3; free -m | head -2
```
**Melde mir die vollständige Ausgabe.** Danach richten sich die Paketnamen in 2.2/2.3, und die
Zeitzonen-Angabe brauche ich für Schritt 6.
### 2.2 .NET-10-Runtime installieren
Die Anwendung ist framework-abhängig gebaut, sie braucht die **ASP.NET-Core-Runtime nicht**, wohl
aber die **.NET-Runtime 10**.
**Debian/Ubuntu:**
```bash
sudo apt-get update && sudo apt-get install -y dotnet-runtime-10.0
```
Falls das Paket in den Distributionsquellen fehlt, über Microsofts Quelle:
```bash
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --runtime dotnet --install-dir /usr/share/dotnet
sudo ln -sf /usr/share/dotnet/dotnet /usr/bin/dotnet
```
**RHEL/Fedora:** `sudo dnf install -y dotnet-runtime-10.0`
Prüfen:
```bash
dotnet --list-runtimes
```
**Erwartet:** eine Zeile `Microsoft.NETCore.App 10.0.x`. **Melde mir die Ausgabe.**
### 2.3 Native Abhängigkeiten
`libSkiaSharp.so` und `libHarfBuzzSharp.so` liegen zwar bei, brauchen aber Systembibliotheken.
> **Hypothese, die dieser Test klärt:** Im `--headless`-Betrieb wird Avalonia gar nicht
> hochgefahren (`BuildAvaloniaApp()` wird nicht aufgerufen), also sollte Skia nie geladen werden
> und **keine** GUI-Bibliothek nötig sein. Das ist die Erwartung — bestätigt ist sie nicht.
> **Deshalb: 2.3 zunächst überspringen** und direkt Schritt 3 versuchen. Nur wenn dort ein
> `DllNotFoundException` oder `Unable to load shared library` auftaucht, hierher zurückkommen:
```bash
sudo apt-get install -y libfontconfig1 libice6 libsm6 libx11-6
```
**Melde mir:** ob Schritt 3 ohne diese Pakete lief. Das ist ein echtes Ergebnis — es entscheidet,
was später ins Server-Grundimage muss.
---
## Teil 3 — Erststart von Hand
Bewusst noch **ohne** systemd: erst wissen, ob das Programm überhaupt läuft.
```bash
cd /tmp/polytrader
chmod +x PolyTrader.App.Avalonia
./PolyTrader.App.Avalonia --headless
```
**Erwartet:** Startmeldungen; darin sinngemäß
`Kopfloser Betrieb: keine Oberfläche, Trading-Dienste laufen. Beenden mit Strg+C.`
Dazu eine Warnung über die fehlende Lizenz und den **eingeschränkten Modus** — das ist **richtig
so**: Ohne Deploymentcenter-Lizenz startet die Core-Shell ohne Trading-Module, und genau das
wollen wir hier.
Ein paar Minuten laufen lassen, dann `Strg+C`.
**Melde mir:**
1. Die **vollständige Ausgabe** vom Start bis nach `Strg+C`.
2. Ob der Prozess auf `Strg+C` **sauber** endet oder hängt.
3. Ob eine `Logs/`-Datei entstanden ist: `ls -la Logs/`
**Wenn es hier abbricht** — Ausgabe schicken, nicht weitermachen. Typische Kandidaten:
fehlende Runtime (2.2), fehlende native Bibliothek (2.3), Datenbank nicht erreichbar.
### 3.1 Gegenprobe: Verhalten ohne Datenbank
Kurz die Verbindung unbrauchbar machen und erneut starten:
```bash
cp appsettings.Local.json appsettings.Local.json.bak
sed -i 's/Server=[^;]*/Server=192.0.2.1/' appsettings.Local.json
./PolyTrader.App.Avalonia --headless
```
**Erwartet:** eine **verständliche Fehlermeldung**, kein Stacktrace-Absturz, kein endloser
Neustart-Sturm. `192.0.2.1` ist eine Adresse, die per Definition nirgends antwortet — es gibt
also einen Verbindungs-Timeout.
Danach zurücksetzen:
```bash
mv appsettings.Local.json.bak appsettings.Local.json
```
**Melde mir:** wie sich die Anwendung verhält und wie lange es bis zur Meldung dauert.
---
## Teil 4 — Als systemd-Dienst
### 4.1 Einrichten
```bash
sudo useradd --system --home-dir /opt/polytrader --shell /usr/sbin/nologin polytrader
sudo mkdir -p /opt/polytrader
sudo cp -r /tmp/polytrader/* /opt/polytrader/
sudo chown -R polytrader:polytrader /opt/polytrader
sudo chmod +x /opt/polytrader/PolyTrader.App.Avalonia
sudo cp /tmp/polytrader/polytrader.service /etc/systemd/system/polytrader.service
```
Die Unit ist unverändert übernehmbar: Sie erwartet genau `/opt/polytrader` als
`WorkingDirectory` und das Konto `polytrader` — beides oben so angelegt. Dann:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now polytrader
sleep 10
sudo systemctl status polytrader --no-pager
```
**Erwartet:** `active (running)`.
**Melde mir:** die Statusausgabe und
```bash
sudo journalctl -u polytrader -n 60 --no-pager
```
### 4.2 Die `WorkingDirectory`-Falle prüfen (D-11)
**Der interessanteste Einzelpunkt dieses Prüfplans.** `server_settings.xml` wird relativ zum
**Arbeitsverzeichnis** geladen, `master.key` relativ zur **Programmdatei**. Stimmen die nicht
überein, legt `ServerSettings.Load()` **kommentarlos eine neue Einstellungsdatei an** — der
Dienst läuft dann mit Standardwerten, ohne dass irgendwo eine Warnung erscheint.
```bash
ls -la /opt/polytrader/server_settings.xml
sudo find / -name "server_settings.xml" -newermt "-10 minutes" 2>/dev/null
```
**Erwartet:** die Datei liegt in `/opt/polytrader/` und **nirgends sonst**. Taucht sie in einem
anderen Verzeichnis auf (etwa `/` oder im Home des Dienstkontos), hat die Falle zugeschnappt.
**Melde mir beide Ausgaben.**
---
## Teil 5 — Beenden und Neustart
### 5.1 Geordnetes Beenden
```bash
time sudo systemctl stop polytrader
sudo journalctl -u polytrader -n 30 --no-pager
```
**Erwartet:** Beenden deutlich **unter 45 Sekunden** (`TimeoutStopSec=45`); im Journal Meldungen
über das geordnete Herunterfahren, **kein** `SIGKILL`/`Killed`. Der Shutdown-Pfad ist wichtig:
Im Echtbetrieb hängen daran offene Positionen und der DB-Flush.
**Melde mir:** die Dauer aus `time` und ob `SIGKILL` im Journal auftaucht.
### 5.2 Neustart nach Absturz
```bash
sudo systemctl start polytrader
sleep 5
sudo pkill -9 -f PolyTrader.App.Avalonia
sleep 20
sudo systemctl status polytrader --no-pager | head -5
```
**Erwartet:** `Restart=on-failure` greift, der Dienst läuft nach ~10 s wieder (`RestartSec=10`).
**Melde mir:** ob er von selbst zurückkommt.
---
## Teil 6 — Zeitzonen-Befund (T2)
Hier wird ein bisher **nur theoretischer** Befund erstmals nachweisbar: Der `TerminalLogger`
stempelt mit `DateTime.Now` statt der konfigurierten `AppTimeZone`.
```bash
timedatectl | grep "Time zone"
grep -i timezone /opt/polytrader/server_settings.xml
ls -la /opt/polytrader/Logs/
date
```
**Zu klären:** Steht die VM auf **UTC** und `ApplicationTimeZoneId` auf `Europe/Berlin`, dann
müsste die **Logdatei nach UTC-Datum benannt** sein, während die Zeitstempel in der Anzeige
Berliner Zeit zeigen. Zwischen 00:00 und 02:00 Berliner Zeit fallen sie auf **verschiedene Tage**.
**Melde mir alle vier Ausgaben** plus die ersten Zeilen der aktuellen Logdatei:
```bash
head -5 /opt/polytrader/Logs/$(date -u +%Y-%m-%d).jsonl
```
Falls die VM auf `Europe/Berlin` steht, ist der Effekt nicht sichtbar — dann bitte
`sudo timedatectl set-timezone UTC`, Dienst neu starten und erneut prüfen. Das ist der
realistische Fall: Server laufen üblicherweise auf UTC.
---
## Teil 7 — Logrotate
```bash
sudo tee /etc/logrotate.d/polytrader > /dev/null <<'EOF'
/opt/polytrader/Logs/*.jsonl {
daily
rotate 14
compress
missingok
notifempty
copytruncate
su polytrader polytrader
}
EOF
sudo logrotate -d /etc/logrotate.d/polytrader
```
`-d` ist ein Trockenlauf, er ändert nichts.
> `copytruncate` ist bewusst gewählt: Die Anwendung hält die Logdatei offen und bekommt ein
> Umbenennen nicht mit — sie würde sonst in die rotierte Datei weiterschreiben.
**Melde mir:** die Ausgabe des Trockenlaufs.
---
## Teil 8 — Master-Key unter Linux-Rechten
Bisher nur unter Windows erprobt.
```bash
sudo systemctl stop polytrader
sudo mkdir -p /etc/systemd/system/polytrader.service.d
sudo tee /etc/systemd/system/polytrader.service.d/masterkey.conf > /dev/null <<'EOF'
[Service]
Environment="POLYTRADER_MASTER_KEY=nur-fuer-den-test-bitte-spaeter-ersetzen"
EOF
sudo systemctl daemon-reload
sudo systemctl start polytrader
sleep 10
sudo journalctl -u polytrader -n 40 --no-pager | grep -iE "master|key|verschl|encrypt|permission|denied"
```
**Erwartet:** keine Fehler zu Dateirechten. `FilePermissions` setzt unter Linux restriktive Rechte
(0600) — das ist der Pfad, der hier erstmals wirklich läuft.
**Melde mir:** die gefilterte Ausgabe. Der Testschlüssel wird später ersetzt (Roadmap A5).
---
## Teil 9 — Was ich am Ende brauche
Sammle bitte:
1. `cat /etc/os-release` und `dotnet --list-runtimes` (Schritt 2.1/2.2)
2. Ob **Schritt 3 ohne** die GUI-Pakete aus 2.3 lief
3. Die vollständige Startausgabe aus Schritt 3
4. Verhalten ohne Datenbank (3.1)
5. `systemctl status` + Journal (4.1)
6. Ergebnis der `WorkingDirectory`-Prüfung (4.2)
7. Stopp-Dauer und ob `SIGKILL` auftrat (5.1), Neustart-Verhalten (5.2)
8. Die vier Zeitzonen-Ausgaben (Teil 6)
9. Logrotate-Trockenlauf (Teil 7)
10. Master-Key-Ausgabe (Teil 8)
Daraus werte ich aus, korrigiere die `polytrader.service` und die Doku, und hake A4a in der
Roadmap ab — oder trage die gefundenen Fehler als neue Punkte ein.
**Aufräumen** (die VM wieder freigeben):
```bash
sudo systemctl disable --now polytrader
sudo rm -f /etc/systemd/system/polytrader.service /etc/logrotate.d/polytrader
sudo rm -rf /etc/systemd/system/polytrader.service.d
sudo systemctl daemon-reload
sudo rm -rf /opt/polytrader /tmp/polytrader
sudo userdel polytrader
```
Und die Testdatenbank, falls nicht mehr gebraucht: `DROP DATABASE polytrader_test;`