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:
Richard
2026-09-08 23:39:50 +02:00
co-authored by Claude Opus 5
parent a511480b77
commit db7abf699f
18 changed files with 496 additions and 42 deletions
+65 -1
View File
@@ -32,6 +32,16 @@ public partial class App : Application
/// <summary>Vorgewaehlte Instanz aus der Befehlszeile; wird genau einmal verbraucht.</summary>
private string? _preselectedInstancePath;
/// <summary>
/// Vom Instanzmanager gestartet (<c>--managed</c>).
///
/// <para>Dann gehoert der Instanzwechsel dorthin: Der Manager haelt die Fenster aller
/// Instanzen im Hintergrund und holt das gewuenschte nach vorn (E-M2). Ein Fenster,
/// das sich selbst mit einer anderen Instanz neu startet, naehme genau die Instanz
/// mit, aus der heraus jemand wechselt.</para>
/// </summary>
private bool _managed;
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
@@ -44,6 +54,7 @@ public partial class App : Application
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
_preselectedInstancePath = CommandLine.ReadInstancePath(desktop.Args);
_managed = CommandLine.IsManaged(desktop.Args);
ApplySavedThemeEarly();
@@ -127,7 +138,11 @@ public partial class App : Application
// Instanzverzeichnis, ueber den der Instanzmanager die Instanz erreicht (#528).
_web = InstanceHttpHost.TryStart(_host, _host.LoggerFactory);
var mainViewModel = new MainWindowViewModel(_host);
var mainViewModel = new MainWindowViewModel(_host)
{
// Wer beaufsichtigt wird, wechselt nicht selbst - siehe _managed.
CanSwitchInstance = !_managed
};
// Der Wechsel gehoert hierher und nicht ins Fenster: Er braucht den Aufbau und
// die Lebensdauer der Anwendung, und beides kennt nur diese Klasse.
@@ -135,6 +150,10 @@ public partial class App : Application
desktop.MainWindow = new MainWindow { DataContext = mainViewModel };
// Der Manager bittet ueber den Socket, dieses Fenster nach vorn zu holen (E-M2).
if (_web is not null)
_web.Control.FocusRequested += () => BringToFront(desktop);
desktop.MainWindow.Show();
desktop.MainWindow.Closed += (_, _) => desktop.Shutdown();
@@ -311,6 +330,38 @@ public partial class App : Application
return await completion.Task;
});
/// <summary>
/// Holt das Fenster dieser Instanz nach vorn — auf Bitte des Instanzmanagers.
///
/// <para><b>Die Instanz macht das selbst, statt dass jemand von aussen an ihrem
/// Fenster zieht.</b> Der Manager kennt kein Fenstersystem und soll keines kennen; er
/// bittet über den Socket, und wer ein Fenster hat, holt es nach vorn. Kopflos läuft
/// die Bitte ins Leere, und das ist die richtige Antwort.</para>
///
/// <para><b>Das letzte Wort hat der Fenstermanager.</b> Windows lässt einen Prozess
/// im Hintergrund den Vordergrund nicht einfach an sich reissen — <c>Activate</c>
/// blinkt dann nur in der Leiste. Der kurze Ausflug über <c>Topmost</c> bringt es in
/// den meisten Fällen trotzdem nach vorn; unter Linux entscheidet der Fenstermanager,
/// und manche sind da streng. Mehr als bitten kann eine Anwendung nicht, und mehr
/// sollte sie auch nicht tun.</para>
/// </summary>
private static void BringToFront(IClassicDesktopStyleApplicationLifetime desktop)
=> Dispatcher.UIThread.Post(() =>
{
if (desktop.MainWindow is not { } fenster) return;
if (fenster.WindowState == WindowState.Minimized)
fenster.WindowState = WindowState.Normal;
fenster.Show();
var vorher = fenster.Topmost;
fenster.Topmost = true;
fenster.Topmost = vorher;
fenster.Activate();
});
/// <summary>
/// Instanzwechsel aus der laufenden Anwendung.
///
@@ -326,6 +377,19 @@ public partial class App : Application
{
if (_host is null) return;
if (_managed)
{
// Sollte gar nicht erst aufrufbar sein (CanSwitchInstance), aber ein zweites
// Schloss an der Tuer, hinter der ein Selbst-Neustart wartet, ist billig.
await new AvaloniaLicensePrompt().ShowInfoAsync(
"ClawdDotNet – Instanzwechsel",
"Diese Instanz wird vom Instanzmanager geführt. Gewechselt wird dort — "
+ "er holt das Fenster der gewünschten Instanz nach vorn, statt eine "
+ "laufende zu beenden.");
return;
}
var viewModel = new InstancePickerViewModel(_host.Directories, _host.InstancePath);
var window = new InstancePickerWindow { DataContext = viewModel };