Merge nucleic/lucid-river-toad-6efj into dev
This commit is contained in:
@@ -112,6 +112,23 @@ public sealed class BrokerServiceTests
|
||||
Assert.Equal(new SessionSpec("nucleic-dev", @"C:\data", 4, 8192), wslc.LastSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// D13 Tier 1 (docs/WINDOWS_PORT.md §13.2): the facade auto-recovers a session orphaned by a
|
||||
/// dead broker, but when the internal COM arm did not bind there is nothing it can do — and
|
||||
/// hostd must be able to tell *that* apart from an ordinary start failure, because the remedy
|
||||
/// is `wsl --shutdown` rather than a retry. This pins the kind the Swift side branches on.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SessionEnsure_UnrecoverableConflict_SurfacesSessionExists()
|
||||
{
|
||||
wslc.NextError = new WslcError(WslcError.SessionExists, "already running, cannot re-adopt");
|
||||
var lines = await RoundTrip(
|
||||
"""{"jsonrpc":"2.0","id":9,"method":"session.ensure","params":{"name":"nucleic-dev","dataDir":"C:\\data"}}""");
|
||||
var error = Assert.Single(lines).GetProperty("error");
|
||||
Assert.Equal(Rpc.FacadeError, error.GetProperty("code").GetInt32());
|
||||
Assert.Equal("session_exists", error.GetProperty("data").GetProperty("kind").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImagePull_EmitsProgressNotificationsBeforeResult()
|
||||
{
|
||||
|
||||
@@ -12,7 +12,10 @@ Console.OutputEncoding = Encoding.UTF8;
|
||||
|
||||
IWslc wslc =
|
||||
#if USE_WSLC
|
||||
new NucleicBroker.Wslc.WslcFacade();
|
||||
// Create(), not `new`: it configures COM security first, and that MUST precede the process's
|
||||
// first COM call (docs/WINDOWS_PORT.md §13.2). Constructing the facade directly compiles fine
|
||||
// and silently loses session recovery to a 0x80070542 that reads as "not found".
|
||||
NucleicBroker.Wslc.WslcFacade.Create();
|
||||
#else
|
||||
new UnavailableWslc();
|
||||
#endif
|
||||
|
||||
@@ -48,6 +48,25 @@ public sealed class WslcFacade : IWslc
|
||||
private string? gateway;
|
||||
private readonly SemaphoreSlim sessionGate = new(1, 1);
|
||||
|
||||
/// D13's Tier 1 internal-COM arm, or null where it could not bind. Only used to recover an
|
||||
/// orphaned session after a broker restart; every other call rides the compat SDK.
|
||||
private readonly WslcInternal? recovery = WslcInternal.TryBind();
|
||||
|
||||
/// <summary>
|
||||
/// Construct the facade with COM security configured first.
|
||||
///
|
||||
/// The ordering is load-bearing and easy to lose: `CoInitializeSecurity` must precede the
|
||||
/// **first COM call in the process**, and the compat SDK makes its own. Doing it in a factory
|
||||
/// keeps that constraint next to the code that depends on it rather than in `Program.cs`,
|
||||
/// where a later reorder would silently break session recovery with a `0x80070542` that reads
|
||||
/// as "not found".
|
||||
/// </summary>
|
||||
public static WslcFacade Create()
|
||||
{
|
||||
WslcInternal.InitializeSecurity();
|
||||
return new WslcFacade();
|
||||
}
|
||||
|
||||
/// name → the handle CreateContainer returned. See the class remarks: without this there is
|
||||
/// no way to address a container at all, because Sdk.Container carries no name.
|
||||
private readonly Dictionary<string, Entry> containers = [];
|
||||
@@ -76,10 +95,18 @@ public sealed class WslcFacade : IWslc
|
||||
}
|
||||
}
|
||||
|
||||
/// Compat-only, so: no enumeration, no reattach, no pty. Stats ARE offered — not from the SDK
|
||||
/// (there is no GetStatistics()) but from an in-guest cgroup read, which is the escape hatch
|
||||
/// §13.1 names and is indistinguishable to hostd.
|
||||
public IReadOnlyList<string> Capabilities => ["stats"];
|
||||
/// <summary>
|
||||
/// Stats are always offered — not from the SDK (there is no `GetStatistics()`) but from an
|
||||
/// in-guest cgroup read, which is indistinguishable to hostd. `recover` is added when D13's
|
||||
/// Tier 1 arm bound: a broker restart re-adopts and clears its orphaned session instead of
|
||||
/// leaving the user a sandbox only `wsl --shutdown` can fix.
|
||||
///
|
||||
/// Still absent, and deliberately: `enumerate` (`container.list` answers from this broker's
|
||||
/// own roster, not the service), `reattach` (containers do not survive a restart — Tier 2,
|
||||
/// blocked, §13.2) and `tty` (the compat surface has no pty).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Capabilities =>
|
||||
recovery is not null ? ["stats", "recover"] : ["stats"];
|
||||
|
||||
public void SetEvents(IBrokerEvents events) => this.events = events;
|
||||
|
||||
@@ -113,34 +140,19 @@ public sealed class WslcFacade : IWslc
|
||||
{
|
||||
if (session is not null) return gateway!;
|
||||
|
||||
var settings = new Sdk.SessionSettings(spec.Name, spec.DataDir);
|
||||
if (spec.Cpu is { } cpu) settings.CpuCount = (uint)cpu;
|
||||
if (spec.MemoryMB is { } memory) settings.MemorySizeInMB = (uint)memory;
|
||||
|
||||
var created = new Sdk.Session(settings);
|
||||
// Subscribe BEFORE Start(): a session that dies during boot must still report down.
|
||||
created.Terminated += reason => events?.SessionDown(reason.ToString());
|
||||
// Not surfaced as an RPC, but it is the only crash detail wslc offers and it is what
|
||||
// makes a SIGKILLed agent explicable in the host log (the Swift `diagnoseKill` seam).
|
||||
created.ProcessCrashed += crash => Console.Error.WriteLine(
|
||||
$"wslc: process {crash.ProcessName} (pid {crash.Pid}) crashed with signal "
|
||||
+ $"{crash.Signal}; dump at {crash.DumpPath}");
|
||||
|
||||
var created = NewSession(spec);
|
||||
try
|
||||
{
|
||||
created.Start();
|
||||
}
|
||||
catch (Exception e) when (HResultOf(e) == ErrorAlreadyExists)
|
||||
{
|
||||
created.Dispose();
|
||||
// The constructor is lazy — it only captures settings — so reaching this means a
|
||||
// session of this name is genuinely RUNNING, started by a previous broker or
|
||||
// another process. The compat surface cannot re-adopt it, and there is no handle
|
||||
// to terminate it through either, so this is terminal for this broker.
|
||||
throw new WslcError(
|
||||
WslcError.SessionExists,
|
||||
$"a wslc session named '{spec.Name}' is already running and the compat SDK "
|
||||
+ "cannot re-adopt it; run `wsl --shutdown` to clear it");
|
||||
// session of this name is genuinely RUNNING, started by a previous broker that
|
||||
// died. The compat surface cannot re-adopt it, so recovery goes through D13's
|
||||
// internal-COM arm: open it, note what it was running, terminate it, retry.
|
||||
created.Dispose();
|
||||
created = await RecoverAndRestartAsync(spec, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is not WslcError)
|
||||
{
|
||||
@@ -158,6 +170,73 @@ public sealed class WslcFacade : IWslc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A settings-configured session with its handlers already attached. Subscribing
|
||||
/// must happen BEFORE `Start()`, or a session that dies during boot never reports down.</summary>
|
||||
private Sdk.Session NewSession(SessionSpec spec)
|
||||
{
|
||||
var settings = new Sdk.SessionSettings(spec.Name, spec.DataDir);
|
||||
if (spec.Cpu is { } cpu) settings.CpuCount = (uint)cpu;
|
||||
if (spec.MemoryMB is { } memory) settings.MemorySizeInMB = (uint)memory;
|
||||
|
||||
var created = new Sdk.Session(settings);
|
||||
created.Terminated += reason => events?.SessionDown(reason.ToString());
|
||||
// Not surfaced as an RPC, but it is the only crash detail wslc offers and it is what
|
||||
// makes a SIGKILLed agent explicable in the host log (the Swift `diagnoseKill` seam).
|
||||
created.ProcessCrashed += crash => Console.Error.WriteLine(
|
||||
$"wslc: process {crash.ProcessName} (pid {crash.Pid}) crashed with signal "
|
||||
+ $"{crash.Signal}; dump at {crash.DumpPath}");
|
||||
return created;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A session of this name is already running and we do not own it — the signature of a broker
|
||||
/// that died with its sandbox up (docs/WINDOWS_PORT.md §13.2, D13 Tier 1).
|
||||
///
|
||||
/// Clear it through the internal COM arm and start fresh. The orphan's containers are lost,
|
||||
/// which is the deliberate Tier 1 trade: they are lost today too (nothing could reach that
|
||||
/// session at all), `ContainerManager.reconcile` already copes with an empty sandbox, and the
|
||||
/// alternative — keeping them alive — is Tier 2 and blocked. What this buys is that a broker
|
||||
/// restart stops requiring the user to run `wsl --shutdown` by hand.
|
||||
/// </summary>
|
||||
private async Task<Sdk.Session> RecoverAndRestartAsync(SessionSpec spec, CancellationToken ct)
|
||||
{
|
||||
if (recovery is null)
|
||||
throw new WslcError(
|
||||
WslcError.SessionExists,
|
||||
$"a wslc session named '{spec.Name}' is already running, the compat SDK cannot "
|
||||
+ "re-adopt it, and the internal COM interface did not bind; run `wsl --shutdown`");
|
||||
|
||||
if (recovery.RecoverSession(spec.Name) is null)
|
||||
throw new WslcError(
|
||||
WslcError.SessionExists,
|
||||
$"a wslc session named '{spec.Name}' is already running and could not be "
|
||||
+ "recovered; run `wsl --shutdown` to clear it");
|
||||
|
||||
// Terminate() returns before the VM is gone — the service tears it down asynchronously —
|
||||
// so the next Start() can still see the old name. Retry rather than reporting a failure
|
||||
// that a second attempt a moment later would not have hit.
|
||||
for (var attempt = 0; ; attempt++)
|
||||
{
|
||||
await Task.Delay(500, ct).ConfigureAwait(false);
|
||||
var retry = NewSession(spec);
|
||||
try
|
||||
{
|
||||
retry.Start();
|
||||
Console.Error.WriteLine($"wslc: session '{spec.Name}' restarted after recovery");
|
||||
return retry;
|
||||
}
|
||||
catch (Exception e) when (HResultOf(e) == ErrorAlreadyExists && attempt < 20)
|
||||
{
|
||||
retry.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
retry.Dispose();
|
||||
throw e is WslcError ? e : Translate(e, WslcError.StartFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task TerminateSessionAsync(CancellationToken ct)
|
||||
{
|
||||
lock (containersLock)
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
#if USE_WSLC
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace NucleicBroker.Wslc;
|
||||
|
||||
/// <summary>
|
||||
/// D13's internal-COM arm, **Tier 1: recover** (docs/WINDOWS_PORT.md §13.2).
|
||||
///
|
||||
/// The compat SDK cannot re-adopt a running session — its `Session` constructor is lazy and
|
||||
/// `Start()` refuses an existing name with `ERROR_ALREADY_EXISTS`. So a broker that restarts
|
||||
/// while its session is up cannot reach the sandbox again, and today that costs the user a manual
|
||||
/// `wsl --shutdown`. This class fixes exactly that: it opens the orphaned session through the
|
||||
/// **service-internal** COM interface, reports what was running, terminates it, and lets the
|
||||
/// facade create a fresh one through the ordinary compat path.
|
||||
///
|
||||
/// It deliberately does NOT try to keep those containers alive — that is Tier 2, and it is
|
||||
/// blocked (§13.2): `Session.FromAbi()` throws on a service-side pointer, because the WinRT layer
|
||||
/// the C# projection wraps lives client-side in `wslcsdk.dll`. Everything here is confirmed on
|
||||
/// hardware; nothing here depends on that unresolved question.
|
||||
///
|
||||
/// **Four things that are not obvious and each cost a debugging round:**
|
||||
///
|
||||
/// 1. **There is no CLSID for `IWSLCSessionManager`.** `wslc.idl` declares interfaces and no
|
||||
/// activatable class. The entry point is the *compat* coclass — `WSLCCompatSessionManager`
|
||||
/// also implements the internal interface. One object, two faces.
|
||||
/// 2. **The proxy must grant IMPERSONATE.** Per-user calls like `OpenSessionByName` fail
|
||||
/// `0x80070542` (`ERROR_BAD_IMPERSONATION_LEVEL`) under .NET's default `IDENTIFY` — a security
|
||||
/// error that reads exactly like "not found". `GetVersion`/`ListSessions` don't impersonate,
|
||||
/// so they succeed and make it look like a per-method gap. <see cref="InitializeSecurity"/>
|
||||
/// handles this process-wide; the per-proxy blanket here is belt and braces.
|
||||
/// 3. **Vtable slots are fixed by declaration ORDER, not signature.** Only the methods actually
|
||||
/// called need accurate signatures, which is what makes reaching `ListContainers` (method #19,
|
||||
/// behind four methods taking a by-value `WSLCHandle` union) tractable at all.
|
||||
/// 4. **The ABI is explicitly unstable.** `wslc.idl` says breaking changes are fine because
|
||||
/// Microsoft ships both ends. We are not both ends, so every entry point here is probed and
|
||||
/// every failure degrades to "no recovery" rather than propagating.
|
||||
/// </summary>
|
||||
internal sealed class WslcInternal : IDisposable
|
||||
{
|
||||
private IWSLCSessionManager? manager;
|
||||
private IntPtr managerPtr;
|
||||
|
||||
/// <summary>What a recovery found and did, for the host log and the `session.down` story.</summary>
|
||||
internal sealed record Recovery(IReadOnlyList<string> Containers, bool Terminated);
|
||||
|
||||
/// <summary>
|
||||
/// Bind the internal interface, or return null. Called once at facade construction so the
|
||||
/// result can be reported in the `capabilities` hello (§2.3) rather than discovered when a
|
||||
/// user's broker restarts.
|
||||
/// </summary>
|
||||
internal static WslcInternal? TryBind()
|
||||
{
|
||||
var clsid = ClsidWslcCompatSessionManager;
|
||||
var iid = IidWslcSessionManager;
|
||||
// Ask for the internal interface directly. The compat coclass implements both, and going
|
||||
// straight for it means a machine where this arm is unavailable fails here rather than
|
||||
// half-way through a recovery.
|
||||
var hr = CoCreateInstance(in clsid, IntPtr.Zero, ClsctxAll, in iid, out var ptr);
|
||||
if (hr < 0)
|
||||
{
|
||||
// REGDB_E_CLASSNOTREG here means WSL simply isn't installed — the §8 onboarding
|
||||
// state, not a defect. Saying "recovery unavailable" without that distinction reads
|
||||
// as a broker fault on a machine that has not been set up yet.
|
||||
Console.Error.WriteLine((uint)hr == RegdbEClassNotReg
|
||||
? "wslc: WSL is not installed — internal COM absent, as expected before onboarding"
|
||||
: $"wslc: internal COM did not bind (0x{hr:X8}) — a broker restart will not "
|
||||
+ "auto-recover a running session; D13 Tier 1 is unavailable on this machine");
|
||||
return null;
|
||||
}
|
||||
|
||||
RaiseImpersonation(ptr);
|
||||
try
|
||||
{
|
||||
return new WslcInternal
|
||||
{
|
||||
managerPtr = ptr,
|
||||
manager = (IWSLCSessionManager)Marshal.GetObjectForIUnknown(ptr),
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Marshal.Release(ptr);
|
||||
Console.Error.WriteLine($"wslc: internal COM bound but unusable: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the orphaned session named <paramref name="name"/>, note what was running in it, and
|
||||
/// terminate it. Returns null when there is nothing to recover — which is the ordinary case
|
||||
/// and not an error.
|
||||
///
|
||||
/// Terminating rather than adopting is the deliberate Tier 1 choice: the containers are lost,
|
||||
/// but they are lost today too, and `ContainerManager.reconcile` already copes with a sandbox
|
||||
/// that came back empty. What it buys is that the *next* `Start()` succeeds.
|
||||
/// </summary>
|
||||
internal Recovery? RecoverSession(string name)
|
||||
{
|
||||
if (manager is null) return null;
|
||||
|
||||
int hr;
|
||||
IntPtr sessionPtr;
|
||||
try
|
||||
{
|
||||
hr = manager.OpenSessionByName(name, out sessionPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"wslc: OpenSessionByName('{name}') threw: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
if (hr < 0)
|
||||
{
|
||||
Console.Error.WriteLine($"wslc: no recoverable session '{name}' (0x{hr:X8})"
|
||||
+ ((uint)hr == ErrorBadImpersonationLevel
|
||||
? " — IMPERSONATE was not granted; CoInitializeSecurity must run before the "
|
||||
+ "first COM call in the process"
|
||||
: ""));
|
||||
return null;
|
||||
}
|
||||
|
||||
// The session proxy is a separate object from the manager, so it needs its own blanket.
|
||||
RaiseImpersonation(sessionPtr);
|
||||
try
|
||||
{
|
||||
var session = (IWSLCSession)Marshal.GetObjectForIUnknown(sessionPtr);
|
||||
var containers = ListContainers(session);
|
||||
var terminated = Terminate(session);
|
||||
Console.Error.WriteLine(
|
||||
$"wslc: recovered orphaned session '{name}' — {containers.Count} container(s) "
|
||||
+ $"[{string.Join(", ", containers)}], terminated={terminated}");
|
||||
return new Recovery(containers, terminated);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"wslc: recovery of '{name}' failed: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.Release(sessionPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The container roster of a session, by name. Enumeration the compat SDK has no call for at
|
||||
/// all — <c>Session</c> exposes no listing and <c>Container</c> carries no <c>Name</c>.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<string> ListContainers(IWSLCSession session)
|
||||
{
|
||||
// Flags=All, or the listing is running-containers-only and a stopped container silently
|
||||
// vanishes from the recovery report.
|
||||
var options = new WslcListContainersOptions
|
||||
{
|
||||
Flags = WslcListContainersFlagsAll,
|
||||
Limit = 0,
|
||||
Filters = IntPtr.Zero,
|
||||
FiltersCount = 0,
|
||||
};
|
||||
var optionsPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf<WslcListContainersOptions>());
|
||||
var containers = IntPtr.Zero;
|
||||
var ports = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(options, optionsPtr, fDeleteOld: false);
|
||||
var hr = session.ListContainers(optionsPtr, out containers, out var count,
|
||||
out ports, out _);
|
||||
if (hr < 0)
|
||||
{
|
||||
Console.Error.WriteLine($"wslc: ListContainers failed (0x{hr:X8})");
|
||||
return [];
|
||||
}
|
||||
|
||||
var size = Marshal.SizeOf<WslcContainerEntry>();
|
||||
var names = new List<string>((int)count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var entry = Marshal.PtrToStructure<WslcContainerEntry>(containers + i * size);
|
||||
names.Add(string.IsNullOrEmpty(entry.Name) ? entry.Id : entry.Name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(optionsPtr);
|
||||
// Both out-arrays are callee-allocated; nobody else frees them.
|
||||
if (containers != IntPtr.Zero) Marshal.FreeCoTaskMem(containers);
|
||||
if (ports != IntPtr.Zero) Marshal.FreeCoTaskMem(ports);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Terminate(IWSLCSession session)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hr = session.Terminate();
|
||||
if (hr >= 0) return true;
|
||||
Console.Error.WriteLine($"wslc: session Terminate failed (0x{hr:X8})");
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"wslc: session Terminate threw: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
manager = null;
|
||||
if (managerPtr != IntPtr.Zero)
|
||||
{
|
||||
Marshal.Release(managerPtr);
|
||||
managerPtr = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - COM security
|
||||
|
||||
/// <summary>
|
||||
/// Grant servers the right to impersonate this process, for **every** proxy it will hold.
|
||||
///
|
||||
/// Must run before the first COM call in the process or it fails `RPC_E_TOO_LATE` — and the
|
||||
/// compat SDK makes COM calls of its own, so this has to precede any `WslcService`/`Session`
|
||||
/// use, not merely precede the internal arm. Failing is not fatal: only the per-user internal
|
||||
/// calls need it, so the sandbox still runs and recovery is what degrades.
|
||||
/// </summary>
|
||||
internal static void InitializeSecurity()
|
||||
{
|
||||
var hr = CoInitializeSecurity(
|
||||
IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero,
|
||||
RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, IntPtr.Zero, EoacNone, IntPtr.Zero);
|
||||
// RPC_E_TOO_LATE means something already initialised security — worth saying, because it
|
||||
// silently removes session recovery and nothing else will mention it.
|
||||
if (hr < 0)
|
||||
Console.Error.WriteLine(
|
||||
$"wslc: CoInitializeSecurity failed (0x{hr:X8})"
|
||||
+ ((uint)hr == RpcETooLate
|
||||
? " — RPC_E_TOO_LATE: a COM call ran first. Session recovery will fail "
|
||||
+ "0x80070542."
|
||||
: ""));
|
||||
}
|
||||
|
||||
private static void RaiseImpersonation(IntPtr proxy) =>
|
||||
// Per-proxy, and harmless if CoInitializeSecurity already covered it. Kept because the
|
||||
// process-wide call is order-dependent and this one is not.
|
||||
CoSetProxyBlanket(
|
||||
proxy, RpcCAuthnDefault, RpcCAuthzDefault, ColeDefaultPrincipal,
|
||||
RpcCAuthnLevelDefault, RpcCImpLevelImpersonate, ColeDefaultAuthinfo, EoacNone);
|
||||
|
||||
// MARK: - Interop
|
||||
|
||||
/// `WSLCCompatSessionManager` from WSLCCompat.idl. Not a typo that this is the *compat*
|
||||
/// class: `wslc.idl` declares no coclass, and this one answers a QI for the internal
|
||||
/// interface (confirmed on hardware, §13.2).
|
||||
private static readonly Guid ClsidWslcCompatSessionManager =
|
||||
new("a9b7a1b9-0671-405c-95f1-e0612cb4ce8f");
|
||||
|
||||
private static readonly Guid IidWslcSessionManager = new("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760");
|
||||
|
||||
private const uint ClsctxAll = 0x17;
|
||||
private const uint RpcCAuthnDefault = 0xFFFFFFFF;
|
||||
private const uint RpcCAuthzDefault = 0xFFFFFFFF;
|
||||
private const uint RpcCAuthnLevelDefault = 0;
|
||||
private const uint RpcCImpLevelImpersonate = 3;
|
||||
private const uint EoacNone = 0;
|
||||
private const uint RpcETooLate = 0x80010119;
|
||||
private const uint RegdbEClassNotReg = 0x80040154;
|
||||
private const uint ErrorBadImpersonationLevel = 0x80070542;
|
||||
private const uint WslcListContainersFlagsAll = 1;
|
||||
private static readonly IntPtr ColeDefaultPrincipal = new(-1);
|
||||
private static readonly IntPtr ColeDefaultAuthinfo = new(-1);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int CoCreateInstance(
|
||||
in Guid clsid, IntPtr outer, uint clsContext, in Guid iid, out IntPtr instance);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int CoSetProxyBlanket(
|
||||
IntPtr proxy, uint authnService, uint authzService, IntPtr serverPrincipalName,
|
||||
uint authnLevel, uint impersonationLevel, IntPtr authInfo, uint capabilities);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int CoInitializeSecurity(
|
||||
IntPtr securityDescriptor, int authSvcCount, IntPtr authSvc, IntPtr reserved1,
|
||||
uint authnLevel, uint impersonationLevel, IntPtr authList, uint capabilities,
|
||||
IntPtr reserved3);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WslcListContainersOptions
|
||||
{
|
||||
public uint Flags;
|
||||
public int Limit;
|
||||
public IntPtr Filters;
|
||||
public uint FiltersCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// `WSLCContainerEntry` from wslc.idl. The three char arrays are **inline fixed buffers**,
|
||||
/// not pointers — `ByValTStr`/`Ansi`, with the sizes straight from the IDL's `+ 1` constants
|
||||
/// (255+1, 255+1, 64+1). Getting a size wrong here does not fail loudly; it silently shifts
|
||||
/// every later field. The equivalent layout was validated on hardware via
|
||||
/// `ListSessions`, whose entry struct has the same shape (§13.2).
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
private struct WslcContainerEntry
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Name;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Image;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] public string Id;
|
||||
public ulong StateChangedAt;
|
||||
public ulong CreatedAt;
|
||||
public uint State;
|
||||
}
|
||||
|
||||
[ComImport, Guid("82A7ABC8-6B50-43FC-AB96-15FBBE7E8760"),
|
||||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IWSLCSessionManager
|
||||
{
|
||||
[PreserveSig] int GetVersion(out WslcVersion version);
|
||||
[PreserveSig] int CreateSession(IntPtr settings, uint flags, IntPtr warningCallback, out IntPtr session);
|
||||
[PreserveSig] int EnterSession(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string displayName,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string storagePath,
|
||||
IntPtr warningCallback, out IntPtr session);
|
||||
[PreserveSig] int ListSessions(out IntPtr sessions, out uint count);
|
||||
[PreserveSig] int OpenSession(uint id, out IntPtr session);
|
||||
[PreserveSig] int OpenSessionByName(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string displayName, out IntPtr session);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct WslcVersion
|
||||
{
|
||||
public uint Major;
|
||||
public uint Minor;
|
||||
public uint Revision;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// `IWSLCSession`, declared only as far as <c>Terminate</c> (method #23).
|
||||
///
|
||||
/// **Every method ahead of the ones we call must still be declared**, because a COM vtable is
|
||||
/// addressed by slot — but only the called ones need accurate signatures, since a method that
|
||||
/// is never invoked is never marshalled. That is what makes this tractable: four of the
|
||||
/// placeholders (`LoadImage`, `ImportImage`, `SaveImage`, `SaveImages`) take `WSLCHandle` — a
|
||||
/// tagged union — **by value**, which would be genuinely awkward to marshal and does not have
|
||||
/// to be. Parameter *counts* are kept faithful to the IDL purely as documentation.
|
||||
///
|
||||
/// Do not reorder. Do not delete an unused entry. Either silently shifts every slot below it.
|
||||
/// </summary>
|
||||
[ComImport, Guid("EF0661E4-6364-40EA-B433-E2FDF11F3519"),
|
||||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IWSLCSession
|
||||
{
|
||||
[PreserveSig] int GetId(out uint id); // 1
|
||||
[PreserveSig] int GetDisplayName(out IntPtr displayName); // 2
|
||||
[PreserveSig] int GetState(out uint state); // 3
|
||||
[PreserveSig] int GetTerminationEvent(out IntPtr eventHandle); // 4
|
||||
[PreserveSig] int GetTerminationReason(out uint reason, out IntPtr details); // 5
|
||||
[PreserveSig] int PullImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 6
|
||||
[PreserveSig] int BuildImage(IntPtr a, IntPtr b, IntPtr c); // 7
|
||||
[PreserveSig] int LoadImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 8 (WSLCHandle by value)
|
||||
[PreserveSig] int ImportImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 9 (WSLCHandle by value)
|
||||
[PreserveSig] int SaveImage(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 10 (WSLCHandle by value)
|
||||
[PreserveSig] int SaveImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d); // 11 (WSLCHandle by value)
|
||||
[PreserveSig] int ListImages(IntPtr a, IntPtr b, IntPtr c); // 12
|
||||
[PreserveSig] int DeleteImage(IntPtr a, IntPtr b, IntPtr c); // 13
|
||||
[PreserveSig] int TagImage(IntPtr a); // 14
|
||||
[PreserveSig] int InspectImage(IntPtr a, IntPtr b); // 15
|
||||
[PreserveSig] int PruneImages(IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e); // 16
|
||||
[PreserveSig] int CreateContainer(IntPtr a, IntPtr b, IntPtr c); // 17
|
||||
[PreserveSig] int OpenContainer(IntPtr a, IntPtr b); // 18
|
||||
[PreserveSig] int ListContainers( // 19
|
||||
IntPtr options, out IntPtr containers, out uint count,
|
||||
out IntPtr ports, out uint portsCount);
|
||||
[PreserveSig] int PruneContainers(IntPtr a, IntPtr b, IntPtr c); // 20
|
||||
[PreserveSig] int CreateRootNamespaceProcess(
|
||||
IntPtr a, IntPtr b, IntPtr c, IntPtr d, IntPtr e, IntPtr f); // 21
|
||||
[PreserveSig] int FormatVirtualDisk(IntPtr a); // 22
|
||||
[PreserveSig] int Terminate(); // 23
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+5
-5
@@ -180,11 +180,11 @@ for, and nothing above it should move. These three are different:
|
||||
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.
|
||||
- **The internal arm, Tier 1 — "recover"** (`WslcInternal.cs` in the broker, not a spike).
|
||||
Everything it needs is confirmed on hardware: entry point, vtable, impersonation, and
|
||||
`OpenSessionByName`. On broker restart, open the orphaned session, `ListContainers` for
|
||||
reporting, `Terminate` it, and create a fresh one through the compat SDK — automatic clean
|
||||
recovery instead of a manual `wsl --shutdown`. Enough for M2.
|
||||
- ~~**The internal arm, Tier 1 — "recover"**~~ **written**: `windows/NucleicBroker/Wslc/WslcInternal.cs`.
|
||||
On broker restart it opens the orphaned session, lists its containers for the log, terminates
|
||||
it, and lets the facade start fresh — automatic clean recovery instead of a manual
|
||||
`wsl --shutdown`. Still needs one live test: kill a broker mid-session and confirm the next one
|
||||
recovers (see below).
|
||||
- **Tier 2 — "adopt"** (keep containers running across a broker restart) is **blocked**. The
|
||||
`Session.FromAbi()` handoff throws `InvalidCastException` even though the QI to
|
||||
`IWSLCCompatSession` succeeds: the WinRT layer appears to be a client-side wrapper in
|
||||
|
||||
Reference in New Issue
Block a user