#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> MissingComponentsAsync(CancellationToken ct) { var flags = WslcService.GetMissingComponents(); var missing = new List(); foreach (var flag in Enum.GetValues()) if (flag != ComponentFlags.None && flags.HasFlag(flag)) missing.Add(flag.ToString().ToLowerInvariant()); return Task.FromResult>(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 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> ListImagesAsync(CancellationToken ct) => Task.FromResult>( 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 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(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> ListContainersAsync(CancellationToken ct) => Task.FromResult>( RequireSession().GetContainers() .Select(c => new ContainerInfo(c.Name, c.Image, c.State.ToString().ToLowerInvariant())) .ToList()); public Task 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 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(stats is null ? null : new ContainerStatsInfo(stats.CpuUsageUsec, stats.MemoryUsedBytes, stats.MemoryLimitBytes, stats.OomKills)); } public Task 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(new WslcProcess(process)); } private sealed class WslcProcess(Process process) : IWslcProcess { public Task WriteStdinAsync(ReadOnlyMemory 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