9. Controlar contenedores desde C#
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Si ya has arrancado contenedores desde código .NET o Java, seguramente lo hiciste mediante un cliente de Docker: Testcontainers para las pruebas de integración, o las bibliotecas Docker.DotNet y docker-java. Todos hablan HTTP con la Docker Engine API, a través de una named pipe o de un socket, y comparten un único motor con todas las demás herramientas de la máquina.
La API de WSL container funciona de otra manera. Microsoft.WSL.Containers es un paquete NuGet al que una aplicación Windows llama dentro de su propio proceso. La aplicación crea su propia sesión, con su propia VM, su propio disco y sus propios límites, y luego descarga imágenes y ejecuta contenedores en ella. No hay ningún demonio que instalar ni ningún socket que encontrar: si WSL 2.9 o posterior está presente, la aplicación puede usar contenedores Linux como parte de su propia lógica.
Esta lección construye el programa útil más pequeño posible, tropieza con lo que la documentación preliminar dice mal y luego va un paso más allá: una imagen construida durante dotnet build y distribuida como un archivo junto al programa. El código está en code/wsl-containers/wslc-host; las salidas vienen del diario, con el paquete 2.9.9 en WSL 2.9.11.
Los objetos
Sección titulada «Los objetos»La API sigue el ciclo de vida de un contenedor:
| Objeto | Función |
|---|---|
WslcService |
comprobar que los componentes de WSL están instalados, versión del servicio |
Session |
host WSL que gestiona las imágenes y crea los contenedores |
Container |
iniciar, detener, inspeccionar, eliminar; lanzar procesos |
Process |
leer stdout/stderr, escribir en stdin, enviar señales |
Si conoces la CLI de Docker, una Session hace el papel del motor, Container el de docker create, start, inspect y rm, y Process el de la salida adjunta de docker run o docker exec.
Un proyecto que compila
Sección titulada «Un proyecto que compila»Parte del extracto de la página de Microsoft Learn, pégalo en un proyecto de consola que referencie el paquete, y el build falla con cinco errores:
error CS0103: The name 'ComponentFlags' does not exist in the current contexterror CS0117: 'SessionSettings' does not contain a definition for 'MemoryMB'error CS0117: 'ProcessSettings' does not contain a definition for 'CmdLine'error CS0103: The name 'DeleteContainerFlags' does not exist in the current contexterror CS1705: Assembly 'wslcsdkcs' with identity 'wslcsdkcs, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' uses 'Microsoft.Windows.SDK.NET, Version=10.0.26100.79, Culture=neutral, PublicKeyToken=31bf3856ad364e35' which has a higher version than referenced assembly 'Microsoft.Windows.SDK.NET' with identity 'Microsoft.Windows.SDK.NET, Version=10.0.19041.38, Culture=neutral, PublicKeyToken=31bf3856ad364e35'Cuatro nombres han cambiado
Sección titulada «Cuatro nombres han cambiado»La página describe un estado anterior de la API. Los nombres reales se obtienen leyendo los tipos públicos de wslcsdkcs.dll, el ensamblado administrado del paquete, con MetadataLoadContext, que carga un ensamblado para inspeccionarlo sin ejecutarlo:
| Microsoft Learn | Paquete 2.9.9 |
|---|---|
ComponentFlags GetMissingComponents() |
IReadOnlyList<Component> GetMissingComponents() (VirtualMachinePlatform, WslPackage, SdkNeedsUpdate) |
SessionSettings.MemoryMB |
SessionSettings.MemorySizeInMB |
ProcessSettings.CmdLine |
ProcessSettings.CommandLine (IList<string>) |
DeleteContainerFlags.None |
DeleteContainerOption.None / Force |
En un IDE, el examinador de objetos o Ir a definición muestran lo mismo. Con un paquete en versión preliminar, fíate más del compilador que de la página.
CS1705: la proyección del SDK de Windows
Sección titulada «CS1705: la proyección del SDK de Windows»El paquete es una proyección C#/WinRT, con una DLL nativa solo para x64 y arm64. Declara el destino net8.0-windows10.0.19041.0, pero su wslcsdkcs.dll está compilado con Microsoft.Windows.SDK.NET 10.0.26100.79, una proyección más reciente que la que aporta ese destino.
Pasar el proyecto a net10.0-windows10.0.26100.0 no basta: el SDK de .NET elige entonces la proyección 10.0.26100.38, y el error persiste. Hay que forzar la versión con WindowsSdkPackageVersion, y la propia 10.0.26100.79 no existe en nuget.org: NU1102, y la más cercana es 10.0.26100.80. El archivo de proyecto que funciona:
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> <WindowsSdkPackageVersion>10.0.26100.80</WindowsSdkPackageVersion> <RuntimeIdentifier>win-x64</RuntimeIdentifier></PropertyGroup><ItemGroup> <PackageReference Include="Microsoft.WSL.Containers" Version="2.9.9" /></ItemGroup>RuntimeIdentifier elige la DLL nativa; usa win-arm64 en una máquina Arm.
El programa
Sección titulada «El programa»using System.Runtime.InteropServices;using System.Text;using Microsoft.WSL.Containers;
const string ContainerName = "wslc-host-hello";
var missing = WslcService.GetMissingComponents();if (missing.Count > 0){ Console.WriteLine($"Missing WSL components: {string.Join(", ", missing)} (run wsl --install)"); return 1;}var version = WslcService.GetVersion();Console.WriteLine($"WSL container service {version.Major}.{version.Minor}.{version.Revision}");
// La sesión guarda sus imágenes y contenedores en su propio storage.vhdx.var storage = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WslcHost");using var session = new Session(new SessionSettings("wslc-host", storage){ CpuCount = 2, MemorySizeInMB = 2048});session.Start();Console.WriteLine($"Session started, storage in {storage}");
// Un contenedor dejado por una ejecución que falló sobrevive en storage.vhdx y bloquea el nombre.try{ using var leftover = session.OpenContainer(ContainerName, ProcessOutputMode.Discard); leftover.Delete(DeleteContainerOption.Force); Console.WriteLine($"Removed leftover container {ContainerName}");}catch (COMException) { }
await session.PullImageAsync(new PullImageOptions("docker.io/library/alpine:latest"));foreach (var image in session.GetImages()) Console.WriteLine($"Image {image.Name} ({image.Size / 1024 / 1024} MB)");
using var container = session.CreateContainer(new ContainerSettings("alpine:latest"){ Name = ContainerName, InitProcess = new ProcessSettings { CommandLine = args.Length > 0 ? [.. args] : ["/bin/sh", "-c", "echo Hello from $(cat /etc/alpine-release) on $(uname -r)"], OutputMode = ProcessOutputMode.Event }});
try{ var exited = new TaskCompletionSource<int>(); container.InitProcess.OutputReceived += data => Console.Write(Encoding.UTF8.GetString(data)); container.InitProcess.Exited += code => exited.TrySetResult(code); container.Start();
var exitCode = await exited.Task.WaitAsync(TimeSpan.FromMinutes(2)); Console.WriteLine($"Container {container.Id[..12]} exited with code {exitCode}"); return exitCode;}finally{ container.Delete(DeleteContainerOption.Force); session.Terminate();}Algunas cosas que un lector de C# reconocerá:
- La
Sessiony elContainersonIDisposable, así queusing varlibera sus handles nativos al final del método. - La salida llega como eventos:
OutputReceivedcon los bytes en bruto yExitedcon el código de salida. UnTaskCompletionSource<int>convierte el evento de salida en una tarea, yWaitAsyncle pone un tiempo límite. - Los errores del lado nativo aparecen como
COMException, o comoArgumentExceptionpara un parámetro no válido. - Los argumentos de la línea de comandos, si se dan, sustituyen al comando por defecto:
dotnet run -- /bin/cat /etc/os-releaseejecuta ese comando en su lugar.
WSL container service 2.9.11Session started, storage in C:\Users\spare\AppData\Local\WslcHostImage alpine:latest (8 MB)Hello from 3.24.1 on 6.18.40.1-microsoft-standard-WSL2Container b27dd2f80927 exited with code 0La ejecución completa tardó 7 segundos, incluida la descarga de alpine en una sesión nueva y vacía. El contenedor ve los límites de la sesión, no los de settings.yaml: nproc mostró 2, y free -m un total de 1907 MB para MemorySizeInMB = 2048.
El programa usa los cuatro objetos en orden, y su bloque finally limpia incluso cuando el contenedor no arranca.
flowchart TB svc["WslcService: componentes que faltan, versión"] --> sess["Session: iniciar, descargar la imagen"] sess --> create["Session: crear el contenedor"] create --> run["Container: iniciar el proceso inicial"] run --> proc["Process: salida y código de salida, como eventos"] proc --> clean["finally: eliminar el contenedor, terminar la sesión"] run -->|"Start falla"| cleanDónde aparece la sesión de la aplicación
Sección titulada «Dónde aparece la sesión de la aplicación»Mientras se ejecutaba un contenedor de 20 segundos, la CLI veía la sesión de la aplicación junto a sus dos propias sesiones:
> wslc system session listID Creator PID Display Name6 350476 wslc-cli-admin-spare8 275812 wslc-cli-spare16 110952 wslc-host> Get-Process vmmem*vmmemCmZygote 0vmmemWSL 3307vmmemwslc-host 510> wslc --session wslc-host container listCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES2e75803c92a9 alpine:latest "/bin/sh -c 'echo st…" 4 seconds ago Up 3 seconds wslc-host-hello- La sesión de la aplicación es una sesión
wslcnormal: el mismo tipo de VMvmmem<session>, visible y controlable desde la CLI con--session. Es la primera herramienta a la que recurrir cuando el programa se comporta mal. - Su disco está donde lo eligió la aplicación,
%LocalAppData%\WslcHost\storage.vhdx, 67 MB tras la descarga, no bajo%LocalAppData%\wslc\sessions. - Sus límites vienen de
SessionSettings: las sesiones de la CLI tienen 8 CPU en esta máquina, y los contenedores de la aplicación ven 2. - Tras
session.Terminate(), la sesión y su procesovmmemdesaparecen de inmediato, sin el retardo de inactividad de 30 segundos de las sesiones de la CLI (lección 6).
Trampa: un arranque fallido deja el contenedor atrás
Sección titulada «Trampa: un arranque fallido deja el contenedor atrás»La primera versión del programa no limpiaba contenedores sobrantes ni tenía Delete en un finally. Una ejecución lanzada desde Git Bash con /bin/sh como argumento falló: MSYS, la capa que ejecuta Bash en Windows, convirtió la ruta con aspecto de Linux en C:/Program Files/Git/usr/bin/sh, y container.Start() lanzó:
Unhandled exception. System.ArgumentException: The parameter is incorrect.
failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "C:/Program Files/Git/usr/bin/sh": stat C:/Program Files/Git/usr/bin/sh: no such file or directory: unknownLa siguiente ejecución, con un comando correcto, falló antes, en CreateContainer:
Unhandled exception. System.Runtime.InteropServices.COMException (0x800700B7): Cannot create a file when that file already exists.
Conflict. The container name "/wslc-host-hello" is already in use by container "176a59b772bebfb974a8404150407157d49cc279cbe2f66c2d6e2788f9bfbadc". You have to remove (or rename) that container to be able to reuse that name.El contenedor se había creado, nunca se inició, y sobrevivió al proceso en storage.vhdx. El programa de arriba tiene las dos correcciones: Delete(DeleteContainerOption.Force) en un finally y, al arrancar, OpenContainer seguido de Delete de un contenedor sobrante con el mismo nombre, donde una COMException simplemente significa que no hay ninguno. Verificado: el contenedor sobrante se eliminó (Removed leftover container wslc-host-hello), y un comando que no existe, /nope, ya no deja nada atrás.
Es la misma disciplina que con cualquier recurso no administrado: un objeto nativo creado por tu proceso no desaparece con el proceso.
Construir la imagen durante dotnet build
Sección titulada «Construir la imagen durante dotnet build»Descargar alpine de Docker Hub está bien para una prueba. Una aplicación real distribuye su propia imagen, y el paquete tiene una forma de construirla junto con el programa. Sus targets MSBuild, en build\Microsoft.WSL.Containers.common.targets, añaden un elemento WslcImage: después de Build, ejecutan wslc image build y luego wslc image save hacia un .tar junto al programa. Un proyecto de prueba:
<ItemGroup> <PackageReference Include="Microsoft.WSL.Containers" Version="2.9.9" /> <WslcImage Include="greeter" Image="wslc-host/greeter:1.0" Dockerfile="container/Containerfile" Context="container/" /></ItemGroup>FROM docker.io/library/alpine:3.24COPY greet.sh /usr/local/bin/greetRUN chmod +x /usr/local/bin/greetENTRYPOINT ["/usr/local/bin/greet"]La primera trampa es la de la lección 2: los targets llaman a wslc sin ruta, así que una terminal o un IDE abiertos antes de actualizar WSL no lo encuentran:
error WSLC0001: The wslc CLI check failed: 'wslc --version' returned exit code 9009. Install WSL by running 'wsl --install --no-distribution', or set the WslcCliPath property to a specific wslc.exe path.Con -p:WslcCliPath="C:\Program Files\WSL\wslc.exe", o desde una terminal nueva:
WSLC: Building image 'wslc-host/greeter:1.0'... | naming to docker.io/wslc-host/greeter:1.0 WSLC: Saving image 'wslc-host/greeter:1.0' to 'bin\Debug\net10.0-windows10.0.26100.0\win-x64\greeter.tar'...Build succeeded.Tardó 9 segundos y produjo un greeter.tar de 8,3 MB. La imagen se construye en la sesión de la CLI, wslc-cli-spare, donde se queda.
La construcción es incremental. El target compara los archivos de Context, y un archivo wslc.greeter.options que guarda las opciones, con el .tar:
| Cambio | dotnet build |
.tar |
|---|---|---|
| ninguno | 1 s, ninguna línea WSLC: |
sin cambios |
greet.sh tocado |
2 s, reconstruida (caché de capas) | reescrito |
echo edited añadido a greet.sh |
3 s, reconstruida | reescrito |
| ninguno | 2 s, ninguna línea WSLC: |
sin cambios |
Cada cambio real deja la imagen anterior sin etiqueta, como <none>, en la sesión de la CLI. Para eso sirve la propiedad WslcPruneAfterBuild.
Cargar el .tar en la sesión de la aplicación
Sección titulada «Cargar el .tar en la sesión de la aplicación»LoadImageAsync, el equivalente de wslc load, conserva la etiqueta y el ENTRYPOINT. CommandLine pasa entonces argumentos al entrypoint, como docker run image args:
await session.LoadImageAsync(Path.Combine(AppContext.BaseDirectory, "greeter.tar"));// ... new ContainerSettings("wslc-host/greeter:1.0") con CommandLine = ["Claude"]before:load 427 msafter: wslc-host/greeter:1.0Hello Claude from an image built by dotnet build (alpine 3.24.1)editedexit 0La sesión no tenía ninguna imagen antes, y la carga tardó 427 ms. La cadena completa funciona sin registro: dotnet build produce la imagen, el .tar se distribuye con el programa y el programa lo carga en su propia sesión.
flowchart LR build["dotnet build"] -->|"wslc image build"| cli["sesión de la CLI wslc-cli-user"] cli -->|"wslc image save"| tar["greeter.tar junto al programa"] tar -->|"LoadImageAsync"| app["sesión de la aplicación wslc-host"] app --> ctr["contenedor de wslc-host/greeter:1.0"]ImportImageAsync es otra cosa
Sección titulada «ImportImageAsync es otra cosa»ImportImageAsync, el equivalente de wslc import, espera un sistema de archivos plano, como el que produce wslc export a partir de un contenedor. Con el .tar de image save, un layout OCI que empieza por blobs/sha256/…, acepta el archivo, pero la imagen resultante no tiene entrypoint, así que CommandLine = ["Claude"] falla con exec: "Claude": executable file not found in $PATH, y tampoco tiene /bin/sh:
failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknownComprobado con la CLI: wslc export de un contenedor Alpine, luego wslc import, y después wslc run --rm exptest/rootfs:1 /bin/cat /etc/alpine-release mostró 3.24.1, e image inspect mostró "Cmd": null y "Entrypoint": null. save y load transportan una imagen; export e import transportan un sistema de archivos.
Integración continua
Sección titulada «Integración continua»En GitHub Actions, los runners Windows alojados no tienen el servicio WSL containers. El workflow de este curso solo compila wslc-host, lo que al menos detecta los errores del tipo CS0117 y CS1705 cuando el paquete cambia. Ejecutarlo requiere una máquina Windows con WSL 2.9 o posterior.
Puntos clave
Sección titulada «Puntos clave»Microsoft.WSL.Containersse ejecuta en el proceso de la aplicación: sin demonio, sin socket, y con una sesión propia, con su propia VM, su propio disco y sus propios límites.- El paquete 2.9.9 necesita
net10.0-windows10.0.26100.0,WindowsSdkPackageVersion10.0.26100.80 y un identificador de runtime; si no, el build falla conCS1705. - El extracto de Microsoft Learn usa nombres antiguos:
GetMissingComponentsdevuelve una lista, y las propiedades sonMemorySizeInMB,CommandLineyDeleteContainerOption. - La sesión de la aplicación es una sesión normal:
wslc --session <name>lista e inspecciona sus contenedores. - Un contenedor que no arranca sobrevive al proceso. Elimínalo en un
finally, y elimina los sobrantes al arrancar. - Un elemento
WslcImageconstruye y guarda una imagen durantedotnet build; carga el.tarconLoadImageAsync, no conImportImageAsync. - Los contenedores de la API no tienen red a menos que
NetworkingModeseaBridged.
Ejercicios
Sección titulada «Ejercicios»- Reescribe estas tres líneas de la página de Microsoft Learn para que compilen con el paquete 2.9.9:
if (WslcService.GetMissingComponents() != ComponentFlags.None) return 1;var settings = new SessionSettings("demo", storage) { MemoryMB = 1024 };container.Delete(DeleteContainerFlags.None);Solución
if (WslcService.GetMissingComponents().Count > 0) return 1;var settings = new SessionSettings("demo", storage) { MemorySizeInMB = 1024 };container.Delete(DeleteContainerOption.None);GetMissingComponents devuelve un IReadOnlyList<Component>, vacío cuando todo está instalado. Por verificar: estas tres líneas se comprobaron con los nombres leídos del ensamblado, no se compilaron por separado.
- Tu programa se cerró de golpe durante una ejecución. La siguiente ejecución lanza
COMException (0x800700B7)conConflict. The container name … is already in use. ¿Cómo sales de este estado desde la línea de comandos, sin cambiar el código?
Solución
El contenedor sobrante está en la sesión de la aplicación, no en la sesión de la CLI. Indica la sesión con la opción global:
wslc --session wslc-host container list --allwslc --session wslc-host container remove wslc-host-helloEsto solo funciona mientras la sesión de la aplicación está en marcha; queda por verificar si la CLI puede abrir por su nombre una sesión de aplicación detenida. La corrección duradera está en el código: Delete en un finally, y eliminar el contenedor sobrante al arrancar.
- ¿Por qué el programa pasa
ProcessOutputMode.Eventpara el proceso inicial, y qué perderías conProcessOutputMode.Discard, el modo usado para abrir el contenedor sobrante?
Solución
Con Event, el proceso emite OutputReceived por cada fragmento de salida, que el programa escribe en la consola. Con Discard, la salida se descarta: está bien para un contenedor que solo abres para eliminarlo, pero ya no verías Hello from 3.24.1 …. El código de salida sigue llegando por Exited en los dos casos, por verificar con Discard.
- Una persona de tu equipo compila el proyecto en Visual Studio y obtiene
WSLC0001con el código de salida 9009, mientras quewslc --versionfunciona en su terminal. Explícalo y da dos soluciones.
Solución
El código de salida 9009 significa «comando no encontrado». Visual Studio se inició antes de instalar WSL 2.9 y conservó el PATH antiguo, así que el target MSBuild no encuentra wslc sin ruta. O bien reinicia Visual Studio, para que herede el nuevo PATH, o bien define la propiedad en el proyecto o en la línea de comandos: -p:WslcCliPath="C:\Program Files\WSL\wslc.exe".
- Recibes dos archivos:
app.tar, hecho conwslc save, yrootfs.tar, hecho conwslc export. ¿Qué método de la API carga cada uno, y qué tienes que añadir cuando ejecutas el segundo?
Solución
app.tar es una imagen con sus capas y su configuración: cárgala con LoadImageAsync, y conserva su etiqueta y su ENTRYPOINT. rootfs.tar es solo un sistema de archivos: cárgalo con ImportImageAsync. La imagen resultante no tiene Cmd ni Entrypoint, así que debes dar el comando completo en CommandLine, empezando por un ejecutable que exista en ese sistema de archivos, como /bin/cat.
Fuentes
Sección titulada «Fuentes»- WSL container — Microsoft Learn: presentación y ejemplo de la API
- Referencia de la API de WSL container, y los ejemplos completos en aka.ms/wslc-samples
Microsoft.WSL.Containers— NuGet- C#/WinRT — Microsoft Learn
- Inspect assembly contents using MetadataLoadContext — Microsoft Learn
- Target frameworks: OS version in TFMs — Microsoft Learn
- Testcontainers