using System.Text.Json.Serialization; namespace NucleicBroker; /// /// The one seam between broker logic and Microsoft.WSL.Containers (docs/WINDOWS_PORT.md /// §3.3): everything the RPC surface needs from wslc, and nothing WinRT. The real facade /// (Wslc/WslcFacade.cs, compiled with -p:UseWslc=true) adapts the preview API; unit tests /// substitute FakeWslc; non-Windows builds get UnavailableWslc. Async events (process stdio, /// pull progress, session death) flow out through so no wslc /// event thread ever blocks on the stdio pipe. /// public interface IWslc { /// Reported in the `hello` capabilities exchange; null when wslc is absent. string? WslcVersion { get; } /// /// What this facade can actually do, merged into the `hello` capabilities so hostd degrades /// instead of discovering the gap at the call site (docs/WINDOWS_PORT.md §2.3, D13). The /// compat SDK alone cannot serve four of them, so a compat-only broker reports none of: /// /// enumerate — service-backed container enumeration. Without it /// container.list answers from the broker's OWN roster, so it goes empty across a /// broker restart and ContainerManager.reconcile sees an empty sandbox. /// reattach — re-adopting a running session or container. Without it a broker /// restart cannot recover the session and session.ensure fails /// . /// tty — pty allocation and resize, i.e. §7's Terminal panel. /// stats — per-container resource sampling. Reported when stats are available /// by ANY means, including the in-guest cgroup read the compat facade falls back to. /// /// IReadOnlyList Capabilities { get; } /// Install the sink BEFORE any operation that can emit events. void SetEvents(IBrokerEvents events); /// Missing OS components (WSL, the container service, …), empty when ready. Task> MissingComponentsAsync(CancellationToken ct); /// Install missing components; progress via . Task InstallComponentsAsync(CancellationToken ct); /// Create-or-attach the per-channel wslc session; returns the WSL-facing host /// gateway address guests reach the host on (docs/WINDOWS_PORT.md §5). Task EnsureSessionAsync(SessionSpec spec, CancellationToken ct); Task TerminateSessionAsync(CancellationToken ct); /// Pull an OCI image; progress via . Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct); Task> ListImagesAsync(CancellationToken ct); Task DeleteImageAsync(string reference, CancellationToken ct); /// Digest/size for a local image, or null when not present. Task InspectImageAsync(string reference, CancellationToken ct); Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct); Task StartContainerAsync(string name, CancellationToken ct); Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct); Task DeleteContainerAsync(string name, bool force, CancellationToken ct); Task> ListContainersAsync(CancellationToken ct); /// One of "running" | "stopped" | "absent" (kept coarse on purpose — the Swift /// policy layer only distinguishes these three). Task ContainerStateAsync(string name, CancellationToken ct); /// cgroup counters for the resource monitor; null when not running. Task ContainerStatsAsync(string name, CancellationToken ct); /// /// Create a process in a running container and attach its event handlers, but **do not run /// it** — the caller runs it with once it has sent the /// `procId` downstream. `procId` is minted by the broker and keys every event this process /// emits through . /// /// The two-step split is not ceremony. See . /// Task ExecAsync(long procId, ProcSpec spec, CancellationToken ct); } /// Control half of a running in-container process (output arrives via events). public interface IWslcProcess { /// /// Actually run the process. Separate from because a short /// command can finish before the `proc.exec` RESPONSE has been written: output and exit ride /// the same ordered outbound queue, so starting first puts `proc.exit` on the wire ahead of /// the `procId` that identifies it, and a client that registers interest on receiving that /// procId waits forever. Observed on hardware with `echo` (docs/WINDOWS_PORT.md §13.3). /// /// This is the same reasoning that makes wslc itself split `CreateProcess` from `Start` — so /// handlers can attach before output flows — applied one level up, to the RPC boundary. /// Task StartAsync(CancellationToken ct); Task WriteStdinAsync(ReadOnlyMemory data, CancellationToken ct); Task CloseStdinAsync(CancellationToken ct); Task SignalAsync(int signal, CancellationToken ct); /// tty mode only (the Terminal panel); no-op for pipe-mode processes. Task ResizeAsync(int cols, int rows, CancellationToken ct); } /// Event sink the broker hands to the facade; implementations must be /// non-blocking (they enqueue onto the outbound writer). public interface IBrokerEvents { void ProcOutput(long procId, bool stderr, ReadOnlySpan chunk); void ProcExited(long procId, int code); void SessionDown(string reason); void PullProgress(string reference, string status, long current, long total); void InstallProgress(string status, double percent); } /// A structured facade failure, surfaced to hostd as JSON-RPC error -32000 with /// `data.kind` so the Swift side can branch (e.g. `brokerLost` vs `imagePullFailed`). public sealed class WslcError(string kind, string message) : Exception(message) { public string Kind { get; } = kind; public const string Unavailable = "wslc_unavailable"; public const string NotFound = "not_found"; public const string NotRunning = "not_running"; public const string PullFailed = "image_pull_failed"; public const string StartFailed = "start_failed"; public const string AiUnavailable = "ai_unavailable"; /// The installed wslc cannot do this at all (no pty, an unmappable signal). A /// permanent capability gap, not a transient failure — hostd must not retry. public const string Unsupported = "unsupported"; /// /// A CONTAINER of that name already exists in the session. Distinct from /// because wslc answers `ERROR_ALREADY_EXISTS` for both and the /// remedies differ completely — remove one container, versus restart the whole WSL stack. /// /// Reachable today because this broker's container roster is process-local (there is no /// enumeration on the compat surface), so a container left behind by a crashed broker holds /// its name against every later one. See docs/WINDOWS_PORT.md §13.3. /// public const string AlreadyExists = "already_exists"; /// A session of that name is already running and this facade cannot re-adopt it /// (the compat SDK's `Start()` answers ERROR_ALREADY_EXISTS, and its constructor is lazy, so /// a second handle is not a second session). Distinct from because /// the remedy differs: the sandbox is UP, this broker just cannot reach it. public const string SessionExists = "session_exists"; } // DTOs — property names (after camel-casing) match the §3.3 wire keys exactly. public sealed record SessionSpec( string Name, string DataDir, int? Cpu, long? MemoryMB); public sealed record RegistryAuth(string? Username, string? Password); public sealed record ImageInfo( [property: JsonPropertyName("ref")] string Ref, string? Digest, long? SizeBytes); public sealed record VolumeSpec( string Host, string Guest, [property: JsonPropertyName("ro")] bool ReadOnly); public sealed record ContainerCreateSpec( string Name, string Image, IReadOnlyList? Volumes, string? NetworkingMode, string? Hostname, IReadOnlyDictionary? Env, IReadOnlyList? InitArgv); public sealed record ContainerInfo(string Name, string Image, string State); public sealed record ContainerStatsInfo( long CpuUsageUsec, long MemoryUsedBytes, long MemoryLimitBytes, long? OomKills); public sealed record ProcSpec( string Container, IReadOnlyList Argv, IReadOnlyDictionary? Env, string? Cwd, int? Uid, int? Gid, bool Tty);