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

This commit is contained in:
2026-07-29 03:57:12 -07:00
parent 71157c0d6e
commit f5cd4a3023
3 changed files with 102 additions and 7 deletions
+11 -6
View File
@@ -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 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 itself** (`WslcInternal.cs` in the broker, not a spike) — now fully - **The internal arm, Tier 1 — "recover"** (`WslcInternal.cs` in the broker, not a spike).
de-risked by `--internal-call`: entry point, vtable, impersonation requirement and the Everything it needs is confirmed on hardware: entry point, vtable, impersonation, and
session→compat handoff are all confirmed on hardware. Shape is *find, then hand off*: `OpenSessionByName`. On broker restart, open the orphaned session, `ListContainers` for
`OpenSessionByName` / `ListContainers` / `OpenContainer`, each result QI'd onto its compat reporting, `Terminate` it, and create a fresh one through the compat SDK — automatic clean
interface and passed to `FromAbi()`, after which the existing facade code drives it. The recovery instead of a manual `wsl --shutdown`. Enough for M2.
container-level QI is the one step still unobserved — confirm it in `WslcSpike` 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
`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 - **`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, `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 under the **default** Windows firewall, and does `control-bridge.js` complete an MCP round trip
+87 -1
View File
@@ -334,7 +334,7 @@ internal static class InternalComProbe
Console.WriteLine(" → a re-adopted session can be handed to " Console.WriteLine(" → a re-adopted session can be handed to "
+ "Session.FromAbi() and driven by the EXISTING compat facade code."); + "Session.FromAbi() and driven by the EXISTING compat facade code.");
Console.WriteLine(" → D13's internal arm shrinks to: find, then hand off."); Console.WriteLine(" → D13's internal arm shrinks to: find, then hand off.");
ProbeFromAbiOwnership(projected); ProbeObjectIdentity(session, projected);
Marshal.Release(projected); Marshal.Release(projected);
} }
else else
@@ -353,6 +353,92 @@ internal static class InternalComProbe
private static string? sessionName; private static string? sessionName;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary> /// <summary>
/// Does <c>Session.FromAbi(ptr)</c> take a reference, or borrow the caller's? /// Does <c>Session.FromAbi(ptr)</c> take a reference, or borrow the caller's?
/// ///
+4
View File
@@ -395,6 +395,10 @@ internal static class Program
var first = CreateSession(settingsType, sessionType, name, dataDir, "first"); var first = CreateSession(settingsType, sessionType, name, dataDir, "first");
if (first is null) return; 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). // Start it — construction alone may not boot the VM (`Session.Start()` is separate).
var start = sessionType.GetMethods(Public) var start = sessionType.GetMethods(Public)