qemu

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// VirtioDriversURL is the stable direct-download link for the latest VirtIO drivers ISO.
	VirtioDriversURL = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"

	// WindowsISODownloadURL is the Microsoft page for downloading Windows 11 ARM64 ISO.
	// Kept for the manual-download fallback message in ResolveWindowsISO.
	WindowsISODownloadURL = "https://www.microsoft.com/en-us/software-download/windows11arm64"

	// PwshVersion is the PowerShell 7 release shipped on the answer volume.
	PwshVersion = "7.6.5"
	// PwshReleaseURL is the direct GitHub download for the self-contained ARM64 zip.
	PwshReleaseURL = "https://github.com/PowerShell/PowerShell/releases/download/v" + PwshVersion + "/PowerShell-" + PwshVersion + "-win-arm64.zip"
	// PwshZipName is the cached zip filename.
	PwshZipName = "pwsh-arm64.zip"

	// AlpineVersion is the Alpine minirootfs release used as the WSL2
	// smoke-test distro — a ~4 MB tarball, the cheapest real Linux there is.
	AlpineVersion = "3.22.1"
	// AlpineRootfsURL is the direct CDN download for the aarch64 minirootfs.
	AlpineRootfsURL = "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/aarch64/alpine-minirootfs-" + AlpineVersion + "-aarch64.tar.gz"
	// AlpineRootfsName is the cached tarball filename.
	AlpineRootfsName = "alpine-minirootfs-" + AlpineVersion + "-aarch64.tar.gz"
)
View Source
const (
	// GoSSHDPayloadName is the server's filename on the agent volume.
	GoSSHDPayloadName = "devcell-gosshd.exe"

	// GoSSHDPackage is the package cross-compiled into that payload.
	GoSSHDPackage = "github.com/devcell-sh/go-winkit/gosshd/cmd/gosshd"

	// GoSSHDLogFile is the server's log on the shared volume. It is not
	// written to the guest ramdisk: a session that fails minutes in still
	// has to be explainable after the VM is gone.
	GoSSHDLogFile = "devcell-gosshd.log"
)
View Source
const (
	// KeepAliveScriptName is the probe script's filename on the agent volume.
	KeepAliveScriptName = `devcell-keepalive.ps1`

	// KeepAliveProbeFile is a host-written file on the shared FAT volume.
	// Echoing it back proves the host->guest file channel end to end.
	KeepAliveProbeFile = `devcell-probe.txt`

	KeepAliveBanner = `=== DEVCELL KEEPALIVE PROBE ===`
)
View Source
const CDBusID = "cd-scsi-bus"

BuildInstallCommand constructs the QEMU argv for initial Windows installation. windowsISO and any virtioISO are attached as USB CD-ROMs. autounattendImage is attached as a further CD-ROM when it is an .iso, or as a removable usb-storage disk for a raw FAT image; Windows Setup searches both kinds of removable media for autounattend.xml. CDBusID names the virtio-scsi controller used for CD-ROM devices when CDBus is "scsi". Separate from the disk's NVMe controller.

View Source
const DefaultDiskSizeGB = 64

DefaultDiskSizeGB is the default Windows VM disk size.

View Source
const DefaultSSHPort = 2222

DefaultSSHPort is the *preferred* forwarded SSH port, not a fixed one. ApplyDefaults falls back to a kernel-assigned free port when it is taken.

It used to be assigned unconditionally, so two overlapping VMs raced for the same hostfwd and the loser aborted at launch with

hostfwd=tcp:127.0.0.1:2222-:22: Could not set up host forwarding rule

which surfaced downstream only as "QMP socket did not appear within 30s" and an empty results directory — see TestSpec_ApplyDefaults_SkipsAPortAlreadyInUse.

View Source
const DefaultStageTimeout = 90 * time.Minute

DefaultStageTimeout bounds a single stage. The slowest legitimate stage (nix or a driver install under TCG) runs well under an hour; a dev-env stage once sat wedged for three because nothing bounded it.

View Source
const DefaultTCGAccel = "tcg,thread=multi"

DefaultTCGAccel is the software-emulation fallback. thread=multi lets TCG spread guest vCPUs across host threads, which is the single largest win available without hardware virtualization.

View Source
const EFIShellMarker = `"EFI Internal Shell"`
View Source
const GuestControlDir = "/devcell"

GuestControlDir is where the guest tree lands on the control volume.

View Source
const GuestLogVolumeMarker = "devcell-guest-logs.txt"

The guest log volume: a FAT image any post-install VM can write logs to — the same channel the install's answer volume provides, for the same reason. The SSH stream dies with its connection, while FAT survives anything short of losing the image file. The guest finds the volume by marker file, never by drive letter.

View Source
const InstallerCDDeviceID = "installer-cd"

InstallerCDDeviceID is the qdev id of the installer CD. QMP addresses devices by qdev id, not drive id, so ejecting requires this name.

View Source
const KVMDevice = "/dev/kvm"

KVMDevice is the character device QEMU opens to use hardware virtualization on Linux. Inside a container it is present only when the launcher passed --device=/dev/kvm (see `[cell] kvm` in .devcell.toml).

View Source
const KernelFirmwareCacheName = "QEMU_EFI.kernel.fd"

KernelFirmwareCacheName is where a kernel-bootable EDK2 image lives in the devcell cache (~/.devcell/cache/qemu/). Named distinctly from QEMU_EFI.fd on purpose: every distro ships a *different, incompatible* build under that name, and telling them apart by filename is exactly the trap.

View Source
const MinTemplateSizeBytes int64 = 4 * 1024 * 1024 * 1024

MinTemplateSizeBytes is the minimum valid template disk size (4 GB). A template smaller than this is corrupt or incomplete (e.g. a failed build left a 193 KB stub) and should trigger a rebuild instead of a clone.

View Source
const NixOSWSLDistro = "NixOS"

NixOSWSLDistro is the distro name NixOS-WSL's own documentation uses.

View Source
const NoChangeMarker = "DEVCELL-NO-CHANGE"

NoChangeMarker is what a stage prints to say it changed nothing, so the runner can skip a reboot it does not need. A TCG reboot costs ~8 minutes (run 20260803T075624 paid exactly that after an engine stage that found the engine already installed).

View Source
const ProgressPortName = `devcell.progress.0`

ProgressPortName is the virtio-serial port name used for guest→host progress reporting. The guest writes to \\.\Global\<name>, the host reads from a chardev file wired to GuestProgressLogPath.

View Source
const StartupNSHFailMarker = "BOOTAA64.EFI not found"

StartupNSHFailMarker is the message startup.nsh prints when none of FS0–FS4 contain BOOTAA64.EFI. After the firmware drops to the EFI shell, startup.nsh attempts to chainload the Windows bootloader — this marker means it gave up.

View Source
const StructuredPortName = `devcell.structured.0`

StructuredPortName is a second virtio-serial port for structured JSONL logging. The guest writes JSON lines to \\.\Global\<name>, the host reads from a chardev file wired to GuestStructuredLogPath.

View Source
const SyncExceptionMarker = "Synchronous Exception at"

SyncExceptionMarker is the firmware's fatal crash message. A synchronous exception means the loaded bootloader dereferenced an unmapped address — the boot is dead and will never recover.

View Source
const USBBusID = "usb-bus"

USBBusID names the xhci controller so every storage device can state its bus explicitly. Leaving the bus implicit lets QEMU pick, which is how a device ends up somewhere the firmware never enumerates.

View Source
const WSLDistroUser = "nixos"

WSLDistroUser is the account the WSL side runs as — a DIFFERENT identity from the Windows session user (SessionUsername(), the host's $USER, which autounattend creates and SSH lands as).

It must equal the username nixhome's WSL home-manager config was built for (`wslUser` in nixhome/flake.nix). home-manager refuses to activate a config whose username differs from the invoking user:

Error: USER is set to "dmitry" but we expect "nixos"

Conflating the two identities is why home-manager activation had never completed: the stages renamed the distro to the Windows user while nix was asked for a config built for "nixos".

Parameterizing this per-user is CELL-404; until then the two identities stay explicitly separate rather than silently equal.

View Source
const WSLEngineDestDir = "Program Files/WSL"

WSLEngineDestDir is where the WSL engine lands inside boot.wim — the same path the MSI installs to, so wslservice finds its own layout.

View Source
const WindowsBootManagerMarker = `"Windows Boot Manager"`

WindowsBootManagerMarker is the firmware log line when bootmgfw.efi loads.

Variables

This section is empty.

Functions

func Accelerator

func Accelerator() string

Accelerator returns the appropriate QEMU accelerator for the current platform.

func AlpineRootfsPath

func AlpineRootfsPath(home string) string

AlpineRootfsPath returns the cached Alpine minirootfs tarball path.

func BaseProfileImagePath

func BaseProfileImagePath(home, stack string, modules []string) string

BaseProfileImagePath returns where the dev-env pipeline's finished product is saved: an ssh-able Windows with WSL1, nix and the nixhome base profile activated. This is the state `cell build --engine=qemu` is ultimately meant to end at for the base stack.

func BluePixelRatio

func BluePixelRatio(ppmPath string) (float64, error)

BluePixelRatio reads a PPM (P6) file and returns the fraction of pixels that are "blue" — i.e. B >= 120, B > R+40, B > G+30. This detects the Windows Setup installer's characteristic blue background.

func BuildControlVolume

func BuildControlVolume(destPath string, payload map[string][]byte) error

BuildControlVolume writes the per-run control volume: the marker the guest resolves its drive letter by, plus any payload to deliver INTO the guest (the PowerShell module and stage scripts — see CELL-402). Logs come back on the same volume, so one attachment carries both directions.

Built fresh on the host every run and attached at boot, so it is never inside the qcow2: a checkpoint image cannot freeze a stale copy, which is the failure mode that ruled out installing the module onto the guest disk.

func BuildGoSSHDPayload

func BuildGoSSHDPayload(dir string) (string, error)

BuildGoSSHDPayload cross-compiles the guest SSH server for windows/arm64 into dir and returns its path.

Building beats downloading: the previous Win32-OpenSSH payload was a pinned GitHub release that had to be cached, checksummed and version- guarded, and an unpinned URL silently moved us onto a release whose split binaries changed the failure mode mid-investigation. This binary is the tree's own code, so it cannot drift from the harness that talks to it.

CGO is off so the result is a single static binary with no DLL dependencies — WinPE has a reduced System32 and cannot be assumed to carry any particular runtime.

func BuildGuestLogVolume

func BuildGuestLogVolume(destPath string) error

BuildGuestLogVolume creates the FAT image guests write their logs to. Attach it via Spec.LogVolumePath; read it back with CollectVolumeLogs.

func BuildInstallCommand

func BuildInstallCommand(spec Spec, windowsISO, autounattendImage string) []string

func BuildProjectPullArgv

func BuildProjectPullArgv(spec Spec) []string

BuildProjectPullArgv builds the scp argv that copies the guest's project tree back over the local one (write-back for "two-way" sync). The destination is the project's parent directory so the tree overlays in place. Returns nil when the spec has no project directory.

func BuildProjectPushArgv

func BuildProjectPushArgv(spec Spec) []string

BuildProjectPushArgv builds the scp argv that copies the project tree into the guest's home (creating ~\<basename>, where BuildSSHArgv cd's to). Returns nil when the spec has no project directory.

