Merge nucleic/olive-jade-civet-rznt into dev
This commit is contained in:
+130
-61
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace WslcApiDump;
|
||||
@@ -13,10 +14,11 @@ namespace WslcApiDump;
|
||||
/// 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.
|
||||
/// additionally calls the two safe statics (`GetVersion`, `GetMissingComponents`). `--session`
|
||||
/// goes further and creates a real session, then creates a SECOND one with the same name — which
|
||||
/// is the open question D13 turns on: whether the compat SDK's constructor can re-adopt a session
|
||||
/// by name, or refuses with `WSLC_E_SESSION_RESERVED` and leaves reattach to the internal COM
|
||||
/// interface (docs/WINDOWS_PORT.md §13.1).
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
@@ -90,21 +92,38 @@ internal static class Program
|
||||
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)})");
|
||||
// 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;
|
||||
|
||||
@@ -115,7 +134,8 @@ internal static class Program
|
||||
{
|
||||
if (t is null) return "void";
|
||||
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))}>";
|
||||
}
|
||||
|
||||
@@ -204,7 +224,11 @@ internal static class Program
|
||||
|
||||
// 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)
|
||||
{
|
||||
Console.WriteLine();
|
||||
@@ -212,11 +236,16 @@ internal static class Program
|
||||
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" })
|
||||
foreach (var name in new[] { "GetVersion", "GetMissingComponents" })
|
||||
{
|
||||
var method = service.GetMethods(Public)
|
||||
.FirstOrDefault(m => m.Name == name && m.GetParameters().Length == 0);
|
||||
if (method is null) { Console.WriteLine($" {name}: absent"); continue; }
|
||||
.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
|
||||
{
|
||||
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
|
||||
// §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}");
|
||||
Console.WriteLine($" {name}() threw {Describe(e.InnerException)}");
|
||||
}
|
||||
}
|
||||
Console.WriteLine(" (a non-empty GetMissingComponents is onboarding work, not a failure — §8)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a real session and print every readable property of it.
|
||||
/// Create a real session, then create a SECOND one with the same name.
|
||||
///
|
||||
/// 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.
|
||||
/// 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 — `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>
|
||||
/// 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)
|
||||
{
|
||||
Console.WriteLine();
|
||||
@@ -259,7 +287,41 @@ internal static class Program
|
||||
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");
|
||||
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;
|
||||
try
|
||||
{
|
||||
@@ -267,46 +329,53 @@ internal static class Program
|
||||
}
|
||||
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");
|
||||
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
|
||||
{
|
||||
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}: "
|
||||
+ $"{e.InnerException?.Message}");
|
||||
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;
|
||||
}
|
||||
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))
|
||||
Console.WriteLine($"{heading}:");
|
||||
foreach (var property in properties)
|
||||
{
|
||||
string rendered;
|
||||
try { rendered = Render(property.GetValue(live)); }
|
||||
catch (Exception e) { rendered = $"<threw {e.InnerException?.GetType().Name ?? e.GetType().Name}>"; }
|
||||
try { rendered = Render(property.GetValue(instance)); }
|
||||
catch (Exception e) { rendered = $"<threw {Describe(Unwrap(e))}>"; }
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user