using System.Reflection; using System.Text; namespace WslcApiDump; /// /// M1 spike (a), phase 1 (docs/WINDOWS_PORT.md §13): what does Microsoft.WSL.Containers /// ACTUALLY look like, and where is WslcFacade.cs 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 (service version, missing components). `--session` /// goes one step further and creates a real session, because the single most important unknown — /// which property carries the WSL gateway address (§5) — can only be answered by looking at a /// live one. /// internal static class Program { private const string AssemblyName = "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 types = assembly.GetExportedTypes().OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray(); Console.WriteLine($"{AssemblyName} {assembly.GetName().Version} — {types.Length} public types"); 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); 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"); 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)."); // 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; } foreach (var ctor in type.GetConstructors()) report.AppendLine($" .ctor({Parameters(ctor)})"); 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; " : "")}}}"); foreach (var evt in type.GetEvents(Public).OrderBy(e => e.Name, StringComparer.Ordinal)) report.AppendLine($" event {Short(evt.EventHandlerType)} {evt.Name}"); 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 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 name = t.Name[..t.Name.IndexOf('`')]; 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()})"); Console.WriteLine($" why: {assumption.Why}"); 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 MemberNames(Type type) => type.IsEnum ? Enum.GetNames(type) : type.GetMembers(Public).Where(m => !m.Name.StartsWith('.')).Select(m => m.Name).Distinct(); /// 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. private static string[] Nearest(string wanted, IEnumerable 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 statics that are safe to call on any machine: they only read state. private static void 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; } foreach (var name in new[] { "GetServiceVersion", "GetMissingComponents" }) { var method = service.GetMethods(Public) .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == 0); if (method is null) { Console.WriteLine($" {name}: absent"); continue; } try { Console.WriteLine($" {name}() = {Render(method.Invoke(null, null))}"); } 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 {e.InnerException?.GetType().Name}: " + $"{e.InnerException?.Message}"); } } } /// /// Create a real session and print every readable property of it. /// /// This is here for one question: §5's control plane needs the WSL-facing host address, the /// facade guesses it is Session.HostGatewayAddress, and no amount of type-metadata /// reading tells us whether that property holds what we need. Dumping the live VALUES of a /// session lets us recognise a gateway IP when we see one, whatever it happens to be called. /// /// Leaves the session running on purpose — `wslc session ls` should show it, and whether it /// survives this process exiting is itself one of the §2.3 questions (service-backed state is /// what makes broker-crash reattach possible). Tear it down with `wslc` when done. /// private static void ProbeSession(Type[] types, string name) { 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); object? settings; try { settings = Activator.CreateInstance(settingsType, name, dataDir); } catch (Exception e) { Console.WriteLine($" SessionSettings(name, dataDir) rejected: {e.InnerException?.Message ?? e.Message}"); Console.WriteLine(" → constructor shape differs; see the .ctor lines in the dump file"); return; } var factory = sessionType.GetMethods(Public) .FirstOrDefault(m => m.IsStatic && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == settingsType); if (factory is null) { Console.WriteLine(" no static Session factory taking SessionSettings — see the dump"); return; } Console.WriteLine($" using {sessionType.Name}.{factory.Name}(SessionSettings)"); object? live; try { live = factory.Invoke(null, [settings]); } catch (TargetInvocationException e) { Console.WriteLine($" session create threw {e.InnerException?.GetType().Name}: " + $"{e.InnerException?.Message}"); return; } if (live is null) { Console.WriteLine(" factory returned null"); return; } Console.WriteLine(" live session properties (look for the gateway address — §5):"); foreach (var property in sessionType.GetProperties(Public) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) .OrderBy(p => p.Name, StringComparer.Ordinal)) { string rendered; try { rendered = Render(property.GetValue(live)); } catch (Exception e) { rendered = $""; } Console.WriteLine($" {property.Name} = {rendered}"); } Console.WriteLine(" (session left running — `wslc session ls`; whether it outlives this " + "process is the §2.3 reattach question)"); } private static string Render(object? value) => value switch { null => "null", string s => $"\"{s}\"", System.Collections.IEnumerable e and not string => "[" + string.Join(", ", e.Cast().Select(Render)) + "]", _ => value.ToString() ?? "?", }; 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; } }