Help us shape the future of Go! Take the 2026 Go Developer Survey

playground

module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause

README

playground

Docs CI Pages status

An in-browser container playground: a dashboard of WASI containers, images and volumes, with a terminal running wctr, for cloud and container training labs. A learner types container commands in a web page and sees what they do — no virtual machine, no server: the engine runs in the page.

Open it: https://go-wasmcontainers.github.io/playground/ — the dashboard (Containers, Images, Volumes, Terminal; logs and stats of the selected container, stop, start and remove buttons that print the wctr command they ran). The terminal alone, on xterm.js, stays at ?ui=terminal.

Status: M1. wctr pull, run (--rm, -d, --name, -e, -w, -m, --entrypoint), ps, logs, start, diff, rm, rmi, images, inspect, kill, system df, system prune, with the same semantics and exit codes as the native engine, tested by typing them into the page in Chromium and Firefox. Images and containers survive a reload; the page works offline. Containers talk to each other over TCP and UDP, by name (Networking). One wctr command per line; the shell image gives a shell, and a line with exec -i gets what you type (v0.4.0). No -p yet.

Try

$ wctr run --rm hello
Unable to find image 'hello' locally
…
Hello from a WebAssembly container!
$ wctr run --rm -e NAME=you greet
Hello, you!
$ wctr run -d --name counter lab count 5
$ wctr logs -f counter
$ wctr run --name failing lab exit 42
bye
[exit status 42]
$ wctr ps -a
$ wctr run --rm shfmt --version
$ wctr images

help lists the commands and the images; clear clears the screen; Ctrl-C stops waiting for a command.

Line editing, as in bash. Tab completes commands, flags, flag values, images (local ones for run, the catalogue for pull), container names and IDs, and lab's commands; a unique match gets a trailing space, several complete to their common prefix, and Tab Tab lists them in columns with their descriptions (asking first beyond 100). Completion works mid-line and inside quotes. Up/Down and Ctrl-R search the history, kept for the tab's session; Left/Right, Home/End, Ctrl-A/E/B/F, Ctrl-U/K/W, Ctrl-D and Ctrl-L edit. Measured from the key press to the completed line on screen (sampled every 5 ms): 7–14 ms in Chromium, 23 ms in Firefox (CI runner).

It keeps your work. Pulled images, containers (state, exit code, logs, the files they wrote) live in the browser's IndexedDB: a reload, or coming back later, finds them as you left them. A container that was running is then exited with status 137, and a --rm one is gone, as after a daemon restart (wctr diff NAME shows what a container wrote; wctr start NAME runs it again). The page itself is kept by its service worker, so a reload works offline; a new version installs in the background and is used only when you click "Reload to update". One tab runs the engine: a second tab of the same browser offers to take over. wctr system df shows what is stored and the browser's quota; wctr system prune -f (-a for unused images too) frees it. After the first pull or run the page asks the browser to keep its storage (navigator.storage.persist()): Chromium and Safari decide without asking, Firefox asks you.

