diff --git a/spikes/README.md b/spikes/README.md index 9443df2..4384a5b 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -37,11 +37,20 @@ dotnet run -- --session # + create a session, then a SECOND one with the 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. +**If it reports missing components**, the machine cannot run wslc yet, and the two components are +NOT fixed the same way — the tool prints the specific remedy for each: + +| Missing | Fix | +| --- | --- | +| `VirtualMachinePlatform` | `wsl --install`, then **reboot** (an OS optional feature). | +| `WslPackage` | `wsl --update --pre-release`, then `wsl --shutdown`. WSL is installed but older than the SDK, and **2.9.3 is pre-release-only — a plain `wsl --update` will not get there.** Confirm with `wsl --version`. | +| `SdkNeedsUpdate` | The NuGet pin is ahead of the installed service: update WSL further, or pin the package back. | + +The distinction is worth knowing because the failures look alike but mean opposite things: +`REGDB_E_CLASSNOTREG` (0x80040154) is *nothing installed*, `ERROR_NOT_SUPPORTED` (0x80070032) is +*installed but too old*. The tool decodes both, along with the `WSLC_E_*` range from `wslc.idl`, +because these COM exceptions often carry an empty message and leave nothing but a hex code. +`--session` is skipped while components are missing rather than failing the same way. 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.*` @@ -54,10 +63,17 @@ It writes the full public object model to `wslc-api-dump.txt` (`--out` to reloca 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 — 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. +`--probe` has been **run clean on real hardware** (Windows 11 amd64, WSL 2.9.3): 54/54 `ok`, +no missing components. Every other path is exercised against a stand-in assembly carrying the +observed 2.9.3 shape — the `ABI.` shadow types, a service reporting missing components, an +`0x80070032` with an empty message, a `ServiceVersion` with no `ToString()` override, and a +duplicate-name `Session` throwing `0x80040607` — so a failure on your machine is a finding about +wslc, not about this tool. + +Note the `ToString()` one, because it bit: a WinRT projection class does not override +`ToString()`, so printing a returned object gives you its *type name*. Values are rendered by +their properties instead — `ServiceVersion { Major=2, Minor=9, Revision=3 }`, not +`Microsoft.WSL.Containers.ServiceVersion`. **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 c469d85..4b4ccef 100644 --- a/spikes/WslcApiDump/Program.cs +++ b/spikes/WslcApiDump/Program.cs @@ -74,10 +74,10 @@ internal static class Program var missing = CheckAssumptions(types); - // 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. + // Components are probed BEFORE anything else that touches the service, because that one + // call explains every other failure: nothing installed answers REGDB_E_CLASSNOTREG + // (0x80040154), while an installed-but-too-old WSL answers ERROR_NOT_SUPPORTED + // (0x80070032). Reporting those as unexplained COM errors buries the actual finding. var componentsMissing = probe ? ProbeStatics(types) : null; if (session) { @@ -85,7 +85,7 @@ internal static class Program { Console.WriteLine(); Console.WriteLine("skipping --session: the components above are missing, so creating " - + "a session would only fail with the same 'class not registered'."); + + "a session would only fail the same way. Fix those first."); } else { @@ -97,9 +97,11 @@ internal static class Program 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."); + + $"{string.Join(", ", componentsMissing)}."); + foreach (var component in componentsMissing) + Console.WriteLine($" {component}: {Remedy(component)}"); + Console.WriteLine(" Then run this again. This is the §8 onboarding condition, " + + "not a defect."); } else if (missing == 0) { @@ -324,15 +326,17 @@ internal static class Program { 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."); + Console.WriteLine(" → wslc is not usable here yet. Calls that reach the " + + "service will fail (0x80040154 when nothing is installed, " + + "0x80070032 when WSL is present but too old)."); } } catch (TargetInvocationException e) { var inner = e.InnerException; - var expected = componentsMissing is { Count: > 0 } - && inner is COMException { HResult: unchecked((int)0x80040154) }; + // Any COM failure is expected while components are missing — pinning it to one + // code was wrong: a too-old WSL answers ERROR_NOT_SUPPORTED, not CLASSNOTREG. + var expected = componentsMissing is { Count: > 0 } && inner is COMException; Console.WriteLine($" {name}() threw {Describe(inner)}" + (expected ? " ← expected: the components above are missing" : "")); } @@ -455,18 +459,96 @@ internal static class Program { if (e is null) return "an unknown error"; var code = e is COMException com ? com.HResult : e.HResult; - return $"{e.GetType().Name} (0x{code:X8}): {e.Message}"; + // COM messages from this API are often empty, which leaves a bare hex code and no clue. + var message = string.IsNullOrWhiteSpace(e.Message) ? KnownHResult(code) : e.Message.Trim(); + return $"{e.GetType().Name} (0x{code:X8}): {message}"; } - private static string Render(object? value) => value switch + /// The codes actually seen coming out of wslc, decoded. The WSLC_E_* range is documented in + /// `wslc.idl`; the other two are ordinary Windows errors that mean very different things and + /// are easy to confuse — "not registered" is *nothing installed*, "not supported" is + /// *installed but too old*, which is a completely different fix. + private static string KnownHResult(int code) => (uint)code switch + { + 0x80040154 => "REGDB_E_CLASSNOTREG — the WSLC service class is not registered " + + "(nothing to talk to)", + 0x80070032 => "ERROR_NOT_SUPPORTED — the installed WSL does not implement this call " + + "(almost always: WSL is older than the SDK)", + 0x80070005 => "E_ACCESSDENIED", + 0x80040601 => "WSLC_E_IMAGE_NOT_FOUND", + 0x80040603 => "WSLC_E_CONTAINER_NOT_FOUND", + 0x80040605 => "WSLC_E_CONTAINER_NOT_RUNNING", + 0x80040607 => "WSLC_E_SESSION_RESERVED — that session name is already taken", + 0x80040608 => "WSLC_E_INVALID_SESSION_NAME", + 0x8004060B => "WSLC_E_SDK_UPDATE_NEEDED", + 0x8004060C => "WSLC_E_CONTAINER_DISABLED", + 0x8004060F => "WSLC_E_SESSION_NOT_FOUND", + _ => "no message", + }; + + /// What to actually DO about each missing component. These are not interchangeable, and the + /// difference cost a round trip: `wsl --install` fixes VirtualMachinePlatform and does nothing + /// for WslPackage, which needs an *update* — and specifically a pre-release one, because the + /// SDK's 2.9.3 is ahead of the Store channel. + private static string Remedy(string component) => component switch + { + "VirtualMachinePlatform" => + "`wsl --install`, then REBOOT (this is an OS optional feature)", + "WslPackage" => + "`wsl --update --pre-release` then `wsl --shutdown` — WSL is installed but older than " + + "the SDK. 2.9.3 is pre-release-only, so a plain `wsl --update` will NOT get there. " + + "Confirm with `wsl --version` (need >= 2.9.3).", + "SdkNeedsUpdate" => + "the Microsoft.WSL.Containers pin is NEWER than the installed service — either update " + + "WSL further or pin the package back", + _ => "see `wsl --help`", + }; + + private static string Render(object? value) => Render(value, depth: 0); + + /// Render a value for the console. + /// + /// The wrinkle worth knowing: a WinRT projection class does NOT override `ToString()`, so the + /// default gives you its type name and nothing else — `GetVersion()` printed + /// "Microsoft.WSL.Containers.ServiceVersion" instead of the version it had just fetched. When + /// `ToString()` is that unhelpful, dump the readable properties instead. `depth` bounds the + /// recursion, since a projection object graph can be cyclic. + private static string Render(object? value, int depth) => value switch { null => "null", string s => $"\"{s}\"", System.Collections.IEnumerable e and not string => - "[" + string.Join(", ", e.Cast().Select(Render)) + "]", - _ => value.ToString() ?? "?", + "[" + string.Join(", ", e.Cast().Select(v => Render(v, depth + 1))) + "]", + _ => Structured(value, depth), }; + private static string Structured(object value, int depth) + { + var type = value.GetType(); + var text = value.ToString(); + // A meaningful ToString() is one that isn't just the type's own name. + if (!string.IsNullOrEmpty(text) && text != type.FullName && text != type.Name) return text; + if (depth >= 2) return type.Name; + + PropertyInfo[] properties; + try + { + properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) + .OrderBy(p => p.Name, StringComparer.Ordinal) + .ToArray(); + } + catch { return type.Name; } + if (properties.Length == 0) return text ?? type.Name; + + var parts = properties.Select(p => + { + try { return $"{p.Name}={Render(p.GetValue(value), depth + 1)}"; } + catch (Exception e) { return $"{p.Name}=<{Unwrap(e)?.GetType().Name}>"; } + }); + return $"{type.Name} {{ {string.Join(", ", parts)} }}"; + } + private static string? ArgValue(string[] args, string flag) { var i = Array.IndexOf(args, flag);