672 lines
32 KiB
C#
672 lines
32 KiB
C#
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace WslcApiDump;
|
|
|
|
/// <summary>
|
|
/// M1 spike (a), phase 1 (docs/WINDOWS_PORT.md §13): what does Microsoft.WSL.Containers
|
|
/// ACTUALLY look like, and where is <c>WslcFacade.cs</c> wrong?
|
|
///
|
|
/// Everything the Windows container subsystem rests on — items 5, 6 and 7 — was written against
|
|
/// documentation, on a machine with no WSL. This program is the cheapest possible way to convert
|
|
/// that pile of assumptions into a worklist, and it is written entirely in reflection so that it
|
|
/// cannot fail to build no matter how wrong the assumptions turn out to be.
|
|
///
|
|
/// It never mutates anything unless asked: the default run only reads type metadata. `--probe`
|
|
/// additionally calls the two safe statics (`GetVersion`, `GetMissingComponents`). `--session`
|
|
/// goes further and creates a real session, then a SECOND one with the same name, to settle
|
|
/// whether the compat SDK can re-adopt a running session. Answered on 2.9.4: it cannot. The
|
|
/// constructor is lazy and always succeeds; `Start()` is where the service is consulted, and it
|
|
/// refuses with `ERROR_ALREADY_EXISTS`. So reattach needs the internal COM interface, which is
|
|
/// what D13 assumes (docs/WINDOWS_PORT.md §13.1).
|
|
/// </summary>
|
|
internal static class Program
|
|
{
|
|
/// The C#/WinRT PROJECTION assembly. The NuGet package is "Microsoft.WSL.Containers" but
|
|
/// the managed assembly it ships is `lib/net8.0-windows10.0.19041.0/wslcsdkcs.dll`, so
|
|
/// 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");
|
|
var session = args.Contains("--session");
|
|
var outPath = ArgValue(args, "--out") ?? "wslc-api-dump.txt";
|
|
|
|
Assembly assembly;
|
|
try
|
|
{
|
|
assembly = Assembly.Load(new AssemblyName(AssemblyName));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.Error.WriteLine($"could not load {AssemblyName}: {e.Message}");
|
|
Console.Error.WriteLine(
|
|
"Is the preview NuGet restored? `dotnet restore windows/spikes/WslcApiDump`.");
|
|
return 2;
|
|
}
|
|
|
|
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();
|
|
// 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);
|
|
|
|
// 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)
|
|
{
|
|
if (componentsMissing is { Count: > 0 })
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("skipping --session: the components above are missing, so creating "
|
|
+ "a session would only fail the same way. Fix those first.");
|
|
}
|
|
else
|
|
{
|
|
ProbeSession(
|
|
types, ArgValue(args, "--session-name") ?? "nucleic-spike",
|
|
keep: args.Contains("--keep"));
|
|
}
|
|
}
|
|
|
|
Console.WriteLine();
|
|
if (componentsMissing is { Count: > 0 })
|
|
{
|
|
Console.WriteLine($"RESULT: this machine cannot run wslc yet — missing "
|
|
+ $"{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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// MARK: - Type dump
|
|
|
|
private static void DescribeType(Type type, StringBuilder report)
|
|
{
|
|
var kind = type.IsEnum ? "enum"
|
|
: type.IsInterface ? "interface"
|
|
: type.IsValueType ? "struct"
|
|
: "class";
|
|
report.AppendLine($"{kind} {type.FullName}"
|
|
+ (type.BaseType is { } b && b != typeof(object) ? $" : {b.Name}" : ""));
|
|
|
|
if (type.IsEnum)
|
|
{
|
|
foreach (var name in Enum.GetNames(type)) report.AppendLine($" .{name}");
|
|
report.AppendLine();
|
|
return;
|
|
}
|
|
|
|
// Every member walk is guarded. These are WinRT projection types: a signature can name a
|
|
// type from an assembly that is present at build time and not at runtime, and one such
|
|
// member must not cost us the other 60 types' worth of report.
|
|
Guarded(report, () => {
|
|
foreach (var ctor in type.GetConstructors())
|
|
report.AppendLine($" .ctor({Parameters(ctor)})");
|
|
});
|
|
Guarded(report, () => {
|
|
foreach (var property in type.GetProperties(Public).OrderBy(p => p.Name, StringComparer.Ordinal))
|
|
report.AppendLine(
|
|
$" {Short(property.PropertyType)} {property.Name} "
|
|
+ $"{{ {(property.CanRead ? "get; " : "")}{(property.CanWrite ? "set; " : "")}}}");
|
|
});
|
|
Guarded(report, () => {
|
|
foreach (var evt in type.GetEvents(Public).OrderBy(e => e.Name, StringComparer.Ordinal))
|
|
report.AppendLine($" event {Short(evt.EventHandlerType)} {evt.Name}");
|
|
});
|
|
Guarded(report, () => {
|
|
foreach (var method in type.GetMethods(Public)
|
|
.Where(m => !m.IsSpecialName)
|
|
.OrderBy(m => m.Name, StringComparer.Ordinal))
|
|
report.AppendLine($" {Short(method.ReturnType)} {method.Name}({Parameters(method)})");
|
|
});
|
|
report.AppendLine();
|
|
}
|
|
|
|
private static void Guarded(StringBuilder report, Action body)
|
|
{
|
|
try { body(); }
|
|
catch (Exception e) { report.AppendLine($" <unreadable: {e.GetType().Name}: {e.Message}>"); }
|
|
}
|
|
|
|
private const BindingFlags Public =
|
|
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
|
|
|
private static string Parameters(MethodBase m) =>
|
|
string.Join(", ", m.GetParameters().Select(p => $"{Short(p.ParameterType)} {p.Name}"));
|
|
|
|
private static string Short(Type? t)
|
|
{
|
|
if (t is null) return "void";
|
|
if (!t.IsGenericType) return t.Name;
|
|
var tick = t.Name.IndexOf('`');
|
|
var name = tick < 0 ? t.Name : t.Name[..tick];
|
|
return $"{name}<{string.Join(", ", t.GetGenericArguments().Select(Short))}>";
|
|
}
|
|
|
|
// MARK: - Assumption check (the actual product)
|
|
|
|
private static int CheckAssumptions(Type[] types)
|
|
{
|
|
Console.WriteLine("WslcFacade.cs assumptions:");
|
|
Console.WriteLine();
|
|
var wrong = 0;
|
|
foreach (var group in FacadeAssumptions.All.GroupBy(a => a.Type))
|
|
{
|
|
var type = types.FirstOrDefault(t => t.Name == group.Key);
|
|
if (type is null)
|
|
{
|
|
// Summarise rather than repeat: if a whole type is renamed or namespaced away,
|
|
// one line naming it beats a paragraph per member. The per-member "why" only
|
|
// earns its space when the type exists and a single member is wrong.
|
|
wrong += group.Count();
|
|
Console.WriteLine($" MISSING TYPE {group.Key} "
|
|
+ $"({group.Count()} member(s): {string.Join(", ", group.Select(a => a.Member))})");
|
|
var near = Nearest(group.Key, types.Select(t => t.Name));
|
|
Console.WriteLine(near.Length > 0
|
|
? $" nearest types: {string.Join(", ", near)}"
|
|
: " no similarly-named type — check the dump file's namespaces");
|
|
continue;
|
|
}
|
|
|
|
foreach (var assumption in group)
|
|
{
|
|
if (assumption.MemberKind is FacadeAssumptions.Kind.Type || Has(type, assumption))
|
|
{
|
|
Console.WriteLine($" ok {type.Name}.{assumption.Member}");
|
|
continue;
|
|
}
|
|
wrong++;
|
|
Console.WriteLine($" MISSING {type.Name}.{assumption.Member} "
|
|
+ $"({assumption.MemberKind.ToString().ToLowerInvariant()})");
|
|
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)}");
|
|
}
|
|
}
|
|
return wrong;
|
|
}
|
|
|
|
private static bool Has(Type type, FacadeAssumptions.Assumption a) => a.MemberKind switch
|
|
{
|
|
FacadeAssumptions.Kind.Constructor => type.GetConstructors().Length > 0,
|
|
FacadeAssumptions.Kind.EnumValue => type.IsEnum && Enum.GetNames(type).Contains(a.Member),
|
|
FacadeAssumptions.Kind.Event => type.GetEvent(a.Member) is not null,
|
|
FacadeAssumptions.Kind.Property =>
|
|
type.GetProperty(a.Member) is not null || type.GetField(a.Member) is not null,
|
|
FacadeAssumptions.Kind.Method => type.GetMethods(Public).Any(m => m.Name == a.Member),
|
|
_ => true,
|
|
};
|
|
|
|
private static IEnumerable<string> MemberNames(Type type) =>
|
|
type.IsEnum
|
|
? Enum.GetNames(type)
|
|
: type.GetMembers(Public).Where(m => !m.Name.StartsWith('.')).Select(m => m.Name).Distinct();
|
|
|
|
/// <summary>Cheap "did they just rename it" hint: shared prefix or containment, no edit
|
|
/// distance. A three-name shortlist is enough to spot <c>HostGateway</c> vs
|
|
/// <c>HostGatewayAddress</c>, which is the realistic failure mode.</summary>
|
|
/// 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<string> 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<string> candidates)
|
|
{
|
|
var needle = wanted.TrimStart('.');
|
|
if (needle.Length == 0) return [];
|
|
return candidates
|
|
.Where(c => c.Contains(needle, StringComparison.OrdinalIgnoreCase)
|
|
|| needle.Contains(c, StringComparison.OrdinalIgnoreCase)
|
|
|| SharedPrefix(c, needle) >= 4)
|
|
.Distinct()
|
|
.Take(3)
|
|
.ToArray();
|
|
}
|
|
|
|
private static int SharedPrefix(string a, string b)
|
|
{
|
|
var n = 0;
|
|
while (n < a.Length && n < b.Length && char.ToLowerInvariant(a[n]) == char.ToLowerInvariant(b[n])) n++;
|
|
return n;
|
|
}
|
|
|
|
// MARK: - Live probes
|
|
|
|
/// The two service statics, which only read state and are safe on any machine.
|
|
///
|
|
/// 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 IReadOnlyList<string>? 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 null; }
|
|
|
|
List<string>? 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);
|
|
if (method is null)
|
|
{
|
|
var any = service.GetMethods(Public).Any(m => m.Name == name);
|
|
Console.WriteLine($" {name}: {(any ? "present but not static — see the dump" : "absent")}");
|
|
continue;
|
|
}
|
|
try
|
|
{
|
|
var value = method.Invoke(null, null);
|
|
Console.WriteLine($" {name}() = {Render(value)}");
|
|
if (name == "GetMissingComponents" && value is System.Collections.IEnumerable list)
|
|
{
|
|
componentsMissing = list.Cast<object?>().Select(v => v?.ToString() ?? "?").ToList();
|
|
if (componentsMissing.Count > 0)
|
|
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;
|
|
// 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" : ""));
|
|
}
|
|
}
|
|
return componentsMissing;
|
|
}
|
|
|
|
/// Create a real session, then create a SECOND one with the same name.
|
|
///
|
|
/// The second one is the point. D13 puts session reattach on the internal COM interface
|
|
/// (`IWSLCSessionManager::OpenSessionByName`) precisely because the compat SDK exposes only a
|
|
/// constructor — but nobody has established what that constructor DOES when the name is
|
|
/// already taken. If it attaches, §2.3 broker reattach may not need the internal interface at
|
|
/// all for the session half; if it throws `WSLC_E_SESSION_RESERVED` (0x80040607), D13's
|
|
/// reasoning is confirmed on hardware rather than inferred from an IDL.
|
|
///
|
|
/// 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, bool keep)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"live probe (creates session '{name}'):");
|
|
var settingsType = types.FirstOrDefault(t => t.Name == "SessionSettings");
|
|
var sessionType = types.FirstOrDefault(t => t.Name == "Session");
|
|
if (settingsType is null || sessionType is null)
|
|
{
|
|
Console.WriteLine(" SessionSettings/Session absent — skipping");
|
|
return;
|
|
}
|
|
|
|
var dataDir = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"Nucleic", "spike", "wslc");
|
|
Directory.CreateDirectory(dataDir);
|
|
Console.WriteLine($" storage: {dataDir}");
|
|
|
|
var first = CreateSession(settingsType, sessionType, name, dataDir, "first");
|
|
if (first is null) return;
|
|
|
|
// Start it — construction alone may not boot the VM (`Session.Start()` is separate).
|
|
var start = sessionType.GetMethods(Public)
|
|
.FirstOrDefault(m => !m.IsStatic && m.Name == "Start" && m.GetParameters().Length == 0);
|
|
if (start is null) { Console.WriteLine(" no Session.Start() — see the dump"); }
|
|
else
|
|
{
|
|
try { start.Invoke(first, null); Console.WriteLine(" Start() ok"); }
|
|
catch (TargetInvocationException e) { Console.WriteLine($" Start() threw {Describe(e.InnerException)}"); }
|
|
}
|
|
|
|
DumpProperties(sessionType, first, " session properties");
|
|
|
|
// THE question (see the doc comment).
|
|
Console.WriteLine();
|
|
Console.WriteLine(" second Session with the SAME name — attach, or reserved?");
|
|
var second = CreateSession(settingsType, sessionType, name, dataDir, "second");
|
|
if (second is null)
|
|
{
|
|
Console.WriteLine(" → REFUSED (see the HRESULT above). D13's premise holds: the compat "
|
|
+ "surface cannot re-adopt a session, so reattach needs "
|
|
+ "IWSLCSessionManager::OpenSessionByName.");
|
|
}
|
|
else
|
|
{
|
|
// The constructor is LAZY — it only captures settings, so its success says nothing.
|
|
// `Start()` is where the service is actually asked for the session, and therefore
|
|
// where exclusivity is enforced. Judge on that.
|
|
var secondStarted = false;
|
|
string? startFailure = null;
|
|
if (start is null)
|
|
{
|
|
Console.WriteLine(" (no Session.Start() — cannot judge)");
|
|
}
|
|
else
|
|
{
|
|
try { start.Invoke(second, null); secondStarted = true; }
|
|
catch (TargetInvocationException e) { startFailure = Describe(e.InnerException); }
|
|
}
|
|
|
|
if (secondStarted)
|
|
{
|
|
Console.WriteLine(" second Start() ok — it did NOT refuse, so it may have re-adopted");
|
|
IdentityTest(sessionType, first, second);
|
|
}
|
|
else if (startFailure is not null)
|
|
{
|
|
Console.WriteLine($" second Start() threw {startFailure}");
|
|
Console.WriteLine();
|
|
Console.WriteLine(" → REFUSED at Start(). The constructor succeeding meant nothing:");
|
|
Console.WriteLine(" it is lazy, and the service is only consulted by Start().");
|
|
Console.WriteLine(" So the compat SDK CANNOT re-adopt a running session, and D13");
|
|
Console.WriteLine(" holds — §2.3 reattach needs IWSLCSessionManager::OpenSessionByName");
|
|
Console.WriteLine(" (or EnterSession) on the internal COM interface.");
|
|
Console.WriteLine(" Note the code is ERROR_ALREADY_EXISTS, not WSLC_E_SESSION_RESERVED;");
|
|
Console.WriteLine(" the reserved code evidently means something narrower.");
|
|
}
|
|
}
|
|
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(" identity test: terminate the FIRST, then read from the SECOND");
|
|
if (!Reads(getImages, second, out var before))
|
|
{
|
|
// Only reachable if Start() reported success but the session is unusable anyway.
|
|
Console.WriteLine($" inconclusive — the second session reads fail already: {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(
|
|
Type settingsType, Type sessionType, string name, string dataDir, string which)
|
|
{
|
|
object? settings;
|
|
try
|
|
{
|
|
settings = Activator.CreateInstance(settingsType, name, dataDir);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine($" SessionSettings(name, storagePath) rejected: {Describe(Unwrap(e))}");
|
|
Console.WriteLine(" → constructor shape differs; see the .ctor lines in the dump file");
|
|
return null;
|
|
}
|
|
try
|
|
{
|
|
return Activator.CreateInstance(sessionType, settings);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine($" {which} Session(settings) threw {Describe(Unwrap(e))}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static void DumpProperties(Type type, object instance, string heading)
|
|
{
|
|
var properties = type.GetProperties(Public)
|
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
|
.OrderBy(p => p.Name, StringComparer.Ordinal)
|
|
.ToArray();
|
|
if (properties.Length == 0)
|
|
{
|
|
Console.WriteLine($"{heading}: none (this type is all methods and events)");
|
|
return;
|
|
}
|
|
Console.WriteLine($"{heading}:");
|
|
foreach (var property in properties)
|
|
{
|
|
string rendered;
|
|
try { rendered = Render(property.GetValue(instance)); }
|
|
catch (Exception e) { rendered = $"<threw {Describe(Unwrap(e))}>"; }
|
|
Console.WriteLine($" {property.Name} = {rendered}");
|
|
}
|
|
}
|
|
|
|
private static Exception? Unwrap(Exception e) =>
|
|
e is TargetInvocationException { InnerException: { } inner } ? inner : e;
|
|
|
|
/// Exceptions from this API are COM HRESULTs, and the number is the useful part: the WSLC
|
|
/// codes are documented in `wslc.idl` (0x80040601 image-not-found … 0x80040607
|
|
/// session-reserved … 0x8004060F session-not-found).
|
|
private static string Describe(Exception? e)
|
|
{
|
|
if (e is null) return "an unknown error";
|
|
var code = e is COMException com ? com.HResult : e.HResult;
|
|
// 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}";
|
|
}
|
|
|
|
/// 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",
|
|
0x800700B7 => "ERROR_ALREADY_EXISTS — a session of that name is already running",
|
|
0x8000000E => "E_ILLEGAL_METHOD_CALL — the object is not in a state to serve this call "
|
|
+ "(e.g. Session.Start() was never called, or failed)",
|
|
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<object?>().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);
|
|
return i >= 0 && i + 1 < args.Length && !args[i + 1].StartsWith("--") ? args[i + 1] : null;
|
|
}
|
|
}
|