Merge nucleic/lucid-river-toad-6efj into dev

This commit is contained in:
2026-07-29 04:04:16 -07:00
parent f5cd4a3023
commit 0f720d8328
5 changed files with 514 additions and 31 deletions
+17
View File
@@ -112,6 +112,23 @@ public sealed class BrokerServiceTests
Assert.Equal(new SessionSpec("nucleic-dev", @"C:\data", 4, 8192), wslc.LastSession); Assert.Equal(new SessionSpec("nucleic-dev", @"C:\data", 4, 8192), wslc.LastSession);
} }
/// <summary>
/// D13 Tier 1 (docs/WINDOWS_PORT.md §13.2): the facade auto-recovers a session orphaned by a
/// dead broker, but when the internal COM arm did not bind there is nothing it can do — and
/// hostd must be able to tell *that* apart from an ordinary start failure, because the remedy
/// is `wsl --shutdown` rather than a retry. This pins the kind the Swift side branches on.
/// </summary>
[Fact]
public async Task SessionEnsure_UnrecoverableConflict_SurfacesSessionExists()
{
wslc.NextError = new WslcError(WslcError.SessionExists, "already running, cannot re-adopt");
var lines = await RoundTrip(
"""{"jsonrpc":"2.0","id":9,"method":"session.ensure","params":{"name":"nucleic-dev","dataDir":"C:\\data"}}""");
var error = Assert.Single(lines).GetProperty("error");
Assert.Equal(Rpc.FacadeError, error.GetProperty("code").GetInt32());
Assert.Equal("session_exists", error.GetProperty("data").GetProperty("kind").GetString());
}
[Fact] [Fact]
public async Task ImagePull_EmitsProgressNotificationsBeforeResult() public async Task ImagePull_EmitsProgressNotificationsBeforeResult()
{ {
+4 -1
View File
@@ -12,7 +12,10 @@ Console.OutputEncoding = Encoding.UTF8;
IWslc wslc = IWslc wslc =
#if USE_WSLC #if USE_WSLC
new NucleicBroker.Wslc.WslcFacade(); // Create(), not `new`: it configures COM security first, and that MUST precede the process's
// first COM call (docs/WINDOWS_PORT.md §13.2). Constructing the facade directly compiles fine
// and silently loses session recovery to a 0x80070542 that reads as "not found".
NucleicBroker.Wslc.WslcFacade.Create();
#else #else
new UnavailableWslc(); new UnavailableWslc();
#endif #endif
+104 -25
View File
@@ -48,6 +48,25 @@ public sealed class WslcFacade : IWslc
private string? gateway; private string? gateway;
private readonly SemaphoreSlim sessionGate = new(1, 1); 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 /// 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. /// no way to address a container at all, because Sdk.Container carries no name.
private readonly Dictionary<string, Entry> containers = []; 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 /// <summary>
/// (there is no GetStatistics()) but from an in-guest cgroup read, which is the escape hatch /// Stats are always offered — not from the SDK (there is no `GetStatistics()`) but from an
/// §13.1 names and is indistinguishable to hostd. /// in-guest cgroup read, which is indistinguishable to hostd. `recover` is added when D13's
public IReadOnlyList<string> Capabilities => ["stats"]; /// 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; public void SetEvents(IBrokerEvents events) => this.events = events;
@@ -113,34 +140,19 @@ public sealed class WslcFacade : IWslc
{ {
if (session is not null) return gateway!; if (session is not null) return gateway!;
var settings = new Sdk.SessionSettings(spec.Name, spec.DataDir); var created = NewSession(spec);
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}");
try try
{ {
created.Start(); created.Start();
} }
catch (Exception e) when (HResultOf(e) == ErrorAlreadyExists) catch (Exception e) when (HResultOf(e) == ErrorAlreadyExists)
{ {
created.Dispose();
// The constructor is lazy — it only captures settings — so reaching this means a // 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 // session of this name is genuinely RUNNING, started by a previous broker that
// another process. The compat surface cannot re-adopt it, and there is no handle // died. The compat surface cannot re-adopt it, so recovery goes through D13's
// to terminate it through either, so this is terminal for this broker. // internal-COM arm: open it, note what it was running, terminate it, retry.
throw new WslcError( created.Dispose();
WslcError.SessionExists, created = await RecoverAndRestartAsync(spec, ct).ConfigureAwait(false);
$"a wslc session named '{spec.Name}' is already running and the compat SDK "
+ "cannot re-adopt it; run `wsl --shutdown` to clear it");
} }
catch (Exception e) when (e is not WslcError) 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) public Task TerminateSessionAsync(CancellationToken ct)
{ {
lock (containersLock) lock (containersLock)
+384
View File
@@ -0,0 +1,384 @@
#if USE_WSLC
using System.Runtime.InteropServices;
namespace NucleicBroker.Wslc;
/// <summary>
/// D13's internal-COM arm, **Tier 1: recover** (docs/WINDOWS_PORT.md §13.2).
///
/// The compat SDK cannot re-adopt a running session — its `Session` constructor is lazy and
/// `Start()` refuses an existing name with `ERROR_ALREADY_EXISTS`. So a broker that restarts
/// while its session is up cannot reach the sandbox again, and today that costs the user a manual
/// `wsl --shutdown`. This class fixes exactly that: it opens the orphaned session through the
/// **service-internal** COM interface, reports what was running, terminates it, and lets the
/// facade create a fresh one through the ordinary compat path.
///
/// It deliberately does NOT try to keep those containers alive — that is Tier 2, and it is
/// blocked (§13.2): `Session.FromAbi()` throws on a service-side pointer, because the WinRT layer
/// the C# projection wraps lives client-side in `wslcsdk.dll`. Everything here is confirmed on
/// hardware; nothing here depends on that unresolved question.
///
/// **Four things that are not obvious and each cost a debugging round:**
///
/// 1. **There is no CLSID for `IWSLCSessionManager`.** `wslc.idl` declares interfaces and no
/// activatable class. The entry point is the *compat* coclass — `WSLCCompatSessionManager`
/// also implements the internal interface. One object, two faces.
/// 2. **The proxy must grant IMPERSONATE.** Per-user calls like `OpenSessionByName` fail
/// `0x80070542` (`ERROR_BAD_IMPERSONATION_LEVEL`) under .NET's default `IDENTIFY` — a security
/// error that reads exactly like "not found". `GetVersion`/`ListSessions` don't impersonate,
/// so they succeed and make it look like a per-method gap. <see cref="InitializeSecurity"/>
/// handles this process-wide; the per-proxy blanket here is belt and braces.
/// 3. **Vtable slots are fixed by declaration ORDER, not signature.** Only the methods actually
/// called need accurate signatures, which is what makes reaching `ListContainers` (method #19,
/// behind four methods taking a by-value `WSLCHandle` union) tractable at all.
/// 4. **The ABI is explicitly unstable.** `wslc.idl` says breaking changes are fine because
/// Microsoft ships both ends. We are not both ends, so every entry point here is probed and
/// every failure degrades to "no recovery" rather than propagating.
/// </summary>
internal sealed class WslcInternal : IDisposable
{
private IWSLCSessionManager? manager;
private IntPtr managerPtr;
/// <summary>What a recovery found and did, for the host log and the `session.down` story.</summary>
internal sealed record Recovery(IReadOnlyList<string> Containers, bool Terminated);
/// <summary>
/// Bind the internal interface, or return null. Called once at facade construction so the
/// result can be reported in the `capabilities` hello (§2.3) rather than discovered when a
/// user's broker restarts.
/// </summary>
internal static WslcInternal? TryBind()
{
var clsid = ClsidWslcCompatSessionManager;
var iid = IidWslcSessionManager;
// Ask for the internal interface directly. The compat coclass implements both, and going
// straight for it means a machine where this arm is unavailable fails here rather than
// half-way through a recovery.
var hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in iid, out var ptr);
if (hr < 0)
{
// REGDB_E_CLASSNOTREG here means WSL simply isn't installed — the §8 onboarding
// state, not a defect. Saying "recovery unavailable" without that distinction reads
// as a broker fault on a machine that has not been set up yet.
Console.Error.WriteLine((uint)hr == RegdbEClassNotReg
? "wslc: WSL is not installed — internal COM absent, as expected before onboarding"
: $"wslc: internal COM did not bind (0x{hr:X8}) — a broker restart will not "
+ "auto-recover a running session; D13 Tier 1 is unavailable on this machine");
return null;
}
RaiseImpersonation(ptr);
try
{
return new WslcInternal
{
managerPtr = ptr,
manager = (IWSLCSessionManager)Marshal.GetObjectForIUnknown(ptr),
};
}
catch (Exception e)
{
Marshal.Release(ptr);
Console.Error.WriteLine($"wslc: internal COM bound but unusable: {e.Message}");
return null;
}
}
/// <summary>
/// Open the orphaned session named <paramref name="name"/>, note what was running in it, and
/// terminate it. Returns null when there is nothing to recover — which is the ordinary case
/// and not an error.
///
/// Terminating rather than adopting is the deliberate Tier 1 choice: the containers are lost,
/// but they are lost today too, and `ContainerManager.reconcile` already copes with a sandbox
/// that came back empty. What it buys is that the *next* `Start()` succeeds.
/// </summary>
internal Recovery? RecoverSession(string name)
{
if (manager is null) return null;
int hr;
IntPtr sessionPtr;
try
{
hr = manager.OpenSessionByName(name, out sessionPtr);
}
catch (Exception e)
{
Console.Error.WriteLine($"wslc: OpenSessionByName('{name}') threw: {e.Message}");
return null;
}
if (hr < 0)
{
Console.Error.WriteLine($"wslc: no recoverable session '{name}' (0x{hr:X8})"
+ ((uint)hr == ErrorBadImpersonationLevel
? " — IMPERSONATE was not granted; CoInitializeSecurity must run before the "
+ "first COM call in the process"
: ""));
return null;
}
// The session proxy is a separate object from the manager, so it needs its own blanket.
RaiseImpersonation(sessionPtr);
try
{
var session = (IWSLCSession)Marshal.GetObjectForIUnknown(sessionPtr);
var containers = ListContainers(session);
var terminated = Terminate(session);
Console.Error.WriteLine(
$"wslc: recovered orphaned session '{name}' — {containers.Count} container(s) "
+ $"[{string.Join(", ", containers)}], terminated={terminated}");
return new Recovery(containers, terminated);
}
catch (Exception e)
{
Console.Error.WriteLine($"wslc: recovery of '{name}' failed: {e.Message}");
return null;
}
finally
{
Marshal.Release(sessionPtr);
}
}
/// <summary>
/// The container roster of a session, by name. Enumeration the compat SDK has no call for at
/// all — <c>Session</c> exposes no listing and <c>Container</c> carries no <c>Name</c>.
/// </summary>
private static IReadOnlyList<string> ListContainers(IWSLCSession session)
{
// Flags=All, or the listing is running-containers-only and a stopped container silently
// vanishes from the recovery report.
var options = new WslcListContainersOptions
{
Flags = WslcListContainersFlagsAll,
Limit = 0,
Filters = IntPtr.Zero,
FiltersCount = 0,
};
var optionsPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf<WslcListContainersOptions>());
var containers = IntPtr.Zero;
var ports = IntPtr.Zero;
try
{
Marshal.StructureToPtr(options, optionsPtr, fDeleteOld: false);
var hr = session.ListContainers(optionsPtr, out containers, out var count,
out ports, out _);
if (hr < 0)
{
Console.Error.WriteLine($"wslc: ListContainers failed (0x{hr:X8})");
return [];
}
var size = Marshal.SizeOf<WslcContainerEntry>();
var names = new List<string>((int)count);
for (var i = 0; i < count; i++)
{
var entry = Marshal.PtrToStructure<WslcContainerEntry>(containers + i * size);
names.Add(string.IsNullOrEmpty(entry.Name) ? entry.Id : entry.Name);
}
return names;
}
finally
{
Marshal.FreeCoTaskMem(optionsPtr);
// Both out-arrays are callee-allocated; nobody else frees them.
if (containers != IntPtr.Zero) Marshal.FreeCoTaskMem(containers);
if (ports != IntPtr.Zero) Marshal.FreeCoTaskMem(ports);
}
}
private static bool Terminate(IWSLCSession session)
{
try
{
var hr = session.Terminate();
if (hr >= 0) return true;
Console.Error.WriteLine($"wslc: session Terminate failed (0x{hr:X8})");
return false;
}
catch (Exception e)
{
Console.Error.WriteLine($"wslc: session Terminate threw: {e.Message}");
return false;
}
}
public void Dispose()
{
manager = null;
if (managerPtr != IntPtr.Zero)
{
Marshal.Release(managerPtr);
managerPtr = IntPtr.Zero;
}
}
// MARK: - COM security
/// <summary>
/// Grant servers the right to impersonate this process, for **every** proxy it will hold.
///
/// Must run before the first COM call in the process or it fails `RPC_E_TOO_LATE` — and the
/// compat SDK makes COM calls of its own, so this has to precede any `WslcService`/`Session`
/// use, not merely precede the internal arm. Failing is not fatal: only the per-user internal
/// calls need it, so the sandbox still runs and recovery is what degrades.
/// </summary>
internal static void InitializeSecurity()
{
var hr = CoInitializeSecurity(
IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero,
RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, IntPtr.Zero, EoacNone, IntPtr.Zero);
// RPC_E_TOO_LATE means something already initialised security — worth saying, because it
// silently removes session recovery and nothing else will mention it.
if (hr < 0)
Console.Error.WriteLine(
$"wslc: CoInitializeSecurity failed (0x{hr:X8})"
+ ((uint)hr == RpcETooLate
? " — RPC_E_TOO_LATE: a COM call ran first. Session recovery will fail "
+ "0x80070542."
: ""));
}
private static void RaiseImpersonation(IntPtr proxy) =>
// Per-proxy, and harmless if CoInitializeSecurity already covered it. Kept because the
// process-wide call is order-dependent and this one is not.
CoSetProxyBlanket(
proxy, RpcCAuthnDefault, RpcCAuthzDefault, ColeDefaultPrincipal,
RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, ColeDefaultAuthinfo, EoacNone);
// MARK: - Interop
/// `WSLCCompatSessionManager` from WSLCCompat.idl. Not a typo that this is the *compat*
/// class: `wslc.idl` declares no coclass, and this one answers a QI for the internal
/// interface (confirmed on hardware, §13.2).
private static readonly Guid ClsidWslcCompatSessionManager =
new("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f");
private static readonly Guid IidWslcSessionManager = new("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760");
private const uint ClsctxAll = 0x17;
private const uint RpcCAuthnDefault = 0xFFFFFFFF;
private const uint RpcCAuthzDefault = 0xFFFFFFFF;
private const uint RpcCAuthnLevelDefault = 0;
private const uint RpcCImpLevelImpersonate = 3;
private const uint EoacNone = 0;
private const uint RpcETooLate = 0x80010119;
private const uint RegdbEClassNotReg = 0x80040154;
private const uint ErrorBadImpersonationLevel = 0x80070542;
private const uint WslcListContainersFlagsAll = 1;
private static readonly IntPtr ColeDefaultPrincipal = new(-1);
private static readonly IntPtr ColeDefaultAuthinfo = new(-1);
[DllImport("ole32.dll")]
private static extern int CoCreateInstance(
in Guid clsid, IntPtr outer, uint clsContext, in Guid iid, out IntPtr instance);
[DllImport("ole32.dll")]
private static extern int CoSetProxyBlanket(
IntPtr proxy, uint authnService, uint authzService, IntPtr serverPrincipalName,
uint authnLevel, uint impersonationLevel, IntPtr authInfo, uint capabilities);
[DllImport("ole32.dll")]
private static extern int CoInitializeSecurity(
IntPtr securityDescriptor, int authSvcCount, IntPtr authSvc, IntPtr reserved1,
uint authnLevel, uint impersonationLevel, IntPtr authList, uint capabilities,
IntPtr reserved3);
[StructLayout(LayoutKind.Sequential)]
private struct WslcListContainersOptions
{
public uint Flags;
public int Limit;
public IntPtr Filters;
public uint FiltersCount;
}
/// <summary>
/// `WSLCContainerEntry` from wslc.idl. The three char arrays are **inline fixed buffers**,
/// not pointers — `ByValTStr`/`Ansi`, with the sizes straight from the IDL's `+ 1` constants
/// (255+1, 255+1, 64+1). Getting a size wrong here does not fail loudly; it silently shifts
/// every later field. The equivalent layout was validated on hardware via
/// `ListSessions`, whose entry struct has the same shape (§13.2).
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
private struct WslcContainerEntry
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Name;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Image;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] public string Id;
public ulong StateChangedAt;
public ulong CreatedAt;
public uint State;
}
[ComImport, Guid("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IWSLCSessionManager
{
[PreserveSig] int GetVersion(out WslcVersion version);
[PreserveSig] int CreateSession(IntPtr settings, uint flags, IntPtr warningCallback, out IntPtr session);
[PreserveSig] int EnterSession(
[MarshalAs(UnmanagedType.LPWStr)] string displayName,
[MarshalAs(UnmanagedType.LPWStr)] string storagePath,
IntPtr warningCallback, out IntPtr session);
[PreserveSig] int ListSessions(out IntPtr sessions, out uint count);
[PreserveSig] int OpenSession(uint id, out IntPtr session);
[PreserveSig] int OpenSessionByName(
[MarshalAs(UnmanagedType.LPWStr)] string displayName, out IntPtr session);
}
[StructLayout(LayoutKind.Sequential)]
internal struct WslcVersion
{
public uint Major;
public uint Minor;
public uint Revision;
}
/// <summary>
/// `IWSLCSession`, declared only as far as <c>Terminate</c> (method #23).
///
/// **Every method ahead of the ones we call must still be declared**, because a COM vtable is
/// addressed by slot — but only the called ones need accurate signatures, since a method that
/// is never invoked is never marshalled. That is what makes this tractable: four of the
/// placeholders (`LoadImage`, `ImportImage`, `SaveImage`, `SaveImages`) take `WSLCHandle` — a
/// tagged union — **by value**, which would be genuinely awkward to marshal and does not have
/// to be. Parameter *counts* are kept faithful to the IDL purely as documentation.
///
/// Do not reorder. Do not delete an unused entry. Either silently shifts every slot below it.
/// </summary>
[ComImport, Guid("EF0661E4-6364-40EA-B433-E2FDF11F3519"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IWSLCSession
{
[PreserveSig] int GetId(out uint id); // 1
[PreserveSig] int GetDisplayName(out IntPtr displayName); // 2
[PreserveSig] int GetState(out uint state); // 3
[PreserveSig] int GetTerminationEvent(out IntPtr eventHandle); // 4
[PreserveSig] int GetTerminationReason(out uint reason, out IntPtr details); // 5
[PreserveSig] int PullImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 6
[PreserveSig] int BuildImage(IntPtr a, IntPtr b, IntPtr c); // 7
[PreserveSig] int LoadImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 8 (WSLCHandle by value)
[PreserveSig] int ImportImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 9 (WSLCHandle by value)
[PreserveSig] int SaveImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 10 (WSLCHandle by value)
[PreserveSig] int SaveImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 11 (WSLCHandle by value)
[PreserveSig] int ListImages(IntPtr a, IntPtr b, IntPtr c); // 12
[PreserveSig] int DeleteImage(IntPtr a, IntPtr b, IntPtr c); // 13
[PreserveSig] int TagImage(IntPtr a); // 14
[PreserveSig] int InspectImage(IntPtr a, IntPtr b); // 15
[PreserveSig] int PruneImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 16
[PreserveSig] int CreateContainer(IntPtr a, IntPtr b, IntPtr c); // 17
[PreserveSig] int OpenContainer(IntPtr a, IntPtr b); // 18
[PreserveSig] int ListContainers( // 19
IntPtr options, out IntPtr containers, out uint count,
out IntPtr ports, out uint portsCount);
[PreserveSig] int PruneContainers(IntPtr a, IntPtr b, IntPtr c); // 20
[PreserveSig] int CreateRootNamespaceProcess(
IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e, IntPtr f); // 21
[PreserveSig] int FormatVirtualDisk(IntPtr a); // 22
[PreserveSig] int Terminate(); // 23
}
}
#endif
+5 -5
View File
@@ -180,11 +180,11 @@ for, and nothing above it should move. These three are different:
mounted vs. in-VM). Deliberately held back until `WslcApiDump` has run: written now, against mounted vs. in-VM). Deliberately held back until `WslcApiDump` has run: written now, against
guessed names, it would not compile, and fixing it blind is the mistake this whole approach guessed names, it would not compile, and fixing it blind is the mistake this whole approach
exists to avoid. exists to avoid.
- **The internal arm, Tier 1 — "recover"** (`WslcInternal.cs` in the broker, not a spike). - ~~**The internal arm, Tier 1 — "recover"**~~ **written**: `windows/NucleicBroker/Wslc/WslcInternal.cs`.
Everything it needs is confirmed on hardware: entry point, vtable, impersonation, and On broker restart it opens the orphaned session, lists its containers for the log, terminates
`OpenSessionByName`. On broker restart, open the orphaned session, `ListContainers` for it, and lets the facade start fresh — automatic clean recovery instead of a manual
reporting, `Terminate` it, and create a fresh one through the compat SDK — automatic clean `wsl --shutdown`. Still needs one live test: kill a broker mid-session and confirm the next one
recovery instead of a manual `wsl --shutdown`. Enough for M2. recovers (see below).
- **Tier 2 — "adopt"** (keep containers running across a broker restart) is **blocked**. The - **Tier 2 — "adopt"** (keep containers running across a broker restart) is **blocked**. The
`Session.FromAbi()` handoff throws `InvalidCastException` even though the QI to `Session.FromAbi()` handoff throws `InvalidCastException` even though the QI to
`IWSLCCompatSession` succeeds: the WinRT layer appears to be a client-side wrapper in `IWSLCCompatSession` succeeds: the WinRT layer appears to be a client-side wrapper in