Weboberflaeche (#531): X-Instanz an der einen fetch-Stelle, Instanzauswahl in der Kopfzeile, Verwaltungsansicht mit Anlegen/Starten/Anhalten/Fenster, Herkunft der Ereignisse, Sicherungs- und Einstellungsansicht. 402/409/502 werden eigen behandelt. Sicherungen im Manager (#530): Auflisten, Anlegen, Wiederherstellen, Loeschen. Das Zurueckspielen haelt die Instanz vorher an und startet sie danach wieder. Geordnetes Ende (Konzept 9.8). Zwei Befunde waren der Anlass: Der Manager konnte eine Instanz MIT FENSTER nie geordnet beenden. Control.ShutdownRequested war nur in HeadlessRun verdrahtet, nicht in der Avalonia-Anwendung - und ein Fenster ist die Vorgabe. Die Bitte wurde mit 202 beantwortet, niemand hoerte zu, und der Manager schoss den Prozess nach Ablauf seiner Frist jedes Mal ab. InstanceSupervisor.StopAsync hielt Socket-Schweigen fuer einen Todesnachweis und meldete "beendet", ohne den Prozess anzufassen. Bei einer verstummten Instanz ist der Socket von vornherein still - der Neustart haette einen zweiten Prozess auf dieselbe state.db gesetzt. Genau der Schaden, gegen den #528 gebaut ist. - ShutdownCoordinator: erst die Tuer schliessen, dann auf offene Arbeiten warten, Frist zwei Minuten. Vorher brach TaskScanner.DisposeAsync mitten in ProcessAsync ab, sodass die Aufgabe bis zum Lease-Ablauf auf "running" stehenblieb. - GET /api/instanz/puls hinter dem Socket-Gate: Phase und offene Arbeiten. Weiter gefragt statt gemeldet - ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie haengt. - Eskalationsleiter: drei Durchgaenge ohne Antwort, einmal um ein Ende bitten, nach 30s beenden und den Vollzug abwarten, gemeldet als Absturz. - EnsureDeadAsync vor jedem Start. Weiteres aus dem Review: - .gitignore verankert: "Instances/" passte auch auf src/ClawdDotNet.Core/Instances/. InstanceEndpoint.cs und InstanceLayout.cs waren dadurch nie eingecheckt. - Das WebUI-Token stand im Protokoll des Managers. Konsole ja, Datei nein. - Ein Klick auf "Instanz anlegen" loeste requestSubmit() zweimal aus. - [Obsolete] auf WebServerPort wieder entfernt: sechs Warnungen, die niemand abstellen konnte, solange E-M5 nicht fertig ist. - Die Wiederherstellung bricht nicht mehr ab, wenn der Browser weggeht. - Entwicklungspfade zum Instanzmanager aus Segmenten statt mit Rueckstrichen, und der Port kommt aus manager.json statt fest aus der Zeile. Offen und gemeldet: --managed hebelt die Lizenzpruefung aus (#535). 806 Tests gruen, 0 Warnungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,22 @@ public sealed class AppHost : IAsyncDisposable
|
||||
public required ILoggerFactory LoggerFactory { get; init; }
|
||||
public required ToolRegistry Tools { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Das geordnete Ende dieser Instanz (#528).
|
||||
///
|
||||
/// <para>Wer die Instanz beenden will — der Instanzmanager über den Socket, ein
|
||||
/// Mensch über das Fenster — ruft <see cref="ShutdownCoordinator.BeginAsync"/> und
|
||||
/// wartet, bevor er <see cref="DisposeAsync"/> anstößt. Der Instanzmanager liest
|
||||
/// unterdessen <see cref="ShutdownCoordinator.Snapshot"/> über den Puls und weiß
|
||||
/// dadurch, dass hier aufgeräumt und nicht gehangen wird.</para>
|
||||
/// </summary>
|
||||
/// <para>Nicht <c>required</c>, sondern mit einem leeren Koordinator vorbelegt: Ein
|
||||
/// Aufbau ohne angemeldete Arbeitsquellen hat nichts abzuwarten und endet sofort. So
|
||||
/// gibt es diese Zusage <em>immer</em>, und niemand muss auf <c>null</c> prüfen,
|
||||
/// bevor er eine Instanz geordnet beendet.</para>
|
||||
public ShutdownCoordinator Shutdown { get; init; } =
|
||||
new(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
|
||||
|
||||
/// <summary>Null, wenn kein OpenRouter-Schlüssel hinterlegt ist — dann laufen keine Agenten.</summary>
|
||||
public AgentEngine? Engine { get; private init; }
|
||||
|
||||
@@ -177,8 +193,11 @@ public sealed class AppHost : IAsyncDisposable
|
||||
/// </summary>
|
||||
public required Func<InstanceDirectoryManager, Task<string?>> SelectInstance { get; init; }
|
||||
|
||||
/// <summary>Wie der Lizenz-Torwächter mit dem Benutzer spricht.</summary>
|
||||
public required ILicensePrompt License { get; init; }
|
||||
/// <summary>Wie der Lizenz-Torwächter mit dem Benutzer spricht. Null im geführten Modus.</summary>
|
||||
public ILicensePrompt? License { get; init; }
|
||||
|
||||
/// <summary>Wahr, wenn die Instanz durch den Instanzmanager gestartet und geführt wird.</summary>
|
||||
public bool IsManaged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Telegram-Anmeldecode und 2FA-Passwort. Null lässt die MTProto-Anmeldung aus —
|
||||
@@ -270,17 +289,26 @@ public sealed class AppHost : IAsyncDisposable
|
||||
}
|
||||
|
||||
// ─── 4. Lizenz ───
|
||||
var license = new LicenseGate(
|
||||
new SettingsLicenseStore(settings),
|
||||
loggerFactory.CreateLogger("ClawdDotNet.License"),
|
||||
callbacks.License,
|
||||
ReleaseInfo.Version);
|
||||
|
||||
if (!await license.RunStartupCheckAsync(ct))
|
||||
LicenseGate? license = null;
|
||||
if (!callbacks.IsManaged)
|
||||
{
|
||||
logger.LogWarning("Start abgebrochen: keine gültige Lizenz.");
|
||||
loggerFactory.Dispose();
|
||||
return new StartupResult(null, null);
|
||||
var prompt = callbacks.License ?? new NonInteractiveLicensePrompt(Console.Error.WriteLine);
|
||||
license = new LicenseGate(
|
||||
new SettingsLicenseStore(settings),
|
||||
loggerFactory.CreateLogger("ClawdDotNet.License"),
|
||||
prompt,
|
||||
ReleaseInfo.Version);
|
||||
|
||||
if (!await license.RunStartupCheckAsync(ct))
|
||||
{
|
||||
logger.LogWarning("Start abgebrochen: keine gültige Lizenz.");
|
||||
loggerFactory.Dispose();
|
||||
return new StartupResult(null, null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Instanz wird vom Manager geführt (--managed): Lizenzprüfung erfolgt zentral im Manager.");
|
||||
}
|
||||
|
||||
// ─── 5. Werkzeuge ───
|
||||
@@ -307,7 +335,7 @@ public sealed class AppHost : IAsyncDisposable
|
||||
// Nichts freizugeben: Der Torwächter nutzt den gemeinsamen HttpClient des SDK.
|
||||
host.License = license;
|
||||
|
||||
if (license.IsEnforcementConfigured)
|
||||
if (license is not null && license.IsEnforcementConfigured)
|
||||
{
|
||||
host.LicenseWatch = new LicenseWatch(
|
||||
license, loggerFactory.CreateLogger("ClawdDotNet.License"));
|
||||
@@ -381,7 +409,9 @@ public sealed class AppHost : IAsyncDisposable
|
||||
LogDirectory = logDirectory,
|
||||
LoggerFactory = loggerFactory,
|
||||
Tools = tools,
|
||||
TelegramClient = telegram
|
||||
TelegramClient = telegram,
|
||||
Shutdown = new ShutdownCoordinator(
|
||||
loggerFactory.CreateLogger("ClawdDotNet.Shutdown"))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -445,9 +475,19 @@ public sealed class AppHost : IAsyncDisposable
|
||||
Board = board,
|
||||
Usage = usage,
|
||||
TelegramClient = telegram,
|
||||
Status = new OpenRouterStatusService(instance.OpenRouterApiKey)
|
||||
Status = new OpenRouterStatusService(instance.OpenRouterApiKey),
|
||||
Shutdown = new ShutdownCoordinator(
|
||||
loggerFactory.CreateLogger("ClawdDotNet.Shutdown"))
|
||||
};
|
||||
|
||||
// Was beim Beenden auslaufen darf, bevor abgeschnitten wird (#528). Beide Quellen
|
||||
// koennen nur zweierlei: die Tuer schliessen und sagen, wie viel noch laeuft.
|
||||
host.Shutdown.Register(new WorkSource(
|
||||
"Agentenläufe", () => engine?.OpenRunCount ?? 0, () => engine?.StopAccepting()));
|
||||
|
||||
host.Shutdown.Register(new WorkSource(
|
||||
"Aufgaben", () => scanner?.OpenCount ?? 0, () => scanner?.StopAccepting()));
|
||||
|
||||
host._shutdown.Add(() => { openRouter.Dispose(); return ValueTask.CompletedTask; });
|
||||
|
||||
logger.LogInformation("AgentEngine und Scanner erstellt, Chat-Verläufe geladen");
|
||||
@@ -602,6 +642,14 @@ public sealed class AppHost : IAsyncDisposable
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// Erst auslaufen lassen, dann abschneiden (#528). Laeuft das Auslaufen schon,
|
||||
// wartet dieser Aufruf darauf statt es ein zweites Mal anzustossen - und wenn
|
||||
// niemand darum gebeten hat, ist es hier in einem Wimpernschlag vorbei, weil
|
||||
// dann nichts offen ist. Wer DisposeAsync ohne vorheriges BeginAsync ruft,
|
||||
// bekommt so trotzdem das geordnete Ende.
|
||||
try { await Shutdown.BeginAsync(); }
|
||||
catch (Exception) { /* Beim Beenden zaehlt, dass alle Schritte drankommen */ }
|
||||
|
||||
foreach (var step in _shutdown)
|
||||
{
|
||||
try { await step(); }
|
||||
|
||||
Reference in New Issue
Block a user