233 lines
9.3 KiB
C#
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
|