diff --git a/NucleicBroker.Tests/BrokerServiceTests.cs b/NucleicBroker.Tests/BrokerServiceTests.cs index 7bce5e8..3c08033 100644 --- a/NucleicBroker.Tests/BrokerServiceTests.cs +++ b/NucleicBroker.Tests/BrokerServiceTests.cs @@ -112,6 +112,23 @@ public sealed class BrokerServiceTests Assert.Equal(new SessionSpec("nucleic-dev", @"C:\data", 4, 8192), wslc.LastSession); } + /// + /// 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. + /// + [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] public async Task ImagePull_EmitsProgressNotificationsBeforeResult() { diff --git a/NucleicBroker/Program.cs b/NucleicBroker/Program.cs index 6830847..3c623db 100644 --- a/NucleicBroker/Program.cs +++ b/NucleicBroker/Program.cs @@ -12,7 +12,10 @@ Console.OutputEncoding = Encoding.UTF8; IWslc 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 new UnavailableWslc(); #endif diff --git a/NucleicBroker/Wslc/WslcFacade.cs b/NucleicBroker/Wslc/WslcFacade.cs index 009f4a9..73001a6 100644 --- a/NucleicBroker/Wslc/WslcFacade.cs +++ b/NucleicBroker/Wslc/WslcFacade.cs @@ -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(); + + /// + /// 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". + /// + 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 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 Capabilities => ["stats"]; + /// + /// 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). + /// + public IReadOnlyList 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 } } + /// A settings-configured session with its handlers already attached. Subscribing + /// must happen BEFORE `Start()`, or a session that dies during boot never reports down. + 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; + } + + /// + /// 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. + /// + private async Task 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) diff --git a/NucleicBroker/Wslc/WslcInternal.cs b/NucleicBroker/Wslc/WslcInternal.cs new file mode 100644 index 0000000..a1abf2e --- /dev/null +++ b/NucleicBroker/Wslc/WslcInternal.cs @@ -0,0 +1,384 @@ +#if USE_WSLC +using System.Runtime.InteropServices; + +namespace NucleicBroker.Wslc; + +/// +/// 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. +/// 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. +/// +internal sealed class WslcInternal : IDisposable +{ + private IWSLCSessionManager? manager; + private IntPtr managerPtr; + + /// What a recovery found and did, for the host log and the `session.down` story. + internal sealed record Recovery(IReadOnlyList Containers, bool Terminated); + + /// + /// 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. + /// + 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; + } + } + + /// + /// Open the orphaned session named , 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. + /// + 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); + } + } + + /// + /// The container roster of a session, by name. Enumeration the compat SDK has no call for at + /// all — Session exposes no listing and Container carries no Name. + /// + private static IReadOnlyList 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()); + 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(); + var names = new List((int)count); + for (var i = 0; i < count; i++) + { + var entry = Marshal.PtrToStructure(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 + + /// + /// 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. + /// + 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; + } + + /// + /// `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). + /// + [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; + } + + /// + /// `IWSLCSession`, declared only as far as Terminate (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. + /// + [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 diff --git a/spikes/README.md b/spikes/README.md index 47de79c..3d219d3 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -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 guessed names, it would not compile, and fixing it blind is the mistake this whole approach exists to avoid. -- **The internal arm, Tier 1 — "recover"** (`WslcInternal.cs` in the broker, not a spike). - Everything it needs is confirmed on hardware: entry point, vtable, impersonation, and - `OpenSessionByName`. On broker restart, open the orphaned session, `ListContainers` for - reporting, `Terminate` it, and create a fresh one through the compat SDK — automatic clean - recovery instead of a manual `wsl --shutdown`. Enough for M2. +- ~~**The internal arm, Tier 1 — "recover"**~~ **written**: `windows/NucleicBroker/Wslc/WslcInternal.cs`. + On broker restart it opens the orphaned session, lists its containers for the log, terminates + it, and lets the facade start fresh — automatic clean recovery instead of a manual + `wsl --shutdown`. Still needs one live test: kill a broker mid-session and confirm the next one + recovers (see below). - **Tier 2 — "adopt"** (keep containers running across a broker restart) is **blocked**. The `Session.FromAbi()` handoff throws `InvalidCastException` even though the QI to `IWSLCCompatSession` succeeds: the WinRT layer appears to be a client-side wrapper in