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,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 };
|
||||
|
||||
|
||||
@@ -16,6 +16,17 @@ internal static class CommandLine
|
||||
/// <summary>Läuft ohne Fenster: nur Aufbau, Socket und Warten (#528, Stufe 2).</summary>
|
||||
public const string HeadlessArgument = "--headless";
|
||||
|
||||
/// <summary>
|
||||
/// Gestartet und beaufsichtigt vom Instanzmanager (#528, Stufe 5).
|
||||
///
|
||||
/// <para>Dann gehört der Instanzwechsel dorthin und nicht in dieses Fenster: Der
|
||||
/// Manager hält die Fenster aller Instanzen im Hintergrund und holt das gewünschte
|
||||
/// nach vorn. Ein Fenster, das sich selbst mit einer anderen Instanz neu startet,
|
||||
/// nähme dabei genau die Instanz mit, aus der heraus jemand wechselt — und das ist
|
||||
/// der Vorfall, mit dem dieses Vorhaben angefangen hat (#529).</para>
|
||||
/// </summary>
|
||||
public const string ManagedArgument = "--managed";
|
||||
|
||||
/// <summary>
|
||||
/// Der Instanzpfad hinter <c>--instance</c>, oder <c>null</c>.
|
||||
///
|
||||
@@ -37,6 +48,13 @@ internal static class CommandLine
|
||||
}
|
||||
|
||||
public static bool IsHeadless(IReadOnlyList<string>? args)
|
||||
=> Has(args, HeadlessArgument);
|
||||
|
||||
/// <inheritdoc cref="ManagedArgument"/>
|
||||
public static bool IsManaged(IReadOnlyList<string>? args)
|
||||
=> Has(args, ManagedArgument);
|
||||
|
||||
private static bool Has(IReadOnlyList<string>? args, string flag)
|
||||
=> args is not null
|
||||
&& args.Any(a => string.Equals(a, HeadlessArgument, StringComparison.OrdinalIgnoreCase));
|
||||
&& args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ internal static class HeadlessRun
|
||||
"Instanz {Name} läuft ohne Fenster auf {Endpunkt}",
|
||||
host.Instance.InstanceName, web.Endpoint);
|
||||
|
||||
await WaitForShutdownAsync(logger, web.ShutdownSignal);
|
||||
await WaitForShutdownAsync(logger, web.Control);
|
||||
|
||||
logger.LogInformation("Instanz {Name} wird beendet.", host.Instance.InstanceName);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ internal static class HeadlessRun
|
||||
/// das geordnete Herunterfahren noch läuft — und genau dabei wird der Schreibpuffer
|
||||
/// des Protokolls geleert. Ein Ende ohne diese Zeilen ist der Zustand aus #529.</para>
|
||||
/// </summary>
|
||||
private static async Task WaitForShutdownAsync(ILogger logger, ShutdownSignal signal)
|
||||
private static async Task WaitForShutdownAsync(ILogger logger, InstanceControl steuerung)
|
||||
{
|
||||
var schluss = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
@@ -124,7 +124,7 @@ internal static class HeadlessRun
|
||||
// Der Instanzmanager, ueber den Socket. Der uebliche Weg, sobald es ihn gibt:
|
||||
// Er bittet, statt zu schiessen - ein Instanzprozess haelt eine offene Datenbank
|
||||
// und laufende Agenten, und beides will zu Ende gebracht werden.
|
||||
signal.Requested += () => Beenden("Instanzmanager");
|
||||
steuerung.ShutdownRequested += () => Beenden("Instanzmanager");
|
||||
|
||||
using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
|
||||
{
|
||||
|
||||
@@ -97,6 +97,16 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
public event Action? RequestExit;
|
||||
public event Action? RequestInstanceManagerDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Ob der Instanzwechsel aus diesem Fenster heraus angeboten wird.
|
||||
///
|
||||
/// <para><c>false</c>, sobald der Instanzmanager diese Instanz führt (#528, E-M2):
|
||||
/// Dann hält er die Fenster aller Instanzen im Hintergrund und holt das gewünschte
|
||||
/// nach vorn. Ein Wechsel von hier aus wäre ein Selbst-Neustart und nähme genau die
|
||||
/// Instanz mit, aus der heraus jemand wechselt.</para>
|
||||
/// </summary>
|
||||
public bool CanSwitchInstance { get; init; } = true;
|
||||
|
||||
/// <summary>Für den Entwurfsmodus des Editors — ohne laufenden Aufbau.</summary>
|
||||
public MainWindowViewModel() : this(null) { }
|
||||
|
||||
|
||||
@@ -52,9 +52,14 @@
|
||||
IsVisible="{Binding IsSidebarExpanded}" />
|
||||
|
||||
<!-- Instanz-Schalter: der einzige Knopf mit Akzentrahmen, weil er als
|
||||
einziger die ganze Anwendung umschaltet. -->
|
||||
einziger die ganze Anwendung umschaltet.
|
||||
|
||||
Ausgegraut, sobald der Instanzmanager diese Instanz fuehrt (#528, E-M2):
|
||||
Gewechselt wird dann dort, und er holt das Fenster der gewuenschten Instanz
|
||||
nach vorn, statt eine laufende zu beenden. -->
|
||||
<Button DockPanel.Dock="Top" Classes="instance" Margin="12,12,12,0"
|
||||
Command="{Binding OpenInstanceManagerCommand}"
|
||||
IsEnabled="{Binding CanSwitchInstance}"
|
||||
ToolTip.Tip="Instanz wechseln">
|
||||
<DockPanel>
|
||||
<ctrl:StrokeIcon Data="{StaticResource IconLogo}" Width="15" Height="15"
|
||||
|
||||
Reference in New Issue
Block a user