func BuildRunCommand

func BuildRunCommand(spec Spec) []string

BuildRunCommand constructs the QEMU argv for normal VM operation (post-install).

func BuildSSHArgv

func BuildSSHArgv(spec Spec) []string

BuildSSHArgv constructs the SSH argv for running a command inside a Windows VM. For Windows guests, we use cmd.exe /c or powershell -NoProfile -Command.

func BuildSSHExecArgv

func BuildSSHExecArgv(host string, port uint16, user, keyPath, command string) []string

BuildSSHExecArgv constructs a simple SSH argv for running a single command. Used during provisioning.

func BuildWimBuilderArgv

func BuildWimBuilderArgv(wbs WimBuilderSpec) []string

BuildWimBuilderArgv constructs the QEMU argv for the WIM builder VM. When Spec.CDBus is "scsi", ISOs are attached on a virtio-scsi-pci controller (scsi-cd) so EDK2 can boot them on QEMU 11.x/HVF where USB-attached ISOs are invisible to the firmware. The shared FAT volume always goes on usb-storage (Windows needs it as removable media).

func BuildWinPECommand

func BuildWinPECommand(spec Spec, winpeISO, answerImage string) []string

BuildWinPECommand constructs the QEMU argv for booting WinPE from a custom ISO (CELL-430). Only one CD (the WinPE ISO) plus an answer volume on usb-storage. No Windows installer ISO, no virtio ISO.

func CacheDir

func CacheDir(home string) string

CacheDir returns the QEMU media cache directory.

DEVCELL_QEMU_CACHE_DIR points it somewhere shared. Inside a cell $HOME is itself a per-cell directory, so the default renders as ~/.devcell/<cell>/.devcell/cache/qemu and every cell re-downloads the same ~6 GB of immutable media. There is no way to reach the real host home from inside the container, so the location has to be pointable rather than inferred (CELL-386).

func CheckKernelBootableFirmware

func CheckKernelBootableFirmware(path string) error

CheckKernelBootableFirmware verifies that path holds the ArmVirtQemuKernel EDK2 build — the relocatable image with the ARM64 kernel-image magic ("ARMd" at offset 56) that QEMU's -kernel loader understands. The common ArmVirtQemu build (what nixpkgs, Debian and openSUSE all ship as QEMU_EFI.fd) is linked for flash address 0 and boots to *silence* when loaded into DRAM, so this must be checked, not assumed.

func CleanStalePIDFile

func CleanStalePIDFile(dir string) error

func CloneDisk

func CloneDisk(templateDisk, instanceDisk string) error

CloneDisk creates a qcow2 snapshot (backing file) from a template disk. The instance disk is thin — only stores delta writes.

func CollectVolumeLogs

func CollectVolumeLogs(imgPath string, names []string) []winpe.GuestLog

CollectVolumeLogs reads the named files off a guest log volume — one entry per name, absence reported rather than skipped, same contract as CollectGuestLogs.

func ConvertPPMtoPNG

func ConvertPPMtoPNG(ppmPath, pngPath string) error

ConvertPPMtoPNG reads a PPM (P6 binary) file and writes a PNG. Returns the PNG path (same base, .png extension).

func CreateDisk

func CreateDisk(path string, sizeGB int) error

CreateDisk creates a new qcow2 disk image at the given path.

func CreateFATQcow2

func CreateFATQcow2(imgPath string, files map[string][]byte, capacity int64) error

CreateFATQcow2 creates a qcow2 disk image containing a FAT32 filesystem with the given files. The virtual disk capacity is set to capacity bytes, but the on-disk qcow2 file is sparse — only clusters with actual data are allocated. This is useful for shared volumes that need large free space for guest writes (e.g. WIM builder writing devcell.wim) without consuming host disk upfront.

func DeterministicMAC

func DeterministicMAC(cellName string) string

DeterministicMAC derives a stable locally-administered MAC address from a cell name. Same cell -> same MAC -> same DHCP lease.

func DiskInfo

func DiskInfo(path string) (string, error)

DiskInfo returns basic information about a qcow2 image.

func DownloadAlpineRootfs

func DownloadAlpineRootfs(ctx context.Context, home string, noCache bool, obs Observer) (string, error)

DownloadAlpineRootfs fetches the Alpine aarch64 minirootfs if not cached.

func DownloadOpenSSH

func DownloadOpenSSH(ctx context.Context, home string, noCache bool, obs Observer) (string, error)

DownloadOpenSSH fetches Microsoft's signed Win32-OpenSSH ARM64 release.

The guest cannot install OpenSSH Server through Windows servicing: our media carries the capability manifest but not its payload, so the capability sits Staged and the install fails 0x80070002 — with Windows Update reachable and permitted. The Server FoD ships on a separate build-matched ISO, and the UUP package has no Server package at all. This release needs no servicing.

func DownloadPwsh

func DownloadPwsh(ctx context.Context, home string, noCache bool, obs Observer) (string, error)

DownloadPwsh fetches the PowerShell 7 ARM64 self-contained zip if not cached.

func DownloadVirtioDrivers

func DownloadVirtioDrivers(ctx context.Context, home string, noCache bool, obs Observer) (string, error)

DownloadVirtioDrivers downloads the VirtIO drivers ISO if not already cached. Uses .done marker pattern (mirrors tart.DownloadIPSW). When noCache is true, removes the .done marker to force re-download.

func DownloadWindowsISO

func DownloadWindowsISO(ctx context.Context, home, language string, noCache bool, obs Observer) (string, error)

DownloadWindowsISO fetches and caches the Windows 11 ARM64 ISO via UUP dump. Downloads an ESD from Microsoft's CDN and assembles it into a bootable ISO using wimlib-imagex and mkisofs (must be on PATH).

func EnsureScreenshotDir

func EnsureScreenshotDir(resultsDir string, source ScreenSource) error

EnsureScreenshotDir creates the directory ScreenshotPath writes into.

func ExceptionVectorSlot

func ExceptionVectorSlot(pc uint64) (base uint64, desc string)

ExceptionVectorSlot maps a program counter onto AArch64 exception-vector geometry: assuming an 0x800-aligned VBAR, it returns the table base the PC would belong to and a description of the slot it falls in.

This is heuristic — any address has *some* offset mod 0x800 — so it is only evidence when combined with a dead-loop instruction at the PC and DAIF masked. With those, it converts a raw parked address into the exception class: the KVM stall's PC=0x13c347200 → slot +0x200 → a synchronous exception taken at the current EL on SP_ELx.

func FirmwareBootCount

func FirmwareBootCount(serial string) int

FirmwareBootCount reports how many times the guest firmware started. More than one during an install that has not finished applying its image means the guest reset prematurely.

func FirmwarePath

func FirmwarePath() string

FirmwarePath returns the path to the EDK2 UEFI firmware for ARM64.

func GenerateCreateSessionUserScript

func GenerateCreateSessionUserScript(username, password string) string

GenerateCreateSessionUserScript returns a PowerShell script that creates a local user matching the host user, with admin privileges and password-free SSH.

func GenerateDevToolsScript

func GenerateDevToolsScript() string

GenerateDevToolsScript returns a PowerShell script that installs essential dev tools (Git) via winget with a Chocolatey fallback.

The fallback fires on winget *failure*, not only on absence: in run 20260801T001059 winget existed, errored, installed nothing — and the step still claimed ok because its stderr was discarded. The step now verifies git actually landed and fails when it did not.

func GenerateDriverTrustScript

func GenerateDriverTrustScript() string

