using System; using System.IO; using System.Xml.Serialization; using PolyTraderSharp.Models; namespace PolyTrader.Core.Configuration { /// /// Lesen und Fortschreiben der server_settings.xml für Änderungen zur Laufzeit (#560, #561). /// /// Warum nicht einfach und ? /// Load verschluckt jeden Lesefehler und liefert leere Vorgaben. Für den Start ist das gewollt: /// Eine fehlende oder kaputte Datei soll den Start nicht verhindern. Für "frisch lesen, ein Feld ändern, /// speichern" ist es gefährlich. Eine gerade gesperrte oder beschädigte Datei würde durch Vorgaben ersetzt, /// und mit ihr Lizenzschlüssel, Watchdog-Token, Mullvad-Account und Alchemy-Key. Save schreibt /// zudem direkt in die Zieldatei; ein Absturz mitten im Schreiben hinterließe sie abgeschnitten. /// /// Hier deshalb: /// /// Existiert die Datei, muss sie lesbar sein – sonst wird nichts geschrieben. /// Geschrieben wird in eine Nachbardatei, die dann atomar an die Stelle der alten tritt. /// Unter Linux behält die Datei ihre Rechte. /// Änderungen laufen nacheinander, nie zwei Lese-Ändere-Schreibe-Zyklen verschränkt. /// /// public sealed class ServerSettingsFile { /// /// Prozessweit, nicht je Instanz: Desktop und WebUI halten eigene Instanzen auf dieselbe Datei. /// Eine Sperre je Instanz liesse ihre Lese-Aendere-Schreibe-Zyklen trotzdem verschraenken. /// private static readonly object Gate = new(); public ServerSettingsFile(string path) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Pfad der Server-Einstellungen fehlt.", nameof(path)); Path = path; } /// Pfad der Datei, wie ihn auch der Desktop benutzt. public string Path { get; } /// /// Liest die aktuelle Datei. Fehlt sie, gelten die Vorgaben. Existiert sie, ist aber nicht lesbar, /// wirft der Aufruf – anders als . /// public ServerSettings Read() { lock (Gate) return ReadStrict(); } /// /// Liest frisch, wendet an und schreibt atomar zurück. Liefert den /// geschriebenen Stand. Ist die vorhandene Datei nicht lesbar, wird nichts geschrieben. /// public ServerSettings Update(Action change) { ArgumentNullException.ThrowIfNull(change); lock (Gate) { var settings = ReadStrict(); change(settings); WriteAtomic(settings); return settings; } } private ServerSettings ReadStrict() { if (!File.Exists(Path)) return new ServerSettings(); try { var serializer = new XmlSerializer(typeof(ServerSettings)); using var fs = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read); return (ServerSettings?)serializer.Deserialize(fs) ?? throw new InvalidDataException("Die Datei enthält keine Einstellungen."); } catch (Exception ex) when (ex is not InvalidDataException) { throw new InvalidDataException( $"{System.IO.Path.GetFileName(Path)} ist nicht lesbar ({ex.Message}). Die Datei wird nicht " + "angefasst, damit gespeicherte Schlüssel nicht durch Vorgaben ersetzt werden.", ex); } } private void WriteAtomic(ServerSettings settings) { string full = System.IO.Path.GetFullPath(Path); string dir = System.IO.Path.GetDirectoryName(full) ?? "."; string temp = System.IO.Path.Combine(dir, $".{System.IO.Path.GetFileName(full)}.{Guid.NewGuid():N}.tmp"); UnixFileMode? mode = null; if (!OperatingSystem.IsWindows() && File.Exists(full)) mode = File.GetUnixFileMode(full); try { using (var fs = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { new XmlSerializer(typeof(ServerSettings)).Serialize(fs, settings); fs.Flush(flushToDisk: true); } // Die Plattformpruefung steht mit in der Bedingung, obwohl mode ausserhalb von Windows ohnehin // null bleibt: Nur so erkennt der Analyzer die Absicherung (CA1416). if (mode.HasValue && !OperatingSystem.IsWindows()) File.SetUnixFileMode(temp, mode.Value); File.Move(temp, full, overwrite: true); } finally { if (File.Exists(temp)) { try { File.Delete(temp); } catch { /* Aufräumen ist Komfort, kein Muss */ } } } } } }