Anonymous pipes and child standard handles

Task PROCESS-PIPE-STDIO-WINBOARD. WinBoard (lib/apps.js id winboard, run as -cp -fcp GNUChess -scp GNUChess) traps in CreatePipe at startup: it makes two anonymous pipes, starts GNUChess.exe with them as the child's stdin/stdout (STARTF_USESTDHANDLES), and talks the xboard protocol over them. GNUChess.exe is a plain MSVC console program: GetStdHandle, ReadFile, PeekNamedPipe (it polls stdin), SetStdHandle. The Cygwin build beside it (GNUChes5.exe) is not on this route.

The goal is the real thing: ordered bytes between two guest processes, with Windows' blocking, EOF and broken-pipe rules, and no protocol simulation.

Status (2026-10-06, end of day)

Piece State Commit / test
Phase 1: pipes inside one process done 1b54be50, test/test-anonymous-pipe.js
Phase 2.1: ends across instances over the wire done 46cf2193, test/test-pipe-cross-instance.js
Phase 2.2: CreateProcess starts a real child (CLI) done f1902bbc
SetWindowPlacement shows a hidden window (WinBoard's board) done d9174bf9
Remote pipe ends pump the wire without WSAStartup done 4eaa29e3
WinBoard 1.e4 → GNUChess 1…c5, cooperative and --threads done test/test-winboard-gnuchess-pipes.js; runs 20261006T0820Z-winboard-move2, 20261006T0825Z-winboard-move-threads
Browser host process_spawn: hidden second in-page instance on a private LoopbackSegment done — WinBoard 1.e4, GNUChess 1…e6 in Chrome 83f0a302; run 20261006T0840Z-winboard-browser2
Phase 3: waitable hProcess (0x00E40000|pid), exit code, TerminateProcess(child) done (CLI + browser) fe279617, test/test-child-process-object.js
Any CreateProcess as a real child (run.js --spawn-processes, app spawnProcesses: true): exe found on the guest's own C:, child runs on a snapshot of C:\ + registry, both merged back before the parent's wait returns; lpApplicationName + command line folded by $pipe_launch_line; worker threads spawn too CLI done; browser: visible child instance on a copy of C:, files merged back on exit (VirtualFS.mergeChildFrom), instmsi reaches msiexec /i and stops on 2761 there test/test-spawn-processes.js; Windows Installer 2.0 instmsi → msiinst → msiexec, run 20261006T1340Z-instmsi-msiexec, docs/re-notes/windows-installer-2.0.md
SetFilePointer / FlushFileBuffers on a pipe handle not implemented; GNUChess has not needed them on this route —

The --threads run shows one rendering defect unrelated to pipes: after 1.e4 the e2 square still draws a white pawn.

What existed before this work (2026-10-06 morning, origin/main)

What the two programs actually import

Measured with tools/pe-imports.js --all (KERNEL32, filtered):

Design

Phase 1 — pipe objects inside one process (built: src/09d7-pipes.wat)

A pipe is a connected pair of the virtual-LAN stream records ($VSOCK_TABLE), like socketpair(): the read end's record owns the ring, the write end's record names it as its peer. Destroying the write record gives the reader an orderly EOF, destroying the read record leaves the writer peerless, and phase 2's cross-instance end is a record whose peer is across the wire (peer = -2) — the transport the virtual LAN already has.

Every check is placed before the VFS fallback in each API, so files, consoles and sockets keep their paths.

Phase 2 — a real child process with inherited standard handles

CreateProcessA for a PE present in the VFS starts a second guest process: its own WASM instance and memory, booted through lib/process-boot.js, run by the same host loop.

Phase 3 — the process object

hProcess becomes a real kernel object: waitable (signalled at child exit), GetExitCodeProcess returns STILL_ACTIVE then the exit code, TerminateProcess(hChild) stops the child and never the caller, and the pid is unique. Child exit closes its endpoints, so the parent's next read sees ERROR_BROKEN_PIPE.

Regression plan

Per phase, before its commit:

  1. Phase 1: a unit test driving the handlers directly (render-helper harness) — ordered bytes, partial read, peek without consume, EOF after the last writer closes (including a duplicate keeping it open), broken pipe on write, GetFileType, bad handle → ERROR_INVALID_HANDLE; and a two-thread test in both backends (a reader thread parked in ReadFile is woken by a write from main). Re-run test-read-file-ex, test-io-wait-threads, test-worker-thread-scheduler, test-queue-user-apc and the console tests.
  2. Phase 2: a two-process test with small hand-built PEs (parent writes, child echoes, parent reads; child exit → EOF) in the CLI; then WinBoard to its board with GNUChess started (no trap).
  3. Phase 3: wait/exit-code/terminate tests; then the Done route — a normal human move in WinBoard and GNUChess's own reply on the board, reviewed screenshots, CLI and browser.

Nothing here may answer for GNUChess, drop the engine, or return success where a transport is missing.