using System.Text.Json; using System.Text.Json.Serialization; using ClawdDotNet.Core.Storage; namespace ClawdDotNet.Manager; /// /// Was der Manager über einen laufenden Instanzprozess auf die Platte schreibt. /// /// Warum es diese Datei gibt (E-M6). Fällt der Manager aus, laufen die /// Instanzen weiter — er darf nicht der Einzelpunkt sein, gegen den dieses ganze Vorhaben /// gebaut wird. Der neu gestartete Manager muss sie also wiederfinden, und zwar ohne /// Datenbank: Alles, was er dafür braucht, liegt als JSON im Instanzverzeichnis. /// /// Die Prozesskennung allein taugt dafür nicht. Nach einem Neustart der /// Maschine ist 4711 mit einiger Wahrscheinlichkeit irgendein anderer Prozess — und den /// dann für die eigene Instanz zu halten, wäre schlimmer als sie gar nicht zu finden. Der /// verlässliche Beweis ist der Socket: Antwortet er mit derselben Instanzkennung, ist es /// dieselbe Instanz. Deshalb steht sie hier mit drin. und /// sind das Zweitmerkmal — sie sagen, welchen Prozess man /// anfassen muss, sobald die Identität feststeht. /// public sealed class InstanceState { /// Der Dateiname im Instanzverzeichnis. public const string FileName = "instance-state.json"; [JsonPropertyName("instanceId")] public string InstanceId { get; set; } = ""; [JsonPropertyName("instanceName")] public string InstanceName { get; set; } = ""; [JsonPropertyName("processId")] public int ProcessId { get; set; } /// /// Startzeit des Prozesses, nicht der Instanz. /// /// Zusammen mit ergibt sie ein Paar, das eine /// wiederverwendete Kennung entlarvt: Ein Prozess, der später gestartet wurde als /// hier steht, ist nicht der, den wir gestartet haben. /// [JsonPropertyName("startedAt")] public DateTimeOffset StartedAt { get; set; } /// Socket oder Named Pipe — wie man mit dieser Instanz spricht. [JsonPropertyName("endpoint")] public string Endpoint { get; set; } = ""; /// Welcher Managerprozess sie gestartet hat. Nur zur Diagnose. [JsonPropertyName("managerProcessId")] public int ManagerProcessId { get; set; } } /// /// Liest und schreibt die Zustandsdatei einer Instanz. /// /// Bewusst nachsichtig beim Lesen: Eine kaputte oder halb geschriebene Datei heißt /// „ich weiß nichts über diese Instanz" und nicht „Ausnahme". Der Manager kann damit /// umgehen — er fragt dann eben den Socket, und wenn der schweigt, läuft sie nicht. /// Geschrieben wird über , damit ein Absturz mitten im Schreiben /// keine halbe Datei hinterlässt. /// public static class InstanceStateStore { private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true, PropertyNameCaseInsensitive = true }; public static string PathFor(string instanceDirectory) => Path.Combine(Path.GetFullPath(instanceDirectory), InstanceState.FileName); /// Der hinterlegte Zustand, oder null: keine Datei, keine lesbare. public static InstanceState? Read(string instanceDirectory) { var path = PathFor(instanceDirectory); try { if (!File.Exists(path)) return null; return JsonSerializer.Deserialize( AtomicFile.ReadAllText(path), JsonOpts); } catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException) { return null; } } public static void Write(string instanceDirectory, InstanceState state) => AtomicFile.WriteAllText( PathFor(instanceDirectory), JsonSerializer.Serialize(state, JsonOpts)); /// /// Räumt die Zustandsdatei weg — nach einem geordneten Ende und nachdem festgestellt /// wurde, dass ein Eintrag ins Leere zeigt. /// /// Eine liegengebliebene Datei ist kein Drama: Der Manager prüft ohnehin den /// Socket, bevor er ihr glaubt. Sie wegzuräumen erspart aber die Frage, warum eine /// Instanz „laut Datei läuft" und trotzdem nicht antwortet. /// public static void Clear(string instanceDirectory) { try { var path = PathFor(instanceDirectory); if (File.Exists(path)) File.Delete(path); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Nicht der Rede wert - siehe oben. } } }