Merge nucleic/olive-jade-civet-rznt into dev
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
# M1 spikes (docs/WINDOWS_PORT.md §13)
|
||||||
|
|
||||||
|
Programs that answer questions the port is currently *guessing* at. They are checked in and
|
||||||
|
runnable on purpose: a spike whose answer nobody can reproduce six months later is a rumour.
|
||||||
|
|
||||||
|
They are deliberately **not** in `windows/Nucleic.sln`. The solution's broker contract tests must
|
||||||
|
keep running on any machine against `FakeWslc`; these need the `Microsoft.WSL.Containers` preview
|
||||||
|
NuGet and a real WSL stack, so they are built by path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `WslcApiDump` — what does the wslc API actually look like?
|
||||||
|
|
||||||
|
**The problem it solves.** Items 5, 6 and 7 — the C# broker, the Swift wslc client, and the
|
||||||
|
control plane — were all built against Microsoft's *documentation*, on machines with no WSL.
|
||||||
|
`NucleicBroker/Wslc/WslcFacade.cs` is therefore a careful transcription that has never been
|
||||||
|
compiled against the real assembly, and `-p:UseWslc=true` is the only thing standing between it
|
||||||
|
and the build. Every line in it might be right. We have no idea which.
|
||||||
|
|
||||||
|
A typed spike cannot tell us: if `Session.CreateOrOpen` is really `Session.Open`, the spike fails
|
||||||
|
to **compile**, which teaches us one name and stops. So this tool is written entirely in
|
||||||
|
reflection. It cannot fail to build, and a single run prints the complete list of what is wrong.
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING.
|
||||||
|
|
||||||
|
### Why `--session` earns its risk
|
||||||
|
|
||||||
|
`--session` creates a real wslc session named `nucleic-spike` under
|
||||||
|
`%LOCALAPPDATA%\Nucleic\spike\wslc` and leaves it running. It exists for one question that type
|
||||||
|
metadata cannot answer: **where does the WSL-facing host gateway address come from?** §5 makes
|
||||||
|
gateway TCP the primary control-plane transport, `WslcContainerEngine.ensureRunning` returns that
|
||||||
|
address to the Swift side, and `control-bridge.js` dials it. The facade guesses
|
||||||
|
`Session.HostGatewayAddress`. Dumping the live *values* of every session property lets us
|
||||||
|
recognise a gateway IP whatever it is called — and if nothing on the session looks like one,
|
||||||
|
that is itself the finding, and §5's hvsocket fallback gets promoted from upside to dependency.
|
||||||
|
|
||||||
|
Leaving the session running is also the cheap version of a §2.3 question: broker supervision
|
||||||
|
assumes wslc state is **service-backed**, so that a crashed `nucleic-brokerd` can reattach and
|
||||||
|
re-enumerate rather than orphaning containers. If `wslc session ls` still shows `nucleic-spike`
|
||||||
|
after this process exits, that assumption holds. Tear it down with `wslc` when you're done.
|
||||||
|
|
||||||
|
### The three answers that change the design
|
||||||
|
|
||||||
|
Most mismatches are a one-line edit in `WslcFacade.cs` — that is exactly what the `IWslc` seam is
|
||||||
|
for, and nothing above it should move. These three are different:
|
||||||
|
|
||||||
|
| Finding | Consequence |
|
||||||
|
| --- | --- |
|
||||||
|
| No create-or-attach on `Session` | Broker-crash reattach (§2.3) has no mechanism; supervision needs redesigning before item 11. |
|
||||||
|
| No gateway address on a live `Session` | §5's primary transport loses its source; promote the AF_HYPERV/AF_VSOCK fallback and spike that instead. |
|
||||||
|
| No uid on `ProcessSettings` | Recoverable, and already planned for: exec wraps argv in `setpriv`/`su agent -c`. Interceptors and nash don't care about the numeric uid (§3.2). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Not yet written
|
||||||
|
|
||||||
|
- **`WslcSpike`** — the typed happy path: session → GHCR pull of `naros-agent` → container with an
|
||||||
|
NTFS `ContainerVolume` → `exec git status` in the bind-mounted worktree → stdio round-trip →
|
||||||
|
SIGTERM, plus the 9P latency numbers §15 wants (`git status` and `npm install` on a real repo,
|
||||||
|
mounted vs. in-VM). Deliberately held back until `WslcApiDump` has run: written now, against
|
||||||
|
guessed names, it would not compile, and fixing it blind is the mistake this whole approach
|
||||||
|
exists to avoid.
|
||||||
|
- **`HvSocketSpike`** — M1 (b): AF_HYPERV host listener ↔ AF_VSOCK dial from inside a wslc
|
||||||
|
container, plus gateway-TCP reachability and default-firewall behaviour in NAT and mirrored
|
||||||
|
modes. Only worth building once a container can be started at all.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
namespace WslcApiDump;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Every member <c>NucleicBroker/Wslc/WslcFacade.cs</c> calls on the preview
|
||||||
|
/// <c>Microsoft.WSL.Containers</c> API, as plain strings.
|
||||||
|
///
|
||||||
|
/// The facade was written from Microsoft's documentation without a machine to run it on
|
||||||
|
/// (docs/WINDOWS_PORT.md §1.5), so each line here is a *claim* — and this spike's job is to say,
|
||||||
|
/// for each one, whether the shipped assembly agrees. Strings rather than typed references is the
|
||||||
|
/// entire trick: a typed spike that names <c>Session.CreateOrOpen</c> fails to COMPILE if that
|
||||||
|
/// method has a different name, which teaches us nothing. Reflection turns every wrong guess into
|
||||||
|
/// a printed line instead of a build error, so one run produces the complete worklist.
|
||||||
|
///
|
||||||
|
/// Keep in lockstep with WslcFacade.cs. A member that stops being used should leave this list;
|
||||||
|
/// a newly-used member should join it, so the next run of this tool still covers the real surface.
|
||||||
|
/// </summary>
|
||||||
|
internal static class FacadeAssumptions
|
||||||
|
{
|
||||||
|
internal sealed record Assumption(string Type, string Member, Kind MemberKind, string Why);
|
||||||
|
|
||||||
|
internal enum Kind { Method, Property, Event, Constructor, EnumValue, Type }
|
||||||
|
|
||||||
|
internal static readonly Assumption[] All =
|
||||||
|
[
|
||||||
|
// ---- Service entry point: onboarding (§8 step 2) ----
|
||||||
|
new("WslcService", "GetServiceVersion", Kind.Method,
|
||||||
|
"hello capabilities — hostd degrades across preview→GA churn on this string"),
|
||||||
|
new("WslcService", "GetMissingComponents", Kind.Method,
|
||||||
|
"components.missing RPC; drives the guided-install onboarding page"),
|
||||||
|
new("WslcService", "InstallComponentsAsync", Kind.Method,
|
||||||
|
"components.install RPC"),
|
||||||
|
new("ComponentFlags", "None", Kind.EnumValue,
|
||||||
|
"the 'nothing missing' sentinel the facade filters on"),
|
||||||
|
|
||||||
|
// ---- Session: one per channel, hosts every container (§3.2) ----
|
||||||
|
new("SessionSettings", ".ctor", Kind.Constructor,
|
||||||
|
"SessionSettings(name, dataDir) — the two-arg shape the facade assumes"),
|
||||||
|
new("SessionSettings", "CpuCount", Kind.Property, "session.ensure cpu"),
|
||||||
|
new("SessionSettings", "MemoryMB", Kind.Property,
|
||||||
|
"session.ensure memoryMB; §3.2 notes a resize needs a sandbox restart"),
|
||||||
|
new("Session", "CreateOrOpen", Kind.Method,
|
||||||
|
"THE create-or-attach primitive. If this is absent, broker reattach after a crash "
|
||||||
|
+ "(§2.3) has no mechanism and the whole supervision design changes"),
|
||||||
|
new("Session", "Terminate", Kind.Method, "session.terminate RPC"),
|
||||||
|
new("Session", "SessionTerminationHandler", Kind.Property,
|
||||||
|
"session.down notification → hostd's reconcile sweep"),
|
||||||
|
new("Session", "HostGatewayAddress", Kind.Property,
|
||||||
|
"THE control-plane address (§5). ensureRunning returns it to the Swift engine and "
|
||||||
|
+ "control-bridge.js dials it. If this member does not exist, §5's primary transport "
|
||||||
|
+ "needs another source (query the vNIC) or the hvsocket fallback gets promoted"),
|
||||||
|
|
||||||
|
// ---- Images ----
|
||||||
|
new("Session", "PullImageAsync", Kind.Method, "image.pull RPC (naros-agent from GHCR)"),
|
||||||
|
new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(reference)"),
|
||||||
|
new("PullImageOptions", "Credentials", Kind.Property, "GHCR auth (registryAuth)"),
|
||||||
|
new("PullImageOptions", "Progress", Kind.Event,
|
||||||
|
"image.pullProgress → the existing controlDownloadProgress UI surface"),
|
||||||
|
new("RegistryCredentials", ".ctor", Kind.Constructor, "RegistryCredentials(user, password)"),
|
||||||
|
new("Session", "GetImages", Kind.Method, "image.list / image.inspect"),
|
||||||
|
new("Session", "DeleteImage", Kind.Method, "image.delete"),
|
||||||
|
|
||||||
|
// ---- Containers ----
|
||||||
|
new("Session", "CreateContainer", Kind.Method, "container.create"),
|
||||||
|
new("Session", "GetContainers", Kind.Method, "container.list; reattach re-enumeration"),
|
||||||
|
new("ContainerSettings", ".ctor", Kind.Constructor, "ContainerSettings(image)"),
|
||||||
|
new("ContainerSettings", "Name", Kind.Property, "channel-suffixed container naming"),
|
||||||
|
new("ContainerSettings", "Hostname", Kind.Property, "container.create hostname"),
|
||||||
|
new("ContainerSettings", "NetworkingMode", Kind.Property,
|
||||||
|
"NAT vs mirrored (§5 item 4) — determines how the guest reaches the host"),
|
||||||
|
new("ContainerSettings", "Volumes", Kind.Property, "the NTFS worktree bind mount (D8)"),
|
||||||
|
new("ContainerSettings", "InitProcess", Kind.Property, "naros-init as PID 1 (docs/NAROS.md)"),
|
||||||
|
new("ContainerVolume", ".ctor", Kind.Constructor,
|
||||||
|
"ContainerVolume(hostPath, guestPath, readOnly) — the 3-arg shape"),
|
||||||
|
new("ContainerNetworkingMode", "", Kind.Type, "enum parsed from the RPC's networkingMode"),
|
||||||
|
new("Container", "Start", Kind.Method, "container.start"),
|
||||||
|
new("Container", "Stop", Kind.Method, "container.stop(signal, grace)"),
|
||||||
|
new("Container", "Delete", Kind.Method, "container.delete"),
|
||||||
|
new("Container", "State", Kind.Property, "container.state → running/stopped/absent"),
|
||||||
|
new("Container", "GetStatistics", Kind.Method,
|
||||||
|
"container.stats → ContainerResourceSample; the Swift engine folds deltas from it"),
|
||||||
|
new("Container", "RunProcess", Kind.Method, "proc.exec — the agent's own exec path"),
|
||||||
|
new("ContainerState", "Running", Kind.EnumValue, "the one state the facade tests by name"),
|
||||||
|
new("DeleteContainerFlags", "Force", Kind.EnumValue, "container.delete force"),
|
||||||
|
|
||||||
|
// ---- Processes: the agent stdio path (§3.2 WslcProcessHandle) ----
|
||||||
|
new("ProcessSettings", "CmdLine", Kind.Property, "argv"),
|
||||||
|
new("ProcessSettings", "Environment", Kind.Property, "env"),
|
||||||
|
new("ProcessSettings", "WorkingDirectory", Kind.Property, "cwd"),
|
||||||
|
new("ProcessSettings", "OutputMode", Kind.Property, "event-mode stdio, not polling"),
|
||||||
|
new("ProcessSettings", "Terminal", Kind.Property, "tty for the Terminal panel (§7)"),
|
||||||
|
new("ProcessSettings", "UserId", Kind.Property,
|
||||||
|
"runAsUID 501 (§3.2). If absent, exec falls back to a setpriv/su wrapper argv — "
|
||||||
|
+ "interceptors and nash don't care about the numeric uid, so this is recoverable"),
|
||||||
|
new("ProcessSettings", "GroupId", Kind.Property, "runAsGID"),
|
||||||
|
new("ProcessOutputMode", "Event", Kind.EnumValue,
|
||||||
|
"the mode that makes stdio push-based; polling would change the whole broker design"),
|
||||||
|
new("Signal", "", Kind.Type, "the enum Stop/Signal take; the RPC carries POSIX ints"),
|
||||||
|
new("Process", "OutputReceived", Kind.Event, "→ proc.stdout / proc.stderr notifications"),
|
||||||
|
new("Process", "Exited", Kind.Event, "→ proc.exit; must never overtake output (OutboundWriter)"),
|
||||||
|
new("Process", "WriteStdin", Kind.Method, "proc.stdin (NDJSON to the agent)"),
|
||||||
|
new("Process", "CloseStdin", Kind.Method, "proc.closeStdin"),
|
||||||
|
new("Process", "Signal", Kind.Method, "proc.signal — the agent's real POSIX Stop path"),
|
||||||
|
new("Process", "ResizeTerminal", Kind.Method, "proc.resize (tty mode)"),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
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 (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.
|
||||||
|
/// </summary>
|
||||||
|
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<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>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>The two statics that are safe to call on any machine: they only read state.</summary>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>Session.HostGatewayAddress</c>, 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.
|
||||||
|
/// </summary>
|
||||||
|
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 = $"<threw {e.InnerException?.GetType().Name ?? e.GetType().Name}>"; }
|
||||||
|
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<object?>().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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!-- M1 spike (a), phase 1 — docs/WINDOWS_PORT.md §13.
|
||||||
|
Reports the REAL Microsoft.WSL.Containers object model, and checks every assumption
|
||||||
|
NucleicBroker/Wslc/WslcFacade.cs makes against it.
|
||||||
|
|
||||||
|
Deliberately NOT in Nucleic.sln: it needs the preview NuGet, which the broker's own
|
||||||
|
contract tests must never require (they run on Linux against FakeWslc). Build it by
|
||||||
|
path — see windows/spikes/README.md. -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AssemblyName>wslc-api-dump</AssemblyName>
|
||||||
|
<RootNamespace>WslcApiDump</RootNamespace>
|
||||||
|
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Same pin as the broker (docs/WINDOWS_PORT.md §15). If restore fails because the
|
||||||
|
version moved, change it HERE first, find out what the new API looks like, and only
|
||||||
|
then bump the broker — that ordering is the whole point of this spike. -->
|
||||||
|
<PackageReference Include="Microsoft.WSL.Containers" Version="[0.1.0-preview.1]" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user