Instanzmanager Stufe 5: Wechseln heisst nach vorn holen (#528, E-M2)
Vorher hiess "Instanz wechseln": Der laufende Prozess faehrt sich herunter und startet sich mit einer anderen Instanz neu. Wer wechselte, beendete zwangslaeufig die Instanz, aus der heraus er es tat - und genau dabei hat sich die Anwendung am 2026-09-08 aufgehaengt (#529). Jetzt laufen alle, und es kommt eine nach vorn. Neu: POST /api/instanz/fenster, wieder ausschliesslich ueber den Socket. Die Instanz holt ihr Fenster SELBST nach vorn; der Manager kennt kein Fenstersystem und soll keines kennen. Kopflos laeuft die Bitte ins Leere, und das ist die richtige Antwort. Das letzte Wort hat der Fenstermanager - Windows laesst einen Hintergrundprozess den Vordergrund nicht einfach an sich reissen; der kurze Ausflug ueber Topmost half im Versuch, unter Linux entscheidet der Fenstermanager. Mehr als bitten kann eine Anwendung nicht. ShutdownSignal ist zu InstanceControl geworden: beenden und Fenster sind zwei Bitten desselben Absenders ueber denselben Weg. Beenden gilt einmal, das Fenster beliebig oft - jeder Wechsel auf diese Instanz ist einer. --managed schaltet den Wechsel im Instanzfenster ab. Der Manager gibt es jedem Instanzprozess mit; das Fenster graut seinen Instanz-Schalter aus und erklaert auf Nachfrage, wo gewechselt wird. Zwei Schloesser an derselben Tuer: Der Knopf ist aus, und der Weg dahinter prueft es noch einmal - denn hinter ihm wartet ein Selbst-Neustart. instanceWindows in manager.json, vorbelegt auf true: Jede Instanz behaelt ihr Fenster. Auf einer Maschine ohne Bildschirmsitzung gehoert false hin. ABNAHME an echten Fenstern: Instanz ueber den Manager gestartet, PID 18128, Fenstertitel "ClawdDotNet", Befehlszeile mit --managed. Fenster von aussen minimiert (IsIconic True), dann POST /instanzen/Instance-Test/fenster: IsIconic False, im Vordergrund - und PID unveraendert 18128. Nichts neu gestartet. Das ist die Abnahmebedingung dieser Stufe. Build ohne Warnungen, 780 Tests gruen, 8 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,31 +6,46 @@ using Microsoft.AspNetCore.Routing;
|
||||
namespace ClawdDotNet.Web.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Der Wunsch, dass dieser Instanzprozess endet.
|
||||
/// Was der Instanzmanager diesem Prozess sagen kann — und was der Prozess damit macht.
|
||||
///
|
||||
/// <para>Ein eigener Typ und kein Rückruf am Endpunkt, weil zwei Seiten ihn brauchen:
|
||||
/// Der Endpunkt löst ihn aus, der Wirtsprozess wartet darauf. Wer wartet, ist verschieden
|
||||
/// — kopflos die Schleife in <c>HeadlessRun</c>, mit Fenster die Avalonia-Anwendung.</para>
|
||||
/// <para>Ein eigener Typ und keine Rückrufe am Endpunkt, weil zwei Seiten ihn brauchen:
|
||||
/// Der Endpunkt löst aus, der Wirtsprozess reagiert. Und wer reagiert, ist verschieden —
|
||||
/// kopflos die Warteschleife in <c>HeadlessRun</c>, mit Fenster die
|
||||
/// Avalonia-Anwendung.</para>
|
||||
/// </summary>
|
||||
public sealed class ShutdownSignal
|
||||
public sealed class InstanceControl
|
||||
{
|
||||
/// <summary>Wird höchstens einmal ausgelöst.</summary>
|
||||
public event Action? Requested;
|
||||
private int _shutdownRaised;
|
||||
|
||||
private int _raised;
|
||||
/// <summary>Der Manager bittet, diesen Prozess zu beenden.</summary>
|
||||
public event Action? ShutdownRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Meldet den Wunsch an. Mehrfaches Aufrufen tut nichts — ein zweiter Aufruf würde
|
||||
/// sonst mitten im Herunterfahren ein zweites Herunterfahren anstoßen.
|
||||
/// Der Manager bittet, das Fenster dieser Instanz nach vorn zu holen (E-M2).
|
||||
///
|
||||
/// <para>Kopflos gibt es nichts nach vorn zu holen; dann läuft dieses Ereignis ins
|
||||
/// Leere, und das ist die richtige Antwort.</para>
|
||||
/// </summary>
|
||||
public bool Raise()
|
||||
public event Action? FocusRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Meldet den Wunsch zu enden an. Ein zweiter Aufruf tut nichts — sonst stiesse er
|
||||
/// mitten im Herunterfahren ein zweites Herunterfahren an.
|
||||
/// </summary>
|
||||
public bool RequestShutdown()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _raised, 1) != 0)
|
||||
if (Interlocked.Exchange(ref _shutdownRaised, 1) != 0)
|
||||
return false;
|
||||
|
||||
Requested?.Invoke();
|
||||
ShutdownRequested?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holt das Fenster nach vorn. Beliebig oft — anders als beim Beenden ist die
|
||||
/// Wiederholung hier der Normalfall: Jeder Wechsel auf diese Instanz ist einer.
|
||||
/// </summary>
|
||||
public void RequestFocus() => FocusRequested?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,8 +55,8 @@ public sealed class ShutdownSignal
|
||||
/// Endpunkte nicht, auch mit gültigem Token nicht, und sie antworten dort mit 404 statt
|
||||
/// mit 403: Was es nicht gibt, muss man auch nicht erklären. Der Grund ist der Schnitt aus
|
||||
/// #528 — die Lebensdauer einer Instanz gehört dem Instanzmanager. Eine Browsersitzung
|
||||
/// soll den Prozess, der sie gerade bedient, nicht beenden können; der Manager soll es,
|
||||
/// und er kommt über den Socket.</para>
|
||||
/// soll den Prozess, der sie gerade bedient, nicht beenden und auch nicht sein Fenster
|
||||
/// aufreissen können; der Manager soll es, und er kommt über den Socket.</para>
|
||||
/// </summary>
|
||||
internal static class ControlEndpoints
|
||||
{
|
||||
@@ -49,7 +64,7 @@ internal static class ControlEndpoints
|
||||
{
|
||||
var control = app.MapGroup("/api/instanz");
|
||||
|
||||
control.MapPost("/beenden", (HttpContext context, ShutdownSignal signal) =>
|
||||
control.MapPost("/beenden", (HttpContext context, InstanceControl steuerung) =>
|
||||
{
|
||||
if (!LocalTransport.Applies(context))
|
||||
return Results.NotFound();
|
||||
@@ -61,12 +76,25 @@ internal static class ControlEndpoints
|
||||
// haengt aber ab, ob er anschliessend geduldig wartet oder abschiesst.
|
||||
context.Response.OnCompleted(() =>
|
||||
{
|
||||
signal.Raise();
|
||||
steuerung.RequestShutdown();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// 202 und nicht 200: Beim Antworten laeuft der Prozess noch.
|
||||
return Results.Accepted(value: new { angenommen = true });
|
||||
});
|
||||
|
||||
control.MapPost("/fenster", (HttpContext context, InstanceControl steuerung) =>
|
||||
{
|
||||
if (!LocalTransport.Applies(context))
|
||||
return Results.NotFound();
|
||||
|
||||
// Hier ohne OnCompleted: Das Fenster nach vorn zu holen beendet nichts, die
|
||||
// Antwort geht ohnehin raus. Und der Manager soll gleich erfahren, ob dieser
|
||||
// Prozess ueberhaupt eines hat.
|
||||
steuerung.RequestFocus();
|
||||
|
||||
return Results.Ok(new { angenommen = true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,19 +51,20 @@ public sealed class InstanceHttpHost : IAsyncDisposable
|
||||
Endpoint = endpoint;
|
||||
Url = url;
|
||||
|
||||
ShutdownSignal = app.Services.GetRequiredService<ShutdownSignal>();
|
||||
Control = app.Services.GetRequiredService<InstanceControl>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der Instanzmanager hat gebeten, diesen Prozess zu beenden
|
||||
/// (<c>POST /api/instanz/beenden</c>, nur über den Socket).
|
||||
/// Was der Instanzmanager diesem Prozess über den Socket sagen kann: sich beenden
|
||||
/// (<c>POST /api/instanz/beenden</c>) oder sein Fenster nach vorn holen
|
||||
/// (<c>POST /api/instanz/fenster</c>).
|
||||
///
|
||||
/// <para>Der Wirtsprozess hängt sich hier an und beendet sich: kopflos die
|
||||
/// Warteschleife, mit Fenster die Avalonia-Anwendung. Diese Klasse tut es nicht
|
||||
/// selbst — sie kennt nur ihren Kestrel und wüsste gar nicht, was sonst noch zu
|
||||
/// beenden wäre.</para>
|
||||
/// <para>Der Wirtsprozess hängt sich hier an und tut es: kopflos beendet sich die
|
||||
/// Warteschleife, mit Fenster reagiert die Avalonia-Anwendung. Diese Klasse tut es
|
||||
/// nicht selbst — sie kennt nur ihren Kestrel und wüsste gar nicht, was sonst noch zu
|
||||
/// beenden oder anzuzeigen wäre.</para>
|
||||
/// </summary>
|
||||
public ShutdownSignal ShutdownSignal { get; }
|
||||
public InstanceControl Control { get; }
|
||||
|
||||
/// <summary>Socket oder Named Pipe — der Weg, den der Instanzmanager nimmt.</summary>
|
||||
public string Endpoint { get; }
|
||||
@@ -184,7 +185,7 @@ public sealed class InstanceHttpHost : IAsyncDisposable
|
||||
builder.WebHost.ConfigureKestrel(kestrel => Listen(kestrel, host.InstancePath, options));
|
||||
|
||||
builder.Services.AddSingleton(host);
|
||||
builder.Services.AddSingleton<ShutdownSignal>();
|
||||
builder.Services.AddSingleton<InstanceControl>();
|
||||
|
||||
if (options is not null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user