| 1 | # w7 — Windows 7 lab VMs on this Mac |
| 2 | |
| 3 | Every native check runs OneNote 2010 inside disposable QEMU clones of a sealed |
| 4 | Windows 7 base image. A model drives a clone by writing AutoHotkey v2 scripts: |
| 5 | each call runs on the clone's desktop and comes back with stdout and a |
| 6 | screenshot. |
| 7 | |
| 8 | Two halves: |
| 9 | |
| 10 | - `payload/` — installed into the base image as `C:\win7-agent`. It starts an |
| 11 | HTTP listener that executes AHK and screenshots; QEMU forwards it to a host |
| 12 | loopback port per clone. See [payload/README.txt](payload/README.txt). |
| 13 | - `mcp_win7.py` — stays on the Mac. An MCP server (stdio) for desktop control, |
| 14 | file transfer, commands, detached processes, and VM lifecycle. System python3, |
| 15 | stdlib only. |
| 16 | |
| 17 | The original physical-box version of the server is kept unchanged in |
| 18 | [wayback/](wayback/README.md); nothing uses it. |
| 19 | |
| 20 | ## Settings |
| 21 | |
| 22 | Copy `.env.example` at the repository root to `.env` and fill in `ONE_VM_HOME` |
| 23 | (the directory holding the base image, `targets.json`, clone overlays and the |
| 24 | Linux appliance) and `ONE_WIN7_ISO` (licensed installation media, used only by |
| 25 | `vm.py install`). Both stay outside the repository. Environment variables of the |
| 26 | same names override the file. |
| 27 | |
| 28 | ## Wire up an MCP client |
| 29 | |
| 30 | Paste [codex-config-snippet.toml](codex-config-snippet.toml) into your client's |
| 31 | MCP configuration with the repository path filled in, then restart it; it should |
| 32 | list `win7` and `one-linux`. |
| 33 | |
| 34 | ## Smoke test |
| 35 | |
| 36 | ```sh |
| 37 | ./vm.py up alpha --wait |
| 38 | ./mcp_win7.py --target alpha health # listener version info |
| 39 | ./mcp_win7.py --target alpha shot # -> ./screenshot.png |
| 40 | ./mcp_win7.py --target alpha exec 'Run("notepad.exe") |
| 41 | WinWait("Untitled - Notepad",, 10) |
| 42 | SendText("hello")' # prints stdout/exit, -> ./screenshot.png |
| 43 | ./mcp_win7.py --target alpha cmd 'ipconfig' |
| 44 | ./vm.py down alpha |
| 45 | ``` |
| 46 | |
| 47 | ## Local QEMU VM |
| 48 | |
| 49 | `vm.py` runs the Windows 7 build VM on Apple silicon. VM state stays under |
| 50 | `ONE_VM_HOME`; the Windows ISO is opened read-only. |
| 51 | |
| 52 | ```sh |
| 53 | ./vm.py install # create the disk and boot the Windows installer |
| 54 | ./vm.py status win7-build |
| 55 | ./vm.py screenshot |
| 56 | ./vm.py poweroff # request a clean Windows shutdown |
| 57 | ./vm.py run # boot the installed system |
| 58 | ./vm.py media # rebuild win7-agent.iso from payload/ |
| 59 | ./vm.py seal # validate and publish the stopped build disk |
| 60 | ``` |
| 61 | |
| 62 | Normal boots attach `win7-agent.iso` as a read-only CD. Copy its contents to |
| 63 | `C:\win7-agent`, run `install-autostart.cmd` as administrator, restart, then |
| 64 | verify the forwarded listener: |
| 65 | |
| 66 | ```sh |
| 67 | ./mcp_win7.py --target local health |
| 68 | ``` |
| 69 | |
| 70 | Do not put the build disk or licensed installation media in this repository. |
| 71 | Create the immutable base image only after Windows, Office, and the agent pass |
| 72 | their checks and the VM has shut down cleanly. |
| 73 | |
| 74 | ## Test VMs |
| 75 | |
| 76 | Each clone is a sparse overlay on the read-only base. Its name derives a unique |
| 77 | Windows hostname (`alpha` becomes `ONE-ALPHA`), MAC address, forwarded control |
| 78 | port, authentication token, and MCP target. Two vCPUs and 4 GiB are the defaults. |
| 79 | |
| 80 | ```sh |
| 81 | ./vm.py up alpha --wait # creates when absent; waits through hostname restart |
| 82 | ./vm.py up alpha --display # visible Cocoa window instead of headless |
| 83 | ./mcp_win7.py --target alpha health |
| 84 | ./vm.py screenshot alpha |
| 85 | ./vm.py status # lists every clone |
| 86 | ./vm.py down alpha # clean shutdown, then delete the clone |
| 87 | ./vm.py down alpha --preserve-machine # keep a stopped clone for reproduction |
| 88 | ``` |
| 89 | |
| 90 | The MCP server exposes `win7_vm_up`, `win7_vm_down`, and `win7_vm_status`. |
| 91 | `up` accepts creation settings and a `wait` flag; `down` deletes by default and |
| 92 | accepts `preserve_machine`. Use `win7_spawn` for a long-lived process such as |
| 93 | Procmon so it cannot inherit and hold the command channel. |
| 94 | |
| 95 | The control agent starts minimized after login. Its taskbar button remains |
| 96 | available for diagnosis without covering OneNote or changing screenshot-based |
| 97 | test coordinates. |
| 98 | |
| 99 | ## Windows 10 and 11 |
| 100 | |
| 101 | `win10` (22H2 x64, emulated like Windows 7, so slow) and `win11` (24H2 arm64, |
| 102 | hardware-virtualized) are further bases for clones. They install unattended and |
| 103 | headless from Microsoft's Media Creation Tool catalog: Pro from the |
| 104 | volume-license image, which installs without a product key and stays |
| 105 | unactivated, with local administrator `one` / `one`. Setup's first logon runs |
| 106 | [unattend/lab-setup.cmd](unattend/lab-setup.cmd), which installs the agent like |
| 107 | the Windows 7 build and restarts into it. |
| 108 | |
| 109 | ```sh |
| 110 | ./windows_media.py win11 # -> $ONE_VM_HOME/media/win11.iso (brew: wimlib xorriso) |
| 111 | ./vm.py install --base win11 # headless; add --display to watch |
| 112 | ./mcp_win7.py --target win11-build health # answers once the build reaches its desktop |
| 113 | ./vm.py poweroff win11-build |
| 114 | ./vm.py seal --base win11 |
| 115 | ./vm.py up w11 --base win11 --wait # clones work as above; --base is kept per clone |
| 116 | ./mcp_win7.py --target w11 shot |
| 117 | ``` |
| 118 | |
| 119 | A Windows 11 install reaches its agent in about 10 minutes and a new clone in |
| 120 | one; Windows 10 takes about 55 minutes and 7, so pass `--timeout 900` to its |
| 121 | `up --wait`. |
| 122 | |
| 123 | Screens are 1024x768, the largest mode edk2 offers the arm64 guest's framebuffer. |
| 124 | The arm64 guest runs x64 programs, including the agent, through Windows' own |
| 125 | emulation. Windows on Arm has no inbox driver for any NIC QEMU emulates, so |
| 126 | `windows_media.py win11` also keeps Red Hat's signed NetKVM driver from the |
| 127 | virtio-win disc. Transparency is on and DWM's `ForceEffectMode` is set, so Mica |
| 128 | and Acrylic render on the emulated display adapter. |
| 129 | |
| 130 | ## Abrupt-stop recovery tests |
| 131 | |
| 132 | `python3 tools/w7/crash.py windows NAME` and `python3 tools/w7/crash.py linux NAME` |
| 133 | (from the repository root) stop a registered disposable QEMU process without |
| 134 | requesting guest shutdown. They verify its VM/socket identity and preserve its |
| 135 | disk and configuration. Restart with the corresponding `up NAME --wait`, and |
| 136 | finish with `down NAME` to delete the owned machine. The existing MCP lifecycle |
| 137 | continues to perform clean shutdowns. |
| 138 | |
| 139 | These stops discard guest memory while host storage remains powered. Pair them |
| 140 | with the library's persisted-image tests when evaluating crash recovery; neither |
| 141 | operation certifies a physical disk's behavior during host power loss. |
| 142 | |
| 143 | ## Linux Samba VM |
| 144 | |
| 145 | `linux_vm.py` creates an SSH-only Debian 13 arm64 appliance from Debian's |
| 146 | official generic-cloud image. The downloaded base is SHA-512 verified and kept |
| 147 | under `ONE_VM_HOME/linux`; instances are sparse copy-on-write |
| 148 | overlays. Cloud-init installs Samba, dnsmasq, CIFS tools, smbclient, and fio. |
| 149 | |
| 150 | ```sh |
| 151 | ./linux_vm.py fetch |
| 152 | ./linux_vm.py up samba --wait |
| 153 | ./linux_vm.py ssh samba -- 'systemctl is-active smbd dnsmasq' |
| 154 | ./linux_vm.py status |
| 155 | ./linux_vm.py down samba |
| 156 | ``` |
| 157 | |
| 158 | While it runs, Windows VMs obtain an address on their second NIC and reach the |
| 159 | guest-writable share at `\\192.168.77.1\agent`. The Mac controls and inspects |
| 160 | the appliance over its forwarded SSH port. One appliance runs at a time because |
| 161 | that stable share address is the synchronization point for concurrent Windows |
| 162 | clients. A local VDE switch joins up to 63 guests without host privileges. The |
| 163 | `one-linux` MCP exposes fetch, up, SSH, down, and status; its `up` and `down` |
| 164 | follow the same auto-create, wait-flag, and preserve conventions as Windows. |
| 165 | |
| 166 | ## Linux desktop VM |
| 167 | |
| 168 | `up --desktop` adds an X display for driving GUI apps: Xvfb `:0` at 1280x800 |
| 169 | under openbox, run as lingering systemd user units, with xdotool, xclip, maim, |
| 170 | AT-SPI and Mesa's software Vulkan (lavapipe) for wgpu. There is no guest agent; |
| 171 | each MCP call pipes [linux_desktop.py](linux_desktop.py) into `python3 -` over |
| 172 | SSH. |
| 173 | |
| 174 | ```sh |
| 175 | ./linux_vm.py up desk --desktop --cpus 8 --memory 8192 --disk 40 --wait |
| 176 | ./linux_vm.py ssh desk -- 'xdotool getmouselocation' |
| 177 | ``` |
| 178 | |
| 179 | `linux_exec` runs a bash script and returns output plus a screenshot, the |
| 180 | counterpart of `win7_exec` with xdotool in place of AutoHotkey. `linux_shot`, |
| 181 | `linux_ui` (AT-SPI tree), `linux_spawn` (transient user unit), `linux_put` and |
| 182 | `linux_get` mirror the Windows tools. A desktop clone is still a Samba |
| 183 | appliance, so it shares the one-at-a-time lab address rule. |
| 184 | |
| 185 | To restore a base from private object storage, upload the sealed qcow2 beside |
| 186 | its JSON manifest and pass the manifest URL. A presigned URL needs no secret; |
| 187 | otherwise set `ONE_VM_AUTHORIZATION` to the complete HTTP Authorization value. |
| 188 | |
| 189 | ```sh |
| 190 | ./vm.py fetch 'https://private.example/one/win7-office-base.json' |
| 191 | ``` |
| 192 | |
| 193 | `install-autostart.cmd` disables UAC so the isolated test guest can rename |
| 194 | itself, start the control agent, and run Procmon without human prompts. QEMU |
| 195 | publishes each guest agent only on host loopback. Do not run that installer on |
| 196 | a physical or network-reachable Windows machine. |
| 197 | |
| 198 | ## Gotchas |
| 199 | |
| 200 | - `-cpu qemu64` lacks SSSE3, which Mesa's llvmpipe (software OpenGL for Snowbound on a clone) |
| 201 | needs: `ONE_VM_CPU=Nehalem ./vm.py up NAME --wait` picks another model. |
| 202 | - Keep Windows display scaling at 100%. Anything else and the screenshot |
| 203 | coordinates no longer match where clicks land. |
| 204 | - The listener must run in the interactive logged-in session — as a scheduled |
| 205 | task or service it gets session 0 and sees a black screen. |