Merge nucleic/olive-jade-civet-rznt into dev
This commit is contained in:
+21
-11
@@ -41,24 +41,34 @@ 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
|
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.
|
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.
|
||||||
|
|
||||||
**What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING —
|
**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.
|
which now means the package moved under us, not that we guessed wrong.
|
||||||
|
|
||||||
### Why `--session` earns its risk
|
### Why `--session` earns its risk
|
||||||
|
|
||||||
`--session` creates a real wslc session named `nucleic-spike` under
|
`--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
|
`%LOCALAPPDATA%\Nucleic\spike\wslc`, starts it, and then **creates a second one with the same
|
||||||
metadata cannot answer: **where does the WSL-facing host gateway address come from?** §5 makes
|
name**. That second construction is the whole point, and it is the last open question D13 turns
|
||||||
gateway TCP the primary control-plane transport, `WslcContainerEngine.ensureRunning` returns that
|
on: the compat SDK exposes a `Session` *constructor* and no attach, so does constructing over an
|
||||||
address to the Swift side, and `control-bridge.js` dials it. The facade guesses
|
existing name re-adopt it, or refuse?
|
||||||
`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
|
- **Refused** (expect `WSLC_E_SESSION_RESERVED`, `0x80040607`) — D13's premise is confirmed on
|
||||||
assumes wslc state is **service-backed**, so that a crashed `nucleic-brokerd` can reattach and
|
hardware rather than inferred from an IDL, and session reattach genuinely requires
|
||||||
re-enumerate rather than orphaning containers. If `wslc session ls` still shows `nucleic-spike`
|
`IWSLCSessionManager::OpenSessionByName` on the internal interface.
|
||||||
after this process exits, that assumption holds. Tear it down with `wslc` when you're done.
|
- **Constructed** — the compat surface may re-adopt by name, and the session half of §2.3 reattach
|
||||||
|
may not need the internal interface at all. Check `wslc container ps` for the first session's
|
||||||
|
containers before believing it.
|
||||||
|
|
||||||
|
It leaves the session running on purpose, because that is the other half of the same question:
|
||||||
|
broker supervision assumes wslc state is **service-backed**, so a crashed `nucleic-brokerd` can
|
||||||
|
re-adopt rather than orphaning containers. If `wslc session ls` still shows `nucleic-spike` after
|
||||||
|
this process exits, that holds. Tear it down with `wslc` when you're done.
|
||||||
|
|
||||||
|
The gateway address is *not* what this probe is for any more — §13.1 established that no API
|
||||||
|
surfaces one, and D13 moves the control plane to hvsocket via `IWSLCVirtualMachine::GetId`.
|
||||||
|
|
||||||
### The three answers that change the design
|
### The three answers that change the design
|
||||||
|
|
||||||
|
|||||||
+118
-49
@@ -1,4 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace WslcApiDump;
|
namespace WslcApiDump;
|
||||||
@@ -13,10 +14,11 @@ namespace WslcApiDump;
|
|||||||
/// cannot fail to build no matter how wrong the assumptions turn out to be.
|
/// 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`
|
/// 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`
|
/// additionally calls the two safe statics (`GetVersion`, `GetMissingComponents`). `--session`
|
||||||
/// goes one step further and creates a real session, because the single most important unknown —
|
/// goes further and creates a real session, then creates a SECOND one with the same name — which
|
||||||
/// which property carries the WSL gateway address (§5) — can only be answered by looking at a
|
/// is the open question D13 turns on: whether the compat SDK's constructor can re-adopt a session
|
||||||
/// live one.
|
/// by name, or refuses with `WSLC_E_SESSION_RESERVED` and leaves reattach to the internal COM
|
||||||
|
/// interface (docs/WINDOWS_PORT.md §13.1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
@@ -90,21 +92,38 @@ internal static class Program
|
|||||||
return;
|
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())
|
foreach (var ctor in type.GetConstructors())
|
||||||
report.AppendLine($" .ctor({Parameters(ctor)})");
|
report.AppendLine($" .ctor({Parameters(ctor)})");
|
||||||
|
});
|
||||||
|
Guarded(report, () => {
|
||||||
foreach (var property in type.GetProperties(Public).OrderBy(p => p.Name, StringComparer.Ordinal))
|
foreach (var property in type.GetProperties(Public).OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||||
report.AppendLine(
|
report.AppendLine(
|
||||||
$" {Short(property.PropertyType)} {property.Name} "
|
$" {Short(property.PropertyType)} {property.Name} "
|
||||||
+ $"{{ {(property.CanRead ? "get; " : "")}{(property.CanWrite ? "set; " : "")}}}");
|
+ $"{{ {(property.CanRead ? "get; " : "")}{(property.CanWrite ? "set; " : "")}}}");
|
||||||
|
});
|
||||||
|
Guarded(report, () => {
|
||||||
foreach (var evt in type.GetEvents(Public).OrderBy(e => e.Name, StringComparer.Ordinal))
|
foreach (var evt in type.GetEvents(Public).OrderBy(e => e.Name, StringComparer.Ordinal))
|
||||||
report.AppendLine($" event {Short(evt.EventHandlerType)} {evt.Name}");
|
report.AppendLine($" event {Short(evt.EventHandlerType)} {evt.Name}");
|
||||||
|
});
|
||||||
|
Guarded(report, () => {
|
||||||
foreach (var method in type.GetMethods(Public)
|
foreach (var method in type.GetMethods(Public)
|
||||||
.Where(m => !m.IsSpecialName)
|
.Where(m => !m.IsSpecialName)
|
||||||
.OrderBy(m => m.Name, StringComparer.Ordinal))
|
.OrderBy(m => m.Name, StringComparer.Ordinal))
|
||||||
report.AppendLine($" {Short(method.ReturnType)} {method.Name}({Parameters(method)})");
|
report.AppendLine($" {Short(method.ReturnType)} {method.Name}({Parameters(method)})");
|
||||||
|
});
|
||||||
report.AppendLine();
|
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 =
|
private const BindingFlags Public =
|
||||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
@@ -115,7 +134,8 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
if (t is null) return "void";
|
if (t is null) return "void";
|
||||||
if (!t.IsGenericType) return t.Name;
|
if (!t.IsGenericType) return t.Name;
|
||||||
var name = t.Name[..t.Name.IndexOf('`')];
|
var tick = t.Name.IndexOf('`');
|
||||||
|
var name = tick < 0 ? t.Name : t.Name[..tick];
|
||||||
return $"{name}<{string.Join(", ", t.GetGenericArguments().Select(Short))}>";
|
return $"{name}<{string.Join(", ", t.GetGenericArguments().Select(Short))}>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +224,11 @@ internal static class Program
|
|||||||
|
|
||||||
// MARK: - Live probes
|
// MARK: - Live probes
|
||||||
|
|
||||||
/// <summary>The two statics that are safe to call on any machine: they only read state.</summary>
|
/// 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 void ProbeStatics(Type[] types)
|
private static void ProbeStatics(Type[] types)
|
||||||
{
|
{
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
@@ -212,11 +236,16 @@ internal static class Program
|
|||||||
var service = types.FirstOrDefault(t => t.Name == "WslcService");
|
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; }
|
||||||
|
|
||||||
foreach (var name in new[] { "GetServiceVersion", "GetMissingComponents" })
|
foreach (var name in new[] { "GetVersion", "GetMissingComponents" })
|
||||||
{
|
{
|
||||||
var method = service.GetMethods(Public)
|
var method = service.GetMethods(Public)
|
||||||
.FirstOrDefault(m => m.Name == name && m.GetParameters().Length == 0);
|
.FirstOrDefault(m => m.IsStatic && m.Name == name && m.GetParameters().Length == 0);
|
||||||
if (method is null) { Console.WriteLine($" {name}: absent"); continue; }
|
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
|
try
|
||||||
{
|
{
|
||||||
Console.WriteLine($" {name}() = {Render(method.Invoke(null, null))}");
|
Console.WriteLine($" {name}() = {Render(method.Invoke(null, null))}");
|
||||||
@@ -225,24 +254,23 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
// The interesting failure: WSL not installed. That IS the onboarding condition
|
// 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.
|
// §8 step 2 exists to handle, so report it plainly rather than as a crash.
|
||||||
Console.WriteLine($" {name}() threw {e.InnerException?.GetType().Name}: "
|
Console.WriteLine($" {name}() threw {Describe(e.InnerException)}");
|
||||||
+ $"{e.InnerException?.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Console.WriteLine(" (a non-empty GetMissingComponents is onboarding work, not a failure — §8)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// Create a real session, then create a SECOND one with the same name.
|
||||||
/// 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
|
/// The second one is the point. D13 puts session reattach on the internal COM interface
|
||||||
/// facade guesses it is <c>Session.HostGatewayAddress</c>, and no amount of type-metadata
|
/// (`IWSLCSessionManager::OpenSessionByName`) precisely because the compat SDK exposes only a
|
||||||
/// reading tells us whether that property holds what we need. Dumping the live VALUES of a
|
/// constructor — but nobody has established what that constructor DOES when the name is
|
||||||
/// session lets us recognise a gateway IP when we see one, whatever it happens to be called.
|
/// 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 — `wslc session ls` should show it, and whether it
|
/// Leaves the session running on purpose: whether it outlives this process is the other half
|
||||||
/// survives this process exiting is itself one of the §2.3 questions (service-backed state is
|
/// of the same question. Tear it down with `wslc` when you are done.
|
||||||
/// what makes broker-crash reattach possible). Tear it down with `wslc` when done.
|
|
||||||
/// </summary>
|
|
||||||
private static void ProbeSession(Type[] types, string name)
|
private static void ProbeSession(Type[] types, string name)
|
||||||
{
|
{
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
@@ -259,7 +287,41 @@ internal static class Program
|
|||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
"Nucleic", "spike", "wslc");
|
"Nucleic", "spike", "wslc");
|
||||||
Directory.CreateDirectory(dataDir);
|
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");
|
||||||
|
Console.WriteLine(second is not null
|
||||||
|
? " → CONSTRUCTED. The compat SDK may attach by name; check `wslc container ps` for "
|
||||||
|
+ "the first session's containers, and record it against D13 (§13.1)."
|
||||||
|
: " → REFUSED (see the HRESULT above). D13's premise holds: the compat surface cannot "
|
||||||
|
+ "re-adopt a session, so reattach needs IWSLCSessionManager::OpenSessionByName.");
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" session left running — `wslc session ls`; whether it outlives this "
|
||||||
|
+ "process is the other half of the §2.3 reattach question.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? CreateSession(
|
||||||
|
Type settingsType, Type sessionType, string name, string dataDir, string which)
|
||||||
|
{
|
||||||
object? settings;
|
object? settings;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -267,46 +329,53 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" SessionSettings(name, dataDir) rejected: {e.InnerException?.Message ?? e.Message}");
|
Console.WriteLine($" SessionSettings(name, storagePath) rejected: {Describe(Unwrap(e))}");
|
||||||
Console.WriteLine(" → constructor shape differs; see the .ctor lines in the dump file");
|
Console.WriteLine(" → constructor shape differs; see the .ctor lines in the dump file");
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
live = factory.Invoke(null, [settings]);
|
return Activator.CreateInstance(sessionType, settings);
|
||||||
}
|
}
|
||||||
catch (TargetInvocationException e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" session create threw {e.InnerException?.GetType().Name}: "
|
Console.WriteLine($" {which} Session(settings) threw {Describe(Unwrap(e))}");
|
||||||
+ $"{e.InnerException?.Message}");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (live is null) { Console.WriteLine(" factory returned null"); return; }
|
Console.WriteLine($"{heading}:");
|
||||||
|
foreach (var property in properties)
|
||||||
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;
|
string rendered;
|
||||||
try { rendered = Render(property.GetValue(live)); }
|
try { rendered = Render(property.GetValue(instance)); }
|
||||||
catch (Exception e) { rendered = $"<threw {e.InnerException?.GetType().Name ?? e.GetType().Name}>"; }
|
catch (Exception e) { rendered = $"<threw {Describe(Unwrap(e))}>"; }
|
||||||
Console.WriteLine($" {property.Name} = {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 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;
|
||||||
|
return $"{e.GetType().Name} (0x{code:X8}): {e.Message}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Render(object? value) => value switch
|
private static string Render(object? value) => value switch
|
||||||
|
|||||||
@@ -11,7 +11,14 @@
|
|||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<AssemblyName>wslc-api-dump</AssemblyName>
|
<AssemblyName>wslc-api-dump</AssemblyName>
|
||||||
<RootNamespace>WslcApiDump</RootNamespace>
|
<RootNamespace>WslcApiDump</RootNamespace>
|
||||||
|
<!-- Windows SDK 19041 is not optional: it is the TFM the package ships for, and it is what
|
||||||
|
makes the SDK reference Microsoft.Windows.SDK.NET.Ref, which supplies the WinRT.Runtime
|
||||||
|
this projection needs. The package declares no dependencies, so nothing else pulls it. -->
|
||||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||||
|
<!-- The dev box has the .NET 10 SDK and may have no .NET 9 runtime; without this a built
|
||||||
|
net9.0 app fails to launch with "framework not found". A spike must not make anyone
|
||||||
|
install a runtime to answer one question. -->
|
||||||
|
<RollForward>LatestMajor</RollForward>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user