L1b/2: Kultur und Dateisystem plattformunabhaengig machen
Kultur - Ausgaben und Parsen haengen nicht mehr am Host:
- PdfExporter formatierte Betraege mit ToString("N2") ohne Formatanbieter,
also CurrentCulture. Auf dem deutschen Desktop "1.234,56", in einem
Container mit LANG=C "1,234.56" - dieselbe Zahl, fuer einen Leser eine
andere. Fuer ein ausdruecklich pruefbares Dokument jetzt fest de-DE.
- CapitolTradesScraper.ParseDate nutzte DateOnly.TryParse ohne
Formatanbieter. Das ist nicht theoretisch: gemessen wurde aus
"2026-08-04" unter th-TH das Jahr 1483 (buddhistischer Kalender), unter
fa-IR das Jahr 2647 (persischer Kalender), unter ar-SA schlug das Parsen
ganz fehl. de-DE und en-US kommen mit ISO klar - genau deshalb faellt so
etwas auf dem Entwicklungsrechner nie auf. Jetzt TryParseExact mit
InvariantCulture; ein Formatwechsel der Quelle faellt damit auf, statt
still ein falsches Datum zu erzeugen. Regressionstest ueber vier Kulturen.
- IBKRGatewayService baute den Query-Parameter mit .ToString().ToLower()
(Tuerkisch-I) - jetzt fest "true"/"false".
PDF-Schriften: PDFsharp 6 loest auf Nicht-Windows-Plattformen nichts von
selbst auf, "Segoe UI" gibt es dort nicht - der Export waere zur Laufzeit
gescheitert. Neuer DocumentFontResolver: unter Windows bleibt die Plattform
zustaendig (unveraenderte Optik), auf Linux wird eine freie Systemschrift
gesucht (DejaVu/Liberation/Noto/FreeSans). Bewusst keine Schrift im Repo -
das erspart eine Lizenzfrage; fehlt sie, nennt die Fehlermeldung das zu
installierende Paket.
BackupWorker:
- Suchte "mysqldump.exe" in C:\Program Files\... und splittete PATH mit ';'.
Auf Linux ist das Trennzeichen ':' - der gesamte PATH waere als ein
Eintrag gelesen worden. Jetzt Path.PathSeparator, plattformabhaengige
Suchpfade und zusaetzlich "mariadb-dump" (MariaDB hat mysqldump ab 10.5
umbenannt).
- Das DB-Passwort stand als Kommandozeilenargument im Prozessbaum. Unter
Linux ist /proc/<pid>/cmdline fuer jeden lokalen Nutzer lesbar - das waere
eine neue Offenlegung gewesen, die es unter Windows so nicht gab. Jetzt
ueber MYSQL_PWD, nur an den Kindprozess vererbt. Argumente einzeln statt
als Zeichenkette (kein Quoting-Problem bei Pfaden mit Leerzeichen).
Verifiziert: 188 Tests gruen (+5), Build 0 Fehler/0 Warnungen, Core + 3
Module + Tests bauen fuer linux-x64, --smoke-ui konstruiert alle 7 Fenster.
Offen aus L1b und nach L2 verschoben: IAppPaths (Logs/Backups/settings.json/
master.key liegen neben der Binaerdatei; unter /opt ist das nicht schreibbar).
Gehoert zum Daemon, wo die Pfade tatsaechlich gebraucht werden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -244,7 +244,7 @@ public class IBKRGatewayService
|
||||
var url = $"iserver/marketdata/history?conid={conid}" +
|
||||
$"&period={Uri.EscapeDataString(period)}" +
|
||||
$"&bar={Uri.EscapeDataString(bar)}" +
|
||||
$"&outsideRth={outsideRth.ToString().ToLower()}";
|
||||
$"&outsideRth={(outsideRth ? "true" : "false")}";
|
||||
|
||||
var response = await _http.GetAsync(url, ct);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using IBKRTrader.Core.Time;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -8,9 +9,9 @@ namespace IBKRTrader.Core.Workers.BuiltIn;
|
||||
|
||||
/// <summary>
|
||||
/// BackupWorker (Core) – läuft alle 30 Minuten (konfigurierbar).
|
||||
/// 1. Ruft mysqldump.exe auf → SQL-Dump in Backups\DB\
|
||||
/// 2. Kopiert Logs\ → Backups\Logs\
|
||||
/// Falls mysqldump nicht gefunden: Warnung und Überspringen.
|
||||
/// 1. Ruft mariadb-dump bzw. mysqldump auf → SQL-Dump nach Backups/DB/
|
||||
/// 2. Kopiert Logs/ → Backups/Logs/
|
||||
/// Wird das Werkzeug nicht gefunden: Warnung und Überspringen, nie ein harter Fehler.
|
||||
/// </summary>
|
||||
public class BackupWorker : WorkerBase
|
||||
{
|
||||
@@ -53,39 +54,50 @@ public class BackupWorker : WorkerBase
|
||||
|
||||
private async Task RunMysqlDumpAsync(string dbDir, string timestamp, CancellationToken ct)
|
||||
{
|
||||
var dump = FindMysqldump();
|
||||
var dump = FindDumpTool();
|
||||
if (dump == null)
|
||||
{
|
||||
Logger.Warn(Module, "mysqldump.exe nicht gefunden – DB-Backup übersprungen.");
|
||||
Logger.Warn(Module, $"{string.Join(" / ", ToolNames())} nicht gefunden – DB-Backup übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
var db = _settings.Settings.Database;
|
||||
var dumpFile = Path.Combine(dbDir, $"{db.Database}_{timestamp}.sql");
|
||||
|
||||
var args = $"--host={db.Host} --port={db.Port} " +
|
||||
$"--user={db.User} --password={db.Password} " +
|
||||
$"--single-transaction --routines --triggers " +
|
||||
$"{db.Database} --result-file=\"{dumpFile}\"";
|
||||
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = dump,
|
||||
Arguments = args,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
// Argumente einzeln statt als eine Zeichenkette: das erspart das Quoting von Pfaden mit
|
||||
// Leerzeichen und ist auf beiden Plattformen dasselbe.
|
||||
psi.ArgumentList.Add($"--host={db.Host}");
|
||||
psi.ArgumentList.Add($"--port={db.Port.ToString(CultureInfo.InvariantCulture)}");
|
||||
psi.ArgumentList.Add($"--user={db.User}");
|
||||
psi.ArgumentList.Add("--single-transaction");
|
||||
psi.ArgumentList.Add("--routines");
|
||||
psi.ArgumentList.Add("--triggers");
|
||||
psi.ArgumentList.Add(db.Database);
|
||||
psi.ArgumentList.Add($"--result-file={dumpFile}");
|
||||
|
||||
// Passwort über die Umgebung statt über die Kommandozeile. Auf Linux ist
|
||||
// /proc/<pid>/cmdline für JEDEN lokalen Nutzer lesbar – das Passwort hätte dort für die
|
||||
// Dauer des Dumps offen im Prozessbaum gestanden. MYSQL_PWD ist der von MySQL/MariaDB
|
||||
// dafür vorgesehene Weg und wird nur an diesen Kindprozess vererbt.
|
||||
psi.Environment["MYSQL_PWD"] = db.Password;
|
||||
|
||||
using var proc = System.Diagnostics.Process.Start(psi)
|
||||
?? throw new InvalidOperationException("mysqldump konnte nicht gestartet werden.");
|
||||
?? throw new InvalidOperationException($"{Path.GetFileName(dump)} konnte nicht gestartet werden.");
|
||||
|
||||
await proc.WaitForExitAsync(ct);
|
||||
|
||||
if (proc.ExitCode != 0)
|
||||
{
|
||||
var err = await proc.StandardError.ReadToEndAsync(ct);
|
||||
Logger.Warn(Module, $"mysqldump Exitcode {proc.ExitCode}: {err}");
|
||||
Logger.Warn(Module, $"{Path.GetFileName(dump)} Exitcode {proc.ExitCode}: {err}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -93,27 +105,57 @@ public class BackupWorker : WorkerBase
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FindMysqldump()
|
||||
/// <summary>
|
||||
/// Namen des Dump-Werkzeugs. MariaDB hat <c>mysqldump</c> ab 10.5 in <c>mariadb-dump</c>
|
||||
/// umbenannt und pflegt den alten Namen nur noch als Alias – auf neueren Distributionen ist
|
||||
/// oft nur der neue vorhanden. Die Dateiendung gibt es nur unter Windows.
|
||||
/// </summary>
|
||||
private static string[] ToolNames() =>
|
||||
OperatingSystem.IsWindows()
|
||||
? ["mariadb-dump.exe", "mysqldump.exe"]
|
||||
: ["mariadb-dump", "mysqldump"];
|
||||
|
||||
/// <summary>
|
||||
/// Sucht das Dump-Werkzeug: erst die üblichen Installationspfade der jeweiligen Plattform,
|
||||
/// dann PATH. Findet es nichts, wird das Backup übersprungen (nie ein harter Fehler).
|
||||
/// </summary>
|
||||
private static string? FindDumpTool()
|
||||
{
|
||||
// Häufige Installationspfade auf Windows-Server
|
||||
var candidates = new[]
|
||||
{
|
||||
"mysqldump.exe",
|
||||
@"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe",
|
||||
@"C:\Program Files\MySQL\MySQL Server 8.4\bin\mysqldump.exe",
|
||||
@"C:\xampp\mysql\bin\mysqldump.exe"
|
||||
};
|
||||
var names = ToolNames();
|
||||
|
||||
foreach (var c in candidates)
|
||||
if (File.Exists(c)) return c;
|
||||
// Übliche Installationspfade – plattformabhängig.
|
||||
string[] dirs = OperatingSystem.IsWindows()
|
||||
?
|
||||
[
|
||||
@"C:\Program Files\MariaDB\bin",
|
||||
@"C:\Program Files\MySQL\MySQL Server 8.0\bin",
|
||||
@"C:\Program Files\MySQL\MySQL Server 8.4\bin",
|
||||
@"C:\xampp\mysql\bin",
|
||||
]
|
||||
:
|
||||
[
|
||||
"/usr/bin",
|
||||
"/usr/local/bin",
|
||||
"/opt/mariadb/bin",
|
||||
"/opt/homebrew/bin",
|
||||
];
|
||||
|
||||
// PATH-Suche
|
||||
foreach (var name in names)
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
var full = Path.Combine(dir, name);
|
||||
if (File.Exists(full)) return full;
|
||||
}
|
||||
|
||||
// PATH-Suche. Das Trennzeichen ist plattformabhängig: ';' unter Windows, ':' unter Linux –
|
||||
// ein fest verdrahtetes ';' hätte auf Linux den gesamten PATH als einen Eintrag gelesen.
|
||||
var pathVar = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
foreach (var dir in pathVar.Split(';'))
|
||||
{
|
||||
var full = Path.Combine(dir.Trim(), "mysqldump.exe");
|
||||
if (File.Exists(full)) return full;
|
||||
}
|
||||
foreach (var dir in pathVar.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||
foreach (var name in names)
|
||||
{
|
||||
var full = Path.Combine(dir.Trim(), name);
|
||||
if (File.Exists(full)) return full;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user