From 88175578fc05f612566934fa6d1f4d889ae60ada Mon Sep 17 00:00:00 2001 From: Nucleic Date: Wed, 29 Jul 2026 01:06:05 -0700 Subject: [PATCH] Merge nucleic/olive-jade-civet-rznt into dev --- spikes/README.md | 29 ++++++---- spikes/WslcApiDump/Program.cs | 100 ++++++++++++++++++++++++++-------- 2 files changed, 96 insertions(+), 33 deletions(-) diff --git a/spikes/README.md b/spikes/README.md index 4384a5b..1e08d89 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -33,7 +33,8 @@ Two facts it discovered that anything referencing this package needs: cd windows/spikes/WslcApiDump dotnet run # dump the API + check every facade assumption dotnet run -- --probe # + GetMissingComponents / GetVersion -dotnet run -- --session # + create a session, then a SECOND one with the same name +dotnet run -- --session # + create a session, a SECOND with the same name, identity-test, tear down +dotnet run -- --session --keep # …and leave the sessions running afterwards dotnet run -- --all-types # include the ABI/marshalling plumbing in the dump ``` @@ -86,17 +87,23 @@ name**. That second construction is the whole point, and it is the last open que on: the compat SDK exposes a `Session` *constructor* and no attach, so does constructing over an existing name re-adopt it, or refuse? -- **Refused** (expect `WSLC_E_SESSION_RESERVED`, `0x80040607`) — D13's premise is confirmed on - hardware rather than inferred from an IDL, and session reattach genuinely requires - `IWSLCSessionManager::OpenSessionByName` on the internal interface. -- **Constructed** — the compat surface may re-adopt by name, and the session half of §2.3 reattach - may not need the internal interface at all. Check `wslc container ps` for the first session's - containers before believing it. +- **Refused** (`WSLC_E_SESSION_RESERVED`, `0x80040607`) — D13's premise is confirmed, and session + reattach genuinely requires `IWSLCSessionManager::OpenSessionByName` on the internal interface. +- **Constructed** — which is what 2.9.4 actually does. That alone proves nothing (construction may + be lazy), so the probe then runs an **identity test**: terminate the FIRST session and read from + the SECOND. A read that worked before and fails after is one underlying session answering both + handles; a read that keeps working means two independent VMs were running. -It leaves the session running on purpose, because that is the other half of the same question: -broker supervision assumes wslc state is **service-backed**, so a crashed `nucleic-brokerd` can -re-adopt rather than orphaning containers. If `wslc session ls` still shows `nucleic-spike` after -this process exits, that holds. Tear it down with `wslc` when you're done. +The identity test deliberately uses only the compat SDK. The obvious check would be +`wslc session ls` — but **`wslc.exe` is not on PATH by default**, so a spike that depends on it +answers nothing on a stock machine. (It ships beside `wsl.exe`; try +`C:\Program Files\WSL\wslc.exe`. That it isn't on PATH is one more small argument for D13's +no-CLI stance.) + +Sessions are **torn down at the end** by default — an earlier version left a WSL VM running and +told you to clean it up with a command that doesn't exist. Pass `--keep` to leave them, which is +how you check the other half of the §2.3 question: whether session state outlives the process that +created it. With `--keep`, `wsl --shutdown` clears everything. The gateway address is *not* what this probe is for any more — §13.1 established that no API surfaces one, and D13 moves the control plane to hvsocket via `IWSLCVirtualMachine::GetId`. diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs index 540777b..ad69bd7 100644 --- a/spikes/WslcApiDump/Program.cs +++ b/spikes/WslcApiDump/Program.cs @@ -89,7 +89,9 @@ internal static class Program } else { - ProbeSession(types, ArgValue(args, "--session-name") ?? "nucleic-spike"); + ProbeSession( + types, ArgValue(args, "--session-name") ?? "nucleic-spike", + keep: args.Contains("--keep")); } } @@ -355,7 +357,7 @@ internal static class Program /// /// Leaves the session running on purpose: whether it outlives this process is the other half /// of the same question. Tear it down with `wslc` when you are done. - private static void ProbeSession(Type[] types, string name) + private static void ProbeSession(Type[] types, string name, bool keep) { Console.WriteLine(); Console.WriteLine($"live probe (creates session '{name}'):"); @@ -411,29 +413,83 @@ internal static class Program } } Console.WriteLine(" → CONSTRUCTED — the constructor did NOT refuse."); - Console.WriteLine(); - // Be honest about what this does and does not show. A constructed object is not proof - // of attachment: construction may simply be lazy, and WSLC_E_SESSION_RESERVED clearly - // fires under SOME condition or it would not exist in wslc.idl. The compat surface has - // no enumeration, so identity cannot be settled from inside this process. - Console.WriteLine(" This does NOT by itself prove it re-adopted the first session —"); - Console.WriteLine(" construction may be lazy, and WSLC_E_SESSION_RESERVED exists in"); - Console.WriteLine(" wslc.idl, so it fires under some condition. The compat surface has"); - Console.WriteLine(" no enumeration, so identity cannot be settled from in here."); - Console.WriteLine(); - Console.WriteLine(" DECISIVE CHECK — run this now: wslc session ls"); - Console.WriteLine($" ONE session named '{name}' → it attached; the SESSION half of"); - Console.WriteLine(" §2.3 reattach may not need the"); - Console.WriteLine(" internal COM interface after all."); - Console.WriteLine($" TWO sessions named '{name}' → it did not; two VMs are running and"); - Console.WriteLine(" the name is not an identity."); - Console.WriteLine(" Either way the CONTAINER half of D13 is unaffected: there is still"); - Console.WriteLine(" no way to enumerate containers on the compat surface."); + IdentityTest(sessionType, first, second); } + Console.WriteLine(); + if (keep) + { + Console.WriteLine(" --keep: session(s) left running, so you can check whether they " + + "outlive this process (the other half of the §2.3 question)."); + Console.WriteLine(" Tear down with `wsl --shutdown` — note that `wslc.exe` is NOT on " + + "PATH by default; it lives beside wsl.exe (try `C:\\Program Files\\WSL\\wslc.exe`)."); + } + else + { + Terminate(sessionType, first, "first"); + Console.WriteLine(" (pass --keep to leave sessions running instead)"); + } + } + + /// Decide whether the second same-named `Session` re-adopted the first, using ONLY the compat + /// SDK — no CLI, which matters because `wslc.exe` is not on PATH by default. + /// + /// The trick: terminate the FIRST session and see whether the SECOND dies with it. A read that + /// worked before termination and fails after is the same underlying session answering; a read + /// that keeps working means two independent sessions were running all along. `GetImages` is + /// the cheapest read on the surface and mutates nothing. + private static void IdentityTest(Type sessionType, object first, object second) + { + var getImages = sessionType.GetMethods(Public) + .FirstOrDefault(m => !m.IsStatic && m.Name == "GetImages" && m.GetParameters().Length == 0); + if (getImages is null) { Console.WriteLine(" (no GetImages — cannot test identity)"); return; } Console.WriteLine(); - Console.WriteLine(" session left running — `wslc session ls`; whether it outlives this " - + "process is the other half of the §2.3 reattach question."); + Console.WriteLine(" identity test: terminate the FIRST, then read from the SECOND"); + if (!Reads(getImages, second, out var before)) + { + Console.WriteLine($" inconclusive — the second session already fails to read: {before}"); + return; + } + Console.WriteLine(" second reads ok before termination"); + + if (!Terminate(sessionType, first, "first")) return; + // The service tears a VM down asynchronously; give it a moment before concluding. + Thread.Sleep(3000); + + if (Reads(getImages, second, out var after)) + { + Console.WriteLine(" second STILL reads ok after the first was terminated"); + Console.WriteLine(" → SEPARATE SESSIONS. The name is not an identity: two VMs were " + + "running. D13 stands — reattach needs IWSLCSessionManager::OpenSessionByName."); + } + else + { + Console.WriteLine($" second now fails: {after}"); + Console.WriteLine(" → SAME SESSION. Constructing over an existing name RE-ADOPTS it, " + + "so the session half of §2.3 reattach is reachable from the compat surface and " + + "D13's reattach row softens (the container half is unaffected — still no " + + "enumeration)."); + } + } + + private static bool Reads(MethodInfo getImages, object session, out string detail) + { + try { getImages.Invoke(session, null); detail = "ok"; return true; } + catch (TargetInvocationException e) { detail = Describe(e.InnerException); return false; } + catch (Exception e) { detail = Describe(e); return false; } + } + + private static bool Terminate(Type sessionType, object session, string which) + { + var terminate = sessionType.GetMethods(Public) + .FirstOrDefault(m => !m.IsStatic && m.Name == "Terminate" && m.GetParameters().Length == 0); + if (terminate is null) { Console.WriteLine(" (no Session.Terminate)"); return false; } + try { terminate.Invoke(session, null); Console.WriteLine($" {which} session terminated"); return true; } + catch (TargetInvocationException e) + { + Console.WriteLine($" {which} Terminate() threw {Describe(e.InnerException)}"); + return false; + } } private static object? CreateSession(