diff --git a/spikes/README.md b/spikes/README.md index 3b65681..9443df2 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -32,17 +32,32 @@ Two facts it discovered that anything referencing this package needs: ```powershell cd windows/spikes/WslcApiDump dotnet run # dump the API + check every facade assumption -dotnet run -- --probe # + call the two read-only statics (service version, missing components) -dotnet run -- --session # + create a real session and print its live property VALUES +dotnet run -- --probe # + GetMissingComponents / GetVersion +dotnet run -- --session # + create a session, then a SECOND one with the same name +dotnet run -- --all-types # include the ABI/marshalling plumbing in the dump ``` +**If it reports missing components** (`VirtualMachinePlatform`, `WslPackage`), the machine cannot +run wslc yet: `wsl --install`, reboot for the Virtual Machine Platform feature, and run again. +Everything that reaches the service fails with `REGDB_E_CLASSNOTREG` (0x80040154) until then, and +the tool says so rather than emitting a string of unexplained COM errors. `--session` is skipped +in that state instead of failing. + +Note that the assumption check reads the **`Microsoft.WSL.Containers`** namespace only. That is +correctness, not tidiness: a C#/WinRT projection also exports `ABI.Microsoft.WSL.Containers.*` +marshalling types with the *same short names*, and matching on short name alone checks every +member against the marshalling struct — which reported 42 false MISSINGs, with `CreateMarshaler` +helpfully offered as the nearest name. + It writes the full public object model to `wslc-api-dump.txt` (`--out` to relocate) and prints an `ok` / `MISSING` line per assumption, each with *why that member matters* and a nearest-name hint. It exits 0 even when assumptions fail — a mismatch is the product, not an error. Only a genuinely broken run (the assembly won't load) exits non-zero. Every path in it has been exercised against a stand-in assembly carrying the observed 2.9.3 type -and member names, so a failure on your machine is a finding about wslc, not about this tool. +and member names — including the `ABI.` shadow types and a service that reports missing components +and throws `0x80040154` — so a failure on your machine is a finding about wslc, not about this +tool. **What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING — which now means the package moved under us, not that we guessed wrong. diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs index e2ac6aa..c469d85 100644 --- a/spikes/WslcApiDump/Program.cs +++ b/spikes/WslcApiDump/Program.cs @@ -27,6 +27,14 @@ internal static class Program /// loading it by package name fails — which is exactly the first thing this tool found. private const string AssemblyName = "wslcsdkcs"; + /// The API namespace. Filtering on it is not tidiness — it is correctness. A C#/WinRT + /// projection also exports `ABI.Microsoft.WSL.Containers.*` marshalling plumbing whose types + /// have the SAME short names (`Session`, `Container`, `ProcessSettings`), and `ABI.` sorts + /// first. Matching assumptions by short name therefore checked every member against the + /// marshalling struct and reported 42 false MISSINGs, with `CreateMarshaler` offered as the + /// nearest name — which is the tell. + private const string ApiNamespace = "Microsoft.WSL.Containers"; + private static int Main(string[] args) { var probe = args.Contains("--probe") || args.Contains("--session"); @@ -46,29 +54,65 @@ internal static class Program return 2; } - var types = assembly.GetExportedTypes().OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray(); - Console.WriteLine($"{AssemblyName} {assembly.GetName().Version} — {types.Length} public types"); + var exported = assembly.GetExportedTypes().OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray(); + var types = exported.Where(t => t.Namespace == ApiNamespace).ToArray(); + Console.WriteLine( + $"{AssemblyName} {assembly.GetName().Version} — {types.Length} types in {ApiNamespace} " + + $"({exported.Length - types.Length} more are ABI/marshalling plumbing)"); Console.WriteLine(); var report = new StringBuilder(); report.AppendLine($"# {AssemblyName} {assembly.GetName().Version}"); report.AppendLine($"# location: {assembly.Location}"); report.AppendLine(); - foreach (var type in types) DescribeType(type, report); + // The dump defaults to the API namespace for the same reason the checks do; `--all-types` + // includes the ABI plumbing for when the projection itself is what's being debugged. + foreach (var type in args.Contains("--all-types") ? exported : types) DescribeType(type, report); File.WriteAllText(outPath, report.ToString()); Console.WriteLine($"full API dump → {Path.GetFullPath(outPath)}"); Console.WriteLine(); var missing = CheckAssumptions(types); - if (probe) ProbeStatics(types); - if (session) ProbeSession(types, ArgValue(args, "--session-name") ?? "nucleic-spike"); + // Components are probed BEFORE anything else that touches the service. On a machine + // where WSL container support isn't installed, every live call fails with + // REGDB_E_CLASSNOTREG (0x80040154) — "Class not registered" — and reporting that as a + // string of mysterious COM errors would bury the one fact that explains them all. + var componentsMissing = probe ? ProbeStatics(types) : null; + if (session) + { + if (componentsMissing is { Count: > 0 }) + { + Console.WriteLine(); + Console.WriteLine("skipping --session: the components above are missing, so creating " + + "a session would only fail with the same 'class not registered'."); + } + else + { + ProbeSession(types, ArgValue(args, "--session-name") ?? "nucleic-spike"); + } + } Console.WriteLine(); - Console.WriteLine(missing == 0 - ? "RESULT: every WslcFacade assumption is present. Fix nothing; write the typed spike." - : $"RESULT: {missing} assumption(s) wrong — each one is a line to change in " - + "windows/NucleicBroker/Wslc/WslcFacade.cs, and nowhere else (that is what IWslc is for)."); + if (componentsMissing is { Count: > 0 }) + { + Console.WriteLine($"RESULT: this machine cannot run wslc yet — missing " + + $"{string.Join(", ", componentsMissing)}. Install with `wsl --install` (the " + + "Virtual Machine Platform component needs a reboot), then run this again."); + Console.WriteLine(" This is the §8 onboarding condition, not a defect."); + } + else if (missing == 0) + { + Console.WriteLine("RESULT: every WslcFacade assumption is present — the package matches " + + "the surface recorded in docs/WINDOWS_PORT.md §13.1."); + } + else + { + Console.WriteLine($"RESULT: {missing} assumption(s) wrong. The recorded surface is 2.9.3, " + + "so this means the package MOVED: reconcile §13.1, then fix " + + "windows/NucleicBroker/Wslc/WslcFacade.cs — and nowhere else (that is what IWslc " + + "is for)."); + } // Exit 0 either way: a mismatch is this tool's PRODUCT, not its failure. Only a genuinely // broken run (assembly missing) is non-zero, so a wrapper script can tell them apart. return 0; @@ -174,7 +218,8 @@ internal static class Program wrong++; Console.WriteLine($" MISSING {type.Name}.{assumption.Member} " + $"({assumption.MemberKind.ToString().ToLowerInvariant()})"); - Console.WriteLine($" why: {assumption.Why}"); + foreach (var line in Wrap(assumption.Why, 94)) + Console.WriteLine($" {line}"); var near = Nearest(assumption.Member, MemberNames(type)); if (near.Length > 0) Console.WriteLine($" nearest: {string.Join(", ", near)}"); @@ -202,6 +247,28 @@ internal static class Program /// Cheap "did they just rename it" hint: shared prefix or containment, no edit /// distance. A three-name shortlist is enough to spot HostGateway vs /// HostGatewayAddress, which is the realistic failure mode. + /// Hard-wrap so a long rationale can't be mangled into an unreadable fragment by the + /// console. First line is prefixed "why:", continuations are indented under it. + private static IEnumerable Wrap(string text, int width) + { + var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var line = new StringBuilder("why: "); + var any = false; + foreach (var word in words) + { + if (line.Length + word.Length + 1 > width && any) + { + yield return line.ToString(); + line = new StringBuilder(" "); + any = false; + } + if (any) line.Append(' '); + line.Append(word); + any = true; + } + if (any) yield return line.ToString(); + } + private static string[] Nearest(string wanted, IEnumerable candidates) { var needle = wanted.TrimStart('.'); @@ -229,14 +296,17 @@ internal static class Program /// Names come from the real 2.9.3 surface (`GetVersion`, `GetMissingComponents`) — the /// documented `GetServiceVersion`/`ComponentFlags` shapes in Microsoft Learn's sample are /// stale against the shipped package (docs/WINDOWS_PORT.md §13.1). - private static void ProbeStatics(Type[] types) + private static IReadOnlyList? ProbeStatics(Type[] types) { Console.WriteLine(); Console.WriteLine("live probe (read-only):"); var service = types.FirstOrDefault(t => t.Name == "WslcService"); - if (service is null) { Console.WriteLine(" no WslcService type — skipping"); return; } + if (service is null) { Console.WriteLine(" no WslcService type — skipping"); return null; } - foreach (var name in new[] { "GetVersion", "GetMissingComponents" }) + List? componentsMissing = null; + // GetMissingComponents first: it answers from OS feature state and works even when the + // service class isn't registered, so it is the call that EXPLAINS the others. + foreach (var name in new[] { "GetMissingComponents", "GetVersion" }) { var method = service.GetMethods(Public) .FirstOrDefault(m => m.IsStatic && m.Name == name && m.GetParameters().Length == 0); @@ -248,16 +318,26 @@ internal static class Program } try { - Console.WriteLine($" {name}() = {Render(method.Invoke(null, null))}"); + var value = method.Invoke(null, null); + Console.WriteLine($" {name}() = {Render(value)}"); + if (name == "GetMissingComponents" && value is System.Collections.IEnumerable list) + { + componentsMissing = list.Cast().Select(v => v?.ToString() ?? "?").ToList(); + if (componentsMissing.Count > 0) + Console.WriteLine(" → WSL container support is NOT installed here. Every " + + "call below that reaches the service will fail with 0x80040154."); + } } catch (TargetInvocationException e) { - // The interesting failure: WSL not installed. That IS the onboarding condition - // §8 step 2 exists to handle, so report it plainly rather than as a crash. - Console.WriteLine($" {name}() threw {Describe(e.InnerException)}"); + var inner = e.InnerException; + var expected = componentsMissing is { Count: > 0 } + && inner is COMException { HResult: unchecked((int)0x80040154) }; + Console.WriteLine($" {name}() threw {Describe(inner)}" + + (expected ? " ← expected: the components above are missing" : "")); } } - Console.WriteLine(" (a non-empty GetMissingComponents is onboarding work, not a failure — §8)"); + return componentsMissing; } /// Create a real session, then create a SECOND one with the same name.