Measured in Chromium (CI numbers are in each run's summary): the engine is ready in 130–190 ms on a first visit, 80–115 ms after a reload, 75 ms offline. Compiled modules are not kept across reloads by default: measured, compiling a 2.8 MB module from Cache Storage with compileStreaming takes 8.4 ms against 1.5 ms from bytes, and the guest runs no faster for it (?codecache=1 turns it on).

Images: hello, greet (a CNCF wasm OCI artifact), lab (a toolbox: echo env pwd ls cat write count sleep spin hog bench fs exit esc), mnist (reads a handwritten digit with wasi-nn: wctr run --rm mnist prints digit 7 (p=1.000), in about 80 ms in Chromium; the engine worker serves wasi_ephemeral_nn with the pure-Go CPU backend of go-wasmcontainers/gpu v0.5.1; wctr run --rm --gpu mnist gpu runs it on the page's WebGPU, and without --gpu the GPU target fails with "unsupported operation", it never falls back to the CPU. The model is MNIST-12 from the ONNX Model Zoo (MIT); the picture is picture 0 of the MNIST test set by Y. LeCun, C. Cortes and C. J. C. Burges (CC BY-SA 3.0). The image carries both notices at /usr/share/doc/mnist/NOTICE), and these:

  • compute: wasi:webgpu compute. wctr run --rm --gpu compute runs a 1M-float vector add and a 256x256 matrix product on the GPU, and checks both against the CPU.
  • triangle: wasi-gfx:surface. wctr run --rm --gpu triangle [frames [input]] draws a spinning triangle on a surface. In the dashboard the surface is a pane under Surfaces; the page acknowledges each frame, and the engine paces the guest on those acknowledgements. Click the pane to give it the focus: its pointer presses and keys go to the guest. The dashboard sends {t:"input"}, which the engine worker hands to webgpu.SurfaceInput. With input, the guest prints what it receives: pointer-down 80,80, then key-down key-a "A" shift=true.

GPU access is opt-in per container. A container without --gpu, or a browser without WebGPU, gets no adapter: these images print no GPU adapter and exit 2. Software (fallback) adapters are refused.

  • shell: go-wasmcontainers/shell 0.5.0, mirrored verbatim from ghcr.io and pinned by digest (go run ./cmd/site pin REF prints the digest to pin). It contains sh (with Tab completion, and the shared line editor: Ctrl-R search, wide characters) and 42 coreutils. Programs started by the shell run as WASIX processes in the browser:

    $ wctr run --rm -it shell
    $ wctr run -d --name c shell sleep 600
    $ wctr exec c sh -c 'ls /bin | wc -l'
    44
    $ wctr exec -it c sh
    

    In the terminal, a line whose wctr flags say -i (run -i, exec -i, start -ai) gets what you type, Ctrl-C and Ctrl-D included. Ctrl-P Ctrl-Q detaches from run -it and leaves the container running. wctr build and wctr compose are wired in as in cmd/wctr.

Last, every pkgx wasip1/wasm bottle, taken from ghcr.io/go-pkgx/packages at build time. The list is not typed anywhere: go run ./cmd/site build --pkgx reads pkgx's signed catalogue (go-pkgx.dev/catalog, bottle.FetchCatalog, which verifies the signature), keeps the projects whose versions is not empty for wasip1/wasm, and mirrors the newest version of each, so a bottle published tomorrow is an image after the next build with no code change. At the time of writing that is 43 images from 41 bottles (30 Go projects such as yq, gojq, jj, shfmt, actionlint, age, and C ones such as lua, luac, sqlite3, zstd, lz4, brotli, simdjson-minify; the C layers are tar+zstd, served as published). The page's wctr images and the dashboard's Images view list whatever catalog.json holds, and each entry names its project, version, layer bytes and checked: true when the build ran the image under the engine and wasmtime and nothing failed or differed (40 of 43 today with engine v0.30.0), false when it is only mirrored (the named expected cases in internal/mirror/testdata/smoke.json: tomlv, gnostic). wctr images and completion show [checked] or [mirrored] after the description. Engine v0.30.1 fixed what made yq differ (piped stdio is no longer reported as a tty) and reports out-of-range exit codes as 1, as gnostic needed; wasmtime still prints its own trap message on stderr for gnostic.

The rules, each tested in internal/mirror:

  • Version: the highest by numeric components, then rebuild counter (1.8.2_1 > 1.8.2), then raw string; the catalogue's order is not trusted; a string that is not version-shaped or not an OCI tag is skipped.
  • Names: an image per bin/<name>.wasm in the bottle (lua gives lua and luac), all sharing the bottle's layer, at most 8 per bottle, names in provides first. provides is only a hint: on wasip1/wasm it is copied from the host recipe and wrong both ways (go-pkgx/bk#376), so a name without a module, and a module it does not list, are findings. An image's entrypoint is the bare binary name, resolved on PATH, so argv[0] is lua, not the module file. A name taken by another project goes to the project whose last path segment equals it, else the smallest project name; the others become <project-with-dashes>-<name>. A first-party image keeps its name.
  • Dependencies are ignored: a wasip1 module is statically linked, and the catalogue's deps are read from the host recipe.
  • Limits (mirror.DefaultBottleLimits): 400 projects, 900 images, 8 images per bottle, 100 MiB per compressed layer, 512 MiB and 20 000 entries unpacked, a 3 minute catalogue timeout and 30 minutes in all. An unverifiable or empty catalogue, a network error and a count over its limit stop the build: there is no built-in list to fall back to. A bottle over a size limit, or whose tar has an absolute path, a .., or a link leaving the root, is refused and listed in findings.json, and costs only itself.
  • Page cost: one catalog.json (about 330 bytes per image, read up to 1 MiB and 1000 entries), then an image's blobs only when a command uses it.
  • Site size: GitHub Pages refuses a site over 1 GB. The mirror publishes the bottles' compressed layers, once per image (every image has its own /v2 repository). Measured on 2026-10-31 with 43 images: 112 MB of v2/, 167 MB for the whole site (engine 29 MB, dashboard 25 MB), about 2.6 MB per image, so roughly 300 images would reach the ceiling. (The 441 MB figure is the modules unpacked, which is not what is served.) Limits.MaxSiteBytes, 700 MiB of layers, is a hard error well under it. If the set outgrows it: first drop the largest layers (gosec is 20 MB), then shard the mirror across a second Pages site (a second origin: the page's CSP allows its own origin only, so this needs a policy change), or serve blobs from a release asset (which needs CORS, which release downloads do not send).
  • Not deterministic: a tag can serve different bytes over time (simdjson 5.0.5, go-pkgx/packages#393). The mirror keeps no per-version cache and the smoke compares behaviour on the bytes it fetched, never a digest.

go run ./cmd/mirror smoke --require-wasmtime --spec internal/mirror/testdata/smoke.json runs every derived image under the engine (wazero) and under wasmtime with --version and --help (or the probe the spec names, e.g. lua -v), and compares exit status, stdout and stderr byte for byte; CI does it on every change (pkgx bottles), with the findings for go-pkgx in the job summary. A failure or difference that the spec does not name fails the job, and a named one that no longer happens fails it too.

lua raises its errors with WebAssembly exception handling, which runs in Chromium and Firefox (tested) and natively:

$ wctr run --rm lua -e 'print(pcall(function() error("boom") end))'
false	(command line):1: boom
$ wctr run --rm sqlite3 :memory: 'select 6*7;'
42

Networking

$ wctr run -d --name web net serve
$ wctr run --rm net get http://web/me
hello me from 10.88.0.3:49153
$ wctr run -d --name echo net udp-echo
$ wctr run --rm net udp echo:7 ping
reply: ping
$ wctr run --rm net lookup nobody
net: lookup nobody on 127.0.0.1:53: no such host
$ wctr network create lab && wctr run -d --network lab --name db net serve

Every container joins the bridge network (or --network NAME; wctr network create|ls|inspect|rm), in the page: the engine (v0.10.0) serves the WasmEdge socket calls of a guest's WASI imports and answers its DNS queries with the network's names, so a stock Go program using stealthrocket/net runs unchanged — the net image is one (serve, get, bench, udp-echo, udp, lookup, dial). Nothing leaves the page: there is no route to the Internet or to the machine.

Measured (net bench: 200 sequential GETs, then one 8 MiB GET, between two containers on the bridge), Apple M4 Max, load 16–19 so indicative:

latency p50 / p90 throughput
Chromium (the page) 1.1 / 1.4 ms 106–112 MB/s
native: the same engine on wazero (perf job) 0.33 / 0.39 ms 1.2 GB/s
native: Go net/http over loopback TCP (upper bound) 0.05 / 0.07 ms 3.8 GB/s

The browser is ~3× slower in latency and ~11× in throughput than the same engine natively: every socket read or write is a WASI call that crosses from the container's worker to the engine's (a SharedArrayBuffer round trip, ~40–100 µs). Larger reads per call are the open item.

Join class. With a teacher's relay, the page joins the class network: in the dashboard's Networks view (Alt+4), Join class… asks for the relay's address (https only; the page reloads once), then your name and the join code (on the ?ui=terminal page, the "Join class" link under the header does the same). A class network then appears (wctr run --network class …): your containers get the class's addresses and names (web.<you>.<domain>), reach your classmates' under the teacher's firewall, and are reached by them. "Leave class" ends the session.

  • The engine worker alone talks to the relay: the service worker extends that worker's CSP connect-src to the one relay origin you entered (https and wss), and the page keeps connect-src 'self' (both tested).
  • The join code is a secret: typed in a password field, posted once to the engine worker, cleared; never stored, logged, put in a URL or the history (the e2e checks every place the page keeps things). The session token is kept in IndexedDB so a reload rejoins without the code, and is deleted by "Leave class".
  • The relay's certificate must be one the browser trusts: a publicly trusted one (wctr-relay serve --tls acme), or the classroom CA (wctr-relay ca export) installed in the browser or the system. A page cannot pin a CA itself; Firefox keeps its own certificate store, so there the classroom CA goes in Firefox's settings (or an enterprise policy), not only the system's.
  • The teacher runs the relay for the playground's origin: wctr-relay serve --cors-origin https://go-wasmcontainers.github.io (relay v0.3.0 or later; repeatable, * refused). That allows the join calls' CORS preflight and the data channel's Origin check. Without it, joining fails as a bare "NetworkError" (in Firefox first: it sends the Connect client's User-Agent, which the preflight must allow). The e2e runs a real relay configured that way.
  • The page and the dashboard meet through window.__wctrClass (relay, useRelay(origin), status()), the protocol of the dashboard's README ("Join class"); the e2e drives the dashboard's own dialog, one key at a time, and checks the code is in no URL, storage, DOM or test hook.

-p in the browser, on an origin of its own. After wctr run -d -p 8080:80 net serve, the bar above the terminal shows open :8080, which opens the container's web pages in a tab of their own. They are not on this origin: a container's page here could read the playground's storage (the class session token) and script it, and a sandboxed page cannot be served by a service worker at all (an opaque origin has no active service worker, Service Workers §2.5.1; measured in Chromium: its stylesheets, scripts, images and fetch all bypass the worker). So containers' pages live on another site, go-wasmcontainers-usercontent.github.io (github.io is on the Public Suffix List):

playground tab (this origin, isolated: the engine)
   ▲ BroadcastChannel, per-request ids bound to a nonce the playground issued
view tab (this origin, not isolated, not frameable)
   └─ iframe <usercontent origin>/shell.html?s=<session>#<nonce>
        └─ iframe /s/<session>/8080/…  ← the container's page; its requests go to
           the usercontent service worker → shell → view → engine → portmap.Virtual
  • The shell (internal/usercontent, written by site usercontent --allow <playground origin>; each <org>-usercontent Pages repository runs it, so one implementation serves several playgrounds) talks to the pinned origin only, accepts only messages carrying the session's nonce (read from its URL fragment, then removed from it), refuses to run outside that origin (ancestorOrigins, else the referrer; its service worker adds frame-ancestors once installed, as Pages sends no headers), and caches nothing.

  • What a container may say is decided in Go (internal/portroute, fuzzed): methods, paths and header fields bounded; Cookie, Host and forwarding fields from the page dropped, X-Forwarded-Host/Proto/Prefix set; Set-Cookie, Clear-Site-Data, Service-Worker-Allowed, hop-by-hop and the origin's own policies never reach the browser; Location to localhost:<port> rewritten to that port's path, so an application can send the browser to an identity provider in another container. A service worker can neither see nor set cookies, so the engine keeps a cookie jar per session (sessions work; document.cookie in the container's page does not see them).

  • The shell is not a boundary against the container's own pages (they share its origin); what they reach through it is HTTP to this session's published ports, nothing of the playground.

  • The engine publishes ports in memory (engine portmap.Virtual) and issues a session per opened port (internal/portrelay). A session is a random id naming the port's pages (/s/<sid>/<port>/) and a nonce for the shell, bound to that one port and to the usercontent origin. A request for a session it did not issue, or for another port, is refused. Each session has its own cookie jar.

  • The view tab (view.html) is this origin, not cross-origin isolated (it embeds the shell, of another site) and not frameable. It takes the nonce out of its URL at once, and relays between the shell and the playground tab over a BroadcastChannel. The playground tab answers only for the sessions it opened, so two playground tabs never answer for each other. Closing the view ends the session.

  • Measured in Chromium (e2e/ports_test.go): 8 MiB from a container to the page, through the service worker, shell, view, playground tab, engine and back, in 32 ms (about 265 MB/s, M4 Max, under load).

Tested end to end: go test -tags e2e -run TestPublishedPort ./e2e/, with the playground and the usercontent site on two local origins. The container's page shows, an 8 MiB body arrives whole, the nonce leaves the URL, and the view is not isolated. With the engine's relay disabled, the test fails.

A file server in the tab: the fileshare image
$ wctr run -d -p 8080:80 --name files fileshare

The image is go-fileshare/fileshare, built for wasip1 and serving WebDAV only (6.5 MB gzip):

  • one share, files, with a README in it;
  • one person: user alice, password playground. The password is a demo, written into the image on purpose, since nothing the share holds belongs to anybody;
  • plaintext WebDAV, because TLS ends at the browser's origin and the hop to the container is the engine's, in memory.

open :8080 shows the server's page, and the route relays WebDAV's methods (PROPFIND, MKCOL, COPY, MOVE, LOCK…; RFC 4918) as well as HTTP's, so a WebDAV client in the page works. e2e/ports_test.go TestFileshare checks, from inside the container's page:

  • without credentials: 401 with a Basic challenge;
  • with a wrong password: 401;
  • with alice's: a PROPFIND of the share (207) lists README.txt, which reads;
  • a PUT lands (201) and reads back.

explore :8080 opens the go-fileshare explorer (v0.4.0, built into the site as explorer.wasm, fetched only when used) as a browser client of the server:

  • It asks for a user and a password first. They are sent as HTTP Basic with each request, and they stay in the page's memory. They are dropped when the explorer closes; nothing is stored.
  • The explorer runs in a frame of the playground, in its bridge mode. Its requests go straight to the engine, on a relay session of their own, with no view tab and no usercontent origin.
  • The explorer is the site's own code, not the container's, and it draws on a canvas. Nothing the container answers becomes HTML in the playground.
  • explorer.html is the only page that may be framed, and only by the playground (frame-ancestors 'self'; everything else keeps 'none').
  • The session's credentials replace any Authorization header the frame sends.
  • A download is read whole, up to the route's 256 MiB bound, and saved under a sanitised name.

TestFileshareExplorer checks:

  • a wrong password: 401, and the explorer says the server refused the login;
  • alice's: the server lists its share;
  • through the bridge, a PUT (201) reads back;
  • a frame-supplied Authorization is replaced;
  • after close, the bridge refuses.

How it works

page (main thread)          engine worker (GOOS=js)                 container workers
dashboard.wasm (canvas) ──► API over postMessage (dashboard bridge) ─► daemon
  its Terminal: session (wctr) in the page, API over the same bridge
or ?ui=terminal:
xterm.js (keys, raw) ─────► console: engine's cli/lineedit + session
                             session: wctr command tree (in-process)
   ▲  filtered output        └─► API (in-memory transport) ─► daemon
   └──────────────────────────  image store (IndexedDB) ◄─ /v2 mirror (same origin)
                                 browser executor ── one Worker per container ──► guest.wasm
                                 package wasi ◄── SharedArrayBuffer + Atomics.wait ── WASI imports
  • The dashboard rides the same engine. dashboard.wasm (go-widgets on a <canvas>, 20 MB) is downloaded and compiled while the engine worker starts, and runs once the engine is ready; it speaks the engine's HTTP API to the worker through the dashboard's postMessage bridge and follows /events. Its Terminal runs the playground's session (wctr, help, clear, the image catalogue for pull) in the page. The service worker caches it with the rest of the shell, so the dashboard works offline.
  • One engine, in a worker. The engine's daemon, its API (a subset compatible with the Docker Engine API) and the wctr command tree run in one dedicated worker built GOOS=js; wctr reaches the API through an in-memory transport (a browser has no unix sockets). The page only forwards keys and prints: editing, history and completion are the engine's pure-Go line editor (cli/lineedit), which completes through the same cobra __complete path as wctr completion bash, in-process, and is tested natively.
  • One worker per container. The guest is compiled once per image (WebAssembly.compile, cached by digest and memory limit) and posted to a fresh worker. Its wasi_snapshot_preview1 imports block in Atomics.wait while the engine's worker serves them with the engine's own WASI host — the same Go code the native engine runs in its tests — so there is one file system per container, the copy-on-write tree the engine already has, and volumes later need nothing new. Measured: a WASI call (stat) round-trips in ~38 µs.
  • Limits as natively. The guest's memory is capped in the binary (the engine rewrites the module's memory limits, which also makes it shared so the engine can read it); the wall-clock limit terminates the worker (exit 137); logs, written bytes and the number of containers are bounded. The preopened / is the container's root and PWD its working directory, which Go's wasip1 port needs to resolve relative paths.
  • Images, same origin. ghcr.io sends no CORS headers and the page's CSP allows its own origin only, so CI publishes a static, read-only /v2 mirror with the site (/playground/v2/library/<name>/…); wctr pull hello resolves docker.io/library/hello to it. A pkgx bottle becomes an image without being repacked: its tar+gzip layer as is, plus a config whose Entrypoint is the bottle's bin/<name>.wasm. Pulled blobs are kept in IndexedDB, verified again when used, so a reload keeps the images.
  • Cross-origin isolation on GitHub Pages. Shared memory needs COOP/COEP, which Pages cannot send; sw.js (after coi-serviceworker) re-serves the site's own responses with them and with the CSP, at the cost of one reload on the first visit.

Security

  • The page and its workers fetch their own origin only: CSP default-src 'self', connect-src 'self', script-src 'self' 'wasm-unsafe-eval' (WebAssembly, no eval), no inline script; xterm.js is served from the site (fetched from npm, checked against the registry's sha512).
  • Container output is hostile: before it reaches xterm.js the engine keeps text, \t \n \r \b, colours (SGR) and erase-in-line, and drops every other escape (OSC 52 clipboard writes, OSC 8 links, title and report sequences that echo back as input, DCS/APC/PM/SOS), C1 controls and invalid UTF-8; output is rate-limited (256 KiB/s, 1 MiB burst). The filter is fuzzed, and the browser test prints an OSC 52 and a javascript: link from a container and checks neither reaches the terminal.
  • Each container is a worker with no DOM; its only door to the engine is the WASI host, which bounds every argument (fuzzed in the engine).

Performance

The same hello module (2.4 MB, Go wasip1), on one GitHub-hosted ubuntu-latest runner in one CI run (e2e and perf jobs); browser numbers are from Enter to the prompt, including compile:

Chromium (headless) Firefox (headless) Native: engine on wazero Native: wasmtime 50
engine ready (page load to prompt) 0.46 s 1.0 s — —
first run (compile + run) 81 ms 291 ms 825 ms 23 ms (wasmtime run, a process)
next run (compiled module cached) 47 ms 231 ms 5.4 ms —
WASI round trip (lab bench: one stat) 97 µs 354 µs — —

On an Apple M4 Max (Chromium, local): ready 0.25 s, first run 31 ms, next 15 ms, WASI round trip 38 µs; the native engine 430 ms / 2.3 ms.

The browsers compile the guest lazily (a baseline tier first), wazero compiles it ahead of time, which is why the engine's native cold start is the slowest here and wasmtime's the fastest; a warm start in a browser pays a worker's creation and instantiation, more so in Firefox. A compiled WebAssembly.Module cannot be stored in IndexedDB (measured: DataCloneError in both browsers), so container modules compile once per page load.

Building and testing

$ go run ./cmd/site build --out _site --pkgx --remote # images + mirror (--remote: the pinned shell), engine.wasm, the page
$ go run ./cmd/site serve --dir _site                  # http://127.0.0.1:8080/playground/
$ go test ./...                                        # native: session transcript, filter, mirror
$ go run ./cmd/site playwright-driver --dir /tmp/pwd   # once: driver from npm playwright-core
$ PLAYWRIGHT_DRIVER_PATH=/tmp/pwd PLAYWRIGHT_NODEJS_PATH=$(command -v node) \
  go run github.com/mxschmitt/playwright-go/cmd/playwright install chromium firefox
$ PLAYWRIGHT_DRIVER_PATH=/tmp/pwd PLAYWRIGHT_NODEJS_PATH=$(command -v node) go test -tags e2e ./e2e/

The e2e has three opt-in parts:

  • E2E_REMOTE=1 mirrors the shell image and runs TestShell: exec, a pipeline, and exec -it and run --rm -it shell typed into the page (echo, then exit 4, the status the line ends with).

  • E2E_GPU=1 runs TestGPU in Chromium (channel chromium):

    • compute matches the CPU;
    • triangle's frames reach the page and change;
    • without --gpu, there is no adapter.

    It also runs TestGPUSurfaceInput. That test clicks a known point of the triangle's pane and types Shift+A. The guest must log pointer-down/pointer-up at the surface coordinates the pane maps the point to (letterbox included), then key-a "A" shift=true, down and up. It fails when the worker does not forward the input.

  • E2E_GPU_REAL=1 makes a software adapter fail the test. CI sets it on the macOS lane, which runs on the Apple GPU. The Linux lane has only SwiftShader on Vulkan (E2E_GPU_ARGS="--enable-features=Vulkan --use-vulkan=swiftshader --use-angle=swiftshader"). The engine refuses that software adapter even with --gpu, and the Linux lane checks the refusal.

The pages workflow publishes the site, then cmd/site verify waits until every file served at the live URL hashes like the build (Pages caches for 600 s), then the same e2e session is typed into the live page in Chromium (E2E_URL=https://go-wasmcontainers.github.io/playground/ go test -tags e2e -run 'TestLive|TestDashboard' ./e2e/). TestDashboard drives the dashboard as a person does — one key press per character, clicks at the coordinates the dashboard reports for its controls (window.__dashboard.state()), never pixels: a run typed in its Terminal appears in Containers, its logs show, the Stop button stops it, a reload restores it, and (service worker) an offline reload brings the dashboard back. Chromium (service worker and headers) and Firefox (headers) in CI, and Chromium against the live page: Playwright's Firefox runs no service workers, and GitHub Pages cannot send the isolation headers, so there is no live Firefox run.

Limitations

  • One wctr command per line, with no pipes in that line (the shell image has sh); a container's stdin is typed into with -i (see Images). Networks stay inside the page (no Internet, no published ports yet: the usercontent origin is deployed, -p is not live).
  • A running container's writes are stored every 2 s: a reload loses at most the last 2 s of them. Storage is best effort unless the browser grants persist(): under disk pressure it may evict the whole origin.
  • When a second tab takes over, the first tab's engine stops at once; a write it had under way is lost (each write is one IndexedDB transaction, so nothing is left half-written).
  • Interrupting wctr run with Ctrl-C stops waiting; the container keeps running (wctr kill).

Licence

BSD-3-Clause. web/vendor/ holds xterm.js and its fit addon (MIT, licences alongside).

Directories

Path Synopsis
cmd
dashboard command
Command dashboard is the playground's main UI (GOOS=js, in the page): the go-wasmcontainers dashboard (github.com/go-wasmcontainers/dashboard) on the #screen canvas, speaking the engine's API to the engine worker the page created (window.__wctrEngine) through the dashboard's bridge.
Command dashboard is the playground's main UI (GOOS=js, in the page): the go-wasmcontainers dashboard (github.com/go-wasmcontainers/dashboard) on the #screen canvas, speaking the engine's API to the engine worker the page created (window.__wctrEngine) through the dashboard's bridge.
engine command
Command engine is the playground's in-browser engine; it builds only for GOOS=js (see main_js.go).
Command engine is the playground's in-browser engine; it builds only for GOOS=js (see main_js.go).
mirror command
Command mirror inspects and smoke-tests the pkgx bottles the playground's static image registry carries.
Command mirror inspects and smoke-tests the pkgx bottles the playground's static image registry carries.
site command
Command site builds, serves and verifies the playground's static site.
Command site builds, serves and verifies the playground's static site.
images
compute command
Command compute is a WASI container that computes on the GPU through wasi:webgpu and checks every result against the CPU: a vector add of 1M floats and a 256x256 matrix product.
Command compute is a WASI container that computes on the GPU through wasi:webgpu and checks every result against the CPU: a vector add of 1M floats and a 256x256 matrix product.
greet command
greet says hello to $NAME (default "world"): try `wctr run -e NAME=you greet`.
greet says hello to $NAME (default "world"): try `wctr run -e NAME=you greet`.
hello command
hello is the smallest container: it greets and exits 0.
hello is the smallest container: it greets and exits 0.
lab command
lab is the playground's toolbox image: small commands that show what a container sees and does, one subcommand each.
lab is the playground's toolbox image: small commands that show what a container sees and does, one subcommand each.
mnist command
Command mnist is a WASI container that reads a handwritten digit: it embeds MNIST-12 (ONNX model zoo, MIT) and a 28x28 picture, runs the model through wasi-nn, and prints the digit with its probability.
Command mnist is a WASI container that reads a handwritten digit: it embeds MNIST-12 (ONNX model zoo, MIT) and a 28x28 picture, runs the model through wasi-nn, and prints the digit with its probability.
triangle command
Command triangle is a WASI container that draws a spinning triangle on a surface through wasi:webgpu and wasi-gfx:surface: in the playground the surface is a pane of the dashboard.
Command triangle is a WASI container that draws a spinning triangle on a surface through wasi:webgpu and wasi-gfx:surface: in the playground the surface is a pane of the dashboard.
internal
class
Package class is the playground's "Join class": the engine worker joins a relay class with a join code the learner typed, puts a "class" network on the engine (network.Manager.AddDriver), keeps the session token in the engine's state store (IndexedDB) so a reload rejoins without the code, and leaves on request (the token is deleted, the network removed).
Package class is the playground's "Join class": the engine worker joins a relay class with a join code the learner typed, puts a "class" network on the engine (network.Manager.AddDriver), keeps the session token in the engine's state store (IndexedDB) so a reload rejoins without the code, and leaves on request (the token is deleted, the network removed).
console
Package console is the playground's terminal behind its adapter: the engine's line editor (cli/lineedit) and renderer, wired to a session that runs the submitted lines.
Package console is the playground's terminal behind its adapter: the engine's line editor (cli/lineedit) and renderer, wired to a session that runs the submitted lines.
idbstore
Package idbstore is an image.Backend over the browser's IndexedDB, so pulled images survive a page reload.
Package idbstore is an image.Backend over the browser's IndexedDB, so pulled images survive a page reload.
mirror
Package mirror writes the playground's image registry as static files: a read-only OCI distribution /v2 tree that GitHub Pages can serve, so the page pulls same-origin (ghcr.io sends no CORS headers, and the page's CSP allows connections to its own origin only).
Package mirror writes the playground's image registry as static files: a read-only OCI distribution /v2 tree that GitHub Pages can serve, so the page pulls same-origin (ghcr.io sends no CORS headers, and the page's CSP allows connections to its own origin only).
portrelay
Package portrelay is the engine worker's side of the HTTP route from a page to a container's published port (`wctr run -p` in a browser; the policy of what passes is internal/portroute).
Package portrelay is the engine worker's side of the HTTP route from a page to a container's published port (`wctr run -p` in a browser; the policy of what passes is internal/portroute).
portroute
Package portroute is the policy of the HTTP route from a page to a container's published port (`wctr run -p` in a browser): what a request relayed from the usercontent origin may carry, what a container's answer may say to the browser, and where its redirects lead.
Package portroute is the policy of the HTTP route from a page to a container's published port (`wctr run -p` in a browser): what a request relayed from the usercontent origin may carry, what a container's answer may say to the browser, and where its redirects lead.
session
Package session turns terminal lines into wctr commands run against an engine, and makes what they print safe for the terminal: escape sequences filtered (termfilter), output rate bounded.
Package session turns terminal lines into wctr commands run against an engine, and makes what they print safe for the terminal: escape sequences filtered (termfilter), output rate bounded.
site
Package site assembles the playground's static site, serves it for tests, and checks a deployed copy against a build.
Package site assembles the playground's static site, serves it for tests, and checks a deployed copy against a build.
termfilter
Package termfilter makes untrusted output safe to hand to a terminal emulator (xterm.js in the playground).
Package termfilter makes untrusted output safe to hand to a terminal emulator (xterm.js in the playground).
thirdparty
Package thirdparty fetches the page's third-party scripts from the npm registry, checks them against the registry's published integrity hash, and writes the files the page serves itself (its CSP allows no other origin).
Package thirdparty fetches the page's third-party scripts from the npm registry, checks them against the registry's published integrity hash, and writes the files the page serves itself (its CSP allows no other origin).
usercontent
Package usercontent writes the site of a usercontent origin (<org>-usercontent.github.io): the shell that shows a container's published port inside a playground, on an origin -- a site, github.io being on the Public Suffix List -- of its own, so that a container's page can never read or script the playground.
Package usercontent writes the site of a usercontent origin (<org>-usercontent.github.io): the shell that shows a container's published port inside a playground, on an origin -- a site, github.io being on the Public Suffix List -- of its own, so that a container's page can never read or script the playground.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL