Added samples to the API (#40930)

* Added samples for the WSLc API

Craig Loewen committed Jul 7, 2026 at 19:33 UTC 4f569ffc8a355dfe9a542456aec0e379f8944d64
21 files changed +1134
doc/samples/.gitignore new
+37
@@ -0,0 +1,37 @@
1 +# Build outputs and restored packages for the WSL Container API samples.
2 +packages/
3 +bin/
4 +obj/
5 +x64/
6 +x86/
7 +ARM64/
8 +Win32/
9 +Debug/
10 +Release/
11 +*.exe
12 +*.pdb
13 +*.ilk
14 +*.obj
15 +*.log
16 +*.tlog
17 +*.user
18 +
19 +# Runtime data created when the samples are run.
20 +WslcNextcloudStorage/
21 +WslcNextcloudData/
22 +WslcStorage/
23 +WslcQrStorage/
24 +*.tar
25 +*_out.txt
26 +*_err.txt
27 +out*.txt
28 +err*.txt
29 +run_nextcloud_test.ps1
30 +
31 +# The repository-root .gitignore excludes project files (*.sln, *.csproj,
32 +# *.vcxproj, *.filters) because they are normally generated by CMake. These
33 +# samples are hand-authored standalone projects, so re-include their sources.
34 +!*.sln
35 +!*.csproj
36 +!*.vcxproj
37 +!*.filters
doc/samples/README.md new
+11
@@ -0,0 +1,11 @@
1 +# WSL Container API samples
2 +
3 +Standalone, self-contained samples that demonstrate the
4 +[WSL Container API](https://aka.ms/wslc) (`Microsoft.WSL.Containers`).
5 +
6 +| Sample | Language | Description |
7 +| --- | --- | --- |
8 +| [WSLC-HelloWorld](WSLC-HelloWorld) | C | Minimal sample that runs `echo` in an `alpine` container and prints its output. |
9 +| [WSLC-Neofetch](WSLC-Neofetch) | C++/WinRT | Runs the Linux `neofetch` command from a native Windows `.exe`. |
10 +| [WSLC-NextCloud](WSLC-NextCloud) | C# | Runs a Nextcloud server in a container, exposed on `http://localhost:8080`. |
11 +| [WSLC-CustomContainer](WSLC-CustomContainer) | C# CLI | Generates a scannable QR code in your terminal with a Python tool in a **custom Containerfile that is auto-built at F5**. |
doc/samples/WSLC-CustomContainer/Container/Containerfile new
+18
@@ -0,0 +1,18 @@
1 +# Custom container image for the WSLC-CustomContainer sample.
2 +#
3 +# A tiny Python tool that turns text (e.g. a URL) into a scannable QR code drawn
4 +# with Unicode block characters, printed straight to the terminal.
5 +#
6 +# This image is built automatically when the C# project is built, via the
7 +# <WslcImage> item in WSLCCustomContainer.csproj (no manual docker/wslc steps).
8 +
9 +FROM python:3.11-slim
10 +
11 +# qrcode renders ASCII/Unicode QR codes with no extra native dependencies.
12 +RUN pip install --no-cache-dir qrcode
13 +
14 +COPY qr.py /app/qr.py
15 +
16 +# No ENTRYPOINT/CMD: the host keeps the container alive with its own init
17 +# process (sleep) and then execs `python /app/qr.py <text>` to render the code.
18 +# (Run it directly with: wslc run customcontainer python /app/qr.py "your text".)
doc/samples/WSLC-CustomContainer/Container/qr.py new
+32
@@ -0,0 +1,32 @@
1 +#!/usr/bin/env python3
2 +"""Turn text (e.g. a URL) into a scannable QR code printed to the terminal.
3 +
4 +Used by the WSLC-CustomContainer sample. The Windows host passes the text to
5 +encode as command-line arguments; the QR is drawn with Unicode block characters
6 +so it scans straight from the terminal.
7 +"""
8 +
9 +import sys
10 +
11 +import qrcode
12 +
13 +
14 +def main() -> int:
15 + text = " ".join(sys.argv[1:]).strip()
16 + if not text:
17 + print("usage: qr.py <text-or-url>", file=sys.stderr)
18 + return 2
19 +
20 + qr = qrcode.QRCode(border=2)
21 + qr.add_data(text)
22 + qr.make(fit=True)
23 +
24 + print(f"QR code for: {text}\n")
25 + # invert=True renders dark modules as spaces on a light background, which
26 + # scans reliably in terminals with a dark color scheme.
27 + qr.print_ascii(invert=True)
28 + return 0
29 +
30 +
31 +if __name__ == "__main__":
32 + sys.exit(main())
doc/samples/WSLC-CustomContainer/Program.cs new
+117
@@ -0,0 +1,117 @@
1 +// WSLC-CustomContainer
2 +//
3 +// A barebones Windows console application that turns text (e.g. a URL) into a
4 +// scannable QR code, rendered by a tiny Python tool running inside a *custom*
5 +// Linux container.
6 +//
7 +// What makes this sample different from the others: it ships its own
8 +// Containerfile that is built automatically as part of the normal build (F5)
9 +// via the <WslcImage> item in WSLCCustomContainer.csproj. The built image is
10 +// saved to customcontainer.tar next to the executable, and this app loads that
11 +// local tar (no registry pull) before running the tool.
12 +//
13 +// dotnet run -- "https://aka.ms/wslc"
14 +
15 +using Microsoft.WSL.Containers;
16 +
17 +const string imageName = "customcontainer:latest";
18 +
19 +// The text to encode comes from the command line; fall back to a sample URL.
20 +string text = args.Length > 0 ? string.Join(' ', args) : "https://aka.ms/wslc";
21 +
22 +// Everything lives beside the executable — no hard-coded absolute paths. The
23 +// container image tar is produced next to the exe by the build.
24 +string baseDir = AppContext.BaseDirectory;
25 +string sessionPath = Path.Combine(baseDir, "WslcQrStorage");
26 +string imageTarPath = Path.Combine(baseDir, "customcontainer.tar");
27 +
28 +if (!File.Exists(imageTarPath))
29 +{
30 + Console.Error.WriteLine($"[wslc] Image tar not found: {imageTarPath}");
31 + Console.Error.WriteLine("[wslc] Build the project first so the custom image is auto-built.");
32 + return 1;
33 +}
34 +
35 +int exitCode = 1;
36 +using var stopEvent = new ManualResetEventSlim(false);
37 +var consoleLock = new object();
38 +using Stream stdout = Console.OpenStandardOutput();
39 +using Stream stderr = Console.OpenStandardError();
40 +
41 +void Write(Stream target, byte[] data)
42 +{
43 + lock (consoleLock)
44 + {
45 + target.Write(data, 0, data.Length);
46 + target.Flush();
47 + }
48 +}
49 +
50 +try
51 +{
52 + // ---- Session ----
53 + Console.Error.WriteLine("[wslc] Creating session...");
54 + var sessionSettings = new SessionSettings("WSLCCustomContainer", sessionPath)
55 + {
56 + CpuCount = 2,
57 + MemorySizeInMB = 2048,
58 + VhdRequirements = new VhdOptions(string.Empty, 4UL * 1024 * 1024 * 1024, VhdType.Dynamic),
59 + };
60 +
61 + using var session = new Session(sessionSettings);
62 + session.Start();
63 +
64 + // ---- Load the locally built image from the tar (no registry pull) ----
65 + Console.Error.WriteLine($"[wslc] Loading image from {Path.GetFileName(imageTarPath)}...");
66 + session.LoadImage(imageTarPath);
67 +
68 + // ---- Create & start container ----
69 + Console.Error.WriteLine("[wslc] Starting container...");
70 +
71 + // The init process keeps the container alive while we exec our tool.
72 + var initProcess = new ProcessSettings
73 + {
74 + CommandLine = new List<string> { "/bin/sleep", "infinity" },
75 + };
76 +
77 + var containerSettings = new ContainerSettings(imageName)
78 + {
79 + InitProcess = initProcess,
80 + EnableAutoRemove = true,
81 + };
82 +
83 + using var container = session.CreateContainer(containerSettings);
84 + container.Start();
85 +
86 + // ---- Exec the QR tool, passing the text to encode ----
87 + Console.Error.WriteLine($"[wslc] Generating QR code for: {text}");
88 + Console.Error.WriteLine();
89 +
90 + var processSettings = new ProcessSettings
91 + {
92 + CommandLine = new List<string> { "python", "/app/qr.py", text },
93 + OutputMode = ProcessOutputMode.Event,
94 + };
95 +
96 + using var process = container.CreateProcess(processSettings);
97 + process.OutputReceived += data => Write(stdout, data);
98 + process.ErrorReceived += data => Write(stderr, data);
99 + process.Exited += code =>
100 + {
101 + exitCode = code;
102 + stopEvent.Set();
103 + };
104 +
105 + process.Start();
106 + stopEvent.Wait();
107 +
108 + Console.Error.WriteLine("[wslc] Shutting down...");
109 + container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));
110 + session.Terminate();
111 +}
112 +catch (Exception ex)
113 +{
114 + Console.Error.WriteLine($"[wslc] Error: {ex.Message}");
115 +}
116 +
117 +return exitCode;
doc/samples/WSLC-CustomContainer/README.md new
+52
@@ -0,0 +1,52 @@
1 +# WSLC-CustomContainer
2 +
3 +A barebones C# console sample that turns text (e.g. a URL) into a **scannable QR
4 +code** printed to your terminal — rendered by a tiny Python tool running inside
5 +a **custom Linux container**, using the `Microsoft.WSL.Containers` SDK.
6 +
7 +Unlike the other samples (which pull public images), this one ships its own
8 +`Containerfile` that is **built automatically as part of the normal build (F5)**
9 +via the SDK's `<WslcImage>` MSBuild item — no manual `docker`/`wslc` steps. The
10 +app then loads that locally built image from a tar (no registry pull) and runs
11 +`qr.py` inside the container.
12 +
13 +## How the auto-build works
14 +
15 +`WSLCCustomContainer.csproj` declares:
16 +
17 +```xml
18 +<WslcImage Include="customcontainer"
19 + Image="customcontainer:latest"
20 + Dockerfile="Container\Containerfile"
21 + Context="Container"
22 + Sources="Container"
23 + TarLocation="$(OutDir)customcontainer.tar" />
24 +```
25 +
26 +After `Build`, the package runs `wslc image build` + `wslc image save`,
27 +producing `customcontainer.tar` next to the executable. The step is incremental:
28 +it only re-runs when files under `Container\` change. (Requires the `wslc` CLI,
29 +installed by WSL — `wsl --install --no-distribution`.)
30 +
31 +## Build
32 +
33 +Requires the .NET 8 SDK. From this folder:
34 +
35 +```
36 +dotnet build -c Debug
37 +```
38 +
39 +## Run
40 +
41 +```
42 +dotnet run -- "https://aka.ms/wslc"
43 +```
44 +
45 +Pass any text or URL; with no argument it encodes a sample URL. The QR is drawn
46 +with Unicode blocks so you can scan it straight from the terminal.
47 +
48 +## Storage
49 +
50 +Everything lives next to the executable (no absolute paths): `WslcQrStorage\`
51 +holds the ephemeral session VHD, and `customcontainer.tar` is the auto-built
52 +image.
doc/samples/WSLC-CustomContainer/WSLCCustomContainer.csproj new
+37
@@ -0,0 +1,37 @@
1 +<Project Sdk="Microsoft.NET.Sdk">
2 +
3 + <PropertyGroup>
4 + <OutputType>Exe</OutputType>
5 + <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
6 + <Nullable>enable</Nullable>
7 + <ImplicitUsings>enable</ImplicitUsings>
8 + <RootNamespace>WSLCCustomContainer</RootNamespace>
9 + <AssemblyName>qr</AssemblyName>
10 + <!-- The native wslcsdk.dll shipped by the SDK package is architecture
11 + specific, and the package picks which copy to deploy from PlatformTarget.
12 + Default to x64 so a plain `dotnet build` / `dotnet run` works out of the
13 + box. On an ARM64 device build with `dotnet build -p:PlatformTarget=ARM64`. -->
14 + <PlatformTarget Condition="'$(PlatformTarget)' == ''">x64</PlatformTarget>
15 + </PropertyGroup>
16 +
17 + <ItemGroup>
18 + <PackageReference Include="Microsoft.WSL.Containers" Version="2.9.3" />
19 + </ItemGroup>
20 +
21 + <!--
22 + Auto-build the custom container image as part of the normal build (F5).
23 + The Microsoft.WSL.Containers package runs `wslc image build` + `wslc image
24 + save` after Build, producing customcontainer.tar next to the executable.
25 + The app loads that local tar at run time (no registry pull). The image step
26 + is incremental: it only re-runs when files under Container\ change.
27 + -->
28 + <ItemGroup>
29 + <WslcImage Include="customcontainer"
30 + Image="customcontainer:latest"
31 + Dockerfile="Container\Containerfile"
32 + Context="Container"
33 + Sources="Container"
34 + TarLocation="$(OutDir)customcontainer.tar" />
35 + </ItemGroup>
36 +
37 +</Project>
doc/samples/WSLC-HelloWorld/README.md new
+25
@@ -0,0 +1,25 @@
1 +# WSLC-HelloWorld
2 +
3 +The simplest [WSL Container API](https://aka.ms/wslc) sample, written in **C**
4 +using the flat C API (`wslcsdk.h`). Running `helloworld.exe` starts a lightweight
5 +WSL container from a small Linux image (`alpine:latest`), runs `echo` inside it,
6 +and prints the output to your terminal.
7 +
8 +## Build
9 +
10 +Open `WSLCHelloWorld.sln` in Visual Studio and build (x64), or from a developer
11 +command prompt:
12 +
13 +```
14 +nuget restore WSLCHelloWorld.sln
15 +msbuild WSLCHelloWorld.sln /p:Configuration=Debug /p:Platform=x64
16 +```
17 +
18 +## Run
19 +
20 +```
21 +x64\Debug\helloworld.exe
22 +```
23 +
24 +You should see `Hello, World from a WSL container!` printed to stdout. Progress
25 +messages (`[wslc] ...`) go to stderr.
doc/samples/WSLC-HelloWorld/WSLCHelloWorld.sln new
+27
@@ -0,0 +1,27 @@
1 +Microsoft Visual Studio Solution File, Format Version 12.00
2 +# Visual Studio Version 17
3 +VisualStudioVersion = 17.14.37027.9 d17.14
4 +MinimumVisualStudioVersion = 10.0.40219.1
5 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WSLCHelloWorld", "WSLCHelloWorld.vcxproj", "{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}"
6 +EndProject
7 +Global
8 + GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 + Debug|ARM64 = Debug|ARM64
10 + Debug|x64 = Debug|x64
11 + Release|ARM64 = Release|ARM64
12 + Release|x64 = Release|x64
13 + EndGlobalSection
14 + GlobalSection(ProjectConfigurationPlatforms) = postSolution
15 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|ARM64.ActiveCfg = Debug|ARM64
16 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|ARM64.Build.0 = Debug|ARM64
17 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|x64.ActiveCfg = Debug|x64
18 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|x64.Build.0 = Debug|x64
19 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|ARM64.ActiveCfg = Release|ARM64
20 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|ARM64.Build.0 = Release|ARM64
21 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|x64.ActiveCfg = Release|x64
22 + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|x64.Build.0 = Release|x64
23 + EndGlobalSection
24 + GlobalSection(SolutionProperties) = preSolution
25 + HideSolutionNode = FALSE
26 + EndGlobalSection
27 +EndGlobal
doc/samples/WSLC-HelloWorld/WSLCHelloWorld.vcxproj new
+72
@@ -0,0 +1,72 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 + <ItemGroup Label="ProjectConfigurations">
4 + <ProjectConfiguration Include="Debug|x64">
5 + <Configuration>Debug</Configuration>
6 + <Platform>x64</Platform>
7 + </ProjectConfiguration>
8 + <ProjectConfiguration Include="Release|x64">
9 + <Configuration>Release</Configuration>
10 + <Platform>x64</Platform>
11 + </ProjectConfiguration>
12 + <ProjectConfiguration Include="Debug|ARM64">
13 + <Configuration>Debug</Configuration>
14 + <Platform>ARM64</Platform>
15 + </ProjectConfiguration>
16 + <ProjectConfiguration Include="Release|ARM64">
17 + <Configuration>Release</Configuration>
18 + <Platform>ARM64</Platform>
19 + </ProjectConfiguration>
20 + </ItemGroup>
21 + <PropertyGroup Label="Globals">
22 + <VCProjectVersion>17.0</VCProjectVersion>
23 + <Keyword>Win32Proj</Keyword>
24 + <ProjectGuid>{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}</ProjectGuid>
25 + <RootNamespace>WSLCHelloWorld</RootNamespace>
26 + <TargetName>helloworld</TargetName>
27 + <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
28 + </PropertyGroup>
29 + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
30 + <PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
31 + <ConfigurationType>Application</ConfigurationType>
32 + <UseDebugLibraries>true</UseDebugLibraries>
33 + <PlatformToolset>v145</PlatformToolset>
34 + <CharacterSet>Unicode</CharacterSet>
35 + </PropertyGroup>
36 + <PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
37 + <ConfigurationType>Application</ConfigurationType>
38 + <UseDebugLibraries>false</UseDebugLibraries>
39 + <PlatformToolset>v145</PlatformToolset>
40 + <WholeProgramOptimization>true</WholeProgramOptimization>
41 + <CharacterSet>Unicode</CharacterSet>
42 + </PropertyGroup>
43 + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
44 + <ItemDefinitionGroup>
45 + <ClCompile>
46 + <WarningLevel>Level3</WarningLevel>
47 + <SDLCheck>true</SDLCheck>
48 + <ConformanceMode>true</ConformanceMode>
49 + <PreprocessorDefinitions>_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
50 + </ClCompile>
51 + <Link>
52 + <SubSystem>Console</SubSystem>
53 + <GenerateDebugInformation>true</GenerateDebugInformation>
54 + </Link>
55 + </ItemDefinitionGroup>
56 + <ItemGroup>
57 + <ClCompile Include="helloworld.c" />
58 + </ItemGroup>
59 + <ItemGroup>
60 + <None Include="packages.config" />
61 + </ItemGroup>
62 + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
63 + <ImportGroup Label="ExtensionTargets">
64 + <Import Project="packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets" Condition="Exists('packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets')" />
65 + </ImportGroup>
66 + <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
67 + <PropertyGroup>
68 + <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
69 + </PropertyGroup>
70 + <Error Condition="!Exists('packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets'))" />
71 + </Target>
72 +</Project>
\ No newline at end of file
doc/samples/WSLC-HelloWorld/helloworld.c new
+214
@@ -0,0 +1,214 @@
1 +// WSLC-HelloWorld
2 +//
3 +// The simplest possible WSL Container SDK sample, written in C using the flat
4 +// C API (wslcsdk.h). It starts a lightweight WSL container from a small Linux
5 +// image and runs `echo` inside it, streaming the output back to the Windows
6 +// console.
7 +
8 +#include <winsock2.h>
9 +#include <windows.h>
10 +#include <stdio.h>
11 +#include <objbase.h>
12 +#include "wslcsdk.h"
13 +
14 +#pragma comment(lib, "ole32.lib")
15 +#pragma comment(lib, "wslcsdk.lib")
16 +
17 +static const char* IMAGE_NAME = "alpine:latest";
18 +
19 +// Process exit is signalled here so wmain can wait for it.
20 +static HANDLE g_exitEvent = NULL;
21 +static INT32 g_exitCode = -1;
22 +
23 +static void PrintError(const wchar_t* context, HRESULT hr, PWSTR error)
24 +{
25 + fwprintf(stderr, L"[wslc] Error: %s (0x%08X)", context, hr);
26 + if (error != NULL)
27 + {
28 + fwprintf(stderr, L": %s", error);
29 + CoTaskMemFree(error);
30 + }
31 + fwprintf(stderr, L"\n");
32 +}
33 +
34 +// Forward container stdout/stderr straight to the Windows console.
35 +static void CALLBACK OnStdIO(WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t dataSize, PVOID context)
36 +{
37 + FILE* output = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? stdout : stderr;
38 + (void)context;
39 + fprintf(output, "%.*s", (int)dataSize, (const char*)data);
40 + fflush(output);
41 +}
42 +
43 +// Record the exit code and wake up wmain.
44 +static void CALLBACK OnProcessExit(INT32 exitCode, PVOID context)
45 +{
46 + (void)context;
47 + g_exitCode = exitCode;
48 + if (!SetEvent(g_exitEvent))
49 + {
50 + fwprintf(stderr, L"[wslc] Warning: SetEvent failed (0x%08X)\n", GetLastError());
51 + }
52 +}
53 +
54 +// Build a storage path in a "WslcStorage" folder next to the executable, so the
55 +// sample doesn't depend on any hard-coded absolute path.
56 +static void GetStoragePath(wchar_t* buffer, size_t count)
57 +{
58 + wchar_t exePath[MAX_PATH];
59 + wchar_t* lastSlash;
60 + GetModuleFileNameW(NULL, exePath, MAX_PATH);
61 + lastSlash = wcsrchr(exePath, L'\\');
62 + if (lastSlash != NULL)
63 + {
64 + *(lastSlash + 1) = L'\0';
65 + }
66 + swprintf(buffer, count, L"%sWslcStorage", exePath);
67 +}
68 +
69 +int wmain(void)
70 +{
71 + HRESULT hr;
72 + PWSTR error = NULL;
73 + int result = 1;
74 +
75 + WslcSession session = NULL;
76 + WslcContainer container = NULL;
77 + WslcProcess process = NULL;
78 +
79 + WslcSessionSettings sessionSettings;
80 + WslcContainerSettings containerSettings;
81 + WslcProcessSettings initProcess;
82 + WslcProcessSettings execProcess;
83 + WslcProcessCallbacks callbacks;
84 + WslcPullImageOptions pullOptions;
85 + wchar_t storagePath[MAX_PATH];
86 + DWORD waitResult;
87 +
88 + PCSTR initArgv[2] = {"/bin/sleep", "60"};
89 + PCSTR echoArgv[2] = {"/bin/echo", "Hello, World from a WSL container!"};
90 +
91 + hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
92 + if (FAILED(hr))
93 + {
94 + PrintError(L"Initialize COM", hr, NULL);
95 + return 1;
96 + }
97 +
98 + g_exitEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
99 + if (g_exitEvent == NULL)
100 + {
101 + PrintError(L"Create exit event", HRESULT_FROM_WIN32(GetLastError()), NULL);
102 + goto cleanup;
103 + }
104 +
105 + // ---- Session ----
106 + fwprintf(stderr, L"[wslc] Creating session...\n");
107 + GetStoragePath(storagePath, ARRAYSIZE(storagePath));
108 + hr = WslcInitSessionSettings(L"WSLCHelloWorld", storagePath, &sessionSettings);
109 + if (FAILED(hr))
110 + {
111 + PrintError(L"Init session settings", hr, NULL);
112 + goto cleanup;
113 + }
114 +
115 + hr = WslcCreateSession(&sessionSettings, &session, &error);
116 + if (FAILED(hr))
117 + {
118 + PrintError(L"Create session", hr, error);
119 + goto cleanup;
120 + }
121 +
122 + // ---- Pull image ----
123 + fwprintf(stderr, L"[wslc] Pulling image '%hs'...\n", IMAGE_NAME);
124 + ZeroMemory(&pullOptions, sizeof(pullOptions));
125 + pullOptions.uri = IMAGE_NAME;
126 + hr = WslcPullSessionImage(session, &pullOptions, &error);
127 + if (FAILED(hr))
128 + {
129 + PrintError(L"Pull image", hr, error);
130 + goto cleanup;
131 + }
132 +
133 + // ---- Create & start container ----
134 + fwprintf(stderr, L"[wslc] Starting container...\n");
135 + WslcInitProcessSettings(&initProcess);
136 + WslcSetProcessSettingsCmdLine(&initProcess, initArgv, 2);
137 +
138 + WslcInitContainerSettings(IMAGE_NAME, &containerSettings);
139 + WslcSetContainerSettingsName(&containerSettings, "wslc-helloworld");
140 + WslcSetContainerSettingsInitProcess(&containerSettings, &initProcess);
141 + WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_AUTO_REMOVE);
142 +
143 + hr = WslcCreateContainer(session, &containerSettings, &container, &error);
144 + if (FAILED(hr))
145 + {
146 + PrintError(L"Create container", hr, error);
147 + goto cleanup;
148 + }
149 +
150 + hr = WslcStartContainer(container, WSLC_CONTAINER_START_FLAG_NONE, &error);
151 + if (FAILED(hr))
152 + {
153 + PrintError(L"Start container", hr, error);
154 + goto cleanup;
155 + }
156 +
157 + // ---- Run echo ----
158 + fwprintf(stderr, L"[wslc] Running echo...\n");
159 + WslcInitProcessSettings(&execProcess);
160 + WslcSetProcessSettingsCmdLine(&execProcess, echoArgv, 2);
161 +
162 + ZeroMemory(&callbacks, sizeof(callbacks));
163 + callbacks.onStdOut = OnStdIO;
164 + callbacks.onStdErr = OnStdIO;
165 + callbacks.onExit = OnProcessExit;
166 + WslcSetProcessSettingsCallbacks(&execProcess, &callbacks, NULL);
167 +
168 + hr = WslcCreateContainerProcess(container, &execProcess, &process, &error);
169 + if (FAILED(hr))
170 + {
171 + PrintError(L"Run echo", hr, error);
172 + goto cleanup;
173 + }
174 +
175 + waitResult = WaitForSingleObject(g_exitEvent, 30000);
176 + if (waitResult == WAIT_OBJECT_0)
177 + {
178 + result = g_exitCode;
179 + }
180 + else if (waitResult == WAIT_TIMEOUT)
181 + {
182 + fwprintf(stderr, L"[wslc] Error: Timed out waiting for the process to exit.\n");
183 + }
184 + else
185 + {
186 + PrintError(L"Wait for process exit", HRESULT_FROM_WIN32(GetLastError()), NULL);
187 + }
188 +
189 +cleanup:
190 + fwprintf(stderr, L"[wslc] Shutting down...\n");
191 +
192 + if (process != NULL)
193 + {
194 + WslcReleaseProcess(process);
195 + }
196 + if (g_exitEvent != NULL)
197 + {
198 + CloseHandle(g_exitEvent);
199 + }
200 + if (container != NULL)
201 + {
202 + WslcStopContainer(container, WSLC_SIGNAL_SIGTERM, 5, NULL);
203 + WslcReleaseContainer(container);
204 + }
205 + if (session != NULL)
206 + {
207 + WslcTerminateSession(session);
208 + WslcReleaseSession(session);
209 + }
210 +
211 + fwprintf(stderr, L"[wslc] Done.\n");
212 + CoUninitialize();
213 + return result;
214 +}
doc/samples/WSLC-HelloWorld/packages.config new
+4
@@ -0,0 +1,4 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<packages>
3 + <package id="Microsoft.WSL.Containers" version="2.9.3" targetFramework="native" />
4 +</packages>
doc/samples/WSLC-Neofetch/README.md new
+26
@@ -0,0 +1,26 @@
1 +# WSLC-Neofetch
2 +
3 +A minimal sample that uses the [WSL Container API](https://aka.ms/wslc) to run the
4 +Linux `neofetch` command from a native Windows executable. Running `neofetch.exe`
5 +starts a lightweight WSL container, runs `neofetch` inside it, and streams the
6 +output back to your terminal. Command-line arguments are forwarded, so
7 +`neofetch.exe --help` works just like `neofetch --help` on Linux.
8 +
9 +## Build
10 +
11 +Open `WSLCNeofetch.sln` in Visual Studio and build (x64), or from a developer
12 +command prompt:
13 +
14 +```
15 +nuget restore WSLCNeofetch.sln
16 +msbuild WSLCNeofetch.sln /p:Configuration=Debug /p:Platform=x64
17 +```
18 +
19 +## Run
20 +
21 +```
22 +x64\Debug\neofetch.exe # show system info
23 +x64\Debug\neofetch.exe --help # forwarded to neofetch
24 +```
25 +
26 +Progress messages (`[wslc] ...`) are written to stderr, so piping stdout is safe.
doc/samples/WSLC-Neofetch/WSLCNeofetch.sln new
+30
@@ -0,0 +1,30 @@
1 +Microsoft Visual Studio Solution File, Format Version 12.00
2 +# Visual Studio Version 17
3 +VisualStudioVersion = 17.14.37027.9 d17.14
4 +MinimumVisualStudioVersion = 10.0.40219.1
5 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WSLCNeofetch", "WSLCNeofetch.vcxproj", "{35158918-896E-42B7-B4E8-CF98816259AA}"
6 +EndProject
7 +Global
8 + GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 + Debug|ARM64 = Debug|ARM64
10 + Debug|x64 = Debug|x64
11 + Release|ARM64 = Release|ARM64
12 + Release|x64 = Release|x64
13 + EndGlobalSection
14 + GlobalSection(ProjectConfigurationPlatforms) = postSolution
15 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|ARM64.ActiveCfg = Debug|ARM64
16 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|ARM64.Build.0 = Debug|ARM64
17 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|x64.ActiveCfg = Debug|x64
18 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|x64.Build.0 = Debug|x64
19 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|ARM64.ActiveCfg = Release|ARM64
20 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|ARM64.Build.0 = Release|ARM64
21 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|x64.ActiveCfg = Release|x64
22 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|x64.Build.0 = Release|x64
23 + EndGlobalSection
24 + GlobalSection(SolutionProperties) = preSolution
25 + HideSolutionNode = FALSE
26 + EndGlobalSection
27 + GlobalSection(ExtensibilityGlobals) = postSolution
28 + SolutionGuid = {FA34E683-A12E-41FE-9821-9474CBF6DD1E}
29 + EndGlobalSection
30 +EndGlobal
doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj new
+82
@@ -0,0 +1,82 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 + <ItemGroup Label="ProjectConfigurations">
4 + <ProjectConfiguration Include="Debug|x64">
5 + <Configuration>Debug</Configuration>
6 + <Platform>x64</Platform>
7 + </ProjectConfiguration>
8 + <ProjectConfiguration Include="Release|x64">
9 + <Configuration>Release</Configuration>
10 + <Platform>x64</Platform>
11 + </ProjectConfiguration>
12 + <ProjectConfiguration Include="Debug|ARM64">
13 + <Configuration>Debug</Configuration>
14 + <Platform>ARM64</Platform>
15 + </ProjectConfiguration>
16 + <ProjectConfiguration Include="Release|ARM64">
17 + <Configuration>Release</Configuration>
18 + <Platform>ARM64</Platform>
19 + </ProjectConfiguration>
20 + </ItemGroup>
21 + <PropertyGroup Label="Globals">
22 + <VCProjectVersion>17.0</VCProjectVersion>
23 + <Keyword>Win32Proj</Keyword>
24 + <ProjectGuid>{35158918-896e-42b7-b4e8-cf98816259aa}</ProjectGuid>
25 + <RootNamespace>WSLCNeofetch</RootNamespace>
26 + <TargetName>neofetch</TargetName>
27 + <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
28 + <CppWinRTOptimized>true</CppWinRTOptimized>
29 + <CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
30 + <AppxPackage>false</AppxPackage>
31 + </PropertyGroup>
32 + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
33 + <PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
34 + <ConfigurationType>Application</ConfigurationType>
35 + <UseDebugLibraries>true</UseDebugLibraries>
36 + <PlatformToolset>v145</PlatformToolset>
37 + <CharacterSet>Unicode</CharacterSet>
38 + </PropertyGroup>
39 + <PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
40 + <ConfigurationType>Application</ConfigurationType>
41 + <UseDebugLibraries>false</UseDebugLibraries>
42 + <PlatformToolset>v145</PlatformToolset>
43 + <WholeProgramOptimization>true</WholeProgramOptimization>
44 + <CharacterSet>Unicode</CharacterSet>
45 + </PropertyGroup>
46 + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
47 + <ImportGroup Label="ExtensionSettings">
48 + <Import Project="packages\Microsoft.Windows.CppWinRT.3.0.260520.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('packages\Microsoft.Windows.CppWinRT.3.0.260520.1\build\native\Microsoft.Windows.CppWinRT.props')" />
49 + </ImportGroup>
50 + <PropertyGroup Label="UserMacros" />
51 + <ItemDefinitionGroup>
52 + <ClCompile>
53 + <WarningLevel>Level3</WarningLevel>
54 + <SDLCheck>true</SDLCheck>
55 + <ConformanceMode>true</ConformanceMode>
56 + <LanguageStandard>stdcpp17</LanguageStandard>
57 + <PreprocessorDefinitions>_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
58 + </ClCompile>
59 + <Link>
60 + <SubSystem>Console</SubSystem>
61 + <GenerateDebugInformation>true</GenerateDebugInformation>
62 + </Link>
63 + </ItemDefinitionGroup>
64 + <ItemGroup>
65 + <ClCompile Include="neofetch.cpp" />
66 + </ItemGroup>
67 + <ItemGroup>
68 + <None Include="packages.config" />
69 + </ItemGroup>
70 + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
71 + <ImportGroup Label="ExtensionTargets">
72 + <Import Project="packages\Microsoft.Windows.CppWinRT.3.0.260520.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('packages\Microsoft.Windows.CppWinRT.3.0.260520.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
73 + <Import Project="packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets" Condition="Exists('packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets')" />
74 + </ImportGroup>
75 + <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
76 + <PropertyGroup>
77 + <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
78 + </PropertyGroup>
79 + <Error Condition="!Exists('packages\Microsoft.Windows.CppWinRT.3.0.260520.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Windows.CppWinRT.3.0.260520.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
80 + <Error Condition="!Exists('packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.WSL.Containers.2.9.3\build\native\Microsoft.WSL.Containers.targets'))" />
81 + </Target>
82 +</Project>
\ No newline at end of file
doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj.filters new
+25
@@ -0,0 +1,25 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 + <ItemGroup>
4 + <Filter Include="Source Files">
5 + <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
6 + <Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
7 + </Filter>
8 + <Filter Include="Header Files">
9 + <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
10 + <Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
11 + </Filter>
12 + <Filter Include="Resource Files">
13 + <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
14 + <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
15 + </Filter>
16 + </ItemGroup>
17 + <ItemGroup>
18 + <ClCompile Include="neofetch.cpp">
19 + <Filter>Source Files</Filter>
20 + </ClCompile>
21 + </ItemGroup>
22 + <ItemGroup>
23 + <None Include="packages.config" />
24 + </ItemGroup>
25 +</Project>
\ No newline at end of file
doc/samples/WSLC-Neofetch/neofetch.cpp new
+139
@@ -0,0 +1,139 @@
1 +// WSLC-Neofetch
2 +//
3 +// A Windows executable that wraps the Linux "neofetch" command using the
4 +// WSL Container SDK, written with the modern C++/WinRT projection
5 +// (winrt/Microsoft.WSL.Containers.h) rather than the flat C API.
6 +//
7 +// All command-line arguments are forwarded into the container, so
8 +// "neofetch.exe --help" behaves just like "neofetch --help" on Linux.
9 +
10 +#include <winsock2.h>
11 +#include <windows.h>
12 +#include <stdio.h>
13 +#include <string>
14 +#include <vector>
15 +
16 +#include <winrt/Windows.Foundation.h>
17 +#include <winrt/Windows.Foundation.Collections.h>
18 +#include <winrt/Microsoft.WSL.Containers.h>
19 +
20 +#pragma comment(lib, "windowsapp.lib")
21 +
22 +using namespace winrt;
23 +using namespace winrt::Microsoft::WSL::Containers;
24 +
25 +namespace {
26 +constexpr std::wstring_view c_imageName = L"anrginit/ubuntu-neofetch:1.0";
27 +
28 +// Forward a chunk of container stdout/stderr straight to the Windows console.
29 +void WriteToConsole(FILE* stream, array_view<uint8_t const> data)
30 +{
31 + fprintf(stream, "%.*s", static_cast<int>(data.size()), reinterpret_cast<const char*>(data.data()));
32 + fflush(stream);
33 +}
34 +
35 +// Build a storage path in a "WslcStorage" folder next to the executable, so
36 +// the sample doesn't depend on any hard-coded absolute path.
37 +std::wstring GetStoragePath()
38 +{
39 + wchar_t exePath[MAX_PATH];
40 + GetModuleFileNameW(nullptr, exePath, MAX_PATH);
41 + wchar_t* lastSlash = wcsrchr(exePath, L'\\');
42 + if (lastSlash != nullptr)
43 + {
44 + *(lastSlash + 1) = L'\0';
45 + }
46 + return std::wstring{exePath} + L"WslcStorage";
47 +}
48 +} // namespace
49 +
50 +int wmain(int argc, wchar_t* argv[])
51 +{
52 + init_apartment();
53 +
54 + // argv[0] (our exe) is replaced with "neofetch"; the rest pass through.
55 + std::vector<hstring> commandLine{L"neofetch"};
56 + for (int i = 1; i < argc; ++i)
57 + {
58 + commandLine.emplace_back(argv[i]);
59 + }
60 +
61 + try
62 + {
63 + // ---- Session ----
64 + fwprintf(stderr, L"[wslc] Creating session...\n");
65 + SessionSettings sessionSettings{L"WSLCNeofetch", GetStoragePath()};
66 + sessionSettings.CpuCount(4);
67 + sessionSettings.MemorySizeInMB(2048);
68 +
69 + Session session{sessionSettings};
70 + session.Start();
71 +
72 + // ---- Pull image ----
73 + fwprintf(stderr, L"[wslc] Pulling image '%ls'...\n", c_imageName.data());
74 + session.PullImage(PullImageOptions{hstring{c_imageName}});
75 +
76 + // ---- Create & start container ----
77 + // The init process keeps the container alive while we exec neofetch.
78 + fwprintf(stderr, L"[wslc] Starting container...\n");
79 + ProcessSettings initProcess;
80 + initProcess.CommandLine(single_threaded_vector<hstring>({L"/bin/sleep", L"60"}));
81 +
82 + ContainerSettings containerSettings{hstring{c_imageName}};
83 + containerSettings.Name(L"wslc-neofetch");
84 + containerSettings.InitProcess(initProcess);
85 + containerSettings.EnableAutoRemove(true);
86 +
87 + Container container = session.CreateContainer(containerSettings);
88 + container.Start();
89 +
90 + // ---- Exec neofetch ----
91 + fwprintf(stderr, L"[wslc] Running neofetch...\n");
92 + ProcessSettings processSettings;
93 + processSettings.CommandLine(single_threaded_vector<hstring>(std::move(commandLine)));
94 + processSettings.OutputMode(ProcessOutputMode::Event);
95 +
96 + Process process = container.CreateProcess(processSettings);
97 +
98 + handle exitEvent{CreateEvent(nullptr, TRUE, FALSE, nullptr)};
99 + if (!exitEvent)
100 + {
101 + throw_last_error();
102 + }
103 + int32_t exitCode = -1;
104 +
105 + process.OutputReceived([](array_view<uint8_t const> data) { WriteToConsole(stdout, data); });
106 + process.ErrorReceived([](array_view<uint8_t const> data) { WriteToConsole(stderr, data); });
107 + process.Exited([&](int32_t code) {
108 + exitCode = code;
109 + if (!SetEvent(exitEvent.get()))
110 + {
111 + fwprintf(stderr, L"[wslc] Warning: SetEvent failed (0x%08X)\n", GetLastError());
112 + }
113 + });
114 +
115 + process.Start();
116 + DWORD waitResult = WaitForSingleObject(exitEvent.get(), 30000);
117 + if (waitResult == WAIT_TIMEOUT)
118 + {
119 + fwprintf(stderr, L"[wslc] Error: Timed out waiting for the process to exit.\n");
120 + }
121 + else if (waitResult != WAIT_OBJECT_0)
122 + {
123 + throw_last_error();
124 + }
125 +
126 + // ---- Cleanup ----
127 + fwprintf(stderr, L"[wslc] Shutting down...\n");
128 + container.Stop(Signal::SIGTERM, std::chrono::seconds{5});
129 + session.Terminate();
130 +
131 + fwprintf(stderr, L"[wslc] Done.\n");
132 + return exitCode;
133 + }
134 + catch (hresult_error const& ex)
135 + {
136 + fwprintf(stderr, L"[wslc] Error: %ls (0x%08X)\n", ex.message().c_str(), static_cast<uint32_t>(ex.code()));
137 + return 1;
138 + }
139 +}
doc/samples/WSLC-Neofetch/packages.config new
+5
@@ -0,0 +1,5 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<packages>
3 + <package id="Microsoft.Windows.CppWinRT" version="3.0.260520.1" targetFramework="native" />
4 + <package id="Microsoft.WSL.Containers" version="2.9.3" targetFramework="native" />
5 +</packages>
doc/samples/WSLC-NextCloud/Program.cs new
+129
@@ -0,0 +1,129 @@
1 +// WSLC-NextCloud
2 +//
3 +// A Windows console application that runs a Nextcloud server using the WSL
4 +// Container SDK, written in modern C# with the C#/WinRT projection
5 +// (Microsoft.WSL.Containers). The container exposes Nextcloud on
6 +// http://localhost:8080 with persistent data stored next to the executable.
7 +
8 +using Microsoft.WSL.Containers;
9 +
10 +const string imageName = "nextcloud:latest";
11 +
12 +// Storage lives beside the executable — no hard-coded absolute paths. The
13 +// session storage directory must be empty for the session to be created (the
14 +// SDK creates and reuses its own VHD inside it), so the persistent Nextcloud
15 +// data volume lives in a separate sibling directory.
16 +string baseDir = AppContext.BaseDirectory;
17 +string sessionPath = Path.Combine(baseDir, "WslcNextcloudStorage");
18 +string volumePath = Path.Combine(baseDir, "WslcNextcloudData");
19 +Directory.CreateDirectory(volumePath);
20 +
21 +int exitCode = 1;
22 +using var stopEvent = new ManualResetEventSlim(false);
23 +var consoleLock = new object();
24 +using Stream stdout = Console.OpenStandardOutput();
25 +using Stream stderr = Console.OpenStandardError();
26 +
27 +void Write(Stream target, byte[] data)
28 +{
29 + lock (consoleLock)
30 + {
31 + target.Write(data, 0, data.Length);
32 + target.Flush();
33 + }
34 +}
35 +
36 +try
37 +{
38 + // ---- Session ----
39 + Console.Error.WriteLine("[wslc] Creating session...");
40 + var sessionSettings = new SessionSettings("WSLCNextCloud", sessionPath)
41 + {
42 + CpuCount = 4,
43 + MemorySizeInMB = 4096,
44 + // Nextcloud image is ~1.5 GB; use a 10 GB dynamic VHD.
45 + VhdRequirements = new VhdOptions(string.Empty, 10UL * 1024 * 1024 * 1024, VhdType.Dynamic),
46 + };
47 +
48 + using var session = new Session(sessionSettings);
49 + session.Start();
50 +
51 + // ---- Pull image ----
52 + Console.Error.WriteLine($"[wslc] Pulling image '{imageName}' (this may take several minutes)...");
53 + session.PullImage(new PullImageOptions(imageName));
54 +
55 + // ---- Create & start container ----
56 + Console.Error.WriteLine("[wslc] Starting container...");
57 +
58 + // The init process keeps the container alive while we exec the entrypoint.
59 + var initProcess = new ProcessSettings
60 + {
61 + CommandLine = new List<string> { "/bin/sleep", "infinity" },
62 + };
63 +
64 + var containerSettings = new ContainerSettings(imageName)
65 + {
66 + InitProcess = initProcess,
67 + EnableAutoRemove = true,
68 + NetworkingMode = ContainerNetworkingMode.Bridged,
69 + // Port mapping: host 8080 -> container 80.
70 + PortMappings = new List<ContainerPortMapping> { new(8080, 80, PortProtocol.TCP) },
71 + // Persistent data volume: bind-mount only the data directory, not the
72 + // entire webroot. Mounting /var/www/html over 9P is extremely slow
73 + // because Nextcloud writes thousands of PHP files there during init.
74 + Volumes = new List<ContainerVolume> { new(volumePath, "/var/www/html/data", false) },
75 + };
76 +
77 + using var container = session.CreateContainer(containerSettings);
78 + container.Start();
79 +
80 + // ---- Exec the Nextcloud entrypoint ----
81 + Console.Error.WriteLine("[wslc] Launching Nextcloud entrypoint...");
82 + var processSettings = new ProcessSettings
83 + {
84 + CommandLine = new List<string> { "/entrypoint.sh", "apache2-foreground" },
85 + OutputMode = ProcessOutputMode.Event,
86 + };
87 +
88 + using var process = container.CreateProcess(processSettings);
89 + process.OutputReceived += data => Write(stdout, data);
90 + process.ErrorReceived += data => Write(stderr, data);
91 + process.Exited += code =>
92 + {
93 + exitCode = code;
94 + stopEvent.Set();
95 + };
96 +
97 + process.Start();
98 +
99 + Console.Error.WriteLine();
100 + Console.Error.WriteLine("[wslc] Nextcloud is running at http://localhost:8080");
101 + Console.Error.WriteLine("[wslc] Press Enter to stop...");
102 + Console.Error.WriteLine();
103 +
104 + // Stop when the user presses Enter (or the entrypoint exits on its own).
105 + var inputThread = new Thread(() =>
106 + {
107 + Console.ReadLine();
108 + if (!stopEvent.IsSet)
109 + {
110 + exitCode = 0;
111 + stopEvent.Set();
112 + }
113 + })
114 + { IsBackground = true };
115 + inputThread.Start();
116 +
117 + stopEvent.Wait();
118 +
119 + Console.Error.WriteLine("[wslc] Shutting down...");
120 + container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));
121 + session.Terminate();
122 + Console.Error.WriteLine("[wslc] Done.");
123 +}
124 +catch (Exception ex)
125 +{
126 + Console.Error.WriteLine($"[wslc] Error: {ex.Message}");
127 +}
128 +
129 +return exitCode;
doc/samples/WSLC-NextCloud/README.md new
+31
@@ -0,0 +1,31 @@
1 +# WSLC-NextCloud
2 +
3 +A sample that uses the [WSL Container API](https://aka.ms/wslc) to run a
4 +**Nextcloud** server from a native Windows executable, written in modern C# with
5 +the `Microsoft.WSL.Containers` C#/WinRT projection. Running `nextcloud.exe`
6 +starts a lightweight WSL container, pulls the official `nextcloud` image, and
7 +serves it on **http://localhost:8080** (host port 8080 → container port 80).
8 +
9 +## Build
10 +
11 +Requires the .NET 8 SDK. From this folder:
12 +
13 +```
14 +dotnet build -c Debug
15 +```
16 +
17 +## Run
18 +
19 +```
20 +dotnet run -c Debug
21 +```
22 +
23 +Open **http://localhost:8080** in your browser, then press **Enter** in the
24 +terminal to stop the server and clean up. The first run pulls a ~1.5 GB image
25 +and may take several minutes.
26 +
27 +## Storage
28 +
29 +Two folders are created next to the executable (no absolute paths):
30 +`WslcNextcloudStorage\` holds the ephemeral session VHD, and `WslcNextcloudData\`
31 +is bind-mounted at `/var/www/html/data` so user data persists between runs.
doc/samples/WSLC-NextCloud/WSLCNextCloud.csproj new
+21
@@ -0,0 +1,21 @@
1 +<Project Sdk="Microsoft.NET.Sdk">
2 +
3 + <PropertyGroup>
4 + <OutputType>Exe</OutputType>
5 + <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
6 + <Nullable>enable</Nullable>
7 + <ImplicitUsings>enable</ImplicitUsings>
8 + <RootNamespace>WSLCNextCloud</RootNamespace>
9 + <AssemblyName>nextcloud</AssemblyName>
10 + <!-- The native wslcsdk.dll shipped by the SDK package is architecture
11 + specific, and the package picks which copy to deploy from PlatformTarget.
12 + Default to x64 so a plain `dotnet build` / `dotnet run` works out of the
13 + box. On an ARM64 device build with `dotnet build -p:PlatformTarget=ARM64`. -->
14 + <PlatformTarget Condition="'$(PlatformTarget)' == ''">x64</PlatformTarget>
15 + </PropertyGroup>
16 +
17 + <ItemGroup>
18 + <PackageReference Include="Microsoft.WSL.Containers" Version="2.9.3" />
19 + </ItemGroup>
20 +
21 +</Project>