namespace IBKRTrader.Core.Configuration; /// /// Die Verzeichnisse, in die die Anwendung schreibt. /// /// Warum das nötig wurde: Bisher lag alles neben der Binärdatei – Logs/, /// Backups/, settings.json, master.key. Unter Windows ist das üblich. Auf /// Linux liegt eine Anwendung typischerweise unter /opt oder /usr/local, und dort /// hat der Dienstbenutzer keinen Schreibzugriff. Der Dienst wäre beim ersten Logeintrag /// gescheitert. /// /// Auflösung, in dieser Reihenfolge: /// /// Umgebungsvariable (IBKRTRADER_CONFIG_DIR, _DATA_DIR, _LOG_DIR) – /// hat immer Vorrang, damit ein Betreiber die Ablage frei bestimmen kann. /// Das Verzeichnis der Binärdatei, wenn dort geschrieben werden darf. Das hält das /// bisherige Verhalten unter Windows und beim Entwickeln unter Linux unverändert. /// Sonst plattformabhängig: unter Linux/macOS die FHS-Konvention /// (/etc/ibkrtrader, /var/lib/ibkrtrader, /var/log/ibkrtrader), unter /// Windows %ProgramData%\IBKRTrader. Der FHS-Pfad darf hier nicht als /// plattformübergreifender Rückfall dienen – .NET löst /etc/ibkrtrader unter /// Windows zu C:\etc\ibkrtrader auf. Genau dieser Fall tritt ein, sobald eine /// Installation nach Program Files zeigt, wo der Dienst- bzw. Nutzerkonto-Kontext /// keinen Schreibzugriff neben der Binärdatei hat. /// /// /// Die Prüfung läuft einmal beim ersten Zugriff; die Verzeichnisse werden dabei angelegt. /// public static class AppPaths { private const string AppFolder = "ibkrtrader"; private static readonly Lazy _config = new(() => Resolve("IBKRTRADER_CONFIG_DIR", $"/etc/{AppFolder}")); private static readonly Lazy _data = new(() => Resolve("IBKRTRADER_DATA_DIR", $"/var/lib/{AppFolder}", windowsSubDir: "Data")); private static readonly Lazy _logs = new(() => Resolve("IBKRTRADER_LOG_DIR", $"/var/log/{AppFolder}", "Logs", windowsSubDir: "Logs")); /// Konfiguration und Schlüsselmaterial: settings.json, master.key, openrouter.key. public static string Config => _config.Value; /// Veränderliche Daten: Backups/. public static string Data => _data.Value; /// Logdateien (Textlog je Modul und JSONL). public static string Logs => _logs.Value; /// Vollständiger Pfad einer Konfigurationsdatei. public static string ConfigFile(string fileName) => Path.Combine(Config, fileName); /// Vollständiger Pfad unterhalb des Datenverzeichnisses. public static string DataPath(string relative) => Path.Combine(Data, relative); /// Kurzfassung für den Startlog – damit im Betrieb sichtbar ist, wohin geschrieben wird. public static string Describe() => $"config={Config}, data={Data}, logs={Logs}"; /// Umgebungsvariable, die alles überstimmt. /// FHS-Pfad unter Linux/macOS, wenn neben der Binärdatei nicht geschrieben werden darf. /// /// Unterverzeichnis im Binärverzeichnis. Für Logs ist das Logs/; Konfiguration und Daten /// lagen bisher direkt daneben und bleiben dort, damit bestehende Installationen unverändert /// weiterlaufen. /// /// /// Unterverzeichnis unter %ProgramData%\IBKRTrader, wenn unter Windows neben der /// Binärdatei nicht geschrieben werden darf (z. B. Installation nach Program Files). /// null = direkt in %ProgramData%\IBKRTrader, wie bei der Konfiguration. /// 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); } /// /// 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. /// 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; } }