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:
@@ -32,6 +32,14 @@ public interface IInstanceChannel
|
||||
/// Socket.</para>
|
||||
/// </summary>
|
||||
Task<bool> RequestStopAsync(string instanceDirectory, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Bittet die Instanz, ihr Fenster nach vorn zu holen (E-M2).
|
||||
///
|
||||
/// <para><c>false</c> heißt: Die Bitte kam nicht an, oder diese Instanz hat kein
|
||||
/// Fenster — sie läuft kopflos. Beides ist kein Fehler, sondern eine Auskunft.</para>
|
||||
/// </summary>
|
||||
Task<bool> RequestFocusAsync(string instanceDirectory, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -93,8 +101,15 @@ public sealed class SocketInstanceChannel(TimeSpan? timeout = null) : IInstanceC
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RequestStopAsync(
|
||||
public Task<bool> RequestFocusAsync(
|
||||
string instanceDirectory, CancellationToken ct = default)
|
||||
=> PostAsync(instanceDirectory, "/api/instanz/fenster", ct);
|
||||
|
||||
public Task<bool> RequestStopAsync(
|
||||
string instanceDirectory, CancellationToken ct = default)
|
||||
=> PostAsync(instanceDirectory, "/api/instanz/beenden", ct);
|
||||
|
||||
private async Task<bool> PostAsync(string instanceDirectory, string pfad, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -102,7 +117,7 @@ public sealed class SocketInstanceChannel(TimeSpan? timeout = null) : IInstanceC
|
||||
client.Timeout = _timeout;
|
||||
|
||||
using var antwort = await client
|
||||
.PostAsync("/api/instanz/beenden", content: null, ct)
|
||||
.PostAsync(pfad, content: null, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return antwort.IsSuccessStatusCode;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace ClawdDotNet.Manager;
|
||||
///
|
||||
/// <para>Bewusst schmal. Die Aufsicht fragt <b>nicht</b> die Prozessliste, ob eine Instanz
|
||||
/// lebt — das beantwortet der Socket, und nur er beantwortet es richtig (siehe
|
||||
/// <see cref="SocketInstanceProbe"/>). Vom Prozess braucht sie dreierlei: seine Kennung
|
||||
/// <see cref="SocketInstanceChannel"/>). Vom Prozess braucht sie dreierlei: seine Kennung
|
||||
/// fürs Protokoll, den Abschiedscode, wenn er weg ist, und die Möglichkeit, ihn zu
|
||||
/// beenden, wenn er nicht mehr auf Bitten hört.</para>
|
||||
/// </summary>
|
||||
@@ -50,11 +50,14 @@ public interface IInstanceLauncher
|
||||
/// <summary>
|
||||
/// Der echte Start: <c>ClawdDotNet --headless --instance <Verzeichnis></c>.
|
||||
///
|
||||
/// <para><b>Vorerst ohne Fenster.</b> Nach E-M2 behält jede Instanz ihr eigenes
|
||||
/// Desktop-Fenster; das kommt mit Stufe 5, wenn der Wechsel das Fenster nach vorn holt.
|
||||
/// Bis dahin ist kopflos die Betriebsart, die sich vollständig beobachten lässt — und
|
||||
/// eine Instanz, die auf einem Server ohne Bildschirmsitzung gestartet wird, braucht sie
|
||||
/// ohnehin.</para>
|
||||
/// <para><b>Mit oder ohne Fenster.</b> Nach E-M2 behält jede Instanz ihr eigenes
|
||||
/// Desktop-Fenster; sie laufen im Hintergrund, und beim Wechsel kommt eines nach vorn.
|
||||
/// Auf einer Maschine ohne Bildschirmsitzung gibt es keines, und dann ist kopflos die
|
||||
/// einzige Betriebsart, die trägt.</para>
|
||||
///
|
||||
/// <para><b><c>--managed</c> geht immer mit.</b> Daran erkennt die Instanz, dass sie
|
||||
/// geführt wird, und bietet den Instanzwechsel nicht mehr selbst an: Der wäre ein
|
||||
/// Selbst-Neustart und nähme genau die Instanz mit, aus der heraus jemand wechselt.</para>
|
||||
/// </summary>
|
||||
public sealed class ProcessInstanceLauncher(string executablePath, bool headless = true)
|
||||
: IInstanceLauncher
|
||||
@@ -85,6 +88,7 @@ public sealed class ProcessInstanceLauncher(string executablePath, bool headless
|
||||
|
||||
if (headless) start.ArgumentList.Add("--headless");
|
||||
|
||||
start.ArgumentList.Add("--managed");
|
||||
start.ArgumentList.Add("--instance");
|
||||
start.ArgumentList.Add(Path.GetFullPath(instanceDirectory));
|
||||
|
||||
|
||||
@@ -201,6 +201,28 @@ public sealed class InstanceRegistry : IAsyncDisposable
|
||||
public Task<bool> StopAsync(string instanceDirectory, CancellationToken ct = default)
|
||||
=> Find(instanceDirectory).StopAsync(ct);
|
||||
|
||||
/// <summary>
|
||||
/// Holt das Fenster einer Instanz nach vorn — der Wechsel, wie E-M2 ihn meint.
|
||||
///
|
||||
/// <para><b>Das ist der Ersatz für den Selbst-Neustart.</b> Vorher hiess „Instanz
|
||||
/// wechseln": Der laufende Prozess fährt sich herunter und startet sich mit einer
|
||||
/// anderen Instanz neu — wer wechselte, beendete zwangsläufig die Instanz, aus der
|
||||
/// heraus er es tat. Jetzt laufen alle, und es kommt nur eine nach vorn.</para>
|
||||
///
|
||||
/// <para><c>false</c> heisst: Sie läuft nicht, sie hört nicht, oder sie hat kein
|
||||
/// Fenster.</para>
|
||||
/// </summary>
|
||||
public async Task<bool> FocusAsync(string instanceDirectory, CancellationToken ct = default)
|
||||
{
|
||||
var aufsicht = Find(instanceDirectory);
|
||||
|
||||
if (aufsicht.Condition != InstanceCondition.Running)
|
||||
return false;
|
||||
|
||||
return await _channel.RequestFocusAsync(aufsicht.InstanceDirectory, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hält alle laufenden Instanzen an. <b>Nicht</b> beim Beenden des Managers, sondern
|
||||
/// nur, wenn es einen Grund gibt, der sie alle betrifft — ein Lizenzwiderruf.
|
||||
|
||||
@@ -189,6 +189,10 @@ public sealed class ManagerCore : IAsyncDisposable
|
||||
public Task<bool> StopInstanceAsync(string instanceDirectory, CancellationToken ct = default)
|
||||
=> Instances.StopAsync(instanceDirectory, ct);
|
||||
|
||||
/// <inheritdoc cref="InstanceRegistry.FocusAsync"/>
|
||||
public Task<bool> FocusInstanceAsync(string instanceDirectory, CancellationToken ct = default)
|
||||
=> Instances.FocusAsync(instanceDirectory, ct);
|
||||
|
||||
// ─── Das Urteil ───
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,6 +39,18 @@ public sealed class ManagerSettings
|
||||
[JsonPropertyName("instanceExecutable")]
|
||||
public string InstanceExecutable { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Ob gestartete Instanzen ein eigenes Fenster bekommen (E-M2).
|
||||
///
|
||||
/// <para>Vorbelegt auf <c>true</c>: Jede Instanz behält ihr Desktop-Fenster, sie laufen
|
||||
/// im Hintergrund, und beim Wechsel kommt eines nach vorn. Auf einer Maschine ohne
|
||||
/// Bildschirmsitzung — einem Server, einem Bauknecht — gehört hier <c>false</c> hin;
|
||||
/// dann laufen die Instanzen kopflos und sind nur über die Weboberfläche zu
|
||||
/// bedienen.</para>
|
||||
/// </summary>
|
||||
[JsonPropertyName("instanceWindows")]
|
||||
public bool InstanceWindows { get; set; } = true;
|
||||
|
||||
// ─── Die eine Weboberfläche (E-M5) ───
|
||||
|
||||
[JsonPropertyName("webUiPort")]
|
||||
|
||||
Reference in New Issue
Block a user