diff --git a/spikes/README.md b/spikes/README.md index 3b7c1c2..47de79c 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -180,12 +180,17 @@ 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 itself** (`WslcInternal.cs` in the broker, not a spike) — now fully - de-risked by `--internal-call`: entry point, vtable, impersonation requirement and the - session→compat handoff are all confirmed on hardware. Shape is *find, then hand off*: - `OpenSessionByName` / `ListContainers` / `OpenContainer`, each result QI'd onto its compat - interface and passed to `FromAbi()`, after which the existing facade code drives it. The - container-level QI is the one step still unobserved — confirm it in `WslcSpike` below. +- **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. +- **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 + `wslcsdk.dll`, not the service object. `--internal-call` now compares COM identity to confirm. + If confirmed, Tier 2 means driving exec/stdio through internal COM directly + (`IWSLCContainer::Exec`, `IWSLCProcess::GetStdHandle` — handle-based, not event-based). - **`GatewaySpike`** — M1 (b), and the control-plane spike that actually matters now: can a `Bridged` wslc container reach the host at the `vEthernet (WSL)` address `WslcFacade` returns, under the **default** Windows firewall, and does `control-bridge.js` complete an MCP round trip diff --git a/spikes/WslcApiDump/InternalComProbe.cs b/spikes/WslcApiDump/InternalComProbe.cs index ae4b8df..f4ae868 100644 --- a/spikes/WslcApiDump/InternalComProbe.cs +++ b/spikes/WslcApiDump/InternalComProbe.cs @@ -334,7 +334,7 @@ internal static class InternalComProbe Console.WriteLine(" → a re-adopted session can be handed to " + "Session.FromAbi() and driven by the EXISTING compat facade code."); Console.WriteLine(" → D13's internal arm shrinks to: find, then hand off."); - ProbeFromAbiOwnership(projected); + ProbeObjectIdentity(session, projected); Marshal.Release(projected); } else @@ -353,6 +353,92 @@ internal static class InternalComProbe private static string? sessionName; + /// + /// Why did `Session.FromAbi` throw `InvalidCastException` on a pointer that had just QI'd + /// successfully to `IWSLCCompatSession`? + /// + /// The hypothesis is that there are **three** layers, not two: `wslc.idl` (service-internal + /// COM), `WSLCCompat.idl` (compat COM), and a **WinRT** layer implemented *client-side* in + /// `wslcsdk.dll` — which exports `DllGetActivationFactory`, so its `Session` is an in-process + /// wrapper object that holds compat COM proxies rather than being one. If so, the service's + /// object can never satisfy `FromAbi`, because the WinRT default interface only ever exists + /// on the client-side wrapper, and no API turns a compat pointer into one. + /// + /// Decisive test, and it needs no knowledge of the WinRT IID: take the `Session` the SDK + /// itself created, reach through to its underlying native pointer, and compare **COM + /// identity** (QI for IUnknown — the canonical pointer) against the one `OpenSessionByName` + /// returned. Same object → the layers coincide and something else broke FromAbi. Different + /// objects → the wrapper is client-side and the handoff is impossible as designed. + /// + private static void ProbeObjectIdentity(IntPtr internalSession, IntPtr compatFace) + { + Console.WriteLine(); + Console.WriteLine(" object identity — is the WinRT Session the SAME object?"); + if (sdkSession is null) + { + Console.WriteLine(" (no SDK-created session in this run; pass --session --keep)"); + return; + } + + IntPtr sdkPtr; + try + { + // CsWinRT keeps the native pointer on IWinRTObject.NativeObject.ThisPtr. Reached by + // reflection so this spike keeps building even if the projection's shape changes. + var winrt = sdkSession.GetType().GetInterface("WinRT.IWinRTObject"); + var native = winrt?.GetProperty("NativeObject")?.GetValue(sdkSession); + var ptr = native?.GetType().GetProperty("ThisPtr")?.GetValue(native); + if (ptr is not IntPtr value || value == IntPtr.Zero) + { + Console.WriteLine(" could not reach IWinRTObject.NativeObject.ThisPtr"); + return; + } + sdkPtr = value; + } + catch (Exception e) + { + Console.WriteLine($" reflection failed: {e.GetType().Name}: {e.Message}"); + return; + } + + // COM identity is defined as the IUnknown pointer, so canonicalise both before comparing. + var iid = IidIUnknown; + if (Marshal.QueryInterface(sdkPtr, in iid, out var sdkIdentity) < 0) return; + if (Marshal.QueryInterface(internalSession, in iid, out var internalIdentity) < 0) + { + Marshal.Release(sdkIdentity); + return; + } + + try + { + var same = sdkIdentity == internalIdentity; + Console.WriteLine($" SDK Session IUnknown = 0x{sdkIdentity:X}"); + Console.WriteLine($" OpenSessionByName IUnknown = 0x{internalIdentity:X}"); + Console.WriteLine($" → {(same ? "SAME object" : "DIFFERENT objects")}"); + + // The other half: does the SDK's own object even implement the compat COM interface? + var compat = IidCompatSession; + var qi = Marshal.QueryInterface(sdkPtr, in compat, out var sdkCompat); + if (qi >= 0) Marshal.Release(sdkCompat); + Console.WriteLine($" SDK Session QI IWSLCCompatSession: {(qi >= 0 ? "yes" : "no")}"); + + Console.WriteLine(same + ? " → the layers coincide; FromAbi failed for some OTHER reason — investigate." + : " → the WinRT Session is a CLIENT-SIDE WRAPPER (wslcsdk.dll exports\n" + + " DllGetActivationFactory), so a service-side pointer can never satisfy\n" + + " FromAbi. The 'find, then hand off' shape does NOT work, and a\n" + + " re-adopted session must be driven through internal COM directly."); + } + finally + { + Marshal.Release(sdkIdentity); + Marshal.Release(internalIdentity); + } + } + + internal static object? sdkSession; + /// /// Does Session.FromAbi(ptr) take a reference, or borrow the caller's? /// diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs index b608569..19cc83a 100644 --- a/spikes/WslcApiDump/Program.cs +++ b/spikes/WslcApiDump/Program.cs @@ -395,6 +395,10 @@ internal static class Program var first = CreateSession(settingsType, sessionType, name, dataDir, "first"); if (first is null) return; + // Hand the live SDK-created session to the internal probe: comparing ITS underlying COM + // identity against the one OpenSessionByName returns is what settles whether the WinRT + // projection is the same object or a client-side wrapper (InternalComProbe). + InternalComProbe.sdkSession = first; // Start it — construction alone may not boot the VM (`Session.Start()` is separate). var start = sessionType.GetMethods(Public)