Files
nucleic-windows/NucleicBroker/Wslc/WslcFacade.cs
T
2026-07-27 22:02:34 -07:00

233 lines
9.3 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. The API is public preview (GA fall 2026) and its
// exact shapes are validated by the M1 wslc spike — every mapping below that spikes prove
// wrong gets fixed HERE, never above the IWslc seam. Until M1 runs on real hardware, treat
// this file as the best-effort transcription of the documented object model:
// 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