GenerateDriverTrustScript prepares the guest to accept the Dev-signed viofs driver: the signer certificates go into the MACHINE Root and TrustedPublisher stores (read back afterwards — exit codes lied twice), and testsigning is switched on. Iteration 8 proved the stores and the token were right and pnputil still refused: Win11's code-integrity policy rejects non-Microsoft kernel packages until testsigning is LIVE, which takes a reboot — so this runs as its own stage, before one.

func GenerateEnvSetupScript

func GenerateEnvSetupScript(envVars map[string]string) string

GenerateEnvSetupScript returns a PowerShell script that sets environment variables for the devcell session.

Built by iteration rather than from a template: the body is one line per variable with no prose around it, so a template would add indirection without removing any escaping hazard.

func GenerateHardenEmulationScript

func GenerateHardenEmulationScript() string

GenerateHardenEmulationScript returns a PowerShell script that disables WerFault and Defender real-time monitoring — the two biggest resource wasters in a TCG-emulated build VM.

func GenerateHomeManagerScript

func GenerateHomeManagerScript(user, drive string) string

GenerateHomeManagerScript links the mounted project share to the agreed repo path inside WSL and activates the repo's nixhome via home-manager.

func GenerateHyperVEnableScript

func GenerateHyperVEnableScript() string

GenerateHyperVEnableScript asks Windows to install and launch its hypervisor — what the WSL2 utility VM is actually created on.

func GenerateHyperVVerifyScript

func GenerateHyperVVerifyScript() string

GenerateHyperVVerifyScript asserts the two independent facts the WSL2 utility VM depends on: the hypervisor is INSTALLED, and it is STARTED. They fail for different reasons — a missing payload versus a hypervisor that cannot launch on emulated EL2 — so they are reported and thrown separately.

func GenerateKeepAliveScript

func GenerateKeepAliveScript() []byte

GenerateKeepAliveScript produces the probe a troubleshooting session opens with: echo back the file the host placed on the FAT volume, confirm the agent shell ran, and dump enough state to debug from. The VM is left running afterwards, so this output is the last thing written before the guest goes idle and waits to be driven through QMP.

func GenerateNixOSWSLImportScript

func GenerateNixOSWSLImportScript() string

GenerateNixOSWSLImportScript installs the official NixOS-WSL image as a WSL2 distro, following the project's own instructions: fetch nixos.wsl from the latest release and `wsl --install --from-file` it (WSL 2.4.4+), falling back to `wsl --import … --version 2` on older engines.

func GenerateNixVerifyScript

func GenerateNixVerifyScript() string

GenerateNixVerifyScript proves the toolchain the NixOS-WSL image already carries. NixOS *is* nix — running the upstream installer inside it would be both redundant and non-idiomatic.

func GenerateProjectMountScript

func GenerateProjectMountScript(projectName, mountLetter string) string

GenerateProjectMountScript returns a PowerShell script that creates a project directory. For QEMU, project files are shared via virtio-fs (see the dev-env pipeline) or copied over SSH.

func GenerateSSHConfigScript

func GenerateSSHConfigScript(pubKey string) string

GenerateSSHConfigScript returns a PowerShell script that configures OpenSSH Server on Windows: sets default shell, authorized keys, and firewall rule.

func GenerateVirtioAgentInstallScript

func GenerateVirtioAgentInstallScript() string

GenerateVirtioAgentInstallScript installs the ARM64 virtio drivers Windows did not need during setup (vioserial, viofs, balloon, rng) and then the qemu guest agent — the x64 MSI under Win11's emulation, since no ARM64 agent build exists (see .scratch/VIRTIO.md).

func GenerateVirtioFSMountScript

func GenerateVirtioFSMountScript(tag, drive string) string

GenerateVirtioFSMountScript registers virtiofs.exe (from the driver CD) as a service mounting the given tag at the given drive, then proves the mount by reading it. Service manager output is kept and dependencies are probed — the first version piped sc.exe to Out-Null and a silent failure explained nothing.

func GenerateVirtualizationProbeScript

func GenerateVirtualizationProbeScript() string

GenerateVirtualizationProbeScript records whether this guest can host a hypervisor — the question that decides WSL1 vs WSL2. Observation only: the probe never enables a feature, so a run can report "WSL2 was impossible" without having changed the guest to find out.

func GenerateWSL2EnableScript

func GenerateWSL2EnableScript() string

