Files
IBKRTrader/src/IBKRTrader.Core/Configuration/AppPaths.cs
T
RichardandClaude Opus 5 0a72ea7e28 Deploymentcenter-Integration (Schritte 0-8 des Konzepts)
Bindet Lizenz, Watchdog, Fehler-Stream, UpdateService und Erstinstallation an
das Deploymentcenter 2.5.1 an. Einbauort ist IBKRTrader.Hosting - den Host
teilen sich Shell und Daemon.

Projekt-Befunde aus dem Konzept vorab bereinigt:
  P1  Echte DB-Zugangsdaten als Vorgabewerte in AppSettings -> Platzhalter.
      Das alte Passwort steht weiterhin in der Git-Historie und ist als
      kompromittiert zu behandeln (Rotation ist Nutzer-Aktion).
  P2  AppPaths fiel unter Windows auf /etc/ibkrtrader zurueck, was .NET zu
      C:\etc\ibkrtrader aufloest. Jetzt %ProgramData%\IBKRTrader.
  P3  Globale Ausnahmebehandler (AppDomain / TaskScheduler) - vorher gab es
      keinen Logeintrag, wenn der Prozess unbehandelt wegbrach.
  P4  Version einmal zentral in Directory.Build.props statt zweimal hartkodiert.

Bewusste Abweichungen vom DC-Leitfaden, beide fuer ein handelndes System:
  - Lizenz-Urteil fuehrt zum Sperrbetrieb (TradingEnabled=false) statt zu
    Environment.Exit(1). Keine neuen Einstiege, aber Risiko-, Exit- und
    Buchhaltungslogik laufen weiter.
  - exitCurrentApp bleibt immer false; der Aufrufer beendet geordnet.

Das SDK haengt als Cross-Repo-ProjectReference am Schwester-Repo
Deploymentcenter (Interim, siehe Konzept 2.2). Damit ist P5 offen: die
Gitea-CI checkt das Schwester-Repo nicht aus und wird rot, bis der Bezug auf
ein NuGet-Paket umgestellt ist.

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

