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>
134 lines
4.4 KiB
C#
134 lines
4.4 KiB
C#
namespace ClawdDotNet.Manager.Tests;
|
|
|
|
/// <summary>
|
|
/// Ein Instanzprozess, den der Test steuert.
|
|
///
|
|
/// <para>Ein echter Prozess ließe die Aufsicht nur oberflächlich prüfen: Ein Absturz zum
|
|
/// gewünschten Zeitpunkt, ein Prozess, der lebt und schweigt, eine wiederverwendete
|
|
/// Prozesskennung — das alles ist mit einem laufenden Programm entweder nicht oder nur
|
|
/// mit Wartezeiten herzustellen, und Tests mit Wartezeiten sind Tests, die man
|
|
/// irgendwann abschaltet.</para>
|
|
/// </summary>
|
|
internal sealed class FakeProcess(int id, DateTimeOffset startedAt) : IInstanceProcess
|
|
{
|
|
public int Id { get; } = id;
|
|
|
|
public DateTimeOffset StartedAt { get; } = startedAt;
|
|
|
|
public bool HasExited { get; private set; }
|
|
|
|
public int? ExitCode { get; private set; }
|
|
|
|
public bool WasKilled { get; private set; }
|
|
|
|
public bool Disposed { get; private set; }
|
|
|
|
/// <summary>Der Prozess endet — mit oder ohne Zutun.</summary>
|
|
public void Exit(int code)
|
|
{
|
|
HasExited = true;
|
|
ExitCode = code;
|
|
}
|
|
|
|
public void Kill()
|
|
{
|
|
WasKilled = true;
|
|
if (!HasExited) Exit(-1);
|
|
}
|
|
|
|
public void Dispose() => Disposed = true;
|
|
}
|
|
|
|
internal sealed class FakeLauncher : IInstanceLauncher
|
|
{
|
|
private int _nextId = 1000;
|
|
|
|
public FakeProcess? Last { get; private set; }
|
|
|
|
public int Starts { get; private set; }
|
|
|
|
/// <summary>Wenn gesetzt, scheitert der Start damit.</summary>
|
|
public Exception? FailStartWith { get; set; }
|
|
|
|
/// <summary>Was <see cref="Attach"/> liefern soll — bereits laufende Prozesse.</summary>
|
|
public Dictionary<int, FakeProcess> Running { get; } = [];
|
|
|
|
public DateTimeOffset Now { get; set; } = DateTimeOffset.UnixEpoch;
|
|
|
|
public IInstanceProcess Start(string instanceDirectory)
|
|
{
|
|
Starts++;
|
|
|
|
if (FailStartWith is not null) throw FailStartWith;
|
|
|
|
Last = new FakeProcess(_nextId++, Now);
|
|
Running[Last.Id] = Last;
|
|
return Last;
|
|
}
|
|
|
|
public IInstanceProcess? Attach(int processId, DateTimeOffset startedAt)
|
|
{
|
|
if (!Running.TryGetValue(processId, out var process))
|
|
return null;
|
|
|
|
// Dieselbe Pruefung wie im Echtbetrieb: gleiche Kennung, aber andere Startzeit
|
|
// heisst anderer Prozess.
|
|
return (process.StartedAt - startedAt).Duration() > TimeSpan.FromSeconds(2)
|
|
? null
|
|
: process;
|
|
}
|
|
}
|
|
|
|
/// <summary>Der Socket, den der Test antworten oder schweigen lässt.</summary>
|
|
internal sealed class FakeChannel : IInstanceChannel
|
|
{
|
|
/// <summary>Die Antwort für alle Verzeichnisse, sofern keine eigene hinterlegt ist.</summary>
|
|
public InstanceIdentity? Answer { get; set; }
|
|
|
|
/// <summary>
|
|
/// Antworten je Instanzverzeichnis.
|
|
///
|
|
/// <para>Nötig, sobald mehr als eine Instanz im Spiel ist: Eine Auskunft, die für jedes
|
|
/// Verzeichnis dieselbe Instanz nennt, lässt Tests grün werden, die im Betrieb zwei
|
|
/// verschiedene Instanzen verwechseln würden.</para>
|
|
/// </summary>
|
|
public Dictionary<string, InstanceIdentity?> AnswerFor { get; } =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public int Calls { get; private set; }
|
|
|
|
/// <summary>Wie oft die Instanz gebeten wurde, sich zu beenden.</summary>
|
|
public int StopRequests { get; private set; }
|
|
|
|
/// <summary>Ob die Bitte ankommt. Falsch heisst: Die Instanz hoert gar nicht mehr zu.</summary>
|
|
public bool StopRequestArrives { get; set; } = true;
|
|
|
|
public Task<InstanceIdentity?> AskAsync(string instanceDirectory, CancellationToken ct = default)
|
|
{
|
|
Calls++;
|
|
|
|
return Task.FromResult(
|
|
AnswerFor.TryGetValue(Path.GetFullPath(instanceDirectory), out var eigene)
|
|
? eigene
|
|
: Answer);
|
|
}
|
|
|
|
public Task<bool> RequestStopAsync(string instanceDirectory, CancellationToken ct = default)
|
|
{
|
|
StopRequests++;
|
|
return Task.FromResult(StopRequestArrives);
|
|
}
|
|
|
|
/// <summary>Welche Instanzen gebeten wurden, ihr Fenster nach vorn zu holen.</summary>
|
|
public List<string> FocusRequests { get; } = [];
|
|
|
|
/// <summary>Ob die Instanz ein Fenster hat, das nach vorn kommen kann.</summary>
|
|
public bool HasWindow { get; set; } = true;
|
|
|
|
public Task<bool> RequestFocusAsync(string instanceDirectory, CancellationToken ct = default)
|
|
{
|
|
FocusRequests.Add(Path.GetFullPath(instanceDirectory));
|
|
return Task.FromResult(HasWindow);
|
|
}
|
|
}
|