GenerateWSL2EnableScript enables both features WSL2 needs. NixOS-WSL does not support WSL1 (https://nix-community.github.io/NixOS-WSL/install.html), so VirtualMachinePlatform is required rather than optional. The reboot belongs to the caller, which can watch SSH drop and come back.

func GenerateWSLEngineInstallScript

func GenerateWSLEngineInstallScript() string

GenerateWSLEngineInstallScript installs the WSL engine MSI. The inbox wsl.exe on current Win11 is a stub — the engine is a separate MSI from the microsoft/WSL releases. Installing it tears down the SSH session, so the stage runs disconnect-tolerant and reboot-terminated.

func GenerateWSLUserScript

func GenerateWSLUserScript(user string) string

GenerateWSLUserScript renames the distro's default user to the cell's session user, following NixOS-WSL's documented procedure. Without it the distro runs as "nixos" while every path the cell uses is /home/<user>.

func GenerateWinFspInstallScript

func GenerateWinFspInstallScript() string

GenerateWinFspInstallScript fetches and installs WinFsp, the userspace filesystem layer virtiofs.exe requires.

func GuestFile

func GuestFile(name string) ([]byte, error)

GuestFile returns one file from the embedded guest tree, addressed the way stages refer to it ("Devcell.psm1", "stages/wsl2-enable.ps1").

func GuestPayload

func GuestPayload() (map[string][]byte, error)

GuestPayload returns the whole guest tree keyed by its path on the control volume, ready for BuildControlVolume. Everything ships every run: the volume is built on the host and attached at boot, so it can never drift from the repo the way a copy written into the qcow2 would.

func GuestPayloadWithNixhome

func GuestPayloadWithNixhome(nixhomeDir string) (map[string][]byte, error)

GuestPayloadWithNixhome is GuestPayload plus the nixhome tarball the home-manager stage extracts inside the distro — one control volume carries both the scripts and the config they activate.

func GuestReboot

func GuestReboot(ctx context.Context, spec Spec, sshUser, sshKeyPath string,
	sshDeadline time.Duration, obs Observer, stateFn VMStateFunc) error

GuestReboot is the one way to reboot a guest between stages: request the restart over SSH, wait for sshd to actually GO DOWN (up to downWindow — if it never goes down the restart likely failed, but the follow-up WaitForSSH decides), then wait for it to come back. Every reboot callback (tests and cell build phases alike) should delegate here rather than re-inventing the sleep-and-hope pattern this replaced.

func ImageName

func ImageName(stack string, modules []string) string

ImageName returns the disk image filename for a stack.

func InstanceDir

func InstanceDir(home, cellName string) string

InstanceDir returns the per-cell instance directory for Windows VMs. Layout: ~/.devcell/<cellName>/windows/

func InstanceVMName

func InstanceVMName(cellName string) string

InstanceVMName returns the QEMU VM name for a running cell instance.

func InteractiveShellCommand

func InteractiveShellCommand() string

InteractiveShellCommand is the agent command for a hands-on troubleshooting session inside a booted image.

It runs the keep-alive probe first — which brings up sshd — and only then hands the console to cmd.exe. Order matters: an SSH session is independent of the console, so it survives anything typed there, while the console itself is fragile. WinPE's shell is bootstrap.cmd running pwsh with `& goto :eof`, so interrupting the agent ends the batch and takes WinPE down with it. Parking the agent inside a blocking cmd.exe keeps that from happening and gives QMP keystrokes a prompt that actually executes them.

-NoNewWindow with no redirection is the load-bearing detail: the child inherits the console's own stdin and stdout rather than the agent's pipeline, so typed keys run and their output is visible on screen.

func IsProcessAlive

func IsProcessAlive(pid int) bool

func KeepAliveProbeCommand

func KeepAliveProbeCommand() string

KeepAliveProbeCommand is the agent command line for the probe script.

func KernelFirmwarePath

func KernelFirmwarePath() (string, error)

KernelFirmwarePath resolves the firmware for the WSL2 machine (secure=on + -kernel): $DEVCELL_QEMU_EFI_KERNEL if set, else the devcell cache. An explicit override that fails validation is an error rather than a fallthrough — the user pointed at a specific file, and using another would hide the mistake behind a silent boot failure later.

func LatestNixReadyTestImage

func LatestNixReadyTestImage(dir string) (string, error)

LatestNixReadyTestImage returns the newest windows-nix-*.qcow in dir.

func LatestSSHAbleTestImage

func LatestSSHAbleTestImage(dir string) (string, error)

LatestSSHAbleTestImage returns the newest windows-sshable-*.qcow in dir.

func LatestWSLReadyTestImage

func LatestWSLReadyTestImage(dir string) (string, error)

LatestWSLReadyTestImage returns the newest windows-wsl-*.qcow in dir.

func NixReadyTestImageName

func NixReadyTestImageName(ts time.Time) string

NixReadyTestImageName returns the versioned filename for a saved nix-ready image: a guest where Windows' own hypervisor launches (EL3 machine), NixOS-WSL is imported and nix answers inside it. This is the furthest checkpoint and the base for nix-based tests — from here only in-distro work (home-manager and beyond) remains.

func NixhomeTarball

func NixhomeTarball(dir string) ([]byte, error)

NixhomeTarball packs a nixhome directory for control-volume delivery, all contents under a top-level "nixhome/" so extraction recreates the layout.

A tarball, not a live reference: activating straight from the project share fails twice over — nix ingests the surrounding repo as a dirty git+file input, and the share's symlinks (36 in the icewm theme alone) die on readlink across virtiofs+drvfs (run 20260804). Inside a tarball the symlinks are just entries; extracted onto the distro's ext4 they work.

func OpenSSHPayloadPath

func OpenSSHPayloadPath(home string) string

OpenSSHPayloadPath returns the cached Win32-OpenSSH release path.

func ParseMajorVersion

func ParseMajorVersion(ver string) (int, error)

ParseMajorVersion extracts the major version number from a QEMU version string like "11.0.93" or "10.0.2".

func ParseQEMUVersion

func ParseQEMUVersion(output string) (string, error)

ParseQEMUVersion extracts the version from qemu --version output.

func PowerShellEncodedCommand

func PowerShellEncodedCommand(script string) string

PowerShellEncodedCommand wraps a script for SSH transport to a Windows guest. The command string a guest receives is re-parsed by its default shell and again by PowerShell's native argument handling, which eats unescaped double quotes — so any literal script with quoting eventually arrives mangled. -EncodedCommand sidesteps every parser in the chain: the script travels as base64 over UTF-16LE and PowerShell decodes it itself.

func PreferredAccel

func PreferredAccel(tcgFallback string) string

PreferredAccel returns hardware virtualization when the host can provide it, and the caller's TCG string otherwise.

It differs from ResolveAccel in who grants consent: there is no cfg layer in a test binary, so a usable device *is* the consent. The fallback stays a caller argument because TCG tuning is workload-specific — tb-size=512 is worth it for a 70-minute Windows install and meaningless elsewhere, and it is rejected outright when passed alongside an accel of kvm.

func PreflightCheck

func PreflightCheck(goos, goarch string) error

PreflightCheck validates the host can run QEMU with hardware acceleration. Pure function (takes OS/arch as params for testability).

func PreflightCheckHost

func PreflightCheckHost() error

PreflightCheckHost calls PreflightCheck with runtime values and verifies that qemu-system-aarch64 is installed.

func PrepareVarsFile

func PrepareVarsFile(firmwarePath, varsPath string) error

PrepareVarsFile copies the UEFI firmware to create a writable vars store.

func ProbeKVM

func ProbeKVM() error

ProbeKVM reports whether /dev/kvm is usable by the current process.

func ProvisionedMarker

func ProvisionedMarker(home, stack string, modules []string) string

ProvisionedMarker returns the path to the ".provisioned" marker file within a template directory.

func PwshZipPath

func PwshZipPath(home string) string

PwshZipPath returns the cached PowerShell 7 zip path.

func QEMUBinaryPath

func QEMUBinaryPath() (string, error)

QEMUBinaryPath returns the path to qemu-system-aarch64, or an error if not found.

func QEMUVersion

func QEMUVersion(binaryPath string) (string, error)

QEMUVersion runs qemu-system-aarch64 --version and extracts the version string.

func QMPBlockStats

func QMPBlockStats(socketPath string) (map[string]BlockDeviceStats, error)

QMPBlockStats returns per-device I/O counters via query-blockstats. Growing read counters on a frozen display prove the guest is still booting.

func QMPDismissFirstLogonUI

func QMPDismissFirstLogonUI(socketPath string) error

QMPDismissFirstLogonUI sends a single Esc to the guest. Windows 11 opens the Start menu on its own at the first sign-in after OOBE, and an unattended VM never produces the input event that would close it — it stays over every subsequent screenshot. One Esc at "SSH ready" (which is also the proof that first logon happened) dismisses it.

func QMPEjectMedium

func QMPEjectMedium(socketPath, id string) error

QMPEjectMedium ejects removable media (id is the drive id, e.g. "cdrom0").

Windows Setup reboots after applying its image. If the installer CD is still attached and holds bootindex=0, the firmware boots the installer again instead of the freshly installed OS, and Setup stops on "It looks like you started an upgrade and booted from installation media". Ejecting once the image is applied sends the next boot to disk. force=true because the guest may hold the tray locked.

func QMPHumanMonitor

func QMPHumanMonitor(socketPath, command string) (string, error)

QMPHumanMonitor runs an HMP command (e.g. "info registers") via QMP and returns its text output. A changing PC across calls proves the vCPU is alive.

func QMPQueryKVM

func QMPQueryKVM(socketPath string) (enabled, present bool, err error)

QMPQueryKVM reports whether the running VM has hardware virtualization enabled, and whether KVM is present on the host at all.

This is how a run proves its own accelerator. `-accel kvm` cannot silently degrade to TCG — QEMU exits with an error instead — but that is an argument from flag semantics, not evidence in the artifact. Asking the live VM turns "this run should have used KVM" into "this run did".

func QMPQuit

func QMPQuit(socketPath string) error

QMPQuit sends the "quit" command which flushes all block device caches and exits the QEMU process cleanly. Unlike Process.Kill(), this ensures writeback-cached qcow2 images are consistent on disk.

func QMPScreendump

func QMPScreendump(socketPath, outputFile string) error

QMPScreendump captures the QEMU display framebuffer to a PPM file via QMP.

func QMPSendKeys

func QMPSendKeys(socketPath string, keystrokes [][]string) error

QMPSendKeys sends a sequence of keystrokes to the VM via QMP. Each element of keystrokes is a set of QKeyCodes pressed simultaneously (e.g. []string{"shift", "f"} for uppercase F).

func QMPSocketPath

func QMPSocketPath(spec Spec) string

QMPSocketPath returns the path to the QMP unix socket for a given spec.

The socket normally lives next to the VM it belongs to, but that directory is the caller's choice and can be arbitrarily deep — `cell build` nests it four levels under $HOME, which overflows sun_path for an ordinary home path. When the natural path does not fit, fall back to a short one in the system temp directory, keyed by a digest of the original so the result stays stable and unique per VM: `cell rdp`/`cell vnc` find a running VM by recomputing this path, so it must be a pure function of the spec.

func ReadFileFromFATQcow2

func ReadFileFromFATQcow2(imgPath, filePath string) ([]byte, error)

ReadFileFromFATQcow2 reads a single file from a qcow2-backed FAT32 disk image. It converts the qcow2 to a temporary raw image, reads the file with the standard FAT reader, and cleans up.

func ReadPIDFile

func ReadPIDFile(dir string) (int, error)

func RemoveDownloadMarkers

func RemoveDownloadMarkers(home string)

RemoveDownloadMarkers removes .done markers for all cached ISOs, forcing re-download on next use. Used by --no-cache.

func ResolveAccel

func ResolveAccel(explicit string, kvmRequested bool, goos string, probe func() error) (accel, reason string)

ResolveAccel picks the QEMU accelerator and returns the choice plus a human-readable reason for the launch log.

Order of authority:

  1. an explicit Spec.Accel — callers (notably tests) always win;
  2. `[cell] kvm = true` AND the device actually opens — KVM (linux only);
  3. otherwise TCG.

darwin used to default to HVF, but QEMU 11.x/HVF has USB xhci enumeration bugs (CELL-427) that break WinPE boot. TCG is slower but reliable. Pass Accel:"hvf" explicitly to opt back in.

Both conditions in (2) are load-bearing. Config alone is not enough: it describes intent, and a launch that trusts it on a host without nested virtualization dies with "Could not access KVM kernel module". A usable device alone is not enough either — config stays the authority, so an unrequested accelerator is never silently adopted.

func ResolveWindowsISO

func ResolveWindowsISO(envISO, configISO, home string) (string, error)

ResolveWindowsISO resolves the Windows ARM64 ISO path. Priority: env DEVCELL_QEMU_WINDOWS_ISO > config path > cached download > error.

func RunGuestStages

func RunGuestStages(ctx context.Context, spec Spec, stages []GuestStage, opts StageRunOptions) error

RunGuestStages runs every stage in order, stopping at the first failure — stages are strictly dependent, so continuing only produces confusing downstream errors.

func SSHAbleImagePath

func SSHAbleImagePath(home, stack string, modules []string) string

SSHAbleImagePath returns where a verified template is promoted to. The "ssh-able" image is the contract between the install test and everything built on top of it: an installed Windows whose SSH access has actually been exercised, not merely stamped.

func SSHAbleTestImageName

func SSHAbleTestImageName(ts time.Time) string

SSHAbleTestImageName returns the versioned filename for a saved ssh-able image: windows-sshable-<compact ISO 8601 UTC>.qcow. Compact (no colons) so the name is filesystem-safe everywhere, and lexicographic order equals chronological order.

func SaveBaseProfileImage

func SaveBaseProfileImage(overlayDisk, dest string) error

SaveBaseProfileImage flattens a dev-env overlay (nix + home-manager on top of ssh-able) into a standalone image — see saveStandaloneImage. The VM writing the overlay must be shut down first.

func SaveSSHAbleImage

func SaveSSHAbleImage(templateDisk, dest string) error

SaveSSHAbleImage copies a verified template disk to dest as a standalone qcow2 — see saveStandaloneImage.

func ScreenshotPath

func ScreenshotPath(resultsDir string, source ScreenSource, now time.Time,
	screen string, screenSeq, globalSeq int, ext string) string

ScreenshotPath returns where a captured frame belongs:

<resultsDir>/screenshots/<source>/<ISO>-<screen>-<screenSeq>-<globalSeq>.<ext>

screenSeq counts frames of the current screen (how long it has persisted), globalSeq counts every frame of the run — together they answer "what was on screen, for how long, and where in the run" from a directory listing. The caller creates the directory (see EnsureScreenshotDir).

func StackTag

func StackTag(stack string, modules []string) string

StackTag returns the canonical tag for a stack + optional modules.

func StageLogName

func StageLogName(seq int, component string) string

StageLogName is the transcript filename for a dev-env component, prefixed with the component's 1-based position in the pipeline. Grouping by component rather than by SSH execution means "what happened with WSL" is one file covering the feature, the engine and the distro import; the number keeps a results directory sorted in execution order.

func StageLogNames

func StageLogNames(stages []GuestStage) []string

StageLogNames maps each stage to its component's log name, numbering components by first appearance. Stages sharing a component share a file.

func StallPollsFor

func StallPollsFor(budgetSeconds, intervalSeconds int) int

StallPollsFor converts a stall budget in seconds into a poll count, given the poll interval. Never returns fewer than 2: detecting "unchanged" needs two samples to compare, so a single poll can never establish a stall.

func StringToQKeyStrokes

func StringToQKeyStrokes(s string) [][]string

StringToQKeyStrokes converts a string to QMP keystroke sequences. Each returned element is a slice of simultaneously-pressed QKeyCodes.

func TailProgressLog

func TailProgressLog(path string, onLine func(string), stop <-chan struct{})

TailProgressLog tails the guest-progress.log file and calls onLine for each complete line. The goroutine opens the file lazily (it may not exist when the VM starts) and exits when stop is closed. Unlike WatchSerialFor, which fires once on a marker, this streams every line — it is the live view of the bootstrap's Send-Progress output.

func TemplateDir

func TemplateDir(home, stack string, modules []string) string

TemplateDir returns the path to per-template VM artifacts. Layout: ~/.devcell/windows/<stackTag>/

func TemplateVMName

func TemplateVMName(stack string, modules []string) string

TemplateVMName returns the QEMU VM name for a built template.

func ValidateISO

func ValidateISO(path string) error

ValidateISO checks that a file carries a recognised disc format by reading the volume descriptor at sector 16 (offset 0x8001). Both ISO 9660 (CD001) and UDF (BEA01/NSR02/NSR03) are accepted — Windows ARM64 ISOs built by UUP dump are pure UDF.

func VirtioISOPath

func VirtioISOPath(home string) string

VirtioISOPath returns the path to the cached VirtIO drivers ISO.

func VirtiofsdCommand

func VirtiofsdCommand(bin, socketPath, sharedDir string) *exec.Cmd

VirtiofsdCommand builds the daemon invocation for a project share. --sandbox none: the default sandbox needs user namespaces the devcell container does not have. The caller owns the process — virtiofsd exits whenever its client disconnects, so it must be started fresh for every VM boot that mounts the share.

func VirtiofsdPath

func VirtiofsdPath() (string, error)

VirtiofsdPath resolves the host-side virtio-fs daemon: $DEVCELL_VIRTIOFSD, then PATH.

func WSLEngineFiles

func WSLEngineFiles() []string

WSLEngineFiles returns the trimmed WSL engine payload, relative to the MSI's WSL directory (PFiles64/WSL in an msiextract layout).

Kept: the engine core plus the Linux kernel side — ~310 MB. Dropped: WSLg/RDP (incl. system.vhd, 411 MB), the wslsettings GUI (a .NET app, WinPE has no CLR anyway), msal auth and the language packs — ~650 MB that a headless WinPE guest cannot use.

func WSLReadyTestImageName

func WSLReadyTestImageName(ts time.Time) string

WSLReadyTestImageName returns the versioned filename for a saved WSL-ready image: a guest that already has the virtio drivers, the project share, the WSL2 features and the WSL engine — everything up to the point where a distro can be imported. Checkpointing there turns a ~40-minute prelude into a ~1-minute boot when iterating on the distro itself.

func WaitForPortDown

func WaitForPortDown(host string, port uint16, window, interval time.Duration) bool

WaitForPortDown polls host:port until connections stop being accepted, returning true the moment the port is gone and false if it never goes down within window. Used after requesting a guest restart: Windows keeps accepting SSH through its shutdown grace period, so "the port answers" right after Restart-Computer proves nothing (run 20260802T094045 ran a stage against exactly such a dying session).

func WaitForSSH

func WaitForSSH(host string, port uint16, timeout, interval time.Duration, obs Observer, vmState ...VMStateFunc) error

WaitForSSH polls until the SSH port accepts connections. If vmState is non-nil, returns immediately when the VM is no longer running.

func WatchSerialFor

func WatchSerialFor(path string, marker string, stop <-chan struct{}) <-chan string

WatchSerialFor tails the serial log file and sends on the returned channel when marker appears. The goroutine exits when stop is closed. The channel is buffered so the caller can select on it without blocking the watcher.

func WatchSerialForDesktopBoot

func WatchSerialForDesktopBoot(path string, n int, stop <-chan struct{}) <-chan string

WatchSerialForDesktopBoot tails the serial log and fires when the firmware boots "Windows Boot Manager" for the Nth time. The first boot is the specialize pass (installs drivers, then reboots); the second is the final boot where OOBE runs, the user logs in, and the bootstrap script fires. Set n=2 for "desktop boot detected".

func WatchSerialForEFIShell

func WatchSerialForEFIShell(path string, stop <-chan struct{}) <-chan string

WatchSerialForEFIShell tails the serial log file and sends on the returned channel when the firmware falls through to the EFI Interactive Shell. The goroutine exits when stop is closed. The channel is buffered so the caller can select on it without blocking the watcher.

func WatchSerialForStartupNSHFail

func WatchSerialForStartupNSHFail(path string, stop <-chan struct{}) <-chan string

WatchSerialForStartupNSHFail tails the serial log and fires when startup.nsh reports that BOOTAA64.EFI was not found on any filesystem. Use this instead of WatchSerialForEFIShell when the answer volume carries startup.nsh — it gives the script a chance to chainload the bootloader before declaring failure (CELL-427).

func WatchSerialForSyncException

func WatchSerialForSyncException(path string, stop <-chan struct{}) <-chan string

WatchSerialForSyncException tails the serial log and fires when the firmware reports a synchronous exception (instruction/data abort). This catches crashes like cdboot_noprompt translation faults that would otherwise sit silently until the test's overall deadline.

func WhitePixelRatio

func WhitePixelRatio(ppmPath string) (float64, error)

WhitePixelRatio returns the fraction of near-white pixels. The Windows 11 Setup wizard is a large white window (~73% of the frame on the real UI).

func WindowsISOPath

func WindowsISOPath(home, language string) string

WindowsISOPath returns the path to the cached Windows ISO for a given language.

func WindowsPurpleRatio

func WindowsPurpleRatio(ppmPath string) (float64, error)

WindowsPurpleRatio returns the fraction of pixels matching the Windows 11 boot/setup backdrop (RGB 24,0,82 observed via QMP screendump). A mostly purple frame means the NT kernel has taken over the display; purple around a large white region means the Setup wizard is up.

func WritePIDFile

func WritePIDFile(dir string, pid int) error

func WritePortMeta

func WritePortMeta(instanceDir string, pm PortMeta) error

Types

type AllocatedPorts

type AllocatedPorts struct {
	SSHPort string
	VNCPort string
	RDPPort string
}

AllocatedPorts holds the bunk-derived port strings for SSH, VNC, and RDP.

func AllocatePorts

func AllocatePorts(portPrefix string, taken map[int]struct{}) AllocatedPorts

AllocatePorts computes SSH, VNC, and RDP ports using the same bunk-based scheme as the Docker runner. portPrefix is SESSION_PORT_PREFIX + bunk. The taken map (may be nil) lists ports already in use by other processes.

func (AllocatedPorts) RDPPortUint16

func (p AllocatedPorts) RDPPortUint16() uint16

RDPPortUint16 returns the RDP port as uint16.

func (AllocatedPorts) SSHPortUint16

func (p AllocatedPorts) SSHPortUint16() uint16

SSHPortUint16 returns the SSH port as uint16.

func (AllocatedPorts) VNCPortUint16

func (p AllocatedPorts) VNCPortUint16() uint16

VNCPortUint16 returns the VNC port as uint16.

type BlockDeviceStats

type BlockDeviceStats struct {
	ReadBytes  int64
	ReadOps    int64
	WriteBytes int64
}

BlockDeviceStats holds I/O counters for one block device from query-blockstats.

type DiscoveredVM

type DiscoveredVM struct {
	CellName string
	Ports    PortMeta
}

DiscoveredVM represents a running QEMU VM found during discovery.

func DiscoverRunningVMs

func DiscoverRunningVMs(home string) []DiscoveredVM

DiscoverRunningVMs scans ~/.devcell/<cell>/windows/ directories for running QEMU VMs with valid PID files and port metadata.

type Engine

type Engine struct {
	Spec Spec
	// contains filtered or unexported fields
}

Engine implements vm.Engine for QEMU Windows VMs.

func NewEngine

func NewEngine(spec Spec, obs Observer) *Engine

NewEngine creates a new QEMU engine with the given spec.

func (*Engine) Boot

func (e *Engine) Boot(ctx context.Context) error

Boot starts the QEMU VM and waits for SSH to become available.

func (*Engine) Preflight

func (e *Engine) Preflight() error

Preflight validates the host can run QEMU Windows VMs.

func (*Engine) SSHArgv

func (e *Engine) SSHArgv(binary string, flags, args []string) []string

SSHArgv constructs the SSH argv for running a command inside the VM.

func (*Engine) Shutdown

func (e *Engine) Shutdown(ctx context.Context) error

Shutdown gracefully stops the QEMU VM.

type FirmwareFault

type FirmwareFault struct {
	SP          string
	ELR         string
	ESR         string
	FAR         string
	Description string // e.g. "Data abort: Translation fault, second level"
}

FirmwareFault is EDK2's CPU exception dump, which it prints to the serial console before giving up. Its presence means the *firmware* died — not the guest OS — so no amount of further waiting can help.

func ParseFirmwareFault

func ParseFirmwareFault(serial string) (FirmwareFault, bool)

ParseFirmwareFault extracts the crash dump from a serial log, if present.

func (FirmwareFault) Summary

func (f FirmwareFault) Summary() string

Summary interprets the dump in one line.

The load-bearing observation for the 2026-07-30 install failure: the faulting address sits just *below* the stack pointer, and the stack pointer sits at the very bottom of guest RAM (QEMU virt puts RAM at 0x40000000, with the PCIe ECAM window immediately below it). That is a stack that ran off the bottom of its region — not a stray pointer.

type GDBConn

type GDBConn struct {
	// contains filtered or unexported fields
}

GDBConn is a minimal GDB Remote Serial Protocol client that can read and write guest virtual memory through QEMU's built-in GDB stub.

func GDBDial

func GDBDial(addr string, timeout time.Duration) (*GDBConn, error)

GDBDial connects to QEMU's GDB stub at the given address (e.g. "tcp:localhost:1234" or "unix:/path/to/sock"). It sends the initial handshake and returns a ready-to-use connection.

func (*GDBConn) Close

func (g *GDBConn) Close() error

func (*GDBConn) Continue

func (g *GDBConn) Continue() error

Continue resumes guest execution.

func (*GDBConn) ReadMemory

func (g *GDBConn) ReadMemory(addr uint64, length int) ([]byte, error)

ReadMemory reads len bytes from virtual address addr.

func (*GDBConn) ReadRegister

func (g *GDBConn) ReadRegister(index int) ([]byte, error)

ReadRegister reads a single register by index via the 'p' packet. AArch64 QEMU register indices: x0-x30 = 0-30, SP = 31, PC = 32, CPSR = 33, V0-V31 = 34-65, FPSR = 66, FPCR = 67, ELR_EL1 = 68 (0x44), ... system regs vary by QEMU version.

func (*GDBConn) ReadRegisters

func (g *GDBConn) ReadRegisters() (string, error)

ReadRegisters reads all general-purpose registers via the 'g' packet. Returns the raw hex-encoded register dump.

func (*GDBConn) ReadUint16LE

func (g *GDBConn) ReadUint16LE(addr uint64) (uint16, error)

ReadUint16LE reads a little-endian uint16 from the given virtual address.

func (*GDBConn) RemoveBreakpoint

func (g *GDBConn) RemoveBreakpoint(addr uint64) error

RemoveBreakpoint removes a software breakpoint (z0) at addr.

func (*GDBConn) SetBreakpoint

func (g *GDBConn) SetBreakpoint(addr uint64) error

SetBreakpoint inserts a software breakpoint (Z0) at addr.

func (*GDBConn) Stop

func (g *GDBConn) Stop() error

Stop halts the guest (equivalent to Ctrl-C in GDB).

func (*GDBConn) WaitBreak

func (g *GDBConn) WaitBreak(timeout time.Duration) (string, error)

WaitBreak waits for the stub to report a stop event (breakpoint hit, signal, etc). Returns the raw stop-reply packet body.

func (*GDBConn) WriteMemory

func (g *GDBConn) WriteMemory(addr uint64, data []byte) error

WriteMemory writes data to virtual address addr.

func (*GDBConn) WriteRegister

func (g *GDBConn) WriteRegister(index int, data []byte) error

WriteRegister writes a single register by index via the 'P' packet.

func (*GDBConn) WriteUint16LE

func (g *GDBConn) WriteUint16LE(addr uint64, val uint16) error

WriteUint16LE writes a little-endian uint16 to the given virtual address.

type GoSSHDServeData

type GoSSHDServeData struct {
	// SSHExe is the server payload's filename on the agent volume.
	SSHExe string
	// ServerLog is the server's log filename on the agent volume.
	ServerLog string
}

GoSSHDServeData is the template context the "gosshd-serve" partial needs. Embed it in a script's own data struct to include the partial:

data := struct {
    GoSSHDServeData
    Banner string
}{GoSSHDServeData: DefaultGoSSHDServeData(), Banner: "..."}

func DefaultGoSSHDServeData

func DefaultGoSSHDServeData() GoSSHDServeData

DefaultGoSSHDServeData wires the partial to the payload names the host stages.

type GuestStage

type GuestStage struct {
	Name string
	// Component groups stages that belong to the same subsystem (provisioning,
	// drivers, virtiofs, WSL, nix…). All stages of a component share one log,
	// so "what happened with WSL" is one file rather than three.
	Component string
	// Script runs in the guest over SSH (already transport-safe once wrapped
	// in PowerShellEncodedCommand). Legacy path: Go-rendered PowerShell.
	Script string
	// ScriptFile names a real PowerShell file in the embedded guest tree
	// (e.g. "wsl2-enable.ps1"), delivered on the control volume and invoked
	// by path. Preferred over Script: real files are lintable, runnable
	// standalone on a guest, and carry no Go interpolation (CELL-402).
	// Args are passed as PowerShell parameters, not string-substituted.
	ScriptFile string
	Args       map[string]string
	// Retries is how many extra attempts the caller should make. Zero means
	// one attempt.
	Retries int
	// RebootAfter: the caller must reboot the guest and wait for SSH to come
	// back before the next stage.
	RebootAfter bool
	// ToleratesDisconnect: the stage's work is expected to tear down the SSH
	// session (e.g. the WSL engine MSI). A "closed by remote host" failure is
	// not a verdict; the next stage verifies the outcome.
	ToleratesDisconnect bool
}

GuestStage is one unit of guest-side work: a PowerShell script run over SSH, plus the contract it imposes on its caller. It is the single stage type for everything the host asks a Windows guest to do — build provisioning and dev-env setup alike — so every such pipeline is one table, named and logged by the same rules.

func DefaultProvisionSteps

func DefaultProvisionSteps(pubKey, username, password string) []GuestStage

DefaultProvisionSteps returns the build-time provisioning pipeline for a new Windows VM, as a GuestStage table — the same shape as DevEnvStages, so both pipelines are named, logged and driven by one set of rules.

func DevEnvStages

func DevEnvStages(user, tag, drive string) []GuestStage

DevEnvStages returns the ordered dev-env provisioning pipeline. Every stage transcripts itself onto the FAT log volume (see BuildDevEnvLogVolume) in addition to its SSH output.

type ISOMetadata

type ISOMetadata struct {
	Version string // e.g. "24H2"
	Arch    string // e.g. "Arm64", "x64"
}

ISOMetadata holds parsed information from an ISO filename.

func ParseISOFilename

func ParseISOFilename(name string) ISOMetadata

ParseISOFilename extracts version and architecture from a Windows ISO filename.

type KVMHostCaps

type KVMHostCaps struct {
	APIVersion int
	PMUv3      bool
	IPABits    int // 0 = cap unsupported → architected default of 40
	MaxVCPUs   int
}

KVMHostCaps holds the host KVM properties that decide whether a Windows ARM64 guest can run at all.

PMUv3 is the load-bearing one: a nested host (macOS vz → Colima) gets no PMU from Apple's hypervisor, the Linux kernel then has no arm_pmu driver, and KVM cannot virtualize a PMU it does not have. Guests on such a host see ID_AA64DFR0_EL1.PMUVer=0 and take an UNDEF on any PMU register access.

func QueryKVMHostCaps

func QueryKVMHostCaps(devPath string) (KVMHostCaps, error)

QueryKVMHostCaps interrogates /dev/kvm directly. It needs only the device fd — no VM is created — so it is safe to run before (or while) a guest is up.

func (KVMHostCaps) Summary

func (c KVMHostCaps) Summary() string

Summary is the one-line form for run-info.txt and the test log.

func (KVMHostCaps) WindowsBootBlocker

func (c KVMHostCaps) WindowsBootBlocker() string

WindowsBootBlocker returns a non-empty reason when this host's KVM cannot boot Windows ARM64 regardless of QEMU configuration, or "" when no known blocker applies.

Root-caused 2026-07-30 (TestWindowsISOBoot_KVM stall diagnostics): bootmgr reads PMCR_EL0 unconditionally; on a PMU-less vCPU KVM injects UNDEF and bootmgr parks in its `b .` panic vector (slot +0x200, DAIF masked). Reproduced without KVM: TCG + pmu=off hangs identically, TCG + PMU boots. No QEMU flag can add a PMU that the host kernel does not have.

type LaunchAction

type LaunchAction int

LaunchAction is one VM-acquisition step.

const (
	ActionUseLocal LaunchAction = iota // instance disk exists locally
	ActionBuild                        // build from Windows ISO (full install + provision)
	ActionDryRun                       // dry-run mode, no VM work
	ActionAttach                       // attach to already-running VM
	ActionClone                        // clone existing template to instance
)

func DecideLaunchActions

func DecideLaunchActions(in LaunchInputs) []LaunchAction

DecideLaunchActions returns the ordered fallback sequence.

DryRun                        → [DryRun]
ExplicitBuild                 → [Build]
VMRunning                     → [Attach]
DiskExists + valid + prov'd   → [UseLocal]
DiskExists + invalid/unprov'd → [Build]  (corrupt leftovers)
TemplateExists + valid        → [Clone]
cold start                    → [Build]

type LaunchInputs

type LaunchInputs struct {
	DryRun            bool  // --dry-run set
	ExplicitBuild     bool  // --force set, force rebuild
	DiskExists        bool  // instance disk image exists at expected path
	DiskSizeBytes     int64 // instance disk file size; 0 means unchecked
	TemplateExists    bool  // template disk image exists (ready to clone)
	TemplateSizeBytes int64 // template disk file size; 0 means unchecked
	VMRunning         bool  // existing QEMU process detected via PID file + QMP
	Provisioned       bool  // .provisioned marker exists
}

LaunchInputs are the inputs to DecideLaunchActions.

type NopObserver

type NopObserver struct{}

NopObserver silently discards all events.

func (NopObserver) Logf

func (NopObserver) Logf(string, ...any)

func (NopObserver) Progress

func (NopObserver) Progress(float64, string)

type Observer

type Observer interface {
	Logf(format string, args ...any)
	Progress(fraction float64, message string)
}

Observer receives progress events from long-running QEMU operations.

type PSTATE

type PSTATE struct {
	Raw   uint64
	EL    int  // exception level, 0-3
	SPSel bool // true = SP_ELx ("h"), false = SP_EL0 ("t")

	// Interrupt masks.
	D bool // debug
	A bool // SError
	I bool // IRQ
	F bool // FIQ

	// Condition flags.
	N, Z, C, V bool
}

PSTATE is a decoded AArch64 PSTATE word.

It exists to separate two states that a PC-based stall detector cannot tell apart on its own:

  • DAIF clear at EL1 with a static PC — the guest is parked in WFI waiting for an interrupt. Normal. It will wake.
  • DAIF fully masked at EL1 with a static PC — the guest took an unrecoverable synchronous exception and its handler is spinning (`b .`) with interrupts off. Nothing will ever wake it.

The KVM firmware hang is the second: PSTATE=600003c5.

func DecodePSTATE

func DecodePSTATE(hex string) (PSTATE, error)

DecodePSTATE parses a PSTATE value as printed by QEMU's "info registers" (hex, no 0x prefix).

func (PSTATE) AllInterruptsMasked

func (p PSTATE) AllInterruptsMasked() bool

AllInterruptsMasked reports whether every DAIF bit is set.

func (PSTATE) CondFlags

func (p PSTATE) CondFlags() string

CondFlags renders NZCV the way QEMU does, e.g. "-ZC-".

func (PSTATE) MaskedFlags

func (p PSTATE) MaskedFlags() string

MaskedFlags lists the masked interrupt types in DAIF order, e.g. "DAIF" when all are masked or "IF" when only IRQ and FIQ are.

func (PSTATE) Mode

func (p PSTATE) Mode() string

Mode renders the exception level and stack selector, e.g. "EL1h".

func (PSTATE) Summary

func (p PSTATE) Summary() string

Summary is the one-line interpretation for a failure message.

type PortMeta

type PortMeta struct {
	SSHPort uint16 `json:"ssh"`
	VNCPort uint16 `json:"vnc"`
	RDPPort uint16 `json:"rdp"`
}

PortMeta records the allocated ports for a running QEMU VM instance. Written to ports.json in the instance directory for discovery by cell vnc/rdp.

func ReadPortMeta

func ReadPortMeta(instanceDir string) (PortMeta, error)

type ScreenSource

type ScreenSource string

screenshotName is `<datetimeISO>-<screenName>-<id>.png`: the capture instant (UTC, ISO 8601 basic — no colons, filename-safe everywhere), the verdict with every measured ratio, then the poll number. Time first makes a directory listing sort chronologically and lets a frame be correlated with guest-side logs by wall clock, which a bare poll number cannot.

The verdict and ratios stay in the name for triage: an earlier form encoded ScreenSource names how a frame was acquired. Frames from different sources see different surfaces — a QMP screendump reads the emulated framebuffer, an RDP capture reads what the guest renders into a session — so each gets its own directory and its own sequence.

const (
	ScreenSourceQMP ScreenSource = "qmp"
	ScreenSourceRDP ScreenSource = "rdp"
)

type Spec

type Spec struct {
	VMName       string
	CPUs         uint
	MemoryGB     uint64
	DiskPath     string // path to qcow2 disk
	FirmwarePath string // path to EDK2 UEFI firmware
	VarsPath     string // path to UEFI variable store (per-VM copy)
	VirtioISO    string // path to VirtIO drivers ISO
	SSHPort      uint16 // forwarded port for SSH
	VNCPort      uint16 // forwarded port for VNC (0 = disabled)
	RDPPort      uint16 // forwarded port for RDP (0 = disabled)
	SSHHost      string // SSH host (default "127.0.0.1")
	SSHUser      string // guest username (defaults to the host $USER)
	SSHKeyPath   string // path to SSH private key
	MACAddr      string // deterministic MAC address
	Binary       string // agent binary (e.g. "claude", "cmd.exe")
	DefaultFlags []string
	UserArgs     []string
	EnvVars      []string // KEY=VALUE pairs
	ProjectDir   string   // host project directory
	DisplayType  string   // "none", "cocoa", "sdl" (default "none")
	QMPSocketDir string   // directory for QMP socket; defaults to /tmp

	// Accel overrides the QEMU accelerator (e.g. "tcg,thread=multi" to force
	// software emulation). Empty means ApplyDefaults resolves it — see
	// ResolveAccel.
	Accel string
	// KVM carries the `[cell] kvm` config intent: the launcher was asked to
	// pass /dev/kvm into the container. It is intent only — ApplyDefaults still
	// probes the device before selecting KVM.
	KVM bool
	// AccelReason explains the resolved Accel in one line, for the launch log.
	// Set by ApplyDefaults; never an input.
	AccelReason string
	// CPU overrides the -cpu string (e.g. "max,pauth-impdef=on,pmu=off").
	// Empty means cpuType picks the per-accelerator default. Exists so a test
	// can vary exactly one CPU feature against an otherwise identical machine
	// — the mechanism behind the PMU evidence table in boot_test.go.
	CPU string
	// SerialLogPath, when set, redirects the guest serial console to this file.
	SerialLogPath string
	// GuestProgressLogPath, when set, attaches a virtio-serial port
	// (ProgressPortName) wired to this file. The guest writes progress via
	// \\.\Global\<ProgressPortName>; this works on ARM64 where pci-serial
	// 16550 devices don't map to user-mode COMx (CELL-430).
	GuestProgressLogPath string
	// GuestStructuredLogPath, when set, attaches a second virtio-serial port
	// (StructuredPortName) for JSONL output. The guest writes structured
	// events to \\.\Global\<StructuredPortName>; the host captures them
	// as build.jsonl for machine-parseable progress and metrics.
	GuestStructuredLogPath string
	// DiskCacheMode sets the qcow2 cache policy (e.g. "unsafe" to drop guest
	// flushes). Empty keeps QEMU's safe default. "unsafe" makes the image
	// worthless if the host dies mid-run, so it is only for throwaway VMs
	// such as the install test, where it removes a large TCG cost.
	DiskCacheMode string
	// NoReboot stops the VM instead of rebooting it — useful when a reboot
	// marks the end of a phase you want to observe.
	NoReboot bool
	// GuestAgentSocketPath, when set, attaches a virtio-serial port named
	// org.qemu.guest_agent.0 (the name qemu-ga looks for by convention) wired
	// to a host unix socket. On ARM64 the agent binary is the x64 MSI running
	// under Win11's emulation (no native build exists — see VIRTIO.md), but
	// the channel wiring is the standard one.
	GuestAgentSocketPath string
	// VirtioFSSocketPath + VirtioFSTag, when both set, attach a
	// vhost-user-fs device backed by a virtiofsd socket. The tag is what the
	// guest mounts by. Requires shareable guest RAM (memory-backend-memfd),
	// which BuildRunCommand adds alongside the device.
	VirtioFSSocketPath string
	VirtioFSTag        string
	// NestedVirt asks for the machine features Windows' own hypervisor needs
	// in order to launch inside the guest: EL2 plus a GICv3 with ITS and a
	// secure world. Opt-in, because it changes the boot environment of an
	// already-installed Windows and the install path is proven without it.
	NestedVirt bool
	// SecureWorld adds secure=on: Arm Security Extensions (TrustZone), which
	// gives the machine an EL3 and a secure flash bank. The firmware in
	// pflash then *becomes* the secure-world firmware and is entered at EL3 —
	// a normal-world EDK2 build cannot do that job. Separate from NestedVirt
	// so the two can be tested apart.
	SecureWorld bool
	// FirmwareKernel loads the UEFI firmware with -kernel instead of pflash.
	// Under secure=on this is what lets a stock, normal-world EDK2 work:
	// QEMU's own ARM boot stub takes the EL3 entry and drops the payload to
	// non-secure. Firmware-in-pflash has no such stub, so the same binary is
	// entered at EL3 and hangs. Costs the pflash NVRAM store.
	FirmwareKernel bool
	// DiskBus selects the system disk controller: "nvme" (default, inbox
	// driver, what our installs use) or "scsi" (virtio-scsi + scsi-hd, what
	// the proven Hyper-V config uses).
	DiskBus string
	// CDBus selects the CD/ISO attachment: "usb" (default, usb-storage on
	// xhci — inbox USBSTOR, no extra driver) or "scsi" (scsi-cd on a
	// dedicated virtio-scsi-pci controller — needs vioscsi drvload in WinPE).
	CDBus string
	// MachineType overrides the -machine string (e.g. "virt,highmem=on").
	// Empty means machineType() picks the per-accelerator default.
	MachineType string
	// LogVolumePath, when set, attaches a raw FAT image as removable USB
	// storage for guest-written logs — the run-time counterpart of the
	// install's answer volume (see BuildDevEnvLogVolume).
	LogVolumePath string
	// DevcellWimImg, when set, attaches a FAT volume containing the
	// custom devcell.wim as an additional USB drive during install. Windows
	// Setup reads from <Path> in autounattend.xml to find it. The volume
	// is a raw or qcow2 FAT image.
	DevcellWimImg string
}

Spec holds everything needed to configure and connect to a QEMU Windows VM.

func FinalizeSpec

func FinalizeSpec(build Spec, kernelFirmware string) Spec

FinalizeSpec derives the dev-env boot from a build spec: the same guest (disk, network identity, credentials) on the EL3 machine — secure=on with a kernel-loaded relocatable firmware — which is what lets Windows' own hypervisor, and therefore WSL2, run (docs/spec/QEMU-ARM64-WINDOWS11-WSL2-NIX.md §2.2).

The install boot and this one are intentionally different machines: the installer is proven on the plain pflash machine, the WSL2 stack on this one. Only the boot environment changes; everything identifying the guest is carried over.

func (*Spec) ApplyDefaults

func (s *Spec) ApplyDefaults()

ApplyDefaults fills in zero-value fields with sensible defaults.

func (*Spec) Validate

func (s *Spec) Validate() error

Validate returns an error if required fields are missing.

type StageRunOptions

type StageRunOptions struct {
	// SSHUser and SSHKeyPath authenticate to the guest.
	SSHUser    string
	SSHKeyPath string
	// LogDir receives one log per component, named by StageLogNames. Empty
	// disables file logging (output is still returned to the observer).
	LogDir string
	// StageTimeout bounds a single stage. Zero means DefaultStageTimeout.
	StageTimeout time.Duration
	// Reboot restarts the guest and returns once it answers SSH again.
	// Required if any stage sets RebootAfter.
	Reboot func(ctx context.Context, reason string) error
	// BeforeStage and AfterStage let a caller checkpoint, screenshot or
	// annotate around a stage. Both are optional; an error from either fails
	// the run.
	BeforeStage func(ctx context.Context, idx int, stage GuestStage) error
	AfterStage  func(ctx context.Context, idx int, stage GuestStage, output string) error
	Observer    Observer
}

RunGuestStages executes a GuestStage table against a running guest over SSH.

This is the one place that knows how to drive guest-side work: retries, reboots, stages whose own command tears down the session, per-stage deadlines, and component-grouped logs streamed to disk while they run. `cell build` and the dev-env test both call it, so what the test proves is what users get — the two had drifted into separate loops, and the CLI's was the weaker one (no reboots, no timeout, nothing readable until exit).

The caller owns the VM. Rebooting and waiting for SSH are supplied as callbacks because only the caller knows how its VM is started and watched.

type StallSignal

type StallSignal struct {
	ScreenHash uint64 // hash of the raw screendump
	ReadBytes  int64  // cumulative bytes read across block devices
	PC         string // vCPU program counter, as reported by "info registers"
}

StallSignal is one poll's worth of liveness evidence.

type StallTracker

type StallTracker struct {
	// contains filtered or unexported fields
}

StallTracker counts consecutive polls in which nothing observable changed.

func (*StallTracker) Consecutive

func (s *StallTracker) Consecutive() int

Consecutive returns the current unchanged-poll count.

func (*StallTracker) Observe

func (s *StallTracker) Observe(sig StallSignal) int

Observe records a poll and returns the number of consecutive unchanged polls seen so far.

It returns 0 until the guest has read at least one byte: before boot has touched the media, identical polls are expected and say nothing about liveness. That guard is what keeps a slow start from being called a hang.

func (*StallTracker) Reset

func (s *StallTracker) Reset()

Reset zeroes the stall counter, e.g. after a runtime intervention that is expected to unblock the guest.

func (*StallTracker) Stalled

func (s *StallTracker) Stalled(threshold int) bool

Stalled reports whether at least threshold consecutive unchanged polls have been observed.

type VM

type VM struct {
	// contains filtered or unexported fields
}

VM wraps a running QEMU process.

func NewVM

func NewVM(spec Spec, obs Observer, pidDir string) *VM

NewVM creates a VM handle. Call Start() to launch it. If pidDir is non-empty, Start() writes a PID file there and process exit cleans it up.

func (*VM) ExitError

func (v *VM) ExitError() error

ExitError returns the error from QEMU process exit (nil if still running or clean exit).

func (*VM) ForceStop

func (v *VM) ForceStop() error

ForceStop kills the QEMU process immediately.

func (*VM) LastOutput

func (v *VM) LastOutput() string

LastOutput returns the tail of QEMU's captured stdout+stderr.

func (*VM) QMPSockPath

func (v *VM) QMPSockPath() string

QMPSockPath returns the QMP unix socket path for this VM.

func (*VM) Shutdown

func (v *VM) Shutdown(ctx context.Context) error

Shutdown sends ACPI powerdown via QMP, then waits for the process to exit.

func (*VM) Start

func (v *VM) Start(ctx context.Context) error

Start launches the QEMU process in the background.

func (*VM) StartInstall

func (v *VM) StartInstall(ctx context.Context, windowsISO, autounattendISO string) error

StartInstall launches the QEMU process for Windows installation.

func (*VM) State

func (v *VM) State() VMState

State returns the current VM state.

func (*VM) StateString

func (v *VM) StateString() string

StateString returns the VM state as a string (for VMStateFunc compatibility).

func (*VM) WaitForExit

func (v *VM) WaitForExit(ctx context.Context) error

WaitForExit blocks until the VM process exits.

type VMState

type VMState string

VMState represents the current state of a QEMU VM.

const (
	StateUnknown VMState = "unknown"
	StateStopped VMState = "stopped"
	StateRunning VMState = "running"
	StateError   VMState = "error"
)

func QueryVMState

func QueryVMState(socketPath string) (VMState, error)

type VMStateFunc

type VMStateFunc func() VMState

VMStateFunc returns the current VM state. WaitForSSH uses it to bail early when the QEMU process exits (e.g. drive collision, missing firmware).

type WimBuilderResult

type WimBuilderResult struct {
	// SharedImg is the path to the shared FAT volume after the builder
	// finished. Contains devcell.wim and the builder log.
	SharedImg string
	// AgentOutput is the agent's combined stdout/stderr (if available).
	AgentOutput string
}

WimBuilderResult holds the output of a successful WIM builder run.

type WimBuilderSpec

type WimBuilderSpec struct {
	// Spec is the base QEMU spec (machine, firmware, etc.).
	Spec Spec
	// WinPEISO is the bootable WinPE ISO (may be stock or custom-injected).
	WinPEISO string
	// SharedImg is the FAT volume with boot.wim + builder script.
	SharedImg string
	// WindowsISO is the full Windows ISO (provides install.wim).
	WindowsISO string
	// VirtIOISO is the virtio-win ISO (provides driver directories for
	// DISM /Add-Driver). Required when any WimPrepOp uses the Driver field.
	VirtIOISO string
	// EFIBootLoader is the raw bytes of BOOTAA64.EFI. When set and CDBus
	// is "scsi", SharedVolumeFiles ships it at /EFI/BOOT/BOOTAA64.EFI
	// alongside startup.nsh so the firmware can chainload WinPE from the
	// FAT volume after failing to read ISO9660 on the SCSI CD.
	EFIBootLoader []byte
}

WimBuilderSpec configures a WIM builder WinPE boot. Callers populate this and pass it to BuildWimBuilderArgv to get a QEMU command line.

type WriteProgressTracker

type WriteProgressTracker struct {
	// Window is how long a guest may write nothing before it counts as stalled.
	// It must exceed the longest legitimate pause: Windows goes quiet for a few
	// minutes at a time under TCG, so minutes, not seconds.
	Window time.Duration
	// contains filtered or unexported fields
}

WriteProgressTracker watches cumulative bytes written to the guest's disk.

It answers a narrower question than StallTracker — "is the install making progress?" rather than "is the guest alive?" — and needs only one signal, which makes it usable from the CLI without QMP register dumps. Windows Setup writes continuously, so a full window with no new bytes means the guest never got as far as applying the image: wrong boot device, dead firmware, or Setup exiting before it started.

func (*WriteProgressTracker) Observe

func (w *WriteProgressTracker) Observe(written int64, elapsed time.Duration) bool

Observe records cumulative bytes written at elapsed time since start, and reports whether the guest has now been silent for longer than Window.

func (*WriteProgressTracker) Reason

func (w *WriteProgressTracker) Reason() string

Reason describes the stall in the terms it was measured in.

Jump to

Keyboard shortcuts

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