Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/samples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build outputs and restored packages for the WSL Container API samples.
packages/
bin/
obj/
x64/
x86/
ARM64/
Win32/
Debug/
Release/
*.exe
*.pdb
*.ilk
*.obj
*.log
*.tlog
*.user

# Runtime data created when the samples are run.
WslcNextcloudStorage/
WslcNextcloudData/
WslcStorage/
WslcQrStorage/
*.tar
*_out.txt
*_err.txt
out*.txt
err*.txt
run_nextcloud_test.ps1

# The repository-root .gitignore excludes project files (*.sln, *.csproj,
# *.vcxproj, *.filters) because they are normally generated by CMake. These
# samples are hand-authored standalone projects, so re-include their sources.
!*.sln
!*.csproj
!*.vcxproj
!*.filters
11 changes: 11 additions & 0 deletions doc/samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# WSL Container API samples

Standalone, self-contained samples that demonstrate the
[WSL Container API](https://aka.ms/wslc) (`Microsoft.WSL.Containers`).

| Sample | Language | Description |
| --- | --- | --- |
| [WSLC-HelloWorld](WSLC-HelloWorld) | C | Minimal sample that runs `echo` in an `alpine` container and prints its output. |
| [WSLC-Neofetch](WSLC-Neofetch) | C++/WinRT | Runs the Linux `neofetch` command from a native Windows `.exe`. |
| [WSLC-NextCloud](WSLC-NextCloud) | C# | Runs a Nextcloud server in a container, exposed on `http://localhost:8080`. |
| [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**. |
18 changes: 18 additions & 0 deletions doc/samples/WSLC-CustomContainer/Container/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Custom container image for the WSLC-CustomContainer sample.
#
# A tiny Python tool that turns text (e.g. a URL) into a scannable QR code drawn
# with Unicode block characters, printed straight to the terminal.
#
# This image is built automatically when the C# project is built, via the
# <WslcImage> item in WSLCCustomContainer.csproj (no manual docker/wslc steps).

FROM python:3.11-slim

# qrcode renders ASCII/Unicode QR codes with no extra native dependencies.
RUN pip install --no-cache-dir qrcode

COPY qr.py /app/qr.py

# No ENTRYPOINT/CMD: the host keeps the container alive with its own init
# process (sleep) and then execs `python /app/qr.py <text>` to render the code.
# (Run it directly with: wslc run customcontainer python /app/qr.py "your text".)
32 changes: 32 additions & 0 deletions doc/samples/WSLC-CustomContainer/Container/qr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Turn text (e.g. a URL) into a scannable QR code printed to the terminal.

Used by the WSLC-CustomContainer sample. The Windows host passes the text to
encode as command-line arguments; the QR is drawn with Unicode block characters
so it scans straight from the terminal.
"""

import sys

import qrcode


def main() -> int:
text = " ".join(sys.argv[1:]).strip()
if not text:
print("usage: qr.py <text-or-url>", file=sys.stderr)
return 2

qr = qrcode.QRCode(border=2)
qr.add_data(text)
qr.make(fit=True)

print(f"QR code for: {text}\n")
# invert=True renders dark modules as spaces on a light background, which
# scans reliably in terminals with a dark color scheme.
qr.print_ascii(invert=True)
return 0


if __name__ == "__main__":
sys.exit(main())
133 changes: 133 additions & 0 deletions doc/samples/WSLC-CustomContainer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// WSLC-CustomContainer
//
// A barebones Windows console application that turns text (e.g. a URL) into a
// scannable QR code, rendered by a tiny Python tool running inside a *custom*
// Linux container.
//
// What makes this sample different from the others: it ships its own
// Containerfile that is built automatically as part of the normal build (F5)
// via the <WslcImage> item in WSLCCustomContainer.csproj. The built image is
// saved to customcontainer.tar next to the executable, and this app loads that
// local tar (no registry pull) before running the tool.
//
// dotnet run -- "https://aka.ms/wslc"

using Microsoft.WSL.Containers;

const string imageName = "customcontainer:latest";

// The text to encode comes from the command line; fall back to a sample URL.
string text = args.Length > 0 ? string.Join(' ', args) : "https://aka.ms/wslc";

// Everything lives beside the executable — no hard-coded absolute paths. The
// container image tar is produced next to the exe by the build.
string baseDir = AppContext.BaseDirectory;
string sessionPath = Path.Combine(baseDir, "WslcQrStorage");
string imageTarPath = Path.Combine(baseDir, "customcontainer.tar");

if (!File.Exists(imageTarPath))
{
Console.Error.WriteLine($"[wslc] Image tar not found: {imageTarPath}");
Console.Error.WriteLine("[wslc] Build the project first so the custom image is auto-built.");
return 1;
}

Session? session = null;
Container? container = null;
Process? process = null;

int exitCode = 1;
using var stopEvent = new ManualResetEventSlim(false);
var consoleLock = new object();
using Stream stdout = Console.OpenStandardOutput();
using Stream stderr = Console.OpenStandardError();

void Write(Stream target, byte[] data)
{
lock (consoleLock)
{
target.Write(data, 0, data.Length);
target.Flush();
}
}

try
{
// ---- Session ----
Console.Error.WriteLine("[wslc] Creating session...");
var sessionSettings = new SessionSettings("WSLCCustomContainer", sessionPath)
{
CpuCount = 2,
MemorySizeInMB = 2048,
VhdRequirements = new VhdOptions(string.Empty, 4UL * 1024 * 1024 * 1024, VhdType.Dynamic),
};

session = new Session(sessionSettings);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: session is IDisposable so we can use using here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Same for containers, processes, and such)

session.Start();

// ---- Load the locally built image from the tar (no registry pull) ----
Console.Error.WriteLine($"[wslc] Loading image from {Path.GetFileName(imageTarPath)}...");
session.LoadImage(imageTarPath);

// ---- Create & start container ----
Console.Error.WriteLine("[wslc] Starting container...");

// The init process keeps the container alive while we exec our tool.
var initProcess = new ProcessSettings
{
CommandLine = new List<string> { "/bin/sleep", "infinity" },
};

var containerSettings = new ContainerSettings(imageName)
{
InitProcess = initProcess,
EnableAutoRemove = true,
};

container = session.CreateContainer(containerSettings);
container.Start();

// ---- Exec the QR tool, passing the text to encode ----
Console.Error.WriteLine($"[wslc] Generating QR code for: {text}");
Console.Error.WriteLine();

var processSettings = new ProcessSettings
{
CommandLine = new List<string> { "python", "/app/qr.py", text },
OutputMode = ProcessOutputMode.Event,
};

process = container.CreateProcess(processSettings);
process.OutputReceived += data => Write(stdout, data);
process.ErrorReceived += data => Write(stderr, data);
process.Exited += code =>
{
exitCode = code;
stopEvent.Set();
};

process.Start();
stopEvent.Wait();
}
catch (Exception ex)
{
Console.Error.WriteLine($"[wslc] Error: {ex.Message}");
}
Comment on lines +112 to +115
finally
{
// ---- Cleanup (single path for both success and failure) ----
Console.Error.WriteLine("[wslc] Shutting down...");
process?.Dispose();
if (container is not null)
{
try { container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10)); } catch { /* best effort */ }
container.Dispose();
}
if (session is not null)
{
try { session.Terminate(); } catch { /* best effort */ }
session.Dispose();
}
}

return exitCode;
52 changes: 52 additions & 0 deletions doc/samples/WSLC-CustomContainer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# WSLC-CustomContainer

A barebones C# console sample that turns text (e.g. a URL) into a **scannable QR
code** printed to your terminal — rendered by a tiny Python tool running inside
a **custom Linux container**, using the `Microsoft.WSL.Containers` SDK.

Unlike the other samples (which pull public images), this one ships its own
`Containerfile` that is **built automatically as part of the normal build (F5)**
via the SDK's `<WslcImage>` MSBuild item — no manual `docker`/`wslc` steps. The
app then loads that locally built image from a tar (no registry pull) and runs
`qr.py` inside the container.

## How the auto-build works

`WSLCCustomContainer.csproj` declares:

```xml
<WslcImage Include="customcontainer"
Image="customcontainer:latest"
Dockerfile="Container\Containerfile"
Context="Container"
Sources="Container"
TarLocation="$(OutDir)customcontainer.tar" />
```

After `Build`, the package runs `wslc image build` + `wslc image save`,
producing `customcontainer.tar` next to the executable. The step is incremental:
it only re-runs when files under `Container\` change. (Requires the `wslc` CLI,
installed by WSL — `wsl --install --no-distribution`.)

## Build

Requires the .NET 8 SDK. From this folder:

```
dotnet build -c Debug
```

## Run

```
dotnet run -- "https://aka.ms/wslc"
```

Pass any text or URL; with no argument it encodes a sample URL. The QR is drawn
with Unicode blocks so you can scan it straight from the terminal.

## Storage

Everything lives next to the executable (no absolute paths): `WslcQrStorage\`
holds the ephemeral session VHD, and `customcontainer.tar` is the auto-built
image.
37 changes: 37 additions & 0 deletions doc/samples/WSLC-CustomContainer/WSLCCustomContainer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>WSLCCustomContainer</RootNamespace>
<AssemblyName>qr</AssemblyName>
<!-- The native wslcsdk.dll shipped by the SDK package is architecture
specific, and the package picks which copy to deploy from PlatformTarget.
Default to x64 so a plain `dotnet build` / `dotnet run` works out of the
box. On an ARM64 device build with `dotnet build -p:PlatformTarget=ARM64`. -->
<PlatformTarget Condition="'$(PlatformTarget)' == ''">x64</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.WSL.Containers" Version="2.9.3" />
</ItemGroup>

<!--
Auto-build the custom container image as part of the normal build (F5).
The Microsoft.WSL.Containers package runs `wslc image build` + `wslc image
save` after Build, producing customcontainer.tar next to the executable.
The app loads that local tar at run time (no registry pull). The image step
is incremental: it only re-runs when files under Container\ change.
-->
<ItemGroup>
<WslcImage Include="customcontainer"
Image="customcontainer:latest"
Dockerfile="Container\Containerfile"
Context="Container"
Sources="Container"
TarLocation="$(OutDir)customcontainer.tar" />
</ItemGroup>

</Project>
25 changes: 25 additions & 0 deletions doc/samples/WSLC-HelloWorld/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# WSLC-HelloWorld

The simplest [WSL Container API](https://aka.ms/wslc) sample, written in **C**
using the flat C API (`wslcsdk.h`). Running `helloworld.exe` starts a lightweight
WSL container from a small Linux image (`alpine:latest`), runs `echo` inside it,
and prints the output to your terminal.

## Build

Open `WSLCHelloWorld.sln` in Visual Studio and build (x64), or from a developer
command prompt:

```
nuget restore WSLCHelloWorld.sln
msbuild WSLCHelloWorld.sln /p:Configuration=Debug /p:Platform=x64
```

## Run

```
x64\Debug\helloworld.exe
```

You should see `Hello, World from a WSL container!` printed to stdout. Progress
messages (`[wslc] ...`) go to stderr.
27 changes: 27 additions & 0 deletions doc/samples/WSLC-HelloWorld/WSLCHelloWorld.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.37027.9 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WSLCHelloWorld", "WSLCHelloWorld.vcxproj", "{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|ARM64.Build.0 = Debug|ARM64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|x64.ActiveCfg = Debug|x64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Debug|x64.Build.0 = Debug|x64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|ARM64.ActiveCfg = Release|ARM64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|ARM64.Build.0 = Release|ARM64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|x64.ActiveCfg = Release|x64
{2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Loading
Loading