Merge nucleic/lucid-river-toad-6efj into dev
This commit is contained in:
@@ -48,6 +48,25 @@ public sealed class WslcFacade : IWslc
|
||||
private string? gateway;
|
||||
private readonly SemaphoreSlim sessionGate = new(1, 1);
|
||||
|
||||
/// D13's Tier 1 internal-COM arm, or null where it could not bind. Only used to recover an
|
||||
/// orphaned session after a broker restart; every other call rides the compat SDK.
|
||||
private readonly WslcInternal? recovery = WslcInternal.TryBind();
|
||||
|
||||
/// <summary>
|
||||
/// Construct the facade with COM security configured first.
|
||||
///
|
||||
/// The ordering is load-bearing and easy to lose: `CoInitializeSecurity` must precede the
|
||||
/// **first COM call in the process**, and the compat SDK makes its own. Doing it in a factory
|
||||
/// keeps that constraint next to the code that depends on it rather than in `Program.cs`,
|
||||
/// where a later reorder would silently break session recovery with a `0x80070542` that reads
|
||||
/// as "not found".
|
||||
/// </summary>
|
||||
public static WslcFacade Create()
|
||||
{
|
||||
WslcInternal.InitializeSecurity();
|
||||
return new WslcFacade();
|
||||
}
|
||||
|
||||
/// name → the handle CreateContainer returned. See the class remarks: without this there is
|
||||
/// no way to address a container at all, because Sdk.Container carries no name.
|
||||
private readonly Dictionary<string, Entry> containers = [];
|
||||
@@ -76,10 +95,18 @@ public sealed class WslcFacade : IWslc
|
||||
}
|
||||
}
|
||||
|
||||
/// Compat-only, so: no enumeration, no reattach, no pty. Stats ARE offered — not from the SDK
|
||||
/// (there is no GetStatistics()) but from an in-guest cgroup read, which is the escape hatch
|
||||
/// §13.1 names and is indistinguishable to hostd.
|
||||
public IReadOnlyList<string> Capabilities => ["stats"];
|
||||
/// <summary>
|
||||
/// Stats are always offered — not from the SDK (there is no `GetStatistics()`) but from an
|
||||
/// in-guest cgroup read, which is indistinguishable to hostd. `recover` is added when D13's
|
||||
/// Tier 1 arm bound: a broker restart re-adopts and clears its orphaned session instead of
|
||||
/// leaving the user a sandbox only `wsl --shutdown` can fix.
|
||||
///
|
||||
/// Still absent, and deliberately: `enumerate` (`container.list` answers from this broker's
|
||||
/// own roster, not the service), `reattach` (containers do not survive a restart — Tier 2,
|
||||
/// blocked, §13.2) and `tty` (the compat surface has no pty).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Capabilities =>
|
||||
recovery is not null ? ["stats", "recover"] : ["stats"];
|
||||
|
||||
public void SetEvents(IBrokerEvents events) => this.events = events;
|
||||
|
||||
@@ -113,34 +140,19 @@ public sealed class WslcFacade : IWslc
|
||||
{
|
||||
if (session is not null) return gateway!;
|
||||
|
||||
var settings = new Sdk.SessionSettings(spec.Name, spec.DataDir);
|
||||
if (spec.Cpu is { } cpu) settings.CpuCount = (uint)cpu;
|
||||
if (spec.MemoryMB is { } memory) settings.MemorySizeInMB = (uint)memory;
|
||||
|
||||
var created = new Sdk.Session(settings);
|
||||
// Subscribe BEFORE Start(): a session that dies during boot must still report down.
|
||||
created.Terminated += reason => events?.SessionDown(reason.ToString());
|
||||
// Not surfaced as an RPC, but it is the only crash detail wslc offers and it is what
|
||||
// makes a SIGKILLed agent explicable in the host log (the Swift `diagnoseKill` seam).
|
||||
created.ProcessCrashed += crash => Console.Error.WriteLine(
|
||||
$"wslc: process {crash.ProcessName} (pid {crash.Pid}) crashed with signal "
|
||||
+ $"{crash.Signal}; dump at {crash.DumpPath}");
|
||||
|
||||
var created = NewSession(spec);
|
||||
try
|
||||
{
|
||||
created.Start();
|
||||
}
|
||||
catch (Exception e) when (HResultOf(e) == ErrorAlreadyExists)
|
||||
{
|
||||
created.Dispose();
|
||||
// The constructor is lazy — it only captures settings — so reaching this means a
|
||||
// session of this name is genuinely RUNNING, started by a previous broker or
|
||||
// another process. The compat surface cannot re-adopt it, and there is no handle
|
||||
// to terminate it through either, so this is terminal for this broker.
|
||||
throw new WslcError(
|
||||
WslcError.SessionExists,
|
||||
$"a wslc session named '{spec.Name}' is already running and the compat SDK "
|
||||
+ "cannot re-adopt it; run `wsl --shutdown` to clear it");
|
||||
// session of this name is genuinely RUNNING, started by a previous broker that
|
||||
// died. The compat surface cannot re-adopt it, so recovery goes through D13's
|
||||
// internal-COM arm: open it, note what it was running, terminate it, retry.
|
||||
created.Dispose();
|
||||
created = await RecoverAndRestartAsync(spec, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is not WslcError)
|
||||
{
|
||||
@@ -158,6 +170,73 @@ public sealed class WslcFacade : IWslc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A settings-configured session with its handlers already attached. Subscribing
|
||||
/// must happen BEFORE `Start()`, or a session that dies during boot never reports down.</summary>
|
||||
private Sdk.Session NewSession(SessionSpec spec)
|
||||
{
|
||||
var settings = new Sdk.SessionSettings(spec.Name, spec.DataDir);
|
||||
if (spec.Cpu is { } cpu) settings.CpuCount = (uint)cpu;
|
||||
if (spec.MemoryMB is { } memory) settings.MemorySizeInMB = (uint)memory;
|
||||
|
||||
var created = new Sdk.Session(settings);
|
||||
created.Terminated += reason => events?.SessionDown(reason.ToString());
|
||||
// Not surfaced as an RPC, but it is the only crash detail wslc offers and it is what
|
||||
// makes a SIGKILLed agent explicable in the host log (the Swift `diagnoseKill` seam).
|
||||
created.ProcessCrashed += crash => Console.Error.WriteLine(
|
||||
$"wslc: process {crash.ProcessName} (pid {crash.Pid}) crashed with signal "
|
||||
+ $"{crash.Signal}; dump at {crash.DumpPath}");
|
||||
return created;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A session of this name is already running and we do not own it — the signature of a broker
|
||||
/// that died with its sandbox up (docs/WINDOWS_PORT.md §13.2, D13 Tier 1).
|
||||
///
|
||||
/// Clear it through the internal COM arm and start fresh. The orphan's containers are lost,
|
||||
/// which is the deliberate Tier 1 trade: they are lost today too (nothing could reach that
|
||||
/// session at all), `ContainerManager.reconcile` already copes with an empty sandbox, and the
|
||||
/// alternative — keeping them alive — is Tier 2 and blocked. What this buys is that a broker
|
||||
/// restart stops requiring the user to run `wsl --shutdown` by hand.
|
||||
/// </summary>
|
||||
private async Task<Sdk.Session> RecoverAndRestartAsync(SessionSpec spec, CancellationToken ct)
|
||||
{
|
||||
if (recovery is null)
|
||||
throw new WslcError(
|
||||
WslcError.SessionExists,
|
||||
$"a wslc session named '{spec.Name}' is already running, the compat SDK cannot "
|
||||
+ "re-adopt it, and the internal COM interface did not bind; run `wsl --shutdown`");
|
||||
|
||||
if (recovery.RecoverSession(spec.Name) is null)
|
||||
throw new WslcError(
|
||||
WslcError.SessionExists,
|
||||
$"a wslc session named '{spec.Name}' is already running and could not be "
|
||||
+ "recovered; run `wsl --shutdown` to clear it");
|
||||
|
||||
// Terminate() returns before the VM is gone — the service tears it down asynchronously —
|
||||
// so the next Start() can still see the old name. Retry rather than reporting a failure
|
||||
// that a second attempt a moment later would not have hit.
|
||||
for (var attempt = 0; ; attempt++)
|
||||
{
|
||||
await Task.Delay(500, ct).ConfigureAwait(false);
|
||||
var retry = NewSession(spec);
|
||||
try
|
||||
{
|
||||
retry.Start();
|
||||
Console.Error.WriteLine($"wslc: session '{spec.Name}' restarted after recovery");
|
||||
return retry;
|
||||
}
|
||||
catch (Exception e) when (HResultOf(e) == ErrorAlreadyExists && attempt < 20)
|
||||
{
|
||||
retry.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
retry.Dispose();
|
||||
throw e is WslcError ? e : Translate(e, WslcError.StartFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task TerminateSessionAsync(CancellationToken ct)
|
||||
{
|
||||
lock (containersLock)
|
||||
|
||||
Reference in New Issue
Block a user