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 */ }
}
}
}
}
}