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.
}
}
}