From b0e31f89ab3e2ad9772b36be7d6bc2c2d5601860 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Fri, 2 Oct 2026 07:26:20 +0100 Subject: [PATCH] docs: updated docs --- .github/workflows/integration-wsl.yml | 6 +- AGENTS.md | 7 +- Justfile | 2 +- README.md | 80 +++--- docs/wiki/Architecture.md | 28 +-- docs/wiki/Getting-Started.md | 2 +- docs/wiki/Home.md | 3 +- docs/wiki/Lifecycle.md | 14 +- docs/wiki/Modules.md | 8 +- docs/wiki/Networking.md | 14 +- docs/wiki/Packaging.md | 4 +- docs/wiki/Release-Flow.md | 4 +- docs/wiki/Testing.md | 4 +- docs/wiki/Wait-Strategies.md | 4 +- docs/wiki/Wslc-Api-Investigation.md | 202 --------------- docs/wiki/Wslc-Capability-Matrix.md | 59 ----- docs/wiki/_Sidebar.md | 2 - purview-build.json | 2 +- .../DynamicLoadSpike/DynamicLoadSpike.csproj | 61 ----- spikes/DynamicLoadSpike/DynamicLoader.cs | 187 -------------- spikes/DynamicLoadSpike/PayloadInspector.cs | 106 -------- spikes/DynamicLoadSpike/Program.cs | 30 --- .../PortableConsumerSpike.csproj | 52 ---- spikes/PortableConsumerSpike/Program.cs | 50 ---- spikes/WslcSpikes/Program.cs | 45 ---- spikes/WslcSpikes/SpikeSupport.cs | 166 ------------ spikes/WslcSpikes/Spikes/BindAddress.cs | 113 --------- spikes/WslcSpikes/Spikes/CleanupBehaviour.cs | 114 --------- .../WslcSpikes/Spikes/ConcurrentContainers.cs | 133 ---------- .../Spikes/ConcurrentExecProcesses.cs | 119 --------- .../WslcSpikes/Spikes/ConcurrentSharedPath.cs | 104 -------- spikes/WslcSpikes/Spikes/DisposeSemantics.cs | 54 ---- spikes/WslcSpikes/Spikes/ExecProcess.cs | 103 -------- spikes/WslcSpikes/Spikes/FullLifecycle.cs | 50 ---- spikes/WslcSpikes/Spikes/ImageCacheScope.cs | 44 ---- spikes/WslcSpikes/Spikes/MountsAndVolumes.cs | 137 ---------- spikes/WslcSpikes/Spikes/Networking.cs | 120 --------- spikes/WslcSpikes/Spikes/OrphanSessions.cs | 91 ------- spikes/WslcSpikes/Spikes/Ports.cs | 184 -------------- spikes/WslcSpikes/Spikes/ProcessIo.cs | 237 ------------------ spikes/WslcSpikes/Spikes/ReopenOrphan.cs | 45 ---- spikes/WslcSpikes/Spikes/SessionLifecycle.cs | 53 ---- spikes/WslcSpikes/Spikes/ThreadSafety.cs | 82 ------ spikes/WslcSpikes/WslcSpikes.csproj | 11 - ...WSLTestContainers.slnx => Containers.slnx} | 0 src/src/Wsl/Wsl.csproj | 2 +- 46 files changed, 77 insertions(+), 2861 deletions(-) delete mode 100644 docs/wiki/Wslc-Api-Investigation.md delete mode 100644 docs/wiki/Wslc-Capability-Matrix.md delete mode 100644 spikes/DynamicLoadSpike/DynamicLoadSpike.csproj delete mode 100644 spikes/DynamicLoadSpike/DynamicLoader.cs delete mode 100644 spikes/DynamicLoadSpike/PayloadInspector.cs delete mode 100644 spikes/DynamicLoadSpike/Program.cs delete mode 100644 spikes/PortableConsumerSpike/PortableConsumerSpike.csproj delete mode 100644 spikes/PortableConsumerSpike/Program.cs delete mode 100644 spikes/WslcSpikes/Program.cs delete mode 100644 spikes/WslcSpikes/SpikeSupport.cs delete mode 100644 spikes/WslcSpikes/Spikes/BindAddress.cs delete mode 100644 spikes/WslcSpikes/Spikes/CleanupBehaviour.cs delete mode 100644 spikes/WslcSpikes/Spikes/ConcurrentContainers.cs delete mode 100644 spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs delete mode 100644 spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs delete mode 100644 spikes/WslcSpikes/Spikes/DisposeSemantics.cs delete mode 100644 spikes/WslcSpikes/Spikes/ExecProcess.cs delete mode 100644 spikes/WslcSpikes/Spikes/FullLifecycle.cs delete mode 100644 spikes/WslcSpikes/Spikes/ImageCacheScope.cs delete mode 100644 spikes/WslcSpikes/Spikes/MountsAndVolumes.cs delete mode 100644 spikes/WslcSpikes/Spikes/Networking.cs delete mode 100644 spikes/WslcSpikes/Spikes/OrphanSessions.cs delete mode 100644 spikes/WslcSpikes/Spikes/Ports.cs delete mode 100644 spikes/WslcSpikes/Spikes/ProcessIo.cs delete mode 100644 spikes/WslcSpikes/Spikes/ReopenOrphan.cs delete mode 100644 spikes/WslcSpikes/Spikes/SessionLifecycle.cs delete mode 100644 spikes/WslcSpikes/Spikes/ThreadSafety.cs delete mode 100644 spikes/WslcSpikes/WslcSpikes.csproj rename src/{WSLTestContainers.slnx => Containers.slnx} (100%) diff --git a/.github/workflows/integration-wsl.yml b/.github/workflows/integration-wsl.yml index 67a12ef..22f8275 100644 --- a/.github/workflows/integration-wsl.yml +++ b/.github/workflows/integration-wsl.yml @@ -49,10 +49,10 @@ jobs: wslc version - name: Restore - run: dotnet restore src/WSLTestContainers.slnx + run: dotnet restore src/Containers.slnx - name: Build - run: dotnet build src/WSLTestContainers.slnx --configuration Release --no-restore + run: dotnet build src/Containers.slnx --configuration Release --no-restore - name: WSLC backend contract # Real containers on a shared WSLC session; ~27 tests and ~4 minutes. @@ -86,4 +86,4 @@ jobs: if ($failed.Count -gt 0) { throw "WSLC integration suites failed: $($failed -join ', ')" - } \ No newline at end of file + } diff --git a/AGENTS.md b/AGENTS.md index 7b95410..5d7c890 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ whenever a target framework, a backend package or the `buildTransitive` assets c | Path | Purpose | | --- | --- | -| `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack | +| `src/Containers.slnx` | Canonical solution for restore, build, test and pack | | `src/src/Containers` | Umbrella package (`Purview.Containers`, `net10.0`): references `Core` and both backends; no code of its own | | `src/src/Core` | Backend-neutral abstractions (`Purview.Containers.Core`, `net10.0`, namespace `Purview.Containers`): `IContainer`/`ContainerConfiguration`, `ContainerBuilder`, `ContainerBase`, `IContainerBackend`/`ContainerBackends`, wait strategies, images, networking, mounts, diagnostics | | `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`, multi-target `net10.0` facade + `net10.0-windows10.0.19041.0` implementation): `WslContainerBackend`, the shared session runtime, `WslContainer`, `WslContainerSession`, and the portable facade (`WslPayload`, `*.Facade.cs`) that loads the implementation at run time | @@ -30,9 +30,6 @@ whenever a target framework, a backend package or the `buildTransitive` assets c | `src/src/` | Service modules (`Purview.Containers.`, `net10.0`); each is a thin layer over the abstractions and carries a bespoke `Sdk/README.md` | | `src/src//Sdk` | Package-only assets. `Sdk/README.md` is packed as the package README (suppressing the repo-root README); `Sdk/buildTransitive/**` ships MSBuild assets to consumers (the abstractions' backend registration, each backend package's own registration entry, and the WSL backend's consumer defaults and guards) | | `src/tests` | TUnit unit (`*.UnitTests`) and container integration (`*.IntegrationTests`) projects: the WSLC suites need a WSLC host, `Docker.IntegrationTests` and `Modules.DockerIntegrationTests` need a Docker daemon | -| `spikes/WslcSpikes` | Phase 0 investigation harness (`s1`..`s14` probes); not part of the test run | -| `spikes/DynamicLoadSpike` | Phase 0 feasibility probe for the portable facade: loads the WSLC projection from a local payload and drives a session from a plain `net10.0` process; not part of the test run | -| `spikes/PortableConsumerSpike` | Acceptance probe for the portable facade: a `net10.0` project that binds the facade and runs a real WSLC container through a `wslc/` payload; not part of the test run | | `docs/wiki` | User-facing documentation wiki, aggregated by the purview-dev website. `Backends.md` is the consumer guide to choosing and configuring WSLC or Docker | | `samples/getting-started` | Runnable consumer-shaped samples (WSLC and Docker) built with the solution; `just sample-wsl` / `just sample-docker` | | `mkdocs.yml` | Wiki site configuration (`docs_dir: docs/wiki`) | @@ -188,7 +185,7 @@ Commit messages follow Conventional Commits enforced by the `commit-msg` lefthoo [Testing](docs/wiki/Testing.md)); `.github/actionlint.yaml` declares that label. It is never attached to `pull_request` or `push`. - The workflows pin `dotnet-version` to `global.json`'s `sdk.version`; keep them in sync, and keep - `purview-build.json` pointing at `src/WSLTestContainers.slnx`. The shared workflow runs on + `purview-build.json` pointing at `src/Containers.slnx`. The shared workflow runs on `ubuntu-latest`, which is why `EnableWindowsTargeting=true` must stay in `src/Directory.Build.props`. ## Completion checklist diff --git a/Justfile b/Justfile index d314846..debbde5 100644 --- a/Justfile +++ b/Justfile @@ -6,7 +6,7 @@ export DOTNET_SKIP_FIRST_TIME_EXPERIENCE := "1" export DO_NOT_TRACK := "1" root_folder := "./src/" -solution_file := root_folder + "WSLTestContainers.slnx" +solution_file := root_folder + "Containers.slnx" test_solution := solution_file build_configuration := "Debug" diff --git a/README.md b/README.md index 967634f..6645adb 100644 --- a/README.md +++ b/README.md @@ -118,56 +118,40 @@ await rabbitMq.StartAsync(); string amqp = rabbitMq.GetConnectionString(); ``` -> These are the target APIs. The generic container is implemented; the module builders arrive in -> Phases 3–6. See the docs for the designed surface. - -## Key design decisions (verified by spikes) +## Key design decisions | Decision | Evidence | | --- | --- | -| One shared process-wide session, shared storage path | image store is keyed by storage path; session start ~20 ms (S2); concurrent sessions can't share the VHD (S18) → auto-fallback to isolated store | -| Unique session names `wslc-{pid}-{rand}` | session names are machine-reserved (S1, S13) | -| Default `NetworkingMode = Bridged` | port mappings require Bridged; default is `none` (S4) | -| Random host ports via native `windowsPort=0` | race-free random assignment (S4) | -| Serialize session/container lifecycle ops | concurrent `Start` can race (`0x8000FFFF`) (S8) | -| No Ryuk-style reaper needed | `Session.Dispose()` frees the session; orphans block only their own name (S10, S14) | -| No container-name DNS | containers reach each other by IP only (S9) | +| One shared process-wide session, shared storage path | the image store is keyed by storage path; session start is ~20 ms; concurrent sessions cannot share the VHD, so the runtime falls back to an isolated store | +| Unique session names `wslc-{pid}-{rand}` | session names are machine-reserved | +| Default `NetworkingMode = Bridged` | port mappings require Bridged; the default is `none` | +| Random host ports via native `windowsPort=0` | race-free random assignment | +| Serialize session/container lifecycle ops | a concurrent `Start` can race (`0x8000FFFF`) | +| No Ryuk-style reaper needed | `Session.Dispose()` frees the session; orphans block only their own name | +| No container-name DNS | containers reach each other by IP only | | `Image`/`WithTag` fail fast | invalid images/tags rejected at configuration time, never at pull time | ## Repository layout ``` src/ - Wsl/ core runtime + WSL Containers backend (package Purview.Containers.Wsl) - PostgreSql/ PostgreSQL module (builder, container, Npgsql connection string) - Redis/ Redis module (builder, container, StackExchange.Redis connection string) - MsSql/ SQL Server module (builder, container, SqlClient connection string) - RabbitMq/ RabbitMQ module (builder, container, AMQP + management endpoints) - Azurite/ Azurite module (builder, container, blob/queue/table endpoints) - Nats/ NATS module (builder, container, client + monitoring endpoints) - MySql/ MySQL module (builder, container, MySqlConnector connection string) -tests/ - SharedTestingFramework/ shared WSLC skip/helper for integration tests - Wsl.UnitTests/ - Wsl.IntegrationTests/ - PostgreSql.UnitTests/ - PostgreSql.IntegrationTests/ - Redis.UnitTests/ - Redis.IntegrationTests/ - MsSql.UnitTests/ - MsSql.IntegrationTests/ - RabbitMq.UnitTests/ - RabbitMq.IntegrationTests/ - Azurite.UnitTests/ - Azurite.IntegrationTests/ -spikes/ - WslcSpikes/ Phase 0 investigation harness (run: see below) + Containers.slnx canonical solution (restore, build, test, pack) + src/ + Core/ backend-neutral abstractions (Purview.Containers.Core) + Wsl/ WSL Containers backend (Purview.Containers.Wsl) + Docker/ Docker backend (Purview.Containers.Docker) + Containers/ umbrella package (Purview.Containers): Core + both backends + PostgreSql/ Redis/ MsSql/ MySql/ + RabbitMq/ Azurite/ Nats/ service modules + tests/ TUnit unit + integration projects (WSLC and Docker suites) +samples/ + getting-started/ runnable samples (WslSample, DockerSample, AutoSample) docs/ - wiki/ project wiki (mkdocs.yml -> docs_dir: docs/wiki) - index.md Home.md _Sidebar.md Getting-Started.md Testing.md - Architecture.md Lifecycle.md Networking.md Wait-Strategies.md Modules.md - Packaging.md Release-Flow.md Contributing.md Contributing-Modules.md - Wslc-Api-Investigation.md Wslc-Capability-Matrix.md + wiki/ project wiki (mkdocs.yml -> docs_dir: docs/wiki) + index.md Home.md _Sidebar.md Getting-Started.md Using-in-Your-Tests.md + Backends.md Consumer-Requirements.md Architecture.md Lifecycle.md + Networking.md Wait-Strategies.md Modules.md Testing.md Packaging.md + Release-Flow.md Contributing.md Contributing-Modules.md ``` ## Documentation @@ -188,7 +172,7 @@ The project documentation lives in [`docs/wiki`](docs/wiki/Home.md) and is publi Every package also ships its own `README.md` (from `src/src//Sdk/README.md`), so `dotnet add package Purview.Containers.` brings documentation specific to that package. -The PostgreSQL, Redis, SQL Server and RabbitMQ modules work today: +All seven service modules work today: ```csharp await using var postgres = new PostgreSqlBuilder() @@ -275,13 +259,13 @@ just sample-wsl # needs WSL Containers just sample-docker # needs a Docker daemon ``` -> The `Wsl.IntegrationTests` module runs 27 real containers in one session and takes ~4 -> minutes because WSLC serialises container operations; expect slow-test warnings while it runs. +> The `Wsl.IntegrationTests` module starts many real containers in one session and can take several +> minutes, because WSLC serialises container operations; expect slow-test warnings while it runs. ### Verifying the consumer contract ```powershell -just verify-consumers # pack, then build 16 throwaway consumer projects against ./artifacts +just verify-consumers # pack, then build 21 throwaway consumer projects against ./artifacts just verify-consumers -Keep # same, keeping the generated projects for inspection ``` @@ -290,14 +274,6 @@ the happy path, the shipped `buildTransitive` defaults, the `PCC0001`/`PCC0002` non-.NET-11 escape hatch that deliberately does not work. It needs network access and is therefore a local step rather than part of the `[Category=Unit]` pipeline filter. -## Running the Phase 0 spikes - -```powershell -dotnet build spikes/WslcSpikes/WslcSpikes.csproj -dotnet run --project spikes/WslcSpikes -- sfull -# or s1..s14 for individual behaviour probes -``` - ## License MIT. This project is not affiliated with, or endorsed by, the Testcontainers project or Microsoft. diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md index 07216b0..8174762 100644 --- a/docs/wiki/Architecture.md +++ b/docs/wiki/Architecture.md @@ -63,18 +63,18 @@ cache. Package consumers get registration from the generated module initializer `Register(...)`. Consumer-facing guidance (including the CI example) is in [Backends: WSLC or Docker](Backends.md). -## Session lifetime model (chosen after spikes) +## Session lifetime model **One shared, process-wide session.** Findings that drove this: -- Session start is cheap (~20-30 ms) because the WSL VM is shared under the session manager. (EXP) -- The image store is keyed by the **storage path** (EXP, S2): a stable storage path gives a warm image cache across sessions and process restarts. +- Session start is cheap (~20-30 ms) because the WSL VM is shared under the session manager. +- The image store is keyed by the **storage path**: a stable storage path gives a warm image cache across sessions and process restarts. - Per-container sessions would force a fresh storage path per container → a cold image store (re-pull) per container, which is the dominant cost (alpine ~2-3 s, python ~3-4 s). Not acceptable for test throughput. -- `Session`/`Container` lifecycle calls are **not reliably thread-safe**: a concurrent `Container.Start()` occasionally fails with `0x8000FFFF` (E_UNEXPECTED) (EXP, S8). Container isolation is achieved by unique container names + unique host ports, not separate sessions. +- `Session`/`Container` lifecycle calls are **not reliably thread-safe**: a concurrent `Container.Start()` occasionally fails with `0x8000FFFF` (E_UNEXPECTED). Container isolation is achieved by unique container names + unique host ports, not separate sessions. Design rules: -1. **One lazily-started session per process** with a deterministic name `wslc-{processId}-{8 hex}` (unique per machine; session names are reserved until the session is disposed — EXP S1/S14) and a **stable storage path** (default `%LOCALAPPDATA%\Purview.WslContainers\sessions\{name}\`), configurable. +1. **One lazily-started session per process** with a deterministic name `wslc-{processId}-{8 hex}` (unique per machine; session names are reserved until the session is disposed) and a **stable storage path** (default `%LOCALAPPDATA%\Purview.WslContainers\sessions\{name}\`), configurable. 2. The session VM is capped at **4096 MB by default** (`WslContainerRuntimeOptions.Default`). This is required for SQL Server (which refuses to start below 2000 MB — `sqlservr: This program requires a machine with at least 2000 megabytes of memory`) and harmless for lighter containers. Override via `WslContainerRuntimeOptions.MemorySizeInMB`. 2. `IContainerBackend` is the seam for backends: a backend package (starting with `Purview.Containers.Wsl`) supplies `IContainer` instances, and `ContainerBackends` resolves which one runs. `IContainerRuntime` remains the WSLC-internal seam so advanced users/tests can substitute a per-container-session runtime for isolation experiments. 3. Container `DisposeAsync` never terminates the shared session; it deletes the container only. @@ -88,25 +88,25 @@ Design rules: process runs. Set `StorageMode.PerSession` (or an explicit `StoragePath` / `PURVIEW_CONTAINERS_STORAGE_PATH`) for isolation. Session names stay unique per process; only the path is shared. - **Concurrent sharing is not possible**: a running session exclusively locks its `storage.vhdx` - (a second session on the same path fails with `0x80070020` — EXP S18). The lock is taken lazily on the + (a second session on the same path fails with `0x80070020`). The lock is taken lazily on the first store access, so contention can surface on `GetImages()` rather than at session start; the runtime detects it there too and falls back to an isolated per-process store. - Sequential reuse works (EXP S2/S13): after a session ends, a new session on the same path sees its images. -- Cleanup: normal shutdown terminates+disposes the session (frees the name, EXP S14). Orphaned sessions from crashed processes block only their own name (EXP S13); they do not block other sessions or storage-path reuse. + Sequential reuse works: after a session ends, a new session on the same path sees its images. +- Cleanup: normal shutdown terminates+disposes the session (frees the name). Orphaned sessions from crashed processes block only their own name; they do not block other sessions or storage-path reuse. ## Image management - `PullPolicy { Missing, Always, Never }`, default `Missing`. - `EnsureImageAsync`: consult `GetImages()` (session store) → pull when missing. -- Concurrent pulls of the same image are **deduplicated** by a keyed async lock (WSLC does not dedupe; EXP S11). +- Concurrent pulls of the same image are **deduplicated** by a keyed async lock (WSLC does not dedupe). - Registry auth via `Session.Authenticate(uri, user, pass)` → `AuthenticateResult.IdentityToken` → `PullImageOptions.RegistryAuth`. Credentials live in a `Secret` type; never logged or serialized. ## Ports - `PortBinding(containerPort, hostPort?, protocol, hostAddress?)`. -- **Random host port = native `windowsPort=0`** (race-free, EXP S4). The assigned port is read from `Inspect().Ports`. +- **Random host port = native `windowsPort=0`** (race-free). The assigned port is read from `Inspect().Ports`. - Explicit host ports are bound by WSLC at `Start`; a conflict throws `0x80072740` (surface as a clear `WslContainerPortInUseException`). -- Default host bind is IPv4 loopback only; IPv6 is not mapped (EXP S12). +- Default host bind is IPv4 loopback only; IPv6 is not mapped. - UDP → `ContainerNotSupportedException`. ## Concurrency @@ -129,10 +129,10 @@ fallback. Isolated stores are transient and are removed when their session termi **No Ryuk-style sidecar process is needed.** -- `Session.Dispose()` removes the session from the manager and frees its name (EXP S14). +- `Session.Dispose()` removes the session from the manager and frees its name. - The library registers a process-exit handler so the session is always disposed on normal termination. -- Crash residue: an orphaned session from a dead process remains registered with a dead `Creator PID` (EXP S10). It reserves only its own name. It can be swept with `wslc --session system session terminate`; the library documents this as an optional maintenance step and may offer a dev-time sweep utility (CLI-based, clearly isolated from the core runtime). -- `Container.DisposeAsync` is idempotent: stop (SIGTERM→SIGKILL) then `Delete(Force)`; swallow `RPC_E_DISCONNECTED`/`ContainerNotFound` on double-delete (EXP S7). +- Crash residue: an orphaned session from a dead process remains registered with a dead `Creator PID`. It reserves only its own name. It can be swept with `wslc --session system session terminate`; the library documents this as an optional maintenance step and may offer a dev-time sweep utility (CLI-based, clearly isolated from the core runtime). +- `Container.DisposeAsync` is idempotent: stop (SIGTERM→SIGKILL) then `Delete(Force)`; swallow `RPC_E_DISCONNECTED`/`ContainerNotFound` on double-delete. ## Observability diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index b8e183e..658c8d0 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -112,7 +112,7 @@ just sample-docker # needs a Docker daemon ## 5. Build and pack locally ```powershell -just build # dotnet build of src/WSLTestContainers.slnx (Debug) +just build # dotnet build of src/Containers.slnx (Debug) just pack # build + dotnet pack into ./artifacts just pipeline-pack-validate # shared pipeline: restore, build, lint, test, pack, validate ``` diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 82fe737..b9c1cb5 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -107,7 +107,7 @@ example and the troubleshooting reference. | Path | Purpose | | --- | --- | -| `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack. | +| `src/Containers.slnx` | Canonical solution for restore, build, test and pack. | | `src/src/Containers` | Umbrella package (`Purview.Containers`): references Core and both backends; no code of its own. | | `src/src/Core` | Backend-neutral abstractions (`Purview.Containers.Core`, namespace `Purview.Containers`): the container contract, builders and backend selection. | | `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`): containers, images, runtime, networking, mounts, diagnostics. | @@ -115,7 +115,6 @@ example and the troubleshooting reference. | `src/src/` | Service modules; each carries a bespoke `Sdk/README.md` that ships as the package README. | | `src/tests` | TUnit unit and integration test projects (WSLC and Docker suites). | | `samples/getting-started` | Runnable WSLC and Docker samples (`just sample-wsl`, `just sample-docker`). | -| `spikes/WslcSpikes` | Phase 0 investigation harness (`s1`..`s14` behaviour probes). | | `docs/wiki` | This wiki, aggregated by the purview-dev website. | | `purview-build.json` | Shared `Purview.Build` pipeline configuration, including the exhaustive pack manifest. | | `.agents` | Package-delivered agent skills, agents and prompts. | diff --git a/docs/wiki/Lifecycle.md b/docs/wiki/Lifecycle.md index 7c95d63..1909a83 100644 --- a/docs/wiki/Lifecycle.md +++ b/docs/wiki/Lifecycle.md @@ -1,6 +1,6 @@ # Lifecycle -Container lifecycle semantics for `Purview.Containers`, derived from the Phase 0 spikes. +Container lifecycle semantics for `Purview.Containers`. ## Public API @@ -16,24 +16,24 @@ public interface IContainer : IAsyncDisposable ## State model -WSLC `ContainerState`: `Invalid → Created → Running → Exited → Deleted`. (SRC/EXP) +WSLC `ContainerState`: `Invalid → Created → Running → Exited → Deleted`. | Operation | Behaviour | |---|---| -| `StartAsync` | Creates the session if needed, ensures image (per pull policy), `CreateContainer`, then `Container.Start()`. A missing init binary throws `ArgumentException 0x80070057` with the OCI message → surfaced as `WslContainerStartupException`. (EXP S3) | -| `StopAsync` | `Container.Stop(SIGTERM, grace)` then, if still running, `Stop(SIGKILL, ...)`. **Idempotent** — WSLC accepts stopping an already-stopped container. (EXP S7) | -| `DeleteAsync` | `Container.Delete(Force)`. Double-delete throws `0x80010108` (`RPC_E_DISCONNECTED`) → swallowed. (EXP S7) | +| `StartAsync` | Creates the session if needed, ensures image (per pull policy), `CreateContainer`, then `Container.Start()`. A missing init binary throws `ArgumentException 0x80070057` with the OCI message → surfaced as `WslContainerStartupException`. | +| `StopAsync` | `Container.Stop(SIGTERM, grace)` then, if still running, `Stop(SIGKILL, ...)`. **Idempotent** — WSLC accepts stopping an already-stopped container. | +| `DeleteAsync` | `Container.Delete(Force)`. Double-delete throws `0x80010108` (`RPC_E_DISCONNECTED`) → swallowed. | | `DisposeAsync` | Stop (if running) → Delete → release the WinRT container object. **Idempotent.** | ## Cleanup guarantees - `DisposeAsync` runs in `finally` by callers, and the container object is also registered for cleanup by the runtime so disposal happens even if a test throws after `StartAsync`. -- `EnableAutoRemove` defaults to `false`: the library owns deletion via `DisposeAsync`. WSLC auto-removal would delete one-shot containers before their state/output could be inspected (verified in Phase 1). Opt in with `.WithAutoRemove()` when desired. +- `EnableAutoRemove` defaults to `false`: the library owns deletion via `DisposeAsync`. WSLC auto-removal would delete one-shot containers before their state/output could be inspected. Opt in with `.WithAutoRemove()` when desired. - Partial startup failure (image pull / create / start / wait): the container is torn down via the same `DisposeAsync` path. - Cancellation: a cancelled `StartAsync`/wait triggers the same teardown. - Container crash: `InitProcess.Exited` fires; the container is then in `Exited` state and `DisposeAsync` deletes it. - Session termination (crash): the session `Terminated` event marks the runtime session as dead; subsequent container operations throw a clear `WslContainerSessionTerminatedException`. -- Test-process termination: a process-exit hook disposes the shared session (frees the name; EXP S14). Crashed processes leave an orphaned session that only reserves its name (EXP S10/S13); sweep with `wslc --session system session terminate`. +- Test-process termination: a process-exit hook disposes the shared session (frees the name). Crashed processes leave an orphaned session that only reserves its name; sweep with `wslc --session system session terminate`. ## Secrets & names diff --git a/docs/wiki/Modules.md b/docs/wiki/Modules.md index e4af0e7..efce69d 100644 --- a/docs/wiki/Modules.md +++ b/docs/wiki/Modules.md @@ -139,10 +139,10 @@ that leans on the Testcontainers modules ports across unchanged: | Module | Image | Readiness | Client | Status | |---|---|---|---|---| -| PostgreSQL | `postgres:17` | `pg_isready` | Npgsql | ✅ implemented (Phase 3) | -| Redis | `redis:7` | `redis-cli ping` | StackExchange.Redis | ✅ implemented (Phase 4) — also usable with Valkey/Garnet via `WithImage` | -| SQL Server | `mcr.microsoft.com/mssql/server:2022-latest` | host `SqlClient` connection | Microsoft.Data.SqlClient | ✅ implemented (Phase 5) — requires `.AcceptLicense()`; the connection string defaults to `Database=master` (`WithDatabase(...)` to change it); session needs ≥ 2000 MB memory | -| RabbitMQ | `rabbitmq:3-management` | log `"Server startup complete"` | RabbitMQ.Client | ✅ implemented (Phase 6) — AMQP + management endpoints | +| PostgreSQL | `postgres:17` | `pg_isready` | Npgsql | ✅ implemented | +| Redis | `redis:7` | `redis-cli ping` | StackExchange.Redis | ✅ implemented — also usable with Valkey/Garnet via `WithImage` | +| SQL Server | `mcr.microsoft.com/mssql/server:2022-latest` | host `SqlClient` connection | Microsoft.Data.SqlClient | ✅ implemented — requires `.AcceptLicense()`; the connection string defaults to `Database=master` (`WithDatabase(...)` to change it); session needs ≥ 2000 MB memory | +| RabbitMQ | `rabbitmq:3-management` | log `"Server startup complete"` | RabbitMQ.Client | ✅ implemented — AMQP + management endpoints | | Azurite | `mcr.microsoft.com/azure-storage/azurite` | log `"successfully listening"` | Azure.Storage.* | ✅ implemented — blob/queue/table endpoints; the well-known `devstoreaccount1` key is a placeholder in `AzuriteAccount.Key` until the consuming repo supplies it | | NATS | `nats:2` | log `"Listening for client connections"` | NATS.Client.Core | ✅ implemented — client + monitoring endpoints | | MySQL | `mysql:8` | host `MySqlConnector` connection | MySqlConnector | ✅ implemented — uses a real connection poll (the image logs `"ready for connections"` during its temporary init server) | diff --git a/docs/wiki/Networking.md b/docs/wiki/Networking.md index 2b5b06c..0265ba6 100644 --- a/docs/wiki/Networking.md +++ b/docs/wiki/Networking.md @@ -1,19 +1,19 @@ # Networking -WSLC networking behaviour (verified in Phase 0) and how `Purview.Containers` models it. +WSLC networking behaviour and how `Purview.Containers` models it. ## Verified behaviour | Mode (`ContainerNetworkingMode`) | Effect | Port mappings | |---|---|---| -| `null` (default) | Docker `NetworkMode: "none"` — no network, no IP | ❌ `CreateContainer` throws `0x80070057` (EXP S4) | +| `null` (default) | Docker `NetworkMode: "none"` — no network, no IP | ❌ `CreateContainer` throws `0x80070057` | | `None` | same as null | ❌ | -| `Bridged` | container gets `172.17.0.x` on the session bridge; host→container mapped ports work | ✅ (EXP S4/S8/S12) | +| `Bridged` | container gets `172.17.0.x` on the session bridge; host→container mapped ports work | ✅ | -- Host bind address is **IPv4 loopback only** (`HostIp: 127.0.0.1`). IPv6 `::1` does not connect to the mapped port. (EXP S12) +- Host bind address is **IPv4 loopback only** (`HostIp: 127.0.0.1`). IPv6 `::1` does not connect to the mapped port. - **Clients must use `127.0.0.1`, not `localhost`.** `localhost` resolves to `::1` first on .NET; most clients (Npgsql, StackExchange.Redis) fall back to IPv4, but **Microsoft.Data.SqlClient hangs on `::1` without falling back**, so the SQL Server module's connection strings use `127.0.0.1,{port}` explicitly. -- **Containers in the same session can reach each other by IP (Bridged)** — `wget http://:8080/` works. (EXP S9) -- **No name/DNS resolution between containers.** `wget http://:8080/` and `http://:8080/` fail with "bad address"; `/etc/hosts` contains only localhost + the container's own `IP `. (EXP S9) +- **Containers in the same session can reach each other by IP (Bridged)** — `wget http://:8080/` works. +- **No name/DNS resolution between containers.** `wget http://:8080/` and `http://:8080/` fail with "bad address"; `/etc/hosts` contains only localhost + the container's own `IP `. - There are **no managed network objects** (no `network create/connect` in `Microsoft.WSL.Containers`, unlike the CLI). ## Library design @@ -21,7 +21,7 @@ WSLC networking behaviour (verified in Phase 0) and how `Purview.Containers` mod 1. **Default `NetworkingMode = Bridged`** for containers that bind ports (or, by default, for all containers to be useful). This is baked into `ContainerBuilder` defaults. 2. `GetMappedPublicPort(containerPort)` reads `Inspect().Ports["/tcp"][0].HostPort`. 3. `GetNetworkIp()` reads `Inspect().NetworkSettings.Networks.bridge.IPAddress` (Bridged only). -4. TCP/HTTP wait strategies connect to `127.0.0.1:` (IPv4 only, per S12). +4. TCP/HTTP wait strategies connect to `127.0.0.1:` (IPv4 only). 5. **Multi-container environments are deliberately deferred.** Because there is no native DNS/alias, a future `ContainerEnvironmentBuilder` would wire containers together by passing each other's IPs (from `GetNetworkIp()`) into configuration/env. The core API must not preclude this (it doesn't — `GetNetworkIp` + environment are enough). ## What NOT to model diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index 89c61c9..3514b29 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -1,6 +1,6 @@ # Packaging -Every project under `src/src` is packable and ships as `Purview.Containers.*`; test and spike projects +Every project under `src/src` is packable and ships as `Purview.Containers.*`; test projects never pack. Packages are produced into `./artifacts` with a matching `.snupkg`. ## What a package contains @@ -84,7 +84,7 @@ alongside the `Sdk/README.md`. Removing content from a package means removing th ## Adding a package 1. Create `src/src//.csproj` with `true`, an assembly/package id, a - description and tags, and add it to `src/WSLTestContainers.slnx`. + description and tags, and add it to `src/Containers.slnx`. 2. Add `Sdk/README.md` documenting the package (see the existing modules for the shape). 3. Add the package to `RequiredContent` in `purview-build.json`. 4. Prove it with `just pack` and `just pipeline-pack-validate`. diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md index 18de8b2..087458f 100644 --- a/docs/wiki/Release-Flow.md +++ b/docs/wiki/Release-Flow.md @@ -19,7 +19,7 @@ The `Justfile` wraps the common steps: | Command | What it does | | --- | --- | -| `just build` | `dotnet build src/WSLTestContainers.slnx` (Debug). | +| `just build` | `dotnet build src/Containers.slnx` (Debug). | | `just test` | `dotnet test` across the solution, one test module at a time (see [Testing](Testing.md)). | | `just lint-check` / `just lint-fix` | CSharpier check / format over the repository root. | | `just pack` | Build (Debug) and `dotnet pack` into `./artifacts`. | @@ -54,7 +54,7 @@ the run with the failing module's output. with `release-mode: NuGet`, which packs, publishes to NuGet and creates the `v` GitHub release. Both workflows pin `dotnet-version` to the SDK in `global.json` (`11.0.100-rc.1.26425.128`); keep them in -sync when the SDK is bumped, and keep `purview-build.json` pointing at `src/WSLTestContainers.slnx`. +sync when the SDK is bumped, and keep `purview-build.json` pointing at `src/Containers.slnx`. The shared workflow runs on **`ubuntu-latest`**, so the Linux agent builds the portable `net10.0` projects and the `net11.0-windows10.0.19041.0` test projects. That only works because `src/Directory.Build.props` diff --git a/docs/wiki/Testing.md b/docs/wiki/Testing.md index 6c32b2d..329d114 100644 --- a/docs/wiki/Testing.md +++ b/docs/wiki/Testing.md @@ -100,8 +100,6 @@ real coverage — the host check is what makes that visible. `actionlint` is tol minutes, because WSLC serialises container operations. Slow-test warnings while it runs are expected. - Integration tests skip themselves when the host lacks the required WSL/WSLC components, so a machine without WSLC can still run the unit suites. -- `spikes/WslcSpikes` is a manual investigation harness (`dotnet run --project spikes/WslcSpikes -- sfull`, - or `s1`..`s14` for individual behaviour probes); it is not part of the test run. ## Verifying the consumer contract @@ -117,7 +115,7 @@ throwaway consumer projects for every documented outcome (see outside the `[Category=Unit]` filter because it packs and restores from nuget.org: ```powershell -just verify-consumers # 19 consumer projects, all assertions +just verify-consumers # 21 consumer projects, all assertions just verify-consumers -Keep # same, keeping the generated projects for inspection ``` diff --git a/docs/wiki/Wait-Strategies.md b/docs/wiki/Wait-Strategies.md index fae89b5..11ff773 100644 --- a/docs/wiki/Wait-Strategies.md +++ b/docs/wiki/Wait-Strategies.md @@ -2,7 +2,7 @@ Composable readiness waits. **A started WSLC container is not necessarily a ready service** — `Container.Start()` returns once the init process is running; readiness is checked separately. -> **Status: implemented (Phase 2).** Verified by integration tests (`tests/Wsl.IntegrationTests/WaitStrategyTests.cs`). +> **Status: implemented.** Verified by integration tests (`tests/Wsl.IntegrationTests/WaitStrategyTests.cs`). ## Model @@ -37,7 +37,7 @@ WaitStrategy.WithTimeout / .WithInterval / .WithRetries (fluent) `ForContainerRunning()` — checks `Container.State == Running`. This only means the init process started; it is NOT a service-readiness check. ### TCP -`ForTcpPort(8080)` — connects to `127.0.0.1:`. Refused/reset/timeout ⇒ not ready. IPv4 only (WSLC maps IPv4 loopback only — EXP S12). +`ForTcpPort(8080)` — connects to `127.0.0.1:`. Refused/reset/timeout ⇒ not ready. IPv4 only (WSLC maps IPv4 loopback only). ### HTTP ```csharp diff --git a/docs/wiki/Wslc-Api-Investigation.md b/docs/wiki/Wslc-Api-Investigation.md deleted file mode 100644 index ed727a3..0000000 --- a/docs/wiki/Wslc-Api-Investigation.md +++ /dev/null @@ -1,202 +0,0 @@ -# WSLC API investigation - -Findings from Phase 0 investigation of **`Microsoft.WSL.Containers` v3.0.1** (NuGet) against **WSLC 3.0.1.0** (`wslc` CLI), on Windows 10.0.28020 / WSL 3.0.1.0 / kernel 6.18.40.1-1. - -Every statement is tagged: - -| Tag | Meaning | -|---|---| -| `DOC` | Official docs (learn.microsoft.com, wsl.dev/api-reference) | -| `SRC` | Verified against `wslcsdkcs.dll` metadata (dotnet-inspect) | -| `EXP` | Experimentally verified by `spikes/WslcSpikes` on this machine | -| `TEST` | Assumed; needs a spike before relying on it | - -## Package shape - -- Package: `Microsoft.WSL.Containers` v3.0.1, TFM `net8.0-windows10.0.19041.0`. `SRC` -- Ships: `lib/net8.0-windows10.0.19041.0/wslcsdkcs.dll` (C#/WinRT projection), `runtimes/win-x64/native/wslcsdk.dll`, `runtimes/win-arm64/native/wslcsdk.dll`, `winmd/Microsoft.WSL.Containers.winmd`, build targets. `SRC` -- Consuming projects must set `PlatformTarget=x64` (or `arm64`) or a matching `RuntimeIdentifier`; otherwise the native `wslcsdk.dll` is not copied. The build target errors if neither is set. `SRC`, `EXP` -- The projection references `Microsoft.Windows.SDK.NET` **10.0.26100.79**. Targeting `net*-windows10.0.19041.0` pulls `Microsoft.Windows.SDK.NET.Ref 10.0.19041.38` by default, which is **lower** and fails with `CS1705`. Fix: set `10.0.26100.80` (`.79` is not published). `SRC`, `EXP` - -## Namespace - -`using Microsoft.WSL.Containers;` `SRC`, `DOC` - -## Types (21 classes, 1 struct, 14 enums, 4 delegates) - -Key surface (SRC): - -``` -WslcService (static, no public ctor) - static ServiceVersion GetVersion() - static IReadOnlyList GetMissingComponents() - static void InstallWithDependencies(InstallOptions) - static IAsyncActionWithProgress InstallWithDependenciesAsync(InstallOptions) - // NOTE: there is NO GetInfoAsync in 3.0.1 - -Session : IDisposable - ctor(SessionSettings) - void Start(); void Terminate(); void Dispose(); - Container CreateContainer(ContainerSettings) - Container OpenContainer(string nameOrId, ProcessOutputMode) - void PullImage(PullImageOptions); IAsyncActionWithProgress PullImageAsync(PullImageOptions) - void ImportImage(path, imageName); ImportImageAsync(path, imageName) - void LoadImage(path); LoadImageAsync(path) - void PushImage(PushImageOptions); PushImageAsync(PushImageOptions) - void DeleteImage(string nameOrId); void TagImage(TagImageOptions) - IReadOnlyList GetImages() - void CreateVhdVolume(VhdOptions); void DeleteVhdVolume(string name) - AuthenticateResult Authenticate(Uri serverAddress, string username, string password) - event SessionTerminationHandler Terminated; event ProcessCrashHandler ProcessCrashed - -Container : IDisposable - string Id; ContainerState State; Process InitProcess - void Start(); void Stop(Signal, TimeSpan); void Delete(DeleteContainerOption) - Process CreateProcess(ProcessSettings); string Inspect() - -Process : IDisposable - uint Pid; ProcessState State; int ExitCode - void Start(); void Signal(Signal) - IOutputStream GetInputStream() // requires ProcessSettings.EnableStandardInput - IInputStream GetOutputStream(ProcessOutputHandle) // requires OutputMode.Stream - event ProcessOutputHandler OutputReceived (byte[]); ErrorReceived (byte[]); event ProcessExitHandler Exited (int) -``` - -Settings / options (SRC): - -- `SessionSettings(name, storagePath)` + `CpuCount?`, `MemorySizeInMB?`, `EnableGpu`, `Timeout?`, `VhdRequirements`, `Name`, `StoragePath` -- `ContainerSettings(imageName)` + `Name`, `HostName`, `DomainName`, `InitProcess`, `PortMappings(IList)`, `Volumes(IList)`, `NamedVolumes(IList)`, `NetworkingMode(ContainerNetworkingMode?)`, `Privileged`, `EnableGpu`, `EnableAutoRemove` -- `ProcessSettings()` + `CommandLine(IList)`, `EnvironmentVariables(IDictionary)`, `WorkingDirectory`, `OutputMode`, `EnableStandardInput` -- `ContainerPortMapping(ushort windowsPort, ushort containerPort, PortProtocol)` + `WindowsAddress(HostName)` -- `ContainerVolume(windowsPath, containerPath, readOnly)` -- `ContainerNamedVolume(name, containerPath, readOnly)` -- `PullImageOptions(uri)` + `RegistryAuth`; `PushImageOptions(image, registryAuth)`; `TagImageOptions(image, repository, tag)` -- `ImageInfo{Name, Sha256(IBuffer), Size, CreatedTimestamp}` -- `ImageProgress{Id, Status(ImageProgressStatus), CurrentBytes, TotalBytes}` -- `VhdOptions(name, size, type)` + `Owner(VhdOwner{Uid,Gid})` - -Enums (SRC): `ContainerState{Invalid=0,Created,Running,Exited,Deleted}` · `ProcessState{Unknown,Running,Exited,Signalled}` · `ProcessOutputMode{Discard=0,Stream,Event}` · `ProcessOutputHandle{StandardOutput=1,StandardError=2}` · `Signal{None,SIGHUP=1,SIGINT,SIGQUIT,SIGKILL=9,SIGTERM=15}` · `DeleteContainerOption{None=0,Force}` · `PortProtocol{TCP=0,UDP}` · **`ContainerNetworkingMode{None=0,Bridged=1}`** · `Component{VirtualMachinePlatform=1,WslPlatform=2,SdkNeedsUpdate=4}` · `IdentityTokenType{Unknown,Token,Credentials}` · `SessionTerminationReason{Unknown,Shutdown,Crashed}` · `ImageProgressStatus{Unknown,Pulling,Waiting,Downloading,Verifying,Extracting,Complete}` · `VhdType{Dynamic=0,Fixed}` · `Error{...negative HRESULTs, see below}` - -## Output modes (DOC, EXP) - -- `OutputReceived` / `ErrorReceived` require `ProcessOutputMode.Event`. -- `GetOutputStream(handle)` requires `ProcessOutputMode.Stream`. -- `Exited` fires in all modes. -- Output mode is a single value per process — capture and stream are mutually exclusive. -- Callbacks deliver `byte[]` chunks, **not lines**. (EXP: echo produced one chunk; multi-chunk/UTF-8-split behaviour not stress-tested, but the API contract is chunked.) - -## Container.Start() semantics (DOC, EXP) - -- `Container.Start()` starts the init process. If `OutputMode` is `Event`/`Stream`, it auto-attaches. There is **no flags parameter**. -- `Process.Start()` is only for secondary processes (`Container.CreateProcess`); the init process is started by `Container.Start()`. -- If the init command does not exist, `Container.Start()` throws `ArgumentException` (`HResult 0x80070057`) whose message contains the OCI runtime error (`runc create failed: ... exec: "...": no such file or directory`). `EXP` -- `InitProcess` is only available when `ContainerSettings.InitProcess` was configured. - -## Lifecycle & error semantics (EXP) - -- Session start is fast (~20-30 ms) because the WSL VM is already running under the session manager. -- **Session names are machine-unique.** Creating a second session with an active name throws `COMException 0x800700B7` ("Cannot create a file when that file already exists"). `EXP` -- **A terminated/disposed session name is immediately reusable.** `EXP` -- `Session.Dispose()` alone removes the session from the session manager and frees the name while the process is still alive. `EXP (S14)` -- **If a process exits without disposing its session, the session is orphaned** — it remains in `wslc info` with a dead `Creator PID` and the name stays reserved. It does **not** block other session names, nor reuse of the same storage path. `EXP (S10, S13)` -- `Container.Stop(signal, timeout)` is **idempotent** (stopping an already-stopped container succeeds). `EXP` -- `Container.Stop(signal, timeout)` can **block far longer than the `timeout` argument** (observed: minutes with `SIGTERM`/10 s while the session held several containers). Because the library serialises session operations, a single slow `Stop` stalls every other container operation in that process. `EXP` -- `Container.Delete(...)` on an already-deleted container throws `COMException 0x80010108` (`RPC_E_DISCONNECTED`). `EXP` -- `OpenContainer` on a missing container throws `COMException` with `HResult == (int)Error.ContainerNotFound` (`0x80040603`, i.e. `-2147219965`). `EXP` — matches docs. -- `Delete(DeleteContainerOption.Force)` works on a running container (state → `Deleted`). `EXP` -- `OpenContainer(nameOrId, ProcessOutputMode)` can reopen a running container and operate on the reopened handle. `EXP` - -`Error` enum values (SRC): `ImageNotFound=-2147219967, ContainerPrefixAmbiguous=-2147219966, ContainerNotFound=-2147219965, VolumeNotFound=-2147219964, ContainerNotRunning=-2147219963, ContainerIsRunning=-2147219962, SessionReserved=-2147219961, InvalidSessionName=-2147219960, NetworkNotFound=-2147219959, WindowsUpdateSearchFailed=-2147219958, SdkUpdateNeeded=-2147219957, ContainerDisabled=-2147219956, RegistryBlockedByPolicy=-2147219955, VolumeNotAvailable=-2147219954, ContainerDeleted=-2147219949`. Exceptions surface as `COMException`/`ArgumentException` with `HResult` matching these values. - -## Image management (EXP) - -- `PullImageAsync` returns an awaitable `IAsyncActionWithProgress`; set `Progress` before awaiting. -- **Image store is keyed by the session storage path** (the storage VHD), not by session instance or session name: - - Pull in session A on path P → a new session B with a different name on the SAME path P sees the image. - - A session C on a DIFFERENT path sees an empty store. - `EXP (S2)` -- Images therefore persist across session restarts and even across process crashes **as long as the storage path is reused**. Warm image cache = reuse a stable storage path. -- Concurrent `PullImageAsync` calls for the same image all succeed but are **not deduplicated** (each performs its own pull). The library must dedupe. `EXP (S11)` -- alpine pull ~2-3 s cold, python:3-alpine ~2-4 s cold, alpine ~0.7 s warm (already in store, still contacts registry). `EXP` - -## Port mappings (EXP) - -- `ContainerPortMapping(hostPort, containerPort, protocol)`. -- **`NetworkingMode` MUST be `Bridged` when port mappings are used.** With the default (`null`, which maps to Docker `NetworkMode: "none"`), `CreateContainer` throws `ArgumentException 0x80070057`. `EXP` -- **`windowsPort = 0` requests a random host port natively.** The assigned port appears in `Inspect()` → `Ports."8080/tcp"[0].HostPort`. This is race-free by construction. `EXP` -- Default bind address is **IPv4 loopback only** (`HostIp: "127.0.0.1"`); IPv6 `::1` does not connect. `EXP (S12)` -- Setting `WindowsAddress = new HostName("127.0.0.1")` binds explicitly; other addresses untested. -- UDP port mappings are **not implemented** (docs: `E_NOTIMPL`). -- Reusing the same host port across two containers in one session → `0x80072740` ("Only one usage of each socket address...") at `Start`. Host ports are exclusive per host. `EXP` - -## Inspect() schema (EXP) - -`Container.Inspect()` returns **Docker-compatible JSON**: -- `State.Status`/`State.Running`/`State.ExitCode` -- `HostConfig.NetworkMode` (`none` | `bridge`) -- `NetworkSettings.Networks.bridge.IPAddress` (e.g. `172.17.0.2`), `Gateway` (`172.17.0.1`) -- `Ports."/tcp"[].HostIp/HostPort` -- `Mounts[]` (`Type`, `Source`, `Destination`, `ReadWrite`) -- `Config.Cmd`, `Config.Entrypoint`, `Config.Env`, `Config.Image`, `Config.Labels`, `Config.WorkingDir` - -The library parses this for `GetMappedPublicPort` and container IP. - -## Networking (EXP) - -- Default `NetworkingMode` (null) → `NetworkMode "none"`: the container has **no network** (no IP). -- `ContainerNetworkingMode.None` → same, no network. -- `ContainerNetworkingMode.Bridged` → container gets a `172.17.0.x` address; host→container via mapped ports works. -- **Container-to-container: reachable by IP but NOT by name.** `wget http://alpha:8080/` and `wget http://:8080/` both fail with "bad address"; `/etc/hosts` contains only localhost plus the container's own `IP `. There is no Docker-style DNS/alias. `EXP (S9)` -- The managed API has **no network objects** (no `network create/connect`), unlike the CLI. - -## Processes / exec (EXP) - -- Secondary processes via `Container.CreateProcess(ProcessSettings)` then `process.Start()`. -- Env vars (`EnvironmentVariables`), `WorkingDirectory`, and exit codes all work. -- stdin works when `EnableStandardInput = true`, writing via `Windows.Storage.Streams.DataWriter` over `process.GetInputStream()`; closing the writer closes stdin. `EXP` -- stderr is distinct from stdout in `Event` mode. -- Avoid shell: `CommandLine` is passed as argv directly. - -## Mounts & volumes (EXP) - -- `ContainerVolume(windowsPath, containerPath, readOnly)` = Windows bind mount; read-only is enforced (`touch` fails, "Read-only file system"). -- `ContainerNamedVolume(name, containerPath, readOnly)` = session VHD volume. -- **Named volumes are auto-provisioned on first reference** — `CreateVhdVolume` is not required before using a named volume. A second container mounting the same named volume sees the first container's data (shared). `EXP` -- `CreateVhdVolume` fails with `0x800700B7` if the volume already exists (e.g. auto-created). Use `CreateVhdVolume` to pre-provision (with explicit size/owner) before any container references the name. -- `DeleteVhdVolume(name)` works after the volume's containers are removed. - -### Image VOLUMEs are auto-provisioned on ext4 (EXP, Phase 6) - -- WSLC auto-mounts image-declared `VOLUME` paths onto an **ext4 device** (e.g. `/dev/sdc`) with the image's intended ownership (RabbitMQ's `/var/lib/rabbitmq` is `rabbitmq:rabbitmq`, mode 1777). -- Bind-mounting a Windows directory onto such a path **replaces** it with **drvfs** (`virtiofs`), which ignores Unix `chown`/`chmod`. Permission-sensitive images (e.g. RabbitMQ's `.erlang.cookie`) then fail. Do not bind-mount over image volumes unless intended. -- `Microsoft.Data.SqlClient` hangs on the IPv6 `::1` address that `localhost` resolves to (WSLC maps IPv4 loopback only, EXP S12); use `127.0.0.1` explicitly for SQL Server. - -### Concurrent exec (secondary) processes are safe; short-lived init processes are not (EXP, S19) - -- Starting many exec processes concurrently across containers in one session works fine (S19: 6 containers × 5 concurrent execs, 0 failures). -- But a shared session is **slow under parallel load**: WSLC calls are serialized, so every container test in the loaded suite takes 100-170s instead of ~10s. A container whose init process is short-lived (`/bin/sleep 60` or less) **exits before a delayed `exec`/`stop` runs**, and `Process.Start()` then throws `COMException` "Container '...' is not running". -- Lesson for parallel integration tests: use a long-lived init process (`/bin/sleep 300`) whenever the test later execs into, or asserts the Running state of, the container. - -### A session exclusively locks its storage VHD (EXP, S18) - -- Each session's image store lives in `{storagePath}\storage.vhdx`. A running session locks that VHD; a **concurrent** session on the same path fails reads with `0x80070020` (`ERROR_SHARING_VIOLATION`, "file is being used by another process"). The VHD is opened **lazily on the first store access** (e.g. the first `GetImages()` call), not at session creation or `Session.Start()`, so the violation can surface well after `Session.Start()` succeeded. `EXP` -- **Sequential** reuse works: after a session ends, a new session on the same path sees the previous session's images (EXP S2/S13). There is no global/shared image cache in WSLC, so sharing images across sessions requires a shared storage path — safe only when sessions do not overlap. The library defaults to a shared store and falls back to an isolated per-process store on contention. - -## Known gaps in the C# projection (DOC) - -- Raw-handle-based `ImportImage`/`LoadImage` overloads not projected (file-path variants are). -- Raw native handles (`WslcGetSessionTerminationEvent`, `WslcGetProcessExitEvent`, `WslcGetProcessIOHandle`) wrapped as events/streams. -- `WslcContainerStartFlags` not exposed; `Start()` auto-attaches for `Event`/`Stream`. -- UDP port mappings and fixed VHDs return `E_NOTIMPL`. -- `WslcService` has no `GetInfoAsync`; runtime info = `GetVersion()` + `GetMissingComponents()`. - -## Documentation vs implementation discrepancies (recorded) - -| Docs (overview page) | Actual 3.0.1 assembly | -|---|---| -| `ComponentFlags` | `Component` (enum, `IReadOnlyList`) | -| `MemoryMB` | `MemorySizeInMB` | -| `CmdLine` | `CommandLine` | -| `DeleteContainerFlags` | `DeleteContainerOption` | - -The C# core-class reference pages (wsl.dev) match the assembly. Prefer the assembly. \ No newline at end of file diff --git a/docs/wiki/Wslc-Capability-Matrix.md b/docs/wiki/Wslc-Capability-Matrix.md deleted file mode 100644 index fe6c262..0000000 --- a/docs/wiki/Wslc-Capability-Matrix.md +++ /dev/null @@ -1,59 +0,0 @@ -# WSLC capability matrix - -Comparison of important Testcontainers-for-.NET capabilities against WSLC (`Microsoft.WSL.Containers` 3.0.1) and the planned implementation status in this library. - -Legend: **Implemented** = planned in this library; **Mapped** = native WSLC API exists; **Gap** = no managed API (CLI-only or missing). - -| Capability | Testcontainers | WSLC API | Implemented | Notes | -|---|---|---|---|---| -| Engine / session management | Docker daemon | `Session` (per-process VM host) | ✅ | session-scoped; image store keyed by storage path | -| Pull image + progress | ✅ | `PullImageAsync` | ✅ | `IAsyncActionWithProgress` | -| Ensure image exists | ✅ | `GetImages()` + pull | ✅ | per-path store; `PullPolicy.Missing/Always/Never` | -| List / delete / tag images | ✅ | `GetImages` / `DeleteImage` / `TagImage` | ✅ | | -| Import / load image | ✅ | `ImportImage*` / `LoadImage*` | ⚠️ | file-path based only | -| Registry authentication | ✅ | `Session.Authenticate` → `RegistryAuth` token | ✅ | `RegistryCredentials` (Phase 7); secrets redacted | -| Container create / start / stop / delete | ✅ | `CreateContainer` / `Start` / `Stop` / `Delete` | ✅ | no restart; stop is idempotent | -| Environment variables | ✅ | `ProcessSettings.EnvironmentVariables` | ✅ | applies to init and exec processes | -| Working directory | ✅ | `ProcessSettings.WorkingDirectory` | ✅ | | -| Entrypoint vs command | ✅ separate fields | ❌ single `CommandLine` argv | ⚠️ | concatenate entrypoint+command; document | -| Hostname / domainname | ✅ | `HostName` / `DomainName` | ✅ | hostname is not resolvable between containers | -| Host port mapping | ✅ | `ContainerPortMapping` | ✅ | requires `NetworkingMode=Bridged` | -| Random host ports | ✅ `-p 0:80` | ✅ `windowsPort=0` | ✅ | **native, race-free**; read from `Inspect()` | -| UDP ports | ✅ | ❌ `E_NOTIMPL` | ❌ | reject at validation with clear error | -| Bind mounts | ✅ | `ContainerVolume` | ✅ | read-only supported | -| Named volumes | ✅ | `ContainerNamedVolume` + VHD | ✅ | auto-provisioned on first reference | -| Container-to-container networking | ✅ networks | ⚠️ by IP only (Bridged) | ⚠️ | no DNS/aliases; no network objects | -| Exec (run command in container) | ✅ | `Container.CreateProcess` + `Process.Start` | ✅ | argv passthrough, no shell by default | -| Exec stdout/stderr | ✅ | events (`Event`) / streams (`Stream`) | ✅ | | -| Exec stdin | ✅ | `GetInputStream()` (`EnableStandardInput`) | ✅ | | -| Init-process logs | ✅ `logs` | events/streams on `InitProcess` | ✅ | library buffers + tails | -| Health check (`HEALTHCHECK`) | ✅ inspect | ❌ managed API (CLI only) | ❌ | use wait strategies instead | -| Resource limits (cpus/mem per container) | ✅ | ❌ session-level only | ❌ | `SessionSettings.CpuCount/MemorySizeInMB` | -| Privileged mode | ✅ | `ContainerSettings.Privileged` | ✅ | | -| GPU access | ✅ | `ContainerSettings.EnableGpu` / session `EnableGpu` | ✅ | | -| Copy files into container | ✅ `cp` | ❌ managed API (CLI only) | ❌ | | -| Build images | ✅ | ❌ managed API (CLI/build targets only) | ❌ | `WslcImage` MSBuild items exist | -| Export/save images | ✅ | ❌ managed API (CLI only) | ❌ | | -| Container inspect | ✅ | `Container.Inspect()` (Docker JSON) | ✅ | used for ports/IP | -| Container stats | ✅ `stats` | ❌ managed API (CLI only) | ❌ | | -| Events | ✅ | ❌ managed API (CLI only) | ❌ | session `Terminated`/`ProcessCrashed` only | -| User (uid/gid) | ✅ | ❌ `ProcessSettings`/`ContainerSettings` | ❌ | CLI `--user` only | -| TTY | ✅ | ❌ | ❌ | no TTY in managed API | -| Labels | ✅ | ❌ `ContainerSettings` | ❌ | CLI `--label` only | -| DNS options | ✅ | ❌ `ContainerSettings` | ❌ | CLI `--dns*` only | -| tmpfs / shm-size / ulimit | ✅ | ❌ | ❌ | CLI only | -| Copy between containers | ✅ | ❌ | ❌ | | -| Auto-remove | ✅ `--rm` | `ContainerSettings.EnableAutoRemove` | ✅ | | -| Reuse (Ryuk `testcontainers.properties` reuse) | ✅ | n/a (warm storage-path reuse) | ✅ | image cache + warm sessions | -| Resource reaper | ✅ Ryuk sidecar | native session manager | ⚠️ | Dispose frees session; orphans need CLI sweep | - -## Not implemented by WSLC managed API (do not fake) - -1. UDP port mappings — `E_NOTIMPL`. -2. Fixed VHDs — `E_NOTIMPL` (dynamic only). -3. User-defined networks / network objects — CLI only. -4. Container health checks, stats, events, cp, export/save/build — CLI only. -5. Per-container CPU/memory limits — session-level only. -6. TTY, `--user`, labels, DNS options, tmpfs/shm/ulimit in `ContainerSettings`/`ProcessSettings`. - -Where a Testcontainers feature has no WSLC equivalent, the library must either (a) provide the closest supported behaviour and document it, or (b) fail fast with a clear `ContainerNotSupportedException`. \ No newline at end of file diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 8b7bdd1..40e77a4 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -13,5 +13,3 @@ - [Release Flow](Release-Flow.md) - [Contributing Modules](Contributing-Modules.md) - [Contributing](Contributing.md) -- [WSLC API Investigation](Wslc-Api-Investigation.md) -- [WSLC Capability Matrix](Wslc-Capability-Matrix.md) diff --git a/purview-build.json b/purview-build.json index 07f44fd..ff82b02 100644 --- a/purview-build.json +++ b/purview-build.json @@ -1,6 +1,6 @@ { "Build": { - "Solution": "src/WSLTestContainers.slnx", + "Solution": "src/Containers.slnx", "TestRoot": "src/tests", "TestPatterns": "*UnitTests.csproj", "TestProjects": "*Tests.csproj", diff --git a/spikes/DynamicLoadSpike/DynamicLoadSpike.csproj b/spikes/DynamicLoadSpike/DynamicLoadSpike.csproj deleted file mode 100644 index 8c5d55d..0000000 --- a/spikes/DynamicLoadSpike/DynamicLoadSpike.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - - Exe - false - net10.0 - enable - enable - x64 - true - $(NoWarn);CA1031;CA1304;CA1305;CA1307;CA1310;CA1849;CA2000;CA2027;CA1707;CA1822;CA1062;CA1515;CA1861;CA1819;IDE0040;IDE0060;IDE0074;IDE0007;IDE0305;CS4014 - - - - - $(NuGetPackageRoot)microsoft.wsl.containers/3.0.1 - $(NuGetPackageRoot)microsoft.windows.sdk.net.ref/10.0.26100.80 - - - - - - - - - diff --git a/spikes/DynamicLoadSpike/DynamicLoader.cs b/spikes/DynamicLoadSpike/DynamicLoader.cs deleted file mode 100644 index c9a4b0d..0000000 --- a/spikes/DynamicLoadSpike/DynamicLoader.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.Loader; - -namespace DynamicLoadSpike; - -/// -/// Loads the WSLC projection (and the native SDK it P/Invokes) from the local payload folder and calls -/// into Microsoft.WSL.Containers.WslcService by reflection. This is the phase 0 proof that a -/// platform-neutral net10.0 process can drive WSLC on a Windows host. -/// -internal static class DynamicLoader -{ - const string ProjectionFile = "wslcsdkcs.dll"; - const string ServiceType = "Microsoft.WSL.Containers.WslcService"; - - public static Task RunAsync(string payloadDir) - { - if (!OperatingSystem.IsWindows()) - { - Console.WriteLine("[dynamic] skipped: this host is not Windows, so the WSLC payload is never loaded."); - Console.WriteLine( - "[dynamic] (a real build would report the 'wsl' backend as unavailable here and fall through to Docker)." - ); - return Task.FromResult(0); - } - - var projection = Path.Combine(payloadDir, ProjectionFile); - if (!File.Exists(projection)) - { - Console.WriteLine($"[dynamic] FAILED: projection not found at {projection}"); - return Task.FromResult(1); - } - - try - { - var context = new PayloadLoadContext(payloadDir); - context.Resolving += (_, name) => context.TryLoad(name); - - var assembly = context.LoadFromAssemblyPath(projection); - Console.WriteLine($"[dynamic] loaded assembly: {assembly.FullName}"); - - var service = assembly.GetType(ServiceType, throwOnError: true)!; - var version = service - .GetMethod("GetVersion", BindingFlags.Public | BindingFlags.Static)! - .Invoke(null, null); - Console.WriteLine($"[dynamic] {ServiceType}.GetVersion() = {Format(version)}"); - PrintProperties("version", version); - - var missing = service - .GetMethod("GetMissingComponents", BindingFlags.Public | BindingFlags.Static)! - .Invoke(null, null); - Console.WriteLine($"[dynamic] {ServiceType}.GetMissingComponents() = {Format(missing)}"); - var missingCount = PrintItems("missing", missing); - - // A statically-available runtime is not enough: the feature delegates real work through the - // WSLC projection, so prove a session can be created, started and released from here too. - Console.WriteLine( - missingCount == 0 - ? "[dynamic] host is WSLC-capable; probing a real session." - : "[dynamic] host is NOT WSLC-capable; skipping the session probe." - ); - var sessionOk = missingCount == 0 && ProbeSession(assembly); - - Console.WriteLine( - $"[dynamic] RESULT: {(sessionOk || missingCount != 0 ? "PASS" : "FAILED")} - a plain " - + $"{RuntimeInformation.FrameworkDescription} net10.0 process drove WSLC." - ); - return Task.FromResult(sessionOk || missingCount != 0 ? 0 : 1); - } - catch (Exception exception) - { - Console.WriteLine($"[dynamic] RESULT: FAILED - {exception.GetType().Name}: {exception.Message}"); - for (var inner = exception.InnerException; inner is not null; inner = inner.InnerException) - { - Console.WriteLine($"[dynamic] inner: {inner.GetType().Name} (0x{inner.HResult:X8}): {inner.Message}"); - } - - return Task.FromResult(1); - } - } - - static string Format(object? value) => value?.ToString() ?? ""; - - static void PrintProperties(string label, object? value) - { - if (value is null) - { - return; - } - - foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - Console.WriteLine($"[dynamic] {label}.{property.Name} = {Format(property.GetValue(value))}"); - } - } - - static int PrintItems(string label, object? value) - { - var count = 0; - if (value is System.Collections.IEnumerable items and not string) - { - foreach (var item in items) - { - Console.WriteLine($"[dynamic] {label}[*] = {Format(item)}"); - count++; - } - } - - return count; - } - - static bool ProbeSession(Assembly assembly) - { - var name = $"dynload-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..8]}"; - var storage = Path.Combine(Path.GetTempPath(), "dynload-spike", name); - object? session = null; - try - { - var settingsType = assembly.GetType("Microsoft.WSL.Containers.SessionSettings", throwOnError: true)!; - var settings = Activator.CreateInstance(settingsType, name, storage); - var sessionType = assembly.GetType("Microsoft.WSL.Containers.Session", throwOnError: true)!; - session = Activator.CreateInstance(sessionType, settings); - - sessionType.GetMethod("Start")!.Invoke(session, null); - Console.WriteLine($"[dynamic] session started: {name} (storage={storage})"); - PrintProperties("session", session); - - sessionType.GetMethod("Terminate")!.Invoke(session, null); - Console.WriteLine("[dynamic] session terminated."); - return true; - } - catch (Exception exception) - { - Console.WriteLine($"[dynamic] session probe FAILED: {exception.GetType().Name}: {exception.Message}"); - for (var inner = exception.InnerException; inner is not null; inner = inner.InnerException) - { - Console.WriteLine($"[dynamic] inner: {inner.GetType().Name} (0x{inner.HResult:X8}): {inner.Message}"); - } - - return false; - } - finally - { - (session as IDisposable)?.Dispose(); - TryDelete(storage); - } - } - - static void TryDelete(string directory) - { - try - { - if (Directory.Exists(directory)) - { - Directory.Delete(directory, recursive: true); - } - } - catch (Exception exception) - { - Console.WriteLine($"[dynamic] (cleanup) could not remove '{directory}': {exception.Message}"); - } - } -} - -/// -/// A dedicated load context so the projection and its Windows SDK dependencies resolve from the payload -/// folder (and the native wslcsdk.dll loads from there too), independently of the host app. -/// -internal sealed class PayloadLoadContext(string directory) : AssemblyLoadContext("wslc-payload", isCollectible: true) -{ - readonly string _directory = directory; - - public Assembly? TryLoad(AssemblyName name) - { - var candidate = Path.Combine(_directory, $"{name.Name}.dll"); - return File.Exists(candidate) ? LoadFromAssemblyPath(candidate) : null; - } - - protected override Assembly? Load(AssemblyName assemblyName) => TryLoad(assemblyName); - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var candidate = Path.Combine(_directory, $"{unmanagedDllName}.dll"); - return File.Exists(candidate) ? LoadUnmanagedDllFromPath(candidate) : IntPtr.Zero; - } -} diff --git a/spikes/DynamicLoadSpike/PayloadInspector.cs b/spikes/DynamicLoadSpike/PayloadInspector.cs deleted file mode 100644 index c39ae7b..0000000 --- a/spikes/DynamicLoadSpike/PayloadInspector.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; - -namespace DynamicLoadSpike; - -/// -/// Reads the WSLC projection's metadata without loading it, so the probe can report the projection's -/// real target framework and the exact dependency set (name + version) that a runtime payload has to -/// ship. Runs on any OS. -/// -internal static class PayloadInspector -{ - public static void Inspect(string path) - { - if (!File.Exists(path)) - { - Console.WriteLine($"[static] payload assembly not found: {path}"); - return; - } - - using var stream = File.OpenRead(path); - using var pe = new PEReader(stream); - if (!pe.HasMetadata) - { - Console.WriteLine("[static] payload has no CLI metadata (unexpected for a managed projection)."); - return; - } - - var md = pe.GetMetadataReader(); - var definition = md.GetAssemblyDefinition(); - Console.WriteLine($"[static] assembly : {md.GetString(definition.Name)} {definition.Version}"); - Console.WriteLine($"[static] file : {path}"); - - foreach (var handle in definition.GetCustomAttributes()) - { - var attribute = md.GetCustomAttribute(handle); - var name = AttributeTypeName(md, attribute); - if ( - !name.EndsWith("TargetFrameworkAttribute", StringComparison.Ordinal) - && !name.EndsWith("TargetPlatformAttribute", StringComparison.Ordinal) - ) - { - continue; - } - - var label = name[(name.LastIndexOf('.') + 1)..]; - Console.WriteLine($"[static] {label, -24}: {StringArgument(md, attribute) ?? ""}"); - } - - Console.WriteLine("[static] references :"); - foreach (var handle in md.AssemblyReferences) - { - var reference = md.GetAssemblyReference(handle); - Console.WriteLine($"[static] ref : {md.GetString(reference.Name)} {reference.Version}"); - } - } - - static string AttributeTypeName(MetadataReader md, CustomAttribute attribute) => - attribute.Constructor.Kind switch - { - HandleKind.MemberReference => TypeName( - md, - md.GetMemberReference((MemberReferenceHandle)attribute.Constructor).Parent - ), - HandleKind.MethodDefinition => TypeName( - md, - md.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType() - ), - _ => "", - }; - - static string TypeName(MetadataReader md, EntityHandle handle) => - handle.Kind switch - { - HandleKind.TypeReference => Define(md, md.GetTypeReference((TypeReferenceHandle)handle)), - HandleKind.TypeDefinition => Define(md, md.GetTypeDefinition((TypeDefinitionHandle)handle)), - _ => "", - }; - - static string Define(MetadataReader md, TypeReference reference) - { - var ns = md.GetString(reference.Namespace); - return string.IsNullOrEmpty(ns) ? md.GetString(reference.Name) : $"{ns}.{md.GetString(reference.Name)}"; - } - - static string Define(MetadataReader md, TypeDefinition definition) - { - var ns = md.GetString(definition.Namespace); - return string.IsNullOrEmpty(ns) ? md.GetString(definition.Name) : $"{ns}.{md.GetString(definition.Name)}"; - } - - // The first fixed argument of TargetFrameworkAttribute/TargetPlatformAttribute is a string. - static string? StringArgument(MetadataReader md, CustomAttribute attribute) - { - try - { - var reader = md.GetBlobReader(attribute.Value); - reader.ReadUInt16(); // attribute prolog 0x0001 - return reader.ReadSerializedString(); - } - catch - { - return null; - } - } -} diff --git a/spikes/DynamicLoadSpike/Program.cs b/spikes/DynamicLoadSpike/Program.cs deleted file mode 100644 index 18c899f..0000000 --- a/spikes/DynamicLoadSpike/Program.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; -using System.Runtime.Versioning; -using DynamicLoadSpike; - -// Phase 0 feasibility probe for "dynamic WSLC from a platform-neutral TFM". -// -// dotnet run --project spikes/DynamicLoadSpike -// -// The static inspection runs on any OS; the dynamic load only runs on Windows. Exit code is 0 when -// the probe reached a verdict (even if the verdict is "WSLC unavailable here") and 1 only when the -// harness itself failed, so the run is useful on Linux CI as well. - -var payload = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "payload")); - -Console.WriteLine("[env] ---------------------------------------------------------------"); -Console.WriteLine( - $"[env] tfm : {typeof(Program).Assembly.GetCustomAttribute()?.FrameworkName}" -); -Console.WriteLine($"[env] runtime : {Environment.Version}"); -Console.WriteLine($"[env] isWindows : {OperatingSystem.IsWindows()}"); -Console.WriteLine($"[env] is64bit : {Environment.Is64BitProcess}"); -Console.WriteLine($"[env] payloadDir : {payload}"); -Console.WriteLine(); - -Console.WriteLine("[static] ------------------------------------------------------------"); -PayloadInspector.Inspect(Path.Combine(payload, "wslcsdkcs.dll")); -Console.WriteLine(); - -Console.WriteLine("[dynamic] -----------------------------------------------------------"); -return await DynamicLoader.RunAsync(payload); diff --git a/spikes/PortableConsumerSpike/PortableConsumerSpike.csproj b/spikes/PortableConsumerSpike/PortableConsumerSpike.csproj deleted file mode 100644 index d6e8626..0000000 --- a/spikes/PortableConsumerSpike/PortableConsumerSpike.csproj +++ /dev/null @@ -1,52 +0,0 @@ - - - - Exe - false - net10.0 - enable - enable - x64 - true - $(NoWarn);CA1031;CA1304;CA1305;CA1307;CA1310;CA1849;CA2000;CA2027;CA1707;CA1822;CA1062;CA1515;CA1861;CA1819;IDE0040;IDE0060;IDE0074;IDE0007;IDE0305;CS4014 - - - - - - - - - - $(MSBuildThisFileDirectory)../../src/src/Wsl/bin/$(Configuration)/net10.0-windows10.0.19041.0/ - $(NuGetPackageRoot)microsoft.wsl.containers/3.0.1 - $(NuGetPackageRoot)microsoft.windows.sdk.net.ref/10.0.26100.80 - - - - - - - - - - - - - - - diff --git a/spikes/PortableConsumerSpike/Program.cs b/spikes/PortableConsumerSpike/Program.cs deleted file mode 100644 index 8b0a39d..0000000 --- a/spikes/PortableConsumerSpike/Program.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Purview.Containers; -using Purview.Containers.Waiting; -using Purview.Containers.Wsl; - -// Phase 1 acceptance probe: this project is a plain net10.0 consumer (no Windows target framework). -// Everything below is the documented, backend-neutral API - the "auto" story the feature exists for. - -Console.WriteLine($"[env] tfm : {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"); -Console.WriteLine($"[env] isWindows: {OperatingSystem.IsWindows()}"); -Console.WriteLine(); - -// A package consumer gets this from the generated module initializer; a project-reference consumer -// registers explicitly. -ContainerBackends.Register(WslContainerBackend.Create()); - -var info = await WslContainerRuntime.Instance.GetInfoAsync(); -Console.WriteLine($"[host] wsl available={info.IsAvailable} compatible={info.IsCompatible} version={info.Version}"); -foreach (var missing in info.MissingComponents) -{ - Console.WriteLine($"[host] missing: {missing}"); -} - -foreach (var backend in await ContainerBackends.ProbeAllAsync()) -{ - Console.WriteLine($"[probe] {backend.Name}: usable={backend.IsUsable} version={backend.Version}"); -} - -if (!info.IsAvailable || !info.IsCompatible) -{ - Console.WriteLine("[result] WSLC is not usable on this host; auto-selection would fall through to Docker."); - return 0; -} - -Console.WriteLine(); - -await using var container = new ContainerBuilder() - .WithImage("alpine:3.19") - .WithCommand("/bin/sh", "-c", "echo ready && sleep 20") - .WithPortBinding(8080, assignRandomHostPort: true) - .WithWaitStrategy(Wait.ForLogMessage("ready")) - .Build(); - -await container.StartAsync(); - -Console.WriteLine($"[container] name : {container.Name}"); -Console.WriteLine($"[container] id : {container.Id[..12]}"); -Console.WriteLine($"[container] port : {container.GetMappedPublicPort(8080)} -> 8080"); -Console.WriteLine($"[container] logs : {(await container.GetLogsAsync()).Trim()}"); -Console.WriteLine("[result] PASS - a net10.0 project ran a real WSLC container through the portable facade."); -return 0; diff --git a/spikes/WslcSpikes/Program.cs b/spikes/WslcSpikes/Program.cs deleted file mode 100644 index 237cbe7..0000000 --- a/spikes/WslcSpikes/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -using WslcSpikes; - -// Phase 0 spike harness. Each subcommand proves one WSLC behaviour. -// Usage: dotnet run --project spikes/WslcSpikes -- [args] - -if (args.Length == 0) -{ - Console.Error.WriteLine( - "No spike specified. Available: s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s10child s10child-norelease s11 s12 s13 s14 s18 s19 sfull" - ); - return 1; -} - -return args[0] switch -{ - "s1" => await SessionLifecycle.RunAsync(), - "s2" => await ImageCacheScope.RunAsync(), - "s3" => await ProcessIo.RunAsync(), - "s4" => await Ports.RunAsync(), - "s5" => await ExecProcess.RunAsync(), - "s6" => await MountsAndVolumes.RunAsync(), - "s7" => await CleanupBehaviour.RunAsync(), - "s8" => await ConcurrentContainers.RunAsync(), - "s9" => await Networking.RunAsync(), - "s10" => await OrphanSessions.RunAsync(), - "s10child" => await OrphanSessions.ChildAsync(args.Length > 1 ? args[1] : "wslcspike-orphan-child", dispose: true), - "s10child-norelease" => await OrphanSessions.ChildAsync( - args.Length > 1 ? args[1] : "wslcspike-orphan-child", - dispose: false - ), - "s11" => await ThreadSafety.RunAsync(), - "s12" => await BindAddress.RunAsync(), - "s13" => await ReopenOrphan.RunAsync(args), - "s14" => await DisposeSemantics.RunAsync(), - "s18" => await ConcurrentSharedPath.RunAsync(), - "s19" => await ConcurrentExecProcesses.RunAsync(), - "sfull" => await FullLifecycle.RunAsync(), - _ => UnknownSpike(args[0]), -}; - -static int UnknownSpike(string spike) -{ - Console.Error.WriteLine($"Spike '{spike}' is not implemented yet."); - return 2; -} diff --git a/spikes/WslcSpikes/SpikeSupport.cs b/spikes/WslcSpikes/SpikeSupport.cs deleted file mode 100644 index 6b4e656..0000000 --- a/spikes/WslcSpikes/SpikeSupport.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Diagnostics; -using System.Text; -using Microsoft.WSL.Containers; -using Windows.Foundation; -using Process = Microsoft.WSL.Containers.Process; - -namespace WslcSpikes; - -static class SpikeSupport -{ - public static string StorageRoot => - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WslcSpikes"); - - // All spikes share one storage path so the per-path image store is reused (only one pull per image). - public static string SharedStorage => Path.Combine(StorageRoot, "shared"); - - public static string RunSuffix { get; } = Guid.NewGuid().ToString("N")[..8]; - - public static string UniqueName(string prefix) => $"{prefix}-{RunSuffix}"; - - public static void PrintRuntimeInfo() - { - ServiceVersion version = WslcService.GetVersion(); - Console.WriteLine("[service] version: {0}.{1}.{2}", version.Major, version.Minor, version.Revision); - IReadOnlyList missing = WslcService.GetMissingComponents(); - Console.WriteLine( - "[service] missing components: {0}", - string.Join(", ", missing.Select(c => c.ToString()).DefaultIfEmpty("")) - ); - } - - public static Session StartSession( - string name, - string? storagePath = null, - uint? memoryMb = null, - uint? cpuCount = null - ) - { - string path = storagePath ?? SharedStorage; - SessionSettings settings = new SessionSettings(name, path) { MemorySizeInMB = memoryMb, CpuCount = cpuCount }; - Stopwatch sw = Stopwatch.StartNew(); - Session session = new Session(settings); - session.Start(); - sw.Stop(); - Console.WriteLine("[session] started '{0}' storage={1} in {2} ms", name, path, sw.ElapsedMilliseconds); - return session; - } - - public static async Task PullImageAsync(Session session, string image, IProgress? progress = null) - { - Stopwatch sw = Stopwatch.StartNew(); - IAsyncActionWithProgress op = session.PullImageAsync(new PullImageOptions(image)); - if (progress is not null) - { - op.Progress = (_, p) => progress.Report(p); - } - - await op; - sw.Stop(); - Console.WriteLine("[image] pulled {0} in {1} ms", image, sw.ElapsedMilliseconds); - } - - public static void PrintImages(Session session, string label) - { - IReadOnlyList images = session.GetImages(); - Console.WriteLine("[image] {0} ({1} entries):", label, images.Count); - foreach (ImageInfo image in images) - { - Console.WriteLine(" {0} size={1} created={2:o}", image.Name, image.Size, image.CreatedTimestamp); - } - } - - public static Container CreateContainer( - Session session, - string image, - string name, - IReadOnlyList commandLine, - ProcessOutputMode outputMode = ProcessOutputMode.Event, - Action? configure = null - ) - { - ProcessSettings initProcess = new ProcessSettings - { - CommandLine = new List(commandLine), - OutputMode = outputMode, - }; - ContainerSettings settings = new ContainerSettings(image) - { - Name = name, - InitProcess = initProcess, - EnableAutoRemove = false, - }; - configure?.Invoke(settings); - Container container = session.CreateContainer(settings); - Console.WriteLine("[container] created '{0}' id={1}", name, container.Id); - return container; - } - - public static ProcessCapture Capture(Process process) - { - StringBuilder stdout = new StringBuilder(); - StringBuilder stderr = new StringBuilder(); - TaskCompletionSource tcs = new TaskCompletionSource( - TaskCreationOptions.RunContinuationsAsynchronously - ); - process.OutputReceived += data => stdout.Append(Encoding.UTF8.GetString(data)); - process.ErrorReceived += data => stderr.Append(Encoding.UTF8.GetString(data)); - process.Exited += code => tcs.TrySetResult(code); - return new ProcessCapture(stdout, stderr, tcs.Task); - } - - public static async Task RunToExitAsync(Container container, TimeSpan? timeout = null) - { - ProcessCapture capture = Capture(container.InitProcess); - Stopwatch sw = Stopwatch.StartNew(); - container.Start(); - Task first = await Task.WhenAny(capture.Exit, Task.Delay(timeout ?? TimeSpan.FromSeconds(90))) - .ConfigureAwait(false); - if (first != capture.Exit) - { - Console.Error.WriteLine("[container] timed out waiting for exit; signalling SIGKILL"); - container.InitProcess.Signal(Signal.SIGKILL); - } - - int exit = await capture.Exit.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false); - sw.Stop(); - Console.WriteLine("[container] '{0}' exited code={1} in {2} ms", container.Id, exit, sw.ElapsedMilliseconds); - Console.WriteLine("[container] stdout: {0}", capture.Stdout); - Console.WriteLine("[container] stderr: {0}", capture.Stderr); - return exit; - } - - public static void Dump(Exception exception) - { - Console.WriteLine("[error] {0} (HResult=0x{1:X8})", exception.GetType().Name, exception.HResult); - Console.WriteLine(" {0}", exception.Message); - Exception? inner = exception.InnerException; - while (inner is not null) - { - Console.WriteLine( - " inner: {0} (HResult=0x{1:X8}) {2}", - inner.GetType().Name, - inner.HResult, - inner.Message - ); - inner = inner.InnerException; - } - } - - public static void Cleanup(Session session) - { - try - { - session.Terminate(); - Console.WriteLine("[cleanup] session terminated"); - } - catch (Exception ex) - { - Console.WriteLine("[cleanup] terminate failed: {0}", ex.Message); - } - - session.Dispose(); - } -} - -sealed record ProcessCapture(StringBuilder Stdout, StringBuilder Stderr, Task Exit); diff --git a/spikes/WslcSpikes/Spikes/BindAddress.cs b/spikes/WslcSpikes/Spikes/BindAddress.cs deleted file mode 100644 index 26743fc..0000000 --- a/spikes/WslcSpikes/Spikes/BindAddress.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.WSL.Containers; -using Windows.Networking; - -namespace WslcSpikes; - -// S12: Host bind address — default vs explicit WindowsAddress, IPv4 vs IPv6 localhost reachability. -static class BindAddress -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S12: host bind address ==="); - Session session = SpikeSupport.StartSession("wslcspike-s12"); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - string www = Path.Combine(SpikeSupport.StorageRoot, "s12-www"); - if (Directory.Exists(www)) - { - Directory.Delete(www, recursive: true); - } - - Directory.CreateDirectory(www); - File.WriteAllText(Path.Combine(www, "index.html"), "hello-bind\n"); - - Console.WriteLine("[s12] case 1: default WindowsAddress"); - Container c1 = SpikeSupport.CreateContainer( - session, - HttpSupport.ServerImage, - SpikeSupport.UniqueName("s12-default"), - HttpSupport.ServerArgs(8080), - configure: settings => - { - settings.NetworkingMode = ContainerNetworkingMode.Bridged; - settings.Volumes = new List { new ContainerVolume(www, "/tmp", false) }; - settings.PortMappings = new List - { - new ContainerPortMapping(0, 8080, PortProtocol.TCP), - }; - } - ); - c1.Start(); - string inspect1 = c1.Inspect(); - Console.WriteLine("[s12] inspect (default):\n{0}", Indent(inspect1)); - int? hp1 = HttpSupport.TryReadAssignedHostPort(inspect1); - if (hp1 is int p1) - { - bool v4 = await HttpSupport.WaitForTcpAsync(p1, TimeSpan.FromSeconds(30)); - bool v6 = await HttpSupport.WaitForTcp6Async(p1, TimeSpan.FromSeconds(10)); - Console.WriteLine("[s12] host port {0}: ipv4={1} ipv6={2}", p1, v4, v6); - } - - Console.WriteLine("[s12] case 2: explicit WindowsAddress 127.0.0.1"); - Container c2 = SpikeSupport.CreateContainer( - session, - HttpSupport.ServerImage, - SpikeSupport.UniqueName("s12-explicit"), - HttpSupport.ServerArgs(8080), - configure: settings => - { - settings.NetworkingMode = ContainerNetworkingMode.Bridged; - settings.Volumes = new List { new ContainerVolume(www, "/tmp", false) }; - settings.PortMappings = new List - { - new ContainerPortMapping(0, 8080, PortProtocol.TCP) - { - WindowsAddress = new HostName("127.0.0.1"), - }, - }; - } - ); - c2.Start(); - string inspect2 = c2.Inspect(); - Console.WriteLine("[s12] inspect (explicit 127.0.0.1):\n{0}", Indent(inspect2)); - int? hp2 = HttpSupport.TryReadAssignedHostPort(inspect2); - if (hp2 is int p2) - { - bool v4 = await HttpSupport.WaitForTcpAsync(p2, TimeSpan.FromSeconds(30)); - bool v6 = await HttpSupport.WaitForTcp6Async(p2, TimeSpan.FromSeconds(10)); - Console.WriteLine("[s12] host port {0}: ipv4={1} ipv6={2}", p2, v4, v6); - } - - c1.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - c1.Delete(DeleteContainerOption.None); - c1.Dispose(); - c2.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - c2.Delete(DeleteContainerOption.None); - c2.Dispose(); - Directory.Delete(www, recursive: true); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s12] S12 complete."); - return 0; - } - - private static string Indent(string json) - { - try - { - return System.Text.Json.JsonSerializer.Serialize( - System.Text.Json.JsonDocument.Parse(json).RootElement, - new System.Text.Json.JsonSerializerOptions { WriteIndented = true } - ); - } - catch (System.Text.Json.JsonException) - { - return json; - } - } -} diff --git a/spikes/WslcSpikes/Spikes/CleanupBehaviour.cs b/spikes/WslcSpikes/Spikes/CleanupBehaviour.cs deleted file mode 100644 index 0cfab48..0000000 --- a/spikes/WslcSpikes/Spikes/CleanupBehaviour.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S7: Stop/delete idempotency, error HResults, and force-delete while running. -static class CleanupBehaviour -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S7: cleanup idempotency + HResults ==="); - Session session = SpikeSupport.StartSession("wslcspike-s7"); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - - Console.WriteLine("[s7] case 1: stop then stop again"); - string nameA = SpikeSupport.UniqueName("s7a"); - Container c1 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - nameA, - new[] { "/bin/sleep", "300" } - ); - c1.Start(); - Console.WriteLine("[s7] started state={0}", c1.State); - c1.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - Console.WriteLine("[s7] after stop state={0}", c1.State); - try - { - c1.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - Console.WriteLine("[s7] second Stop succeeded (unexpected)"); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - Console.WriteLine("[s7] case 2: delete then delete again"); - c1.Delete(DeleteContainerOption.None); - Console.WriteLine("[s7] first Delete succeeded"); - try - { - c1.Delete(DeleteContainerOption.None); - Console.WriteLine("[s7] second Delete succeeded (unexpected)"); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - Console.WriteLine("[s7] case 3: OpenContainer of deleted container"); - try - { - using Container opened = session.OpenContainer(nameA, ProcessOutputMode.Event); - Console.WriteLine("[s7] OpenContainer succeeded (unexpected) id={0}", opened.Id); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - c1.Dispose(); - - Console.WriteLine("[s7] case 4: Force delete while running"); - string nameB = SpikeSupport.UniqueName("s7b"); - Container c2 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - nameB, - new[] { "/bin/sleep", "300" } - ); - c2.Start(); - Console.WriteLine("[s7] started state={0}", c2.State); - try - { - c2.Delete(DeleteContainerOption.Force); - Console.WriteLine("[s7] Force delete while running succeeded, state={0}", c2.State); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - c2.Dispose(); - - Console.WriteLine("[s7] case 5: OpenContainer of running container, then operate via handle"); - string nameC = SpikeSupport.UniqueName("s7c"); - Container c3 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - nameC, - new[] { "/bin/sleep", "300" } - ); - c3.Start(); - using (Container reopened = session.OpenContainer(nameC, ProcessOutputMode.Event)) - { - Console.WriteLine("[s7] reopened id={0} state={1}", reopened.Id, reopened.State); - reopened.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - Console.WriteLine("[s7] reopened stop, state={0}", reopened.State); - reopened.Delete(DeleteContainerOption.None); - Console.WriteLine("[s7] reopened delete succeeded"); - } - - c3.Dispose(); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s7] S7 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/ConcurrentContainers.cs b/spikes/WslcSpikes/Spikes/ConcurrentContainers.cs deleted file mode 100644 index c65e3e8..0000000 --- a/spikes/WslcSpikes/Spikes/ConcurrentContainers.cs +++ /dev/null @@ -1,133 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S8: Multiple containers started concurrently in one session, each with a distinct random host port. -static class ConcurrentContainers -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S8: concurrent containers ==="); - Session session = SpikeSupport.StartSession("wslcspike-s8"); - try - { - await SpikeSupport.PullImageAsync(session, HttpSupport.ServerImage); - string www = Path.Combine(SpikeSupport.StorageRoot, "s8-www"); - if (Directory.Exists(www)) - { - Directory.Delete(www, recursive: true); - } - - Directory.CreateDirectory(www); - File.WriteAllText(Path.Combine(www, "index.html"), "hello-concurrent\n"); - - int count = 4; - Container[] containers = new Container[count]; - ushort[] containerPorts = new ushort[count]; - for (int i = 0; i < count; i++) - { - ushort cport = (ushort)(8080 + i); - containerPorts[i] = cport; - int index = i; - containers[i] = SpikeSupport.CreateContainer( - session, - HttpSupport.ServerImage, - SpikeSupport.UniqueName($"s8-{index}"), - HttpSupport.ServerArgs(cport), - configure: settings => - { - settings.NetworkingMode = ContainerNetworkingMode.Bridged; - settings.Volumes = new List { new ContainerVolume(www, "/tmp", false) }; - settings.PortMappings = new List - { - new ContainerPortMapping(0, cport, PortProtocol.TCP), - }; - } - ); - } - - Console.WriteLine("[s8] starting {0} containers concurrently...", count); - Task[] starts = containers - .Select( - (c, i) => - Task.Run(() => - { - try - { - c.Start(); - return $"s8-{i}: ok"; - } - catch (Exception ex) - { - return $"s8-{i}: FAIL {ex.Message} (HResult=0x{ex.HResult:X8})"; - } - }) - ) - .ToArray(); - string[] startResults = await Task.WhenAll(starts); - foreach (string result in startResults) - { - Console.WriteLine("[s8] {0}", result); - } - - Console.WriteLine("[s8] retrying any failed containers sequentially..."); - for (int i = 0; i < count; i++) - { - if (startResults[i].EndsWith(": ok")) - { - continue; - } - - try - { - containers[i].Start(); - Console.WriteLine("[s8] s8-{0} started on retry", i); - } - catch (Exception ex) - { - Console.WriteLine("[s8] s8-{0} retry FAILED: {1}", i, ex.Message); - } - } - - int?[] hostPorts = containers.Select(c => HttpSupport.TryReadAssignedHostPort(c.Inspect())).ToArray(); - for (int i = 0; i < count; i++) - { - Console.WriteLine( - "[s8] container s8-{0} (cport {1}) host port = {2}", - i, - containerPorts[i], - hostPorts[i]?.ToString() ?? "" - ); - } - - bool distinct = hostPorts.Where(p => p is not null).Select(p => p!.Value).Distinct().Count() == count; - Console.WriteLine("[s8] all host ports distinct = {0}", distinct); - - foreach (int? hostPort in hostPorts) - { - if (hostPort is int hp) - { - bool tcp = await HttpSupport.WaitForTcpAsync(hp, TimeSpan.FromSeconds(30)); - (int status, string body) = await HttpSupport.GetWithRetryAsync(hp); - Console.WriteLine("[s8] host port {0}: tcp={1} http={2} body='{3}'", hp, tcp, status, body.Trim()); - } - } - - for (int i = 0; i < count; i++) - { - containers[i].Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - containers[i].Delete(DeleteContainerOption.None); - containers[i].Dispose(); - } - - Directory.Delete(www, recursive: true); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s8] S8 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs b/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs deleted file mode 100644 index 7bdb322..0000000 --- a/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S19: Do concurrent exec (secondary) processes fail with "container is not running"? -// The integration suite's ExecTests + CommandWaitStrategy fail deterministically in a full parallel -// run (but pass alone) with COMException "Container '...' is not running." from Process.Start(). -// Reproduce that at the raw API level: several sleep-60 containers in one session, concurrent execs. -static class ConcurrentExecProcesses -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S19: concurrent exec processes ==="); - Session session = SpikeSupport.StartSession("wslcspike-s19"); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - - int containerCount = 6; - int execsPerContainer = 5; - Console.WriteLine($"[s19] {containerCount} containers x {execsPerContainer} concurrent execs"); - - Container[] containers = new Container[containerCount]; - for (int i = 0; i < containerCount; i++) - { - containers[i] = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName($"s19-{i}"), - new[] { "/bin/sleep", "60" } - ); - } - - Console.WriteLine("[s19] starting containers concurrently..."); - await Task.WhenAll(containers.Select(c => Task.Run(() => c.Start()))); - await Task.Delay(500); - for (int i = 0; i < containerCount; i++) - { - Console.WriteLine( - "[s19] container s19-{0} state={1} init={2}", - i, - containers[i].State, - containers[i].InitProcess.State - ); - } - - int failures = 0; - int total = containerCount * execsPerContainer; - DateTime start = DateTime.UtcNow; - Task[] execs = new Task[total]; - int index = 0; - for (int c = 0; c < containerCount; c++) - { - for (int e = 0; e < execsPerContainer; e++) - { - int ci = c; - int ei = e; - execs[index++] = Task.Run(async () => - { - Process process = containers[ci] - .CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/echo", "hi" }, - OutputMode = ProcessOutputMode.Event, - } - ); - ProcessCapture capture = SpikeSupport.Capture(process); - try - { - process.Start(); - int code = await capture.Exit.WaitAsync(TimeSpan.FromSeconds(10)); - return $"exec c{ci} e{ei}: ok exit={code}"; - } - catch (Exception ex) - { - return $"exec c{ci} e{ei}: FAIL {ex.Message} (HResult=0x{ex.HResult:X8}) containerState={containers[ci].State} initState={containers[ci].InitProcess.State}"; - } - finally - { - try - { - process.Dispose(); - } - catch { } - } - }); - } - } - - string[] results = await Task.WhenAll(execs); - TimeSpan elapsed = DateTime.UtcNow - start; - foreach (string result in results) - { - Console.WriteLine("[s19] {0}", result); - if (result.Contains("FAIL")) - { - failures++; - } - } - - Console.WriteLine("[s19] failures={0}/{1} elapsed={2}", failures, total, elapsed); - - for (int i = 0; i < containerCount; i++) - { - containers[i].Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - containers[i].Delete(DeleteContainerOption.None); - containers[i].Dispose(); - } - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s19] S19 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs b/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs deleted file mode 100644 index 1c67b2d..0000000 --- a/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S18: Can two sessions share one storage path? This determines whether the image store -// can be shared across sessions/processes by default. -// 1. Session A (name a) on shared path P pulls alpine. -// 2. Session B (name b) on the SAME path P is started while A is still active (concurrent). -// 3. Does B start? Does B see A's image? Does a pull in B become visible to A? -static class ConcurrentSharedPath -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S18: concurrent sessions sharing a storage path ==="); - string path = Path.Combine(SpikeSupport.StorageRoot, "shared-path-spike"); - if (Directory.Exists(path)) - { - Directory.Delete(path, recursive: true); - } - - Session? a = null; - Session? b = null; - try - { - Console.WriteLine("[s18] starting session A on shared path..."); - a = SpikeSupport.StartSession("wslcspike-shared-a", path); - await SpikeSupport.PullImageAsync(a, "docker.io/library/alpine:latest"); - SpikeSupport.PrintImages(a, "A after pulling alpine"); - - Console.WriteLine("[s18] starting session B CONCURRENTLY on the same path..."); - try - { - b = SpikeSupport.StartSession("wslcspike-shared-b", path); - } - catch (Exception ex) - { - Console.WriteLine( - "[s18] RESULT: session B FAILED to start on the shared path (concurrent sharing unsafe):" - ); - SpikeSupport.Dump(ex); - return 0; - } - - SpikeSupport.PrintImages(b, "B (same path, concurrent) - does it see alpine?"); - Console.WriteLine("[s18] pulling busybox in B..."); - try - { - await SpikeSupport.PullImageAsync(b, "docker.io/library/busybox:latest"); - } - catch (Exception ex) - { - Console.WriteLine("[s18] pull in B FAILED:"); - SpikeSupport.Dump(ex); - } - - SpikeSupport.PrintImages(a, "A after B pulled busybox (shared store if busybox appears)"); - SpikeSupport.PrintImages(b, "B after pulling busybox"); - - Console.WriteLine("[s18] starting containers in A and B concurrently..."); - try - { - Container ca = SpikeSupport.CreateContainer( - a, - "docker.io/library/alpine:latest", - "s18-a", - new[] { "/bin/echo", "from-a" } - ); - Container cb = SpikeSupport.CreateContainer( - b, - "docker.io/library/alpine:latest", - "s18-b", - new[] { "/bin/echo", "from-b" } - ); - await Task.WhenAll(Task.Run(() => ca.Start()), Task.Run(() => cb.Start())); - await Task.Delay(1500); - Console.WriteLine("[s18] A container state={0}, B container state={1}", ca.State, cb.State); - ca.Delete(DeleteContainerOption.Force); - cb.Delete(DeleteContainerOption.Force); - } - catch (Exception ex) - { - Console.WriteLine("[s18] concurrent container start FAILED:"); - SpikeSupport.Dump(ex); - } - - Console.WriteLine("[s18] RESULT: concurrent shared-path sessions appear to WORK."); - } - finally - { - if (b is not null) - { - SpikeSupport.Cleanup(b); - } - - if (a is not null) - { - SpikeSupport.Cleanup(a); - } - } - - Console.WriteLine("[s18] S18 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/DisposeSemantics.cs b/spikes/WslcSpikes/Spikes/DisposeSemantics.cs deleted file mode 100644 index fa7ad77..0000000 --- a/spikes/WslcSpikes/Spikes/DisposeSemantics.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Diagnostics; -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S14: Does Session.Dispose() alone (no Terminate) free the session while the process is alive? -static class DisposeSemantics -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S14: Dispose-only semantics ==="); - string name = SpikeSupport.UniqueName("wslcspike-dispose"); - Session session = SpikeSupport.StartSession(name); - Console.WriteLine("[s14] session running. wslc info:"); - await DumpSessionsAsync(); - - Console.WriteLine("[s14] disposing WITHOUT terminate..."); - session.Dispose(); - await Task.Delay(2000); - Console.WriteLine("[s14] after Dispose (process alive). wslc info:"); - await DumpSessionsAsync(); - - Console.WriteLine("[s14] trying to reuse the name after Dispose-only..."); - try - { - Session reuse = SpikeSupport.StartSession(name); - Console.WriteLine("[s14] name reuse AFTER Dispose-only: OK"); - SpikeSupport.Cleanup(reuse); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - Console.WriteLine("[s14] S14 complete."); - return 0; - } - - private static async Task DumpSessionsAsync() - { - ProcessStartInfo psi = new ProcessStartInfo("wslc") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }; - psi.ArgumentList.Add("info"); - using System.Diagnostics.Process p = - System.Diagnostics.Process.Start(psi) ?? throw new InvalidOperationException("failed to start wslc"); - string output = await p.StandardOutput.ReadToEndAsync().ConfigureAwait(false); - await p.WaitForExitAsync().ConfigureAwait(false); - Console.WriteLine(output); - } -} diff --git a/spikes/WslcSpikes/Spikes/ExecProcess.cs b/spikes/WslcSpikes/Spikes/ExecProcess.cs deleted file mode 100644 index 41f1983..0000000 --- a/spikes/WslcSpikes/Spikes/ExecProcess.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Microsoft.WSL.Containers; -using Windows.Storage.Streams; - -namespace WslcSpikes; - -// S5: Secondary processes (exec) — stdout/stderr capture, exit codes, env, workdir, and stdin. -static class ExecProcess -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S5: exec + stdin ==="); - Session session = SpikeSupport.StartSession("wslcspike-s5"); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - Container container = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s5-host"), - new[] { "/bin/sleep", "300" } - ); - container.Start(); - Console.WriteLine("[s5] host container state={0}", container.State); - - Console.WriteLine("[s5] exec case 1: echo"); - Process exec1 = container.CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/echo", "hello-exec" }, - OutputMode = ProcessOutputMode.Event, - } - ); - ProcessCapture cap1 = SpikeSupport.Capture(exec1); - exec1.Start(); - int code1 = await cap1.Exit.WaitAsync(TimeSpan.FromSeconds(20)); - Console.WriteLine("[s5] exec1 exit={0} stdout='{1}' stderr='{2}'", code1, cap1.Stdout, cap1.Stderr); - exec1.Dispose(); - - Console.WriteLine("[s5] exec case 2: env + workdir"); - Process exec2 = container.CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/sh", "-c", "echo $FOO; pwd" }, - EnvironmentVariables = new Dictionary { ["FOO"] = "bar-value" }, - WorkingDirectory = "/tmp", - OutputMode = ProcessOutputMode.Event, - } - ); - ProcessCapture cap2 = SpikeSupport.Capture(exec2); - exec2.Start(); - int code2 = await cap2.Exit.WaitAsync(TimeSpan.FromSeconds(20)); - Console.WriteLine("[s5] exec2 exit={0} stdout='{1}' stderr='{2}'", code2, cap2.Stdout, cap2.Stderr); - exec2.Dispose(); - - Console.WriteLine("[s5] exec case 3: stdin via /bin/cat"); - Process exec3 = container.CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/cat" }, - EnableStandardInput = true, - OutputMode = ProcessOutputMode.Event, - } - ); - ProcessCapture cap3 = SpikeSupport.Capture(exec3); - exec3.Start(); - using (DataWriter writer = new DataWriter(exec3.GetInputStream())) - { - writer.WriteString("hello-stdin\n"); - await writer.StoreAsync(); - await writer.FlushAsync(); - } - - int code3 = await cap3.Exit.WaitAsync(TimeSpan.FromSeconds(20)); - Console.WriteLine("[s5] exec3 exit={0} stdout='{1}' stderr='{2}'", code3, cap3.Stdout, cap3.Stderr); - exec3.Dispose(); - - Console.WriteLine("[s5] exec case 4: failure exit code (exit 9)"); - Process exec4 = container.CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/sh", "-c", "echo boom >&2; exit 9" }, - OutputMode = ProcessOutputMode.Event, - } - ); - ProcessCapture cap4 = SpikeSupport.Capture(exec4); - exec4.Start(); - int code4 = await cap4.Exit.WaitAsync(TimeSpan.FromSeconds(20)); - Console.WriteLine("[s5] exec4 exit={0} stdout='{1}' stderr='{2}'", code4, cap4.Stdout, cap4.Stderr); - exec4.Dispose(); - - container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - container.Delete(DeleteContainerOption.None); - container.Dispose(); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s5] S5 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/FullLifecycle.cs b/spikes/WslcSpikes/Spikes/FullLifecycle.cs deleted file mode 100644 index af40743..0000000 --- a/spikes/WslcSpikes/Spikes/FullLifecycle.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// Sfull: the documented end-to-end lifecycle as a sanity check of the managed API. -static class FullLifecycle -{ - public static async Task RunAsync() - { - Console.WriteLine("=== Sfull: documented end-to-end lifecycle ==="); - - Session session = SpikeSupport.StartSession("wslcspike-full", memoryMb: 2048, cpuCount: 2); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - Container container = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("hello-container"), - new[] { "/bin/echo", "Hello from WSL Container!" }, - ProcessOutputMode.Event - ); - - int exitCode = await SpikeSupport.RunToExitAsync(container, TimeSpan.FromSeconds(30)); - Console.WriteLine("[sfull] init exited code={0}", exitCode); - - if (container.State == ContainerState.Running) - { - container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - } - - Console.WriteLine("[sfull] container state after stop: {0}", container.State); - container.Delete(DeleteContainerOption.None); - Console.WriteLine("[sfull] container deleted"); - container.Dispose(); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - return 1; - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[sfull] Sfull complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/ImageCacheScope.cs b/spikes/WslcSpikes/Spikes/ImageCacheScope.cs deleted file mode 100644 index a80a843..0000000 --- a/spikes/WslcSpikes/Spikes/ImageCacheScope.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S2: Image-store scope. -// A: pull alpine in session A (path PA). -// B: new session name, SAME path PA -> does GetImages see alpine? (persists in storage VHD?) -// C: new session name, DIFFERENT path PB -> does GetImages see alpine? (per-path store?) -static class ImageCacheScope -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S2: Image cache scope ==="); - - string pathA = Path.Combine(SpikeSupport.StorageRoot, "s2a"); - string pathB = Path.Combine(SpikeSupport.StorageRoot, "s2b"); - foreach (string path in new[] { pathA, pathB }) - { - if (Directory.Exists(path)) - { - Directory.Delete(path, recursive: true); - } - } - - Session a = SpikeSupport.StartSession("wslcspike-s2a", pathA); - await SpikeSupport.PullImageAsync(a, "docker.io/library/alpine:latest"); - SpikeSupport.PrintImages(a, "session A after pull"); - SpikeSupport.Cleanup(a); - Console.WriteLine("[s2] session A terminated"); - - Session b = SpikeSupport.StartSession("wslcspike-s2b", pathA); - SpikeSupport.PrintImages(b, "session B (same storage path, new session)"); - SpikeSupport.Cleanup(b); - Console.WriteLine("[s2] session B terminated"); - - Session c = SpikeSupport.StartSession("wslcspike-s2c", pathB); - SpikeSupport.PrintImages(c, "session C (different storage path)"); - SpikeSupport.Cleanup(c); - Console.WriteLine("[s2] session C terminated"); - - Console.WriteLine("[s2] S2 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/MountsAndVolumes.cs b/spikes/WslcSpikes/Spikes/MountsAndVolumes.cs deleted file mode 100644 index 4da1222..0000000 --- a/spikes/WslcSpikes/Spikes/MountsAndVolumes.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S6: Bind mounts (ContainerVolume) and named volumes (ContainerNamedVolume) — auto-provisioned -// vs explicitly pre-created via CreateVhdVolume. -static class MountsAndVolumes -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S6: mounts + named volumes ==="); - Session session = SpikeSupport.StartSession("wslcspike-s6"); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - - Console.WriteLine("[s6] bind mount: read a file from a Windows directory"); - string hostDir = Path.Combine(SpikeSupport.StorageRoot, "s6-hostdir"); - if (Directory.Exists(hostDir)) - { - Directory.Delete(hostDir, recursive: true); - } - - Directory.CreateDirectory(hostDir); - File.WriteAllText(Path.Combine(hostDir, "hello.txt"), "hello-from-host\n"); - - Container m1 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s6-bind"), - new[] { "/bin/cat", "/mnt/data/hello.txt" }, - configure: settings => - settings.Volumes = new List { new ContainerVolume(hostDir, "/mnt/data", false) } - ); - await SpikeSupport.RunToExitAsync(m1, TimeSpan.FromSeconds(30)); - m1.Delete(DeleteContainerOption.None); - m1.Dispose(); - - Console.WriteLine("[s6] bind mount read-only: attempt to write should fail"); - Container m2 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s6-bind-ro"), - new[] { "/bin/sh", "-c", "touch /mnt/data/newfile.txt; echo exit=$?" }, - configure: settings => - settings.Volumes = new List { new ContainerVolume(hostDir, "/mnt/data", true) } - ); - await SpikeSupport.RunToExitAsync(m2, TimeSpan.FromSeconds(30)); - m2.Delete(DeleteContainerOption.None); - m2.Dispose(); - Console.WriteLine( - "[s6] hostdir now contains newfile.txt? {0}", - File.Exists(Path.Combine(hostDir, "newfile.txt")) - ); - - Console.WriteLine("[s6] named volume AUTO-provisioned: write in one container, read in another"); - string autoVol = SpikeSupport.UniqueName("s6vol"); - Container n1 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s6-vol-writer"), - new[] { "/bin/sh", "-c", "echo shared-data > /vol/data.txt; sleep 60" }, - configure: settings => - settings.NamedVolumes = new List - { - new ContainerNamedVolume(autoVol, "/vol", false), - } - ); - n1.Start(); - await Task.Delay(2000); - - Container n2 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s6-vol-reader"), - new[] { "/bin/cat", "/vol/data.txt" }, - configure: settings => - settings.NamedVolumes = new List - { - new ContainerNamedVolume(autoVol, "/vol", false), - } - ); - await SpikeSupport.RunToExitAsync(n2, TimeSpan.FromSeconds(30)); - n2.Delete(DeleteContainerOption.None); - n2.Dispose(); - n1.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - n1.Delete(DeleteContainerOption.None); - n1.Dispose(); - - Console.WriteLine("[s6] named volume PRE-created via CreateVhdVolume, then referenced"); - string preVol = SpikeSupport.UniqueName("s6vol"); - try - { - session.CreateVhdVolume(new VhdOptions(preVol, 256UL * 1024 * 1024, VhdType.Dynamic)); - Console.WriteLine("[s6] CreateVhdVolume('{0}') succeeded", preVol); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - Container n3 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s6-vol-prec"), - new[] { "/bin/sh", "-c", "echo prec > /vol/x.txt; sleep 2" }, - configure: settings => - settings.NamedVolumes = new List - { - new ContainerNamedVolume(preVol, "/vol", false), - } - ); - await SpikeSupport.RunToExitAsync(n3, TimeSpan.FromSeconds(30)); - n3.Delete(DeleteContainerOption.None); - n3.Dispose(); - - try - { - session.DeleteVhdVolume(preVol); - Console.WriteLine("[s6] DeleteVhdVolume('{0}') succeeded", preVol); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - Directory.Delete(hostDir, recursive: true); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s6] S6 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/Networking.cs b/spikes/WslcSpikes/Spikes/Networking.cs deleted file mode 100644 index c465e6d..0000000 --- a/spikes/WslcSpikes/Spikes/Networking.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S9: Container-to-container connectivity within one session. -// Server container A (hostname "alpha") runs httpd; client container B execs wget to http://alpha:8080/. -static class Networking -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S9: container-to-container networking ==="); - Session session = SpikeSupport.StartSession("wslcspike-s9"); - try - { - await SpikeSupport.PullImageAsync(session, HttpSupport.ServerImage); - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - string www = Path.Combine(SpikeSupport.StorageRoot, "s9-www"); - if (Directory.Exists(www)) - { - Directory.Delete(www, recursive: true); - } - - Directory.CreateDirectory(www); - File.WriteAllText(Path.Combine(www, "index.html"), "hello-net\n"); - - ContainerNetworkingMode?[] modes = new ContainerNetworkingMode?[] - { - null, - ContainerNetworkingMode.Bridged, - ContainerNetworkingMode.None, - }; - foreach (ContainerNetworkingMode? mode in modes) - { - string label = mode?.ToString() ?? "Default"; - Console.WriteLine("[s9] --- mode: {0} ---", label); - string suffix = (mode?.ToString() ?? "default").ToLowerInvariant(); - - Container server = SpikeSupport.CreateContainer( - session, - HttpSupport.ServerImage, - SpikeSupport.UniqueName($"s9-server-{suffix}"), - HttpSupport.ServerArgs(8080), - configure: settings => - { - settings.HostName = "alpha"; - settings.NetworkingMode = mode; - settings.Volumes = new List { new ContainerVolume(www, "/tmp", false) }; - } - ); - Container client = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName($"s9-client-{suffix}"), - new[] { "/bin/sleep", "300" }, - configure: settings => settings.NetworkingMode = mode - ); - - Console.WriteLine("[s9] server hostname='alpha' networking={0}", label); - server.Start(); - client.Start(); - await Task.Delay(1500); - - Console.WriteLine("[s9] client tooling check:"); - await ExecInAsync(client, "command -v wget nc busybox"); - string? serverIp = HttpSupport.TryReadContainerIp(server.Inspect()); - Console.WriteLine("[s9] server bridge IP = {0}", serverIp ?? ""); - Console.WriteLine("[s9] server /etc/hosts:"); - await ExecInAsync(client, "cat /etc/hosts"); - - string[] probes = new[] { $"http://alpha:8080/", $"http://{serverIp ?? "??"}:8080/" }; - foreach (string probe in probes) - { - Console.WriteLine("[s9] exec in client: wget {0}", probe); - await ExecInAsync(client, $"wget -q -T 3 -O - {probe} 2>&1; echo wget_exit=$?"); - } - - server.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - server.Delete(DeleteContainerOption.None); - server.Dispose(); - client.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - client.Delete(DeleteContainerOption.None); - client.Dispose(); - } - - Directory.Delete(www, recursive: true); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s9] S9 complete."); - return 0; - } - - private static async Task ExecInAsync(Container container, string command) - { - Process exec = container.CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/sh", "-c", command }, - OutputMode = ProcessOutputMode.Event, - } - ); - ProcessCapture cap = SpikeSupport.Capture(exec); - exec.Start(); - try - { - int code = await cap.Exit.WaitAsync(TimeSpan.FromSeconds(15)); - Console.WriteLine("[s9] exit={0} out='{1}' err='{2}'", code, cap.Stdout, cap.Stderr); - } - catch (TimeoutException) - { - Console.WriteLine("[s9] timed out; killing exec"); - exec.Signal(Signal.SIGKILL); - } - - exec.Dispose(); - } -} diff --git a/spikes/WslcSpikes/Spikes/OrphanSessions.cs b/spikes/WslcSpikes/Spikes/OrphanSessions.cs deleted file mode 100644 index 57425fe..0000000 --- a/spikes/WslcSpikes/Spikes/OrphanSessions.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Diagnostics; -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S10: What happens to a session when its creator process exits without terminating it? -// This harness spawns itself as a child (s10child), the child leaves a session running, -// and then we inspect `wslc info` to see whether the session was reaped. -static class OrphanSessions -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S10: orphaned session on creator exit ==="); - await DumpSessionsAsync("baseline"); - - await RunChildAsync("s10child", "wslcspike-orphan-" + Guid.NewGuid().ToString("N")[..6], dispose: true); - await DumpSessionsAsync("after dispose+exit child + 5s"); - - await RunChildAsync( - "s10child-norelease", - "wslcspike-orphan-" + Guid.NewGuid().ToString("N")[..6], - dispose: false - ); - await DumpSessionsAsync("after no-release exit child + 5s"); - - Console.WriteLine("[s10] S10 complete."); - return 0; - } - - private static async Task RunChildAsync(string mode, string name, bool dispose) - { - Console.WriteLine("[s10] spawning child '{0}' with session name '{1}'", mode, name); - string exe = Environment.ProcessPath ?? throw new InvalidOperationException("no process path"); - ProcessStartInfo psi = new ProcessStartInfo(exe) - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }; - psi.ArgumentList.Add(mode); - psi.ArgumentList.Add(name); - - using System.Diagnostics.Process child = - System.Diagnostics.Process.Start(psi) ?? throw new InvalidOperationException("failed to start child"); - string childOut = await child.StandardOutput.ReadToEndAsync().ConfigureAwait(false); - string childErr = await child.StandardError.ReadToEndAsync().ConfigureAwait(false); - await child.WaitForExitAsync().ConfigureAwait(false); - Console.WriteLine("[s10] child exit code={0}", child.ExitCode); - Console.WriteLine("[s10] child stdout:\n{0}", childOut); - Console.WriteLine("[s10] child stderr:\n{0}", childErr); - - Console.WriteLine("[s10] waiting 5s for the session manager to notice..."); - await Task.Delay(5000).ConfigureAwait(false); - } - - public static async Task ChildAsync(string name, bool dispose) - { - Session session = SpikeSupport.StartSession(name); - Console.WriteLine("[s10child] started, sleeping 15s, then exiting with dispose={0}", dispose); - await Task.Delay(15000).ConfigureAwait(false); - if (dispose) - { - session.Dispose(); - } - - Console.WriteLine("[s10child] exiting"); - return 0; - } - - private static async Task DumpSessionsAsync(string label) - { - Console.WriteLine("[s10] wslc info ({0}):", label); - ProcessStartInfo psi = new ProcessStartInfo("wslc") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }; - psi.ArgumentList.Add("info"); - using System.Diagnostics.Process p = - System.Diagnostics.Process.Start(psi) ?? throw new InvalidOperationException("failed to start wslc"); - string output = await p.StandardOutput.ReadToEndAsync().ConfigureAwait(false); - string error = await p.StandardError.ReadToEndAsync().ConfigureAwait(false); - await p.WaitForExitAsync().ConfigureAwait(false); - Console.WriteLine(output); - if (!string.IsNullOrEmpty(error)) - { - Console.WriteLine("[s10] wslc stderr: {0}", error); - } - } -} diff --git a/spikes/WslcSpikes/Spikes/Ports.cs b/spikes/WslcSpikes/Spikes/Ports.cs deleted file mode 100644 index be27739..0000000 --- a/spikes/WslcSpikes/Spikes/Ports.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Text.Json; -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S4: Port mappings — confirms Bridged is required, that random host ports work via windowsPort=0, -// records the Inspect() schema, and verifies host reachability. -static class Ports -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S4: Port mappings ==="); - Session session = SpikeSupport.StartSession("wslcspike-s4"); - try - { - await SpikeSupport.PullImageAsync(session, HttpSupport.ServerImage); - string www = PrepareWww(); - - Console.WriteLine("[s4] probe A: volume only, default networking"); - Container probeA = Create( - session, - SpikeSupport.UniqueName("s4-probeA"), - s => s.Volumes = new List { new ContainerVolume(www, "/tmp", false) } - ); - probeA.Start(); - Console.WriteLine("[s4] probeA started state={0} (NetworkMode in inspect should be 'none')", probeA.State); - - Console.WriteLine("[s4] probe B: fixed host port + Bridged"); - ushort portB = HttpSupport.GetFreeTcpPort(); - Container probeB = Create( - session, - SpikeSupport.UniqueName("s4-probeB"), - s => - { - s.NetworkingMode = ContainerNetworkingMode.Bridged; - s.PortMappings = new List - { - new ContainerPortMapping(portB, 8080, PortProtocol.TCP), - }; - } - ); - probeB.Start(); - (bool tcpB, (int statusB, string bodyB)) = await ProbeAsync(portB); - - Console.WriteLine("[s4] probe C: random host port (windowsPort=0) + Bridged + volume"); - Container probeC = Create( - session, - SpikeSupport.UniqueName("s4-random"), - s => - { - s.NetworkingMode = ContainerNetworkingMode.Bridged; - s.Volumes = new List { new ContainerVolume(www, "/tmp", false) }; - s.PortMappings = new List - { - new ContainerPortMapping(0, 8080, PortProtocol.TCP), - }; - } - ); - probeC.Start(); - string randomInspect = probeC.Inspect(); - int? assigned = HttpSupport.TryReadAssignedHostPort(randomInspect); - Console.WriteLine("[s4] random assigned host port = {0}", assigned?.ToString() ?? ""); - (bool tcpC, (int statusC, string bodyC)) = assigned is int pc - ? await ProbeAsync(pc) - : (false, (0, string.Empty)); - - Console.WriteLine("[s4] probe D: fixed host port + Bridged + WindowsAddress=127.0.0.1"); - ushort portD = HttpSupport.GetFreeTcpPort(); - Container probeD = Create( - session, - SpikeSupport.UniqueName("s4-probeD"), - s => - { - s.NetworkingMode = ContainerNetworkingMode.Bridged; - s.PortMappings = new List - { - new ContainerPortMapping(portD, 8080, PortProtocol.TCP) - { - WindowsAddress = new Windows.Networking.HostName("127.0.0.1"), - }, - }; - } - ); - probeD.Start(); - (bool tcpD, _) = await ProbeAsync(portD); - - Console.WriteLine( - "[s4] summary: fixedPort={0} tcp={1} http={2} body='{3}'", - portB, - tcpB, - statusB, - bodyB.Trim() - ); - Console.WriteLine( - "[s4] summary: randomPort={0} tcp={1} http={2} body='{3}'", - assigned?.ToString() ?? "?", - tcpC, - statusC, - bodyC.Trim() - ); - Console.WriteLine("[s4] summary: explicitAddrPort={0} tcp={1}", portD, tcpD); - - Console.WriteLine("[s4] Inspect() schema for the running random-mapped container:"); - Console.WriteLine(Indent(randomInspect)); - - Cleanup(probeA); - Cleanup(probeB); - Cleanup(probeC); - Cleanup(probeD); - Directory.Delete(www, recursive: true); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s4] S4 complete."); - return 0; - } - - private static Container Create(Session session, string name, Action configure) - { - return SpikeSupport.CreateContainer( - session, - HttpSupport.ServerImage, - name, - HttpSupport.ServerArgs(8080), - configure: configure - ); - } - - private static async Task<(bool Tcp, (int Status, string Body))> ProbeAsync(int port) - { - bool tcp = await HttpSupport.WaitForTcpAsync(port, TimeSpan.FromSeconds(30)); - (int status, string body) = await HttpSupport.GetWithRetryAsync(port); - Console.WriteLine("[s4] port {0}: tcp={1} http={2} body='{3}'", port, tcp, status, body.Trim()); - return (tcp, (status, body)); - } - - private static void Cleanup(Container container) - { - try - { - container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - } - catch { } - - try - { - container.Delete(DeleteContainerOption.Force); - } - catch { } - - container.Dispose(); - } - - private static string PrepareWww() - { - string dir = Path.Combine(SpikeSupport.StorageRoot, "s4-www"); - if (Directory.Exists(dir)) - { - Directory.Delete(dir, recursive: true); - } - - Directory.CreateDirectory(dir); - File.WriteAllText(Path.Combine(dir, "index.html"), "hello-from-wslc\n"); - return dir; - } - - private static string Indent(string json) - { - try - { - return JsonSerializer.Serialize( - JsonDocument.Parse(json).RootElement, - new JsonSerializerOptions { WriteIndented = true } - ); - } - catch (JsonException) - { - return json; - } - } -} diff --git a/spikes/WslcSpikes/Spikes/ProcessIo.cs b/spikes/WslcSpikes/Spikes/ProcessIo.cs deleted file mode 100644 index 1d8d665..0000000 --- a/spikes/WslcSpikes/Spikes/ProcessIo.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Text.Json; -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S3: stdout/stderr capture and exit codes for the init process. -static class ProcessIo -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S3: init-process stdout/stderr/exit ==="); - Session session = SpikeSupport.StartSession("wslcspike-s3"); - try - { - await SpikeSupport.PullImageAsync(session, "docker.io/library/alpine:latest"); - - Console.WriteLine("[s3] case 1: echo to stdout, exit 0"); - Container c1 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s3-echo"), - new[] { "/bin/echo", "hello-stdout" } - ); - await SpikeSupport.RunToExitAsync(c1, TimeSpan.FromSeconds(30)); - c1.Delete(DeleteContainerOption.None); - c1.Dispose(); - - Console.WriteLine("[s3] case 2: stdout + stderr, exit 7"); - Container c2 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s3-mixed"), - new[] { "/bin/sh", "-c", "echo out-line; echo err-line >&2; exit 7" } - ); - await SpikeSupport.RunToExitAsync(c2, TimeSpan.FromSeconds(30)); - c2.Delete(DeleteContainerOption.None); - c2.Dispose(); - - Console.WriteLine("[s3] case 3: missing executable (start should surface failure)"); - Container c3 = SpikeSupport.CreateContainer( - session, - "docker.io/library/alpine:latest", - SpikeSupport.UniqueName("s3-missing"), - new[] { "/bin/definitely-not-a-binary" } - ); - try - { - await SpikeSupport.RunToExitAsync(c3, TimeSpan.FromSeconds(30)); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - } - - c3.Delete(DeleteContainerOption.Force); - c3.Dispose(); - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s3] S3 complete."); - return 0; - } -} - -static class HttpSupport -{ - public const string ServerImage = "docker.io/library/python:3-alpine"; - - public static IReadOnlyList ServerArgs(int port) => - new[] { "/bin/sh", "-c", $"python3 -m http.server {port} -d /tmp" }; - - public static async Task GetAsync(int port, string path = "/", string host = "127.0.0.1") - { - using HttpClient client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }); - client.Timeout = TimeSpan.FromSeconds(5); - return await client.GetAsync($"http://{host}:{port}{path}").ConfigureAwait(false); - } - - public static ushort GetFreeTcpPort() - { - TcpListener listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - ushort port = (ushort)((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - public static async Task WaitForTcpAsync(int port, TimeSpan timeout, string host = "127.0.0.1") - { - DateTime deadline = DateTime.UtcNow + timeout; - while (DateTime.UtcNow < deadline) - { - try - { - using TcpClient client = new TcpClient(); - await client.ConnectAsync(host, port).ConfigureAwait(false); - return true; - } - catch (SocketException) - { - await Task.Delay(200).ConfigureAwait(false); - } - } - - return false; - } - - public static async Task WaitForTcp6Async(int port, TimeSpan timeout) - { - DateTime deadline = DateTime.UtcNow + timeout; - while (DateTime.UtcNow < deadline) - { - try - { - using TcpClient client = new TcpClient(AddressFamily.InterNetworkV6); - await client.ConnectAsync(IPAddress.IPv6Loopback, port).ConfigureAwait(false); - return true; - } - catch (SocketException) - { - await Task.Delay(200).ConfigureAwait(false); - } - } - - return false; - } - - public static int? TryReadAssignedHostPort(string json) - { - try - { - using JsonDocument doc = JsonDocument.Parse(json); - return FindHostPort(doc.RootElement); - } - catch (JsonException) - { - return null; - } - } - - public static string? TryReadContainerIp(string json, string network = "bridge") - { - try - { - using JsonDocument doc = JsonDocument.Parse(json); - if ( - doc.RootElement.TryGetProperty("NetworkSettings", out JsonElement networkSettings) - && networkSettings.TryGetProperty("Networks", out JsonElement networks) - && networks.TryGetProperty(network, out JsonElement net) - && net.TryGetProperty("IPAddress", out JsonElement ip) - && ip.ValueKind == JsonValueKind.String - ) - { - string value = ip.GetString() ?? string.Empty; - return string.IsNullOrEmpty(value) ? null : value; - } - - return null; - } - catch (JsonException) - { - return null; - } - } - - private static int? FindHostPort(JsonElement element) - { - if (element.ValueKind == JsonValueKind.Object) - { - foreach (JsonProperty property in element.EnumerateObject()) - { - string key = property.Name; - if ( - key.Equals("HostPort", StringComparison.OrdinalIgnoreCase) - || key.Equals("WindowsPort", StringComparison.OrdinalIgnoreCase) - || key.Equals("PublicPort", StringComparison.OrdinalIgnoreCase) - ) - { - int? value = property.Value.ValueKind switch - { - JsonValueKind.Number when property.Value.TryGetInt32(out int n) => n, - JsonValueKind.String when int.TryParse(property.Value.GetString(), out int s) => s, - _ => null, - }; - if (value is int parsed && parsed > 0) - { - return parsed; - } - } - - int? nested = FindHostPort(property.Value); - if (nested is not null) - { - return nested; - } - } - } - else if (element.ValueKind == JsonValueKind.Array) - { - foreach (JsonElement item in element.EnumerateArray()) - { - int? nested = FindHostPort(item); - if (nested is not null) - { - return nested; - } - } - } - - return null; - } - - public static async Task<(int Status, string Body)> GetWithRetryAsync(int port, string path = "/", int attempts = 8) - { - for (int i = 0; i < attempts; i++) - { - try - { - using System.Net.Http.HttpResponseMessage resp = await GetAsync(port, path).ConfigureAwait(false); - string body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); - return ((int)resp.StatusCode, body); - } - catch (System.Net.Http.HttpRequestException) - { - await Task.Delay(500).ConfigureAwait(false); - } - } - - return (0, string.Empty); - } -} diff --git a/spikes/WslcSpikes/Spikes/ReopenOrphan.cs b/spikes/WslcSpikes/Spikes/ReopenOrphan.cs deleted file mode 100644 index a0eb832..0000000 --- a/spikes/WslcSpikes/Spikes/ReopenOrphan.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S13: Can a new process reuse the name of a session orphaned by a dead process? -// Usage: s13 (default: wslcspike-orphan-a9a74c, the S10 orphan) -static class ReopenOrphan -{ - public static async Task RunAsync(string[] args) - { - string name = args.Length > 1 ? args[1] : "wslcspike-orphan-a9a74c"; - Console.WriteLine("=== S13: reuse orphaned session name '{0}' ===", name); - try - { - Session session = SpikeSupport.StartSession(name); - Console.WriteLine("[s13] started WITHOUT error; adopting the orphan's storage."); - SpikeSupport.PrintImages(session, "images visible after adopting orphan name"); - SpikeSupport.Cleanup(session); - Console.WriteLine("[s13] cleaned up"); - } - catch (Exception ex) - { - SpikeSupport.Dump(ex); - Console.WriteLine( - "[s13] same-name reuse failed (expected). Testing fresh name + same shared storage path while orphan is active..." - ); - try - { - Session fresh = SpikeSupport.StartSession(SpikeSupport.UniqueName("wslcspike-fresh")); - Console.WriteLine( - "[s13] fresh name + same storage path started OK; orphan does NOT block the storage path" - ); - SpikeSupport.Cleanup(fresh); - } - catch (Exception ex2) - { - SpikeSupport.Dump(ex2); - Console.WriteLine("[s13] fresh name + same storage path FAILED; orphan blocks the storage path"); - } - } - - await Task.CompletedTask; - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/SessionLifecycle.cs b/spikes/WslcSpikes/Spikes/SessionLifecycle.cs deleted file mode 100644 index dcf7f72..0000000 --- a/spikes/WslcSpikes/Spikes/SessionLifecycle.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S1: Session lifecycle — start, terminate, warm re-open of the same name+storage path, -// and behaviour when a second session with the same name is created while the first is active. -static class SessionLifecycle -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S1: Session lifecycle ==="); - SpikeSupport.PrintRuntimeInfo(); - - string name = "wslcspike-s1"; - string storage = Path.Combine(SpikeSupport.StorageRoot, name); - if (Directory.Exists(storage)) - { - Directory.Delete(storage, recursive: true); - } - - Session first = SpikeSupport.StartSession(name, storage, memoryMb: 1024, cpuCount: 2); - Console.WriteLine("[s1] session started (cold). Attempting a duplicate same-name session while active..."); - try - { - Session duplicate = new Session(new SessionSettings(name, storage)); - duplicate.Start(); - Console.WriteLine("[s1] duplicate same-name session started WITHOUT error (unexpected)"); - SpikeSupport.Cleanup(duplicate); - } - catch (Exception ex) - { - Console.WriteLine("[s1] duplicate same-name session failed as expected:"); - SpikeSupport.Dump(ex); - } - - SpikeSupport.Cleanup(first); - Console.WriteLine("[s1] first session terminated+disposed"); - - Console.WriteLine("[s1] re-opening SAME name+storage (warm restart)..."); - Session second = SpikeSupport.StartSession(name, storage, memoryMb: 1024, cpuCount: 2); - SpikeSupport.Cleanup(second); - Console.WriteLine("[s1] second session terminated+disposed"); - - Console.WriteLine("[s1] re-opening SAME name with DIFFERENT storage path..."); - Session third = SpikeSupport.StartSession(name, Path.Combine(SpikeSupport.StorageRoot, name + "-alt")); - SpikeSupport.Cleanup(third); - Console.WriteLine("[s1] third session terminated+disposed"); - - Console.WriteLine("[s1] S1 complete."); - await Task.CompletedTask; - return 0; - } -} diff --git a/spikes/WslcSpikes/Spikes/ThreadSafety.cs b/spikes/WslcSpikes/Spikes/ThreadSafety.cs deleted file mode 100644 index cdbe735..0000000 --- a/spikes/WslcSpikes/Spikes/ThreadSafety.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Microsoft.WSL.Containers; - -namespace WslcSpikes; - -// S11: Session thread-safety — concurrent pulls of the same image and concurrent create/start/stop/delete. -static class ThreadSafety -{ - public static async Task RunAsync() - { - Console.WriteLine("=== S11: session thread-safety ==="); - Session session = SpikeSupport.StartSession("wslcspike-s11"); - try - { - Console.WriteLine("[s11] 4 concurrent pulls of the same image"); - string image = "docker.io/library/alpine:latest"; - Task[] pulls = Enumerable - .Range(0, 4) - .Select(i => - Task.Run(async () => - { - try - { - await SpikeSupport.PullImageAsync(session, image); - return $"pull {i}: ok"; - } - catch (Exception ex) - { - return $"pull {i}: FAIL {ex.Message} (HResult=0x{ex.HResult:X8})"; - } - }) - ) - .ToArray(); - string[] pullResults = await Task.WhenAll(pulls); - foreach (string result in pullResults) - { - Console.WriteLine("[s11] {0}", result); - } - - Console.WriteLine("[s11] 8 concurrent create+start+stop+delete"); - int count = 8; - Task[] ops = Enumerable - .Range(0, count) - .Select(i => - Task.Run(async () => - { - try - { - Container container = SpikeSupport.CreateContainer( - session, - image, - SpikeSupport.UniqueName($"s11-{i}"), - new[] { "/bin/sleep", "10" } - ); - container.Start(); - await Task.Delay(300); - container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); - container.Delete(DeleteContainerOption.None); - container.Dispose(); - return $"op {i}: ok"; - } - catch (Exception ex) - { - return $"op {i}: FAIL {ex.Message} (HResult=0x{ex.HResult:X8})"; - } - }) - ) - .ToArray(); - string[] opResults = await Task.WhenAll(ops); - foreach (string result in opResults) - { - Console.WriteLine("[s11] {0}", result); - } - } - finally - { - SpikeSupport.Cleanup(session); - } - - Console.WriteLine("[s11] S11 complete."); - return 0; - } -} diff --git a/spikes/WslcSpikes/WslcSpikes.csproj b/spikes/WslcSpikes/WslcSpikes.csproj deleted file mode 100644 index b922801..0000000 --- a/spikes/WslcSpikes/WslcSpikes.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - Exe - false - $(NoWarn);CA1031;CA1304;CA1305;CA1307;CA1310;CA1849;CA2000;CA2027;CA1707;CA1822;CA1062;CA1515;CA1861;CA1819;IDE0040;IDE0060;IDE0074;IDE0007;IDE0305;CS4014 - - - - - - diff --git a/src/WSLTestContainers.slnx b/src/Containers.slnx similarity index 100% rename from src/WSLTestContainers.slnx rename to src/Containers.slnx diff --git a/src/src/Wsl/Wsl.csproj b/src/src/Wsl/Wsl.csproj index 40e8610..2fed058 100644 --- a/src/src/Wsl/Wsl.csproj +++ b/src/src/Wsl/Wsl.csproj @@ -15,7 +15,7 @@ A Windows consumer (net10.0-windows… or net11.0-windows…) binds the implementation and behaves exactly as before; only a platform-neutral consumer gets the facade. The approach rests on the extracted and tested result that the WSLC projection is a net8.0-windows asset loadable by a - net10.0 process - see spikes/DynamicLoadSpike. + net10.0 process. --> net10.0;net10.0-windows10.0.19041.0