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>
116 lines
5.9 KiB
C#
116 lines
5.9 KiB
C#
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;
|
||
}
|
||
}
|