266 lines
11 KiB
C#
266 lines
11 KiB
C#
#if USE_WSLC
|
|
using Microsoft.WSL.Containers;
|
|
|
|
namespace NucleicBroker.Wslc;
|
|
|
|
// The REAL Microsoft.WSL.Containers adapter (docs/WINDOWS_PORT.md §1.5, §3.2), compiled
|
|
// only with -p:UseWslc=true on Windows.
|
|
//
|
|
// !! KNOWN WRONG AS WRITTEN — DO NOT BUILD ON IT. !!
|
|
//
|
|
// M1 spike (a) has run (docs/WINDOWS_PORT.md §13.1) and the real API differs from this
|
|
// transcription in roughly twenty places. Most are renames that belong exactly here and
|
|
// nowhere else, which is what the IWslc seam is for: GetVersion not GetServiceVersion,
|
|
// `new Session(settings)` + Start() not CreateOrOpen, MemorySizeInMB, HostName, ImageName,
|
|
// CreateProcess-then-Start rather than RunProcess, DeleteContainerOption, a named Signal
|
|
// enum, RegistryAuth as a string, ImageInfo.Name/.Sha256, and two separate output events
|
|
// instead of one with a stderr flag.
|
|
//
|
|
// Four things this SDK cannot do at all — enumerate containers, report per-container stats,
|
|
// allocate/resize a pty, and attach to an existing container or session. That is not the
|
|
// projection hiding them: `wslcsdk.dll` wraps `WSLCCompat.idl`, the deliberately-stable
|
|
// SDK-facing COM surface, and that surface genuinely lacks them.
|
|
//
|
|
// They DO exist on `wslc.idl`, the service-internal COM interface `wslc.exe` itself calls
|
|
// (IWSLCSessionManager, IID 82A7ABC8-6B50-43FC-AB96-15FBBE7E8760) — ListContainers, Stats,
|
|
// ResizeTty, OpenContainer/Attach, OpenSessionByName, and IWSLCVirtualMachine::GetId, which
|
|
// is the VM GUID an AF_HYPERV bind needs. Both IDLs are in the open-source WSL repo. The
|
|
// internal one carries an explicit "ABI breaking changes are OK" warning.
|
|
//
|
|
// D13 (§13.1): this class binds BOTH surfaces — compat SDK for everything it covers, internal
|
|
// COM for those five. Shelling out to `wslc.exe` was considered and rejected (a spawn per call,
|
|
// scraped text, no events, a second mechanism to maintain). Because the internal ABI is
|
|
// explicitly unstable, bind it defensively: probe at startup, report what bound in the
|
|
// `capabilities` hello, and degrade — losing reattach, stats and the Terminal panel — rather
|
|
// than failing the sandbox. All of that lives inside this class: `IWslc` does not change, so
|
|
// nothing on the Swift side knows which surface answered.
|
|
//
|
|
// Also note: `ProcessSettings` has no uid/gid, so exec wraps argv in setpriv/su — which
|
|
// §3.2 already anticipated as the fallback, so it costs nothing.
|
|
//
|
|
// Read §13.1 before touching this file. Fixing it is the next step of item 5, and it is
|
|
// now a fast loop: the package restores, so `dotnet build -p:UseWslc=true` compiles it.
|
|
//
|
|
// WslcService (components) → Session (VM host, images) → Container → Process.
|
|
public sealed class WslcFacade : IWslc
|
|
{
|
|
private IBrokerEvents? events;
|
|
private Session? session;
|
|
private readonly SemaphoreSlim sessionGate = new(1, 1);
|
|
|
|
public string? WslcVersion => WslcService.GetServiceVersion()?.ToString();
|
|
|
|
public void SetEvents(IBrokerEvents events) => this.events = events;
|
|
|
|
public Task<IReadOnlyList<string>> MissingComponentsAsync(CancellationToken ct)
|
|
{
|
|
var flags = WslcService.GetMissingComponents();
|
|
var missing = new List<string>();
|
|
foreach (var flag in Enum.GetValues<ComponentFlags>())
|
|
if (flag != ComponentFlags.None && flags.HasFlag(flag))
|
|
missing.Add(flag.ToString().ToLowerInvariant());
|
|
return Task.FromResult<IReadOnlyList<string>>(missing);
|
|
}
|
|
|
|
public async Task InstallComponentsAsync(CancellationToken ct)
|
|
{
|
|
// M1: wire the component-install progress callback into events.InstallProgress.
|
|
await WslcService.InstallComponentsAsync(WslcService.GetMissingComponents())
|
|
.AsTask(ct).ConfigureAwait(false);
|
|
events?.InstallProgress("done", 100);
|
|
}
|
|
|
|
public async Task<string> EnsureSessionAsync(SessionSpec spec, CancellationToken ct)
|
|
{
|
|
await sessionGate.WaitAsync(ct).ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (session is null)
|
|
{
|
|
var settings = new SessionSettings(spec.Name, spec.DataDir);
|
|
if (spec.Cpu is { } cpu) settings.CpuCount = cpu;
|
|
if (spec.MemoryMB is { } mem) settings.MemoryMB = mem;
|
|
session = Session.CreateOrOpen(settings);
|
|
session.SessionTerminationHandler = reason =>
|
|
events?.SessionDown(reason?.ToString() ?? "unknown");
|
|
}
|
|
// M1: confirm how the WSL vEthernet gateway address is surfaced (session
|
|
// property vs. querying the vNIC); NAT mode is preferred for determinism
|
|
// (docs/WINDOWS_PORT.md §5). Mirrored mode reports the reachable host address.
|
|
return session.HostGatewayAddress?.ToString()
|
|
?? throw new WslcError(WslcError.StartFailed, "wslc session has no gateway address");
|
|
}
|
|
finally
|
|
{
|
|
sessionGate.Release();
|
|
}
|
|
}
|
|
|
|
public Task TerminateSessionAsync(CancellationToken ct)
|
|
{
|
|
session?.Terminate();
|
|
session = null;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Session RequireSession() =>
|
|
session ?? throw new WslcError(WslcError.NotRunning, "no wslc session (call session.ensure first)");
|
|
|
|
public async Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct)
|
|
{
|
|
var options = new PullImageOptions(reference);
|
|
if (auth is { Username: { } user, Password: { } pass })
|
|
options.Credentials = new RegistryCredentials(user, pass);
|
|
options.Progress += (status, current, total) =>
|
|
events?.PullProgress(reference, status, current, total);
|
|
try
|
|
{
|
|
await RequireSession().PullImageAsync(options).AsTask(ct).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e) when (e is not WslcError)
|
|
{
|
|
throw new WslcError(WslcError.PullFailed, e.Message);
|
|
}
|
|
}
|
|
|
|
public Task<IReadOnlyList<ImageInfo>> ListImagesAsync(CancellationToken ct) =>
|
|
Task.FromResult<IReadOnlyList<ImageInfo>>(
|
|
RequireSession().GetImages()
|
|
.Select(i => new ImageInfo(i.Reference, i.Digest, i.Size))
|
|
.ToList());
|
|
|
|
public Task DeleteImageAsync(string reference, CancellationToken ct)
|
|
{
|
|
RequireSession().DeleteImage(reference);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<ImageInfo?> InspectImageAsync(string reference, CancellationToken ct)
|
|
{
|
|
var image = RequireSession().GetImages().FirstOrDefault(i => i.Reference == reference);
|
|
return Task.FromResult(image is null ? null : new ImageInfo(image.Reference, image.Digest, image.Size));
|
|
}
|
|
|
|
public Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct)
|
|
{
|
|
var settings = new ContainerSettings(spec.Image) { Name = spec.Name };
|
|
if (spec.Hostname is { } hostname) settings.Hostname = hostname;
|
|
if (spec.NetworkingMode is { } mode)
|
|
settings.NetworkingMode = Enum.Parse<ContainerNetworkingMode>(mode, ignoreCase: true);
|
|
foreach (var volume in spec.Volumes ?? [])
|
|
settings.Volumes.Add(new ContainerVolume(volume.Host, volume.Guest, volume.ReadOnly));
|
|
if (spec.InitArgv is { Count: > 0 } argv)
|
|
settings.InitProcess = new ProcessSettings { CmdLine = argv.ToList() };
|
|
if (spec.Env is { } env)
|
|
foreach (var (key, value) in env)
|
|
settings.InitProcess?.Environment.Add(key, value);
|
|
try
|
|
{
|
|
RequireSession().CreateContainer(settings);
|
|
}
|
|
catch (Exception e) when (e is not WslcError)
|
|
{
|
|
throw new WslcError(WslcError.StartFailed, e.Message);
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Container RequireContainer(string name) =>
|
|
RequireSession().GetContainers().FirstOrDefault(c => c.Name == name)
|
|
?? throw new WslcError(WslcError.NotFound, $"no container named {name}");
|
|
|
|
public Task StartContainerAsync(string name, CancellationToken ct)
|
|
{
|
|
RequireContainer(name).Start();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct)
|
|
{
|
|
RequireContainer(name).Stop((Signal)signal, TimeSpan.FromMilliseconds(graceMs));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task DeleteContainerAsync(string name, bool force, CancellationToken ct)
|
|
{
|
|
RequireContainer(name).Delete(force ? DeleteContainerFlags.Force : DeleteContainerFlags.None);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<IReadOnlyList<ContainerInfo>> ListContainersAsync(CancellationToken ct) =>
|
|
Task.FromResult<IReadOnlyList<ContainerInfo>>(
|
|
RequireSession().GetContainers()
|
|
.Select(c => new ContainerInfo(c.Name, c.Image, c.State.ToString().ToLowerInvariant()))
|
|
.ToList());
|
|
|
|
public Task<string> ContainerStateAsync(string name, CancellationToken ct)
|
|
{
|
|
var container = RequireSession().GetContainers().FirstOrDefault(c => c.Name == name);
|
|
return Task.FromResult(container?.State switch
|
|
{
|
|
null => "absent",
|
|
ContainerState.Running => "running",
|
|
_ => "stopped",
|
|
});
|
|
}
|
|
|
|
public Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct)
|
|
{
|
|
// M1: confirm the stats surface (cgroup v2 counters are what the Swift resource
|
|
// monitor folds into ContainerResourceSample).
|
|
var stats = RequireContainer(name).GetStatistics();
|
|
return Task.FromResult<ContainerStatsInfo?>(stats is null
|
|
? null
|
|
: new ContainerStatsInfo(stats.CpuUsageUsec, stats.MemoryUsedBytes, stats.MemoryLimitBytes, stats.OomKills));
|
|
}
|
|
|
|
public Task<IWslcProcess> ExecAsync(long procId, ProcSpec spec, CancellationToken ct)
|
|
{
|
|
var container = RequireContainer(spec.Container);
|
|
var settings = new ProcessSettings
|
|
{
|
|
CmdLine = spec.Argv.ToList(),
|
|
WorkingDirectory = spec.Cwd,
|
|
OutputMode = ProcessOutputMode.Event,
|
|
Terminal = spec.Tty,
|
|
};
|
|
if (spec.Env is { } env)
|
|
foreach (var (key, value) in env) settings.Environment.Add(key, value);
|
|
if (spec.Uid is { } uid) settings.UserId = uid; // M1: verify uid semantics (§3.2);
|
|
if (spec.Gid is { } gid) settings.GroupId = gid; // fall back to a setpriv wrapper argv.
|
|
|
|
var process = container.RunProcess(settings);
|
|
process.OutputReceived += (stderr, data) => events?.ProcOutput(procId, stderr, data);
|
|
process.Exited += code => events?.ProcExited(procId, code);
|
|
return Task.FromResult<IWslcProcess>(new WslcProcess(process));
|
|
}
|
|
|
|
private sealed class WslcProcess(Process process) : IWslcProcess
|
|
{
|
|
public Task WriteStdinAsync(ReadOnlyMemory<byte> data, CancellationToken ct)
|
|
{
|
|
process.WriteStdin(data.Span);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task CloseStdinAsync(CancellationToken ct)
|
|
{
|
|
process.CloseStdin();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SignalAsync(int signal, CancellationToken ct)
|
|
{
|
|
process.Signal((Signal)signal);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task ResizeAsync(int cols, int rows, CancellationToken ct)
|
|
{
|
|
process.ResizeTerminal(cols, rows);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
}
|
|
#endif
|