From 70cf77fa39b1f5bd0b74a5772d24512d668c2878 Mon Sep 17 00:00:00 2001 From: Craig Loewen Date: Fri, 26 Jun 2026 15:10:06 -0400 Subject: [PATCH] Added samples --- doc/samples/.gitignore | 37 ++++ doc/samples/README.md | 11 ++ .../Container/Containerfile | 18 ++ .../WSLC-CustomContainer/Container/qr.py | 32 ++++ doc/samples/WSLC-CustomContainer/Program.cs | 133 +++++++++++++++ doc/samples/WSLC-CustomContainer/README.md | 52 ++++++ .../WSLCCustomContainer.csproj | 37 ++++ doc/samples/WSLC-HelloWorld/README.md | 25 +++ .../WSLC-HelloWorld/WSLCHelloWorld.sln | 27 +++ .../WSLC-HelloWorld/WSLCHelloWorld.vcxproj | 72 ++++++++ doc/samples/WSLC-HelloWorld/helloworld.c | 159 ++++++++++++++++++ doc/samples/WSLC-HelloWorld/packages.config | 4 + doc/samples/WSLC-Neofetch/README.md | 26 +++ doc/samples/WSLC-Neofetch/WSLCNeofetch.sln | 30 ++++ .../WSLC-Neofetch/WSLCNeofetch.vcxproj | 82 +++++++++ .../WSLCNeofetch.vcxproj.filters | 25 +++ doc/samples/WSLC-Neofetch/neofetch.cpp | 125 ++++++++++++++ doc/samples/WSLC-Neofetch/packages.config | 5 + doc/samples/WSLC-NextCloud/Program.cs | 147 ++++++++++++++++ doc/samples/WSLC-NextCloud/README.md | 31 ++++ .../WSLC-NextCloud/WSLCNextCloud.csproj | 21 +++ 21 files changed, 1099 insertions(+) create mode 100644 doc/samples/.gitignore create mode 100644 doc/samples/README.md create mode 100644 doc/samples/WSLC-CustomContainer/Container/Containerfile create mode 100644 doc/samples/WSLC-CustomContainer/Container/qr.py create mode 100644 doc/samples/WSLC-CustomContainer/Program.cs create mode 100644 doc/samples/WSLC-CustomContainer/README.md create mode 100644 doc/samples/WSLC-CustomContainer/WSLCCustomContainer.csproj create mode 100644 doc/samples/WSLC-HelloWorld/README.md create mode 100644 doc/samples/WSLC-HelloWorld/WSLCHelloWorld.sln create mode 100644 doc/samples/WSLC-HelloWorld/WSLCHelloWorld.vcxproj create mode 100644 doc/samples/WSLC-HelloWorld/helloworld.c create mode 100644 doc/samples/WSLC-HelloWorld/packages.config create mode 100644 doc/samples/WSLC-Neofetch/README.md create mode 100644 doc/samples/WSLC-Neofetch/WSLCNeofetch.sln create mode 100644 doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj create mode 100644 doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj.filters create mode 100644 doc/samples/WSLC-Neofetch/neofetch.cpp create mode 100644 doc/samples/WSLC-Neofetch/packages.config create mode 100644 doc/samples/WSLC-NextCloud/Program.cs create mode 100644 doc/samples/WSLC-NextCloud/README.md create mode 100644 doc/samples/WSLC-NextCloud/WSLCNextCloud.csproj diff --git a/doc/samples/.gitignore b/doc/samples/.gitignore new file mode 100644 index 000000000..56e934c07 --- /dev/null +++ b/doc/samples/.gitignore @@ -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 diff --git a/doc/samples/README.md b/doc/samples/README.md new file mode 100644 index 000000000..d3d40f9e7 --- /dev/null +++ b/doc/samples/README.md @@ -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**. | diff --git a/doc/samples/WSLC-CustomContainer/Container/Containerfile b/doc/samples/WSLC-CustomContainer/Container/Containerfile new file mode 100644 index 000000000..76482c09c --- /dev/null +++ b/doc/samples/WSLC-CustomContainer/Container/Containerfile @@ -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 +# 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 ` to render the code. +# (Run it directly with: wslc run customcontainer python /app/qr.py "your text".) diff --git a/doc/samples/WSLC-CustomContainer/Container/qr.py b/doc/samples/WSLC-CustomContainer/Container/qr.py new file mode 100644 index 000000000..defea388b --- /dev/null +++ b/doc/samples/WSLC-CustomContainer/Container/qr.py @@ -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 ", 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()) diff --git a/doc/samples/WSLC-CustomContainer/Program.cs b/doc/samples/WSLC-CustomContainer/Program.cs new file mode 100644 index 000000000..f477d02db --- /dev/null +++ b/doc/samples/WSLC-CustomContainer/Program.cs @@ -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 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); + 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 { "/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 { "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}"); +} +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; diff --git a/doc/samples/WSLC-CustomContainer/README.md b/doc/samples/WSLC-CustomContainer/README.md new file mode 100644 index 000000000..76c20ad0c --- /dev/null +++ b/doc/samples/WSLC-CustomContainer/README.md @@ -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 `` 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 + +``` + +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. diff --git a/doc/samples/WSLC-CustomContainer/WSLCCustomContainer.csproj b/doc/samples/WSLC-CustomContainer/WSLCCustomContainer.csproj new file mode 100644 index 000000000..1fb2b235b --- /dev/null +++ b/doc/samples/WSLC-CustomContainer/WSLCCustomContainer.csproj @@ -0,0 +1,37 @@ + + + + Exe + net8.0-windows10.0.19041.0 + enable + enable + WSLCCustomContainer + qr + + x64 + + + + + + + + + + + + diff --git a/doc/samples/WSLC-HelloWorld/README.md b/doc/samples/WSLC-HelloWorld/README.md new file mode 100644 index 000000000..f40cbb878 --- /dev/null +++ b/doc/samples/WSLC-HelloWorld/README.md @@ -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. diff --git a/doc/samples/WSLC-HelloWorld/WSLCHelloWorld.sln b/doc/samples/WSLC-HelloWorld/WSLCHelloWorld.sln new file mode 100644 index 000000000..4fc738cdd --- /dev/null +++ b/doc/samples/WSLC-HelloWorld/WSLCHelloWorld.sln @@ -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 diff --git a/doc/samples/WSLC-HelloWorld/WSLCHelloWorld.vcxproj b/doc/samples/WSLC-HelloWorld/WSLCHelloWorld.vcxproj new file mode 100644 index 000000000..1033a5f48 --- /dev/null +++ b/doc/samples/WSLC-HelloWorld/WSLCHelloWorld.vcxproj @@ -0,0 +1,72 @@ + + + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + 17.0 + Win32Proj + {2B5C2E11-2C0A-4F1B-9F3D-7E8A1C2D3E4F} + WSLCHelloWorld + helloworld + 10.0 + + + + Application + true + v145 + Unicode + + + Application + false + v145 + true + Unicode + + + + + Level3 + true + true + _CONSOLE;%(PreprocessorDefinitions) + + + Console + true + + + + + + + + + + + + + + + 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}. + + + + \ No newline at end of file diff --git a/doc/samples/WSLC-HelloWorld/helloworld.c b/doc/samples/WSLC-HelloWorld/helloworld.c new file mode 100644 index 000000000..c472cf9b6 --- /dev/null +++ b/doc/samples/WSLC-HelloWorld/helloworld.c @@ -0,0 +1,159 @@ +// WSLC-HelloWorld +// +// The simplest possible WSL Container SDK sample, written in C using the flat +// C API (wslcsdk.h). It starts a lightweight WSL container from a small Linux +// image and runs `echo` inside it, streaming the output back to the Windows +// console. + +#include +#include +#include +#include +#include "wslcsdk.h" + +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "wslcsdk.lib") + +static const char* IMAGE_NAME = "alpine:latest"; + +// Process exit is signalled here so wmain can wait for it. +static HANDLE g_exitEvent = NULL; +static INT32 g_exitCode = -1; + +static void PrintError(const wchar_t* context, HRESULT hr, PWSTR error) +{ + fwprintf(stderr, L"[wslc] Error: %s (0x%08X)", context, hr); + if (error != NULL) + { + fwprintf(stderr, L": %s", error); + CoTaskMemFree(error); + } + fwprintf(stderr, L"\n"); +} + +// Forward container stdout/stderr straight to the Windows console. +static void CALLBACK OnStdIO(WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t dataSize, PVOID context) +{ + HANDLE output = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? GetStdHandle(STD_OUTPUT_HANDLE) : GetStdHandle(STD_ERROR_HANDLE); + DWORD written = 0; + (void)context; + WriteFile(output, data, dataSize, &written, NULL); +} + +// Record the exit code and wake up wmain. +static void CALLBACK OnProcessExit(INT32 exitCode, PVOID context) +{ + (void)context; + g_exitCode = exitCode; + SetEvent(g_exitEvent); +} + +// Build a storage path in a "WslcStorage" folder next to the executable, so the +// sample doesn't depend on any hard-coded absolute path. +static void GetStoragePath(wchar_t* buffer, size_t count) +{ + wchar_t exePath[MAX_PATH]; + wchar_t* lastSlash; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + lastSlash = wcsrchr(exePath, L'\\'); + if (lastSlash != NULL) + { + *(lastSlash + 1) = L'\0'; + } + swprintf(buffer, count, L"%sWslcStorage", exePath); +} + +int wmain(void) +{ + HRESULT hr; + PWSTR error = NULL; + int result = 1; + + WslcSession session = NULL; + WslcContainer container = NULL; + WslcProcess process = NULL; + + WslcSessionSettings sessionSettings; + WslcContainerSettings containerSettings; + WslcProcessSettings initProcess; + WslcProcessSettings execProcess; + WslcProcessCallbacks callbacks; + WslcPullImageOptions pullOptions; + wchar_t storagePath[MAX_PATH]; + + PCSTR initArgv[2] = { "/bin/sleep", "60" }; + PCSTR echoArgv[2] = { "/bin/echo", "Hello, World from a WSL container!" }; + + CoInitializeEx(NULL, COINIT_MULTITHREADED); + + g_exitEvent = CreateEventW(NULL, TRUE, FALSE, NULL); + + // ---- Session ---- + fwprintf(stderr, L"[wslc] Creating session...\n"); + GetStoragePath(storagePath, ARRAYSIZE(storagePath)); + hr = WslcInitSessionSettings(L"WSLCHelloWorld", storagePath, &sessionSettings); + if (FAILED(hr)) { PrintError(L"Init session settings", hr, NULL); goto cleanup; } + + hr = WslcCreateSession(&sessionSettings, &session, &error); + if (FAILED(hr)) { PrintError(L"Create session", hr, error); goto cleanup; } + + // ---- Pull image ---- + fwprintf(stderr, L"[wslc] Pulling image '%hs'...\n", IMAGE_NAME); + ZeroMemory(&pullOptions, sizeof(pullOptions)); + pullOptions.uri = IMAGE_NAME; + hr = WslcPullSessionImage(session, &pullOptions, &error); + if (FAILED(hr)) { PrintError(L"Pull image", hr, error); goto cleanup; } + + // ---- Create & start container ---- + fwprintf(stderr, L"[wslc] Starting container...\n"); + WslcInitProcessSettings(&initProcess); + WslcSetProcessSettingsCmdLine(&initProcess, initArgv, 2); + + WslcInitContainerSettings(IMAGE_NAME, &containerSettings); + WslcSetContainerSettingsName(&containerSettings, "wslc-helloworld"); + WslcSetContainerSettingsInitProcess(&containerSettings, &initProcess); + WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_AUTO_REMOVE); + + hr = WslcCreateContainer(session, &containerSettings, &container, &error); + if (FAILED(hr)) { PrintError(L"Create container", hr, error); goto cleanup; } + + hr = WslcStartContainer(container, WSLC_CONTAINER_START_FLAG_NONE, &error); + if (FAILED(hr)) { PrintError(L"Start container", hr, error); goto cleanup; } + + // ---- Run echo ---- + fwprintf(stderr, L"[wslc] Running echo...\n"); + WslcInitProcessSettings(&execProcess); + WslcSetProcessSettingsCmdLine(&execProcess, echoArgv, 2); + + ZeroMemory(&callbacks, sizeof(callbacks)); + callbacks.onStdOut = OnStdIO; + callbacks.onStdErr = OnStdIO; + callbacks.onExit = OnProcessExit; + WslcSetProcessSettingsCallbacks(&execProcess, &callbacks, NULL); + + hr = WslcCreateContainerProcess(container, &execProcess, &process, &error); + if (FAILED(hr)) { PrintError(L"Run echo", hr, error); goto cleanup; } + + WaitForSingleObject(g_exitEvent, 30000); + result = g_exitCode; + +cleanup: + fwprintf(stderr, L"[wslc] Shutting down...\n"); + + if (process != NULL) { WslcReleaseProcess(process); } + if (g_exitEvent != NULL) { CloseHandle(g_exitEvent); } + if (container != NULL) + { + WslcStopContainer(container, WSLC_SIGNAL_SIGTERM, 5, NULL); + WslcReleaseContainer(container); + } + if (session != NULL) + { + WslcTerminateSession(session); + WslcReleaseSession(session); + } + + fwprintf(stderr, L"[wslc] Done.\n"); + CoUninitialize(); + return result; +} diff --git a/doc/samples/WSLC-HelloWorld/packages.config b/doc/samples/WSLC-HelloWorld/packages.config new file mode 100644 index 000000000..316d25e5c --- /dev/null +++ b/doc/samples/WSLC-HelloWorld/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/doc/samples/WSLC-Neofetch/README.md b/doc/samples/WSLC-Neofetch/README.md new file mode 100644 index 000000000..06210d68b --- /dev/null +++ b/doc/samples/WSLC-Neofetch/README.md @@ -0,0 +1,26 @@ +# WSLC-Neofetch + +A minimal sample that uses the [WSL Container API](https://aka.ms/wslc) to run the +Linux `neofetch` command from a native Windows executable. Running `neofetch.exe` +starts a lightweight WSL container, runs `neofetch` inside it, and streams the +output back to your terminal. Command-line arguments are forwarded, so +`neofetch.exe --help` works just like `neofetch --help` on Linux. + +## Build + +Open `WSLCNeofetch.sln` in Visual Studio and build (x64), or from a developer +command prompt: + +``` +nuget restore WSLCNeofetch.sln +msbuild WSLCNeofetch.sln /p:Configuration=Debug /p:Platform=x64 +``` + +## Run + +``` +x64\Debug\neofetch.exe # show system info +x64\Debug\neofetch.exe --help # forwarded to neofetch +``` + +Progress messages (`[wslc] ...`) are written to stderr, so piping stdout is safe. diff --git a/doc/samples/WSLC-Neofetch/WSLCNeofetch.sln b/doc/samples/WSLC-Neofetch/WSLCNeofetch.sln new file mode 100644 index 000000000..346f8d82e --- /dev/null +++ b/doc/samples/WSLC-Neofetch/WSLCNeofetch.sln @@ -0,0 +1,30 @@ +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}") = "WSLCNeofetch", "WSLCNeofetch.vcxproj", "{35158918-896E-42B7-B4E8-CF98816259AA}" +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 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|ARM64.Build.0 = Debug|ARM64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|x64.ActiveCfg = Debug|x64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Debug|x64.Build.0 = Debug|x64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|ARM64.ActiveCfg = Release|ARM64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|ARM64.Build.0 = Release|ARM64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|x64.ActiveCfg = Release|x64 + {35158918-896E-42B7-B4E8-CF98816259AA}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FA34E683-A12E-41FE-9821-9474CBF6DD1E} + EndGlobalSection +EndGlobal diff --git a/doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj b/doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj new file mode 100644 index 000000000..56e033662 --- /dev/null +++ b/doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj @@ -0,0 +1,82 @@ + + + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + 17.0 + Win32Proj + {35158918-896e-42b7-b4e8-cf98816259aa} + WSLCNeofetch + neofetch + 10.0 + true + true + false + + + + Application + true + v145 + Unicode + + + Application + false + v145 + true + Unicode + + + + + + + + + Level3 + true + true + stdcpp17 + _CONSOLE;%(PreprocessorDefinitions) + + + Console + true + + + + + + + + + + + + + + + + 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}. + + + + + \ No newline at end of file diff --git a/doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj.filters b/doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj.filters new file mode 100644 index 000000000..e7c20936c --- /dev/null +++ b/doc/samples/WSLC-Neofetch/WSLCNeofetch.vcxproj.filters @@ -0,0 +1,25 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + + + + \ No newline at end of file diff --git a/doc/samples/WSLC-Neofetch/neofetch.cpp b/doc/samples/WSLC-Neofetch/neofetch.cpp new file mode 100644 index 000000000..619fde2b9 --- /dev/null +++ b/doc/samples/WSLC-Neofetch/neofetch.cpp @@ -0,0 +1,125 @@ +// WSLC-Neofetch +// +// A Windows executable that wraps the Linux "neofetch" command using the +// WSL Container SDK, written with the modern C++/WinRT projection +// (winrt/Microsoft.WSL.Containers.h) rather than the flat C API. +// +// All command-line arguments are forwarded into the container, so +// "neofetch.exe --help" behaves just like "neofetch --help" on Linux. + +#include +#include +#include +#include +#include + +#include +#include +#include + +#pragma comment(lib, "windowsapp.lib") + +using namespace winrt; +using namespace winrt::Microsoft::WSL::Containers; + +namespace +{ + constexpr std::wstring_view c_imageName = L"anrginit/ubuntu-neofetch:1.0"; + + // Forward a chunk of container stdout/stderr straight to the Windows console. + void WriteToConsole(DWORD stdHandle, array_view data) + { + DWORD written = 0; + WriteFile(GetStdHandle(stdHandle), data.data(), static_cast(data.size()), &written, nullptr); + } + + // Build a storage path in a "WslcStorage" folder next to the executable, so + // the sample doesn't depend on any hard-coded absolute path. + std::wstring GetStoragePath() + { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(nullptr, exePath, MAX_PATH); + wchar_t* lastSlash = wcsrchr(exePath, L'\\'); + if (lastSlash != nullptr) + { + *(lastSlash + 1) = L'\0'; + } + return std::wstring{ exePath } + L"WslcStorage"; + } +} + +int wmain(int argc, wchar_t* argv[]) +{ + init_apartment(); + + // argv[0] (our exe) is replaced with "neofetch"; the rest pass through. + std::vector commandLine{ L"neofetch" }; + for (int i = 1; i < argc; ++i) + { + commandLine.emplace_back(argv[i]); + } + + try + { + // ---- Session ---- + fwprintf(stderr, L"[wslc] Creating session...\n"); + SessionSettings sessionSettings{ L"WSLCNeofetch", GetStoragePath() }; + sessionSettings.CpuCount(4); + sessionSettings.MemorySizeInMB(2048); + + Session session{ sessionSettings }; + session.Start(); + + // ---- Pull image ---- + fwprintf(stderr, L"[wslc] Pulling image '%ls'...\n", c_imageName.data()); + session.PullImage(PullImageOptions{ hstring{ c_imageName } }); + + // ---- Create & start container ---- + // The init process keeps the container alive while we exec neofetch. + fwprintf(stderr, L"[wslc] Starting container...\n"); + ProcessSettings initProcess; + initProcess.CommandLine(single_threaded_vector({ L"/bin/sleep", L"60" })); + + ContainerSettings containerSettings{ hstring{ c_imageName } }; + containerSettings.Name(L"wslc-neofetch"); + containerSettings.InitProcess(initProcess); + containerSettings.EnableAutoRemove(true); + + Container container = session.CreateContainer(containerSettings); + container.Start(); + + // ---- Exec neofetch ---- + fwprintf(stderr, L"[wslc] Running neofetch...\n"); + ProcessSettings processSettings; + processSettings.CommandLine(single_threaded_vector(std::move(commandLine))); + processSettings.OutputMode(ProcessOutputMode::Event); + + Process process = container.CreateProcess(processSettings); + + handle exitEvent{ CreateEvent(nullptr, TRUE, FALSE, nullptr) }; + int32_t exitCode = -1; + + process.OutputReceived([](array_view data) { WriteToConsole(STD_OUTPUT_HANDLE, data); }); + process.ErrorReceived([](array_view data) { WriteToConsole(STD_ERROR_HANDLE, data); }); + process.Exited([&](int32_t code) { + exitCode = code; + SetEvent(exitEvent.get()); + }); + + process.Start(); + WaitForSingleObject(exitEvent.get(), 30000); + + // ---- Cleanup ---- + fwprintf(stderr, L"[wslc] Shutting down...\n"); + container.Stop(Signal::SIGTERM, std::chrono::seconds{ 5 }); + session.Terminate(); + + fwprintf(stderr, L"[wslc] Done.\n"); + return exitCode; + } + catch (hresult_error const& ex) + { + fwprintf(stderr, L"[wslc] Error: %ls (0x%08X)\n", ex.message().c_str(), static_cast(ex.code())); + return 1; + } +} diff --git a/doc/samples/WSLC-Neofetch/packages.config b/doc/samples/WSLC-Neofetch/packages.config new file mode 100644 index 000000000..e1f66e207 --- /dev/null +++ b/doc/samples/WSLC-Neofetch/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/doc/samples/WSLC-NextCloud/Program.cs b/doc/samples/WSLC-NextCloud/Program.cs new file mode 100644 index 000000000..637b8a7ad --- /dev/null +++ b/doc/samples/WSLC-NextCloud/Program.cs @@ -0,0 +1,147 @@ +// WSLC-NextCloud +// +// A Windows console application that runs a Nextcloud server using the WSL +// Container SDK, written in modern C# with the C#/WinRT projection +// (Microsoft.WSL.Containers). The container exposes Nextcloud on +// http://localhost:8080 with persistent data stored next to the executable. + +using Microsoft.WSL.Containers; + +const string imageName = "nextcloud:latest"; + +// Storage lives beside the executable — no hard-coded absolute paths. The +// session storage directory must be empty for the session to be created (the +// SDK creates and reuses its own VHD inside it), so the persistent Nextcloud +// data volume lives in a separate sibling directory. +string baseDir = AppContext.BaseDirectory; +string sessionPath = Path.Combine(baseDir, "WslcNextcloudStorage"); +string volumePath = Path.Combine(baseDir, "WslcNextcloudData"); +Directory.CreateDirectory(volumePath); + +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("WSLCNextCloud", sessionPath) + { + CpuCount = 4, + MemorySizeInMB = 4096, + // Nextcloud image is ~1.5 GB; use a 10 GB dynamic VHD. + VhdRequirements = new VhdOptions(string.Empty, 10UL * 1024 * 1024 * 1024, VhdType.Dynamic), + }; + + session = new Session(sessionSettings); + session.Start(); + + // ---- Pull image ---- + Console.Error.WriteLine($"[wslc] Pulling image '{imageName}' (this may take several minutes)..."); + session.PullImage(new PullImageOptions(imageName)); + + // ---- Create & start container ---- + Console.Error.WriteLine("[wslc] Starting container..."); + + // The init process keeps the container alive while we exec the entrypoint. + var initProcess = new ProcessSettings + { + CommandLine = new List { "/bin/sleep", "infinity" }, + }; + + var containerSettings = new ContainerSettings(imageName) + { + InitProcess = initProcess, + EnableAutoRemove = true, + NetworkingMode = ContainerNetworkingMode.Bridged, + // Port mapping: host 8080 -> container 80. + PortMappings = new List { new(8080, 80, PortProtocol.TCP) }, + // Persistent data volume: bind-mount only the data directory, not the + // entire webroot. Mounting /var/www/html over 9P is extremely slow + // because Nextcloud writes thousands of PHP files there during init. + Volumes = new List { new(volumePath, "/var/www/html/data", false) }, + }; + + container = session.CreateContainer(containerSettings); + container.Start(); + + // ---- Exec the Nextcloud entrypoint ---- + Console.Error.WriteLine("[wslc] Launching Nextcloud entrypoint..."); + var processSettings = new ProcessSettings + { + CommandLine = new List { "/entrypoint.sh", "apache2-foreground" }, + 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(); + + Console.Error.WriteLine(); + Console.Error.WriteLine("[wslc] Nextcloud is running at http://localhost:8080"); + Console.Error.WriteLine("[wslc] Press Enter to stop..."); + Console.Error.WriteLine(); + + // Stop when the user presses Enter (or the entrypoint exits on its own). + var inputThread = new Thread(() => + { + Console.ReadLine(); + if (!stopEvent.IsSet) + { + exitCode = 0; + stopEvent.Set(); + } + }) + { IsBackground = true }; + inputThread.Start(); + + stopEvent.Wait(); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"[wslc] Error: {ex.Message}"); +} +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(); + } + + Console.Error.WriteLine("[wslc] Done."); +} + +return exitCode; diff --git a/doc/samples/WSLC-NextCloud/README.md b/doc/samples/WSLC-NextCloud/README.md new file mode 100644 index 000000000..596efc028 --- /dev/null +++ b/doc/samples/WSLC-NextCloud/README.md @@ -0,0 +1,31 @@ +# WSLC-NextCloud + +A sample that uses the [WSL Container API](https://aka.ms/wslc) to run a +**Nextcloud** server from a native Windows executable, written in modern C# with +the `Microsoft.WSL.Containers` C#/WinRT projection. Running `nextcloud.exe` +starts a lightweight WSL container, pulls the official `nextcloud` image, and +serves it on **http://localhost:8080** (host port 8080 → container port 80). + +## Build + +Requires the .NET 8 SDK. From this folder: + +``` +dotnet build -c Debug +``` + +## Run + +``` +dotnet run -c Debug +``` + +Open **http://localhost:8080** in your browser, then press **Enter** in the +terminal to stop the server and clean up. The first run pulls a ~1.5 GB image +and may take several minutes. + +## Storage + +Two folders are created next to the executable (no absolute paths): +`WslcNextcloudStorage\` holds the ephemeral session VHD, and `WslcNextcloudData\` +is bind-mounted at `/var/www/html/data` so user data persists between runs. diff --git a/doc/samples/WSLC-NextCloud/WSLCNextCloud.csproj b/doc/samples/WSLC-NextCloud/WSLCNextCloud.csproj new file mode 100644 index 000000000..cef8ea8cf --- /dev/null +++ b/doc/samples/WSLC-NextCloud/WSLCNextCloud.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0-windows10.0.19041.0 + enable + enable + WSLCNextCloud + nextcloud + + x64 + + + + + + +