Files
PolyTraderSharp/src/PolyTrader.Core/Configuration/ServerSettingsFile.cs
T
RichardandClaude Opus 5 1a09d99a63 ServerSettingsFile: Plattformwarnung CA1416 beseitigt
Nachtrag zu #560 (68d8d6f). File.SetUnixFileMode stand hinter "mode.HasValue".
Das ist inhaltlich richtig, denn mode wird nur ausserhalb von Windows gesetzt.
Der Analyzer kann diesen Zusammenhang aber nicht verfolgen und meldete CA1416.
Damit war die Zahl der Build-Warnungen von 15 auf 16 gestiegen (#155 fuehrt die
15). Die Plattformpruefung steht jetzt mit in der Bedingung, und es sind wieder
15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:34:31 +02:00

125 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Xml.Serialization;
using PolyTraderSharp.Models;
namespace PolyTrader.Core.Configuration
{
/// <summary>
/// Lesen und Fortschreiben der <c>server_settings.xml</c> für Änderungen zur Laufzeit (#560, #561).
///
/// <para><b>Warum nicht einfach <see cref="ServerSettings.Load"/> und <see cref="ServerSettings.Save"/>?</b>
/// <c>Load</c> 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. <c>Save</c> schreibt
/// zudem direkt in die Zieldatei; ein Absturz mitten im Schreiben hinterließe sie abgeschnitten.</para>
///
/// <para>Hier deshalb:</para>
/// <list type="bullet">
/// <item>Existiert die Datei, muss sie lesbar sein – sonst wird nichts geschrieben.</item>
/// <item>Geschrieben wird in eine Nachbardatei, die dann atomar an die Stelle der alten tritt.</item>
/// <item>Unter Linux behält die Datei ihre Rechte.</item>
/// <item>Änderungen laufen nacheinander, nie zwei Lese-Ändere-Schreibe-Zyklen verschränkt.</item>
/// </list>
/// </summary>
public sealed class ServerSettingsFile
{
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>Pfad der Datei, wie ihn auch der Desktop benutzt.</summary>
public string Path { get; }
/// <summary>
/// Liest die aktuelle Datei. Fehlt sie, gelten die Vorgaben. Existiert sie, ist aber nicht lesbar,
/// wirft der Aufruf – anders als <see cref="ServerSettings.Load"/>.
/// </summary>
public ServerSettings Read()
{
lock (Gate)
return ReadStrict();
}
/// <summary>
/// Liest frisch, wendet <paramref name="change"/> an und schreibt atomar zurück. Liefert den
/// geschriebenen Stand. Ist die vorhandene Datei nicht lesbar, wird nichts geschrieben.
/// </summary>
public ServerSettings Update(Action<ServerSettings> 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 */ }
}
}
}
}
}