116 lines
5.9 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.
namespace IBKRTrader.Core.Configuration;
/// <summary>
/// Die Verzeichnisse, in die die Anwendung schreibt.
///
/// <para><b>Warum das nötig wurde:</b> Bisher lag alles neben der Binärdatei – <c>Logs/</c>,
/// <c>Backups/</c>, <c>settings.json</c>, <c>master.key</c>. Unter Windows ist das üblich. Auf
/// Linux liegt eine Anwendung typischerweise unter <c>/opt</c> oder <c>/usr/local</c>, und dort
/// hat der Dienstbenutzer <b>keinen Schreibzugriff</b>. Der Dienst wäre beim ersten Logeintrag
/// gescheitert.</para>
///
/// <para><b>Auflösung, in dieser Reihenfolge:</b></para>
/// <list type="number">
/// <item>Umgebungsvariable (<c>IBKRTRADER_CONFIG_DIR</c>, <c>_DATA_DIR</c>, <c>_LOG_DIR</c>) –
/// hat immer Vorrang, damit ein Betreiber die Ablage frei bestimmen kann.</item>
/// <item>Das Verzeichnis der Binärdatei, <b>wenn dort geschrieben werden darf</b>. Das hält das
/// bisherige Verhalten unter Windows und beim Entwickeln unter Linux unverändert.</item>
/// <item>Sonst plattformabhängig: unter Linux/macOS die FHS-Konvention
/// (<c>/etc/ibkrtrader</c>, <c>/var/lib/ibkrtrader</c>, <c>/var/log/ibkrtrader</c>), unter
/// Windows <c>%ProgramData%\IBKRTrader</c>. <b>Der FHS-Pfad darf hier nicht als
/// plattformübergreifender Rückfall dienen</b> – .NET löst <c>/etc/ibkrtrader</c> unter
/// Windows zu <c>C:\etc\ibkrtrader</c> auf. Genau dieser Fall tritt ein, sobald eine
/// Installation nach <c>Program Files</c> zeigt, wo der Dienst- bzw. Nutzerkonto-Kontext
/// keinen Schreibzugriff neben der Binärdatei hat.</item>
/// </list>
///
/// <para>Die Prüfung läuft einmal beim ersten Zugriff; die Verzeichnisse werden dabei angelegt.</para>
/// </summary>
public static class AppPaths
{
private const string AppFolder = "ibkrtrader";
private static readonly Lazy<string> _config = new(() => Resolve("IBKRTRADER_CONFIG_DIR", $"/etc/{AppFolder}"));
private static readonly Lazy<string> _data = new(() => Resolve("IBKRTRADER_DATA_DIR", $"/var/lib/{AppFolder}", windowsSubDir: "Data"));
private static readonly Lazy<string> _logs = new(() => Resolve("IBKRTRADER_LOG_DIR", $"/var/log/{AppFolder}", "Logs", windowsSubDir: "Logs"));
/// <summary>Konfiguration und Schlüsselmaterial: <c>settings.json</c>, <c>master.key</c>, <c>openrouter.key</c>.</summary>
public static string Config => _config.Value;
/// <summary>Veränderliche Daten: <c>Backups/</c>.</summary>
public static string Data => _data.Value;
/// <summary>Logdateien (Textlog je Modul und JSONL).</summary>
public static string Logs => _logs.Value;
/// <summary>Vollständiger Pfad einer Konfigurationsdatei.</summary>
public static string ConfigFile(string fileName) => Path.Combine(Config, fileName);
/// <summary>Vollständiger Pfad unterhalb des Datenverzeichnisses.</summary>
public static string DataPath(string relative) => Path.Combine(Data, relative);
/// <summary>Kurzfassung für den Startlog – damit im Betrieb sichtbar ist, wohin geschrieben wird.</summary>
public static string Describe() => $"config={Config}, data={Data}, logs={Logs}";
/// <param name="envVar">Umgebungsvariable, die alles überstimmt.</param>
/// <param name="fhsFallback">FHS-Pfad unter Linux/macOS, wenn neben der Binärdatei nicht geschrieben werden darf.</param>
/// <param name="localSubDir">
/// Unterverzeichnis im Binärverzeichnis. Für Logs ist das <c>Logs/</c>; Konfiguration und Daten
/// lagen bisher direkt daneben und bleiben dort, damit bestehende Installationen unverändert
/// weiterlaufen.
/// </param>
/// <param name="windowsSubDir">
/// Unterverzeichnis unter <c>%ProgramData%\IBKRTrader</c>, wenn unter Windows neben der
/// Binärdatei nicht geschrieben werden darf (z. B. Installation nach <c>Program Files</c>).
/// <c>null</c> = direkt in <c>%ProgramData%\IBKRTrader</c>, wie bei der Konfiguration.
/// </param>
private static string Resolve(string envVar, string fhsFallback, string? localSubDir = null, string? windowsSubDir = null)
{
var fromEnv = Environment.GetEnvironmentVariable(envVar);
if (!string.IsNullOrWhiteSpace(fromEnv))
return Ensure(fromEnv.Trim());
var local = localSubDir is null
? AppContext.BaseDirectory
: Path.Combine(AppContext.BaseDirectory, localSubDir);
if (IsWritable(AppContext.BaseDirectory))
return Ensure(local);
// Binärverzeichnis nicht beschreibbar - der FHS-Pfad taugt nur unter Linux/macOS als
// Rückfall. Unter Windows löst .NET "/etc/ibkrtrader" zu "C:\etc\ibkrtrader" auf; der
// richtige Rückfall dort ist das maschinenweite Anwendungsdatenverzeichnis.
if (OperatingSystem.IsWindows())
{
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
var winBase = Path.Combine(programData, "IBKRTrader");
return Ensure(windowsSubDir is null ? winBase : Path.Combine(winBase, windowsSubDir));
}
return Ensure(fhsFallback);
}
/// <summary>
/// Prüft den Schreibzugriff, indem tatsächlich geschrieben wird. Eine Rechteprüfung über
/// Attribute trägt nicht: unter Linux entscheiden Besitzer, Gruppe und Modus, unter Windows
/// die ACL – ein Schreibversuch ist die einzige verlässliche Antwort.
/// </summary>
private static bool IsWritable(string dir)
{
try
{
var probe = Path.Combine(dir, $".write-probe-{Guid.NewGuid():N}");
using (File.Create(probe, 1, FileOptions.DeleteOnClose)) { }
return true;
}
catch (UnauthorizedAccessException) { return false; }
catch (IOException) { return false; }
}
private static string Ensure(string dir)
{
Directory.CreateDirectory(dir);
return dir;
}
}