| 151 | * but a missing one shouldn't fail the run. Run it concurrently with the drive. |
| 152 | */ |
| 153 | export const recordGuestScreen = async ( |
| 154 | ip: string, |
| 155 | seconds: number, |
| 156 | localMp4: string, |
| 157 | os: "macos" | "linux" | "windows", |
| 158 | ): Promise<void> => { |
| 159 | if (os === "windows") { |
| 160 | // Windows can't screenshot the interactive desktop from an SSH session, so |
| 161 | // we film the VM framebuffer directly via QEMU's `screendump` (the dockur |
| 162 | // host runs the loop + ffmpeg; we pull the mp4). Host/container/storage come |
| 163 | // from env (no baked-in host); best-effort, so skip filming if unconfigured. |
| 164 | const host = process.env.E2E_DESKTOP_WIN_HOST; |
| 165 | const storage = process.env.E2E_DESKTOP_WIN_STORAGE; |
| 166 | if (!host || !storage) return; |
| 167 | const container = process.env.E2E_DESKTOP_WIN_CONTAINER ?? "exec-win"; |
| 168 | const frames = Math.max(8, seconds * 4); |
| 169 | const py = `import socket,time |
| 170 | s=socket.socket(socket.AF_UNIX); s.connect("/run/shm/monitor.sock"); time.sleep(0.2); s.recv(65536) |
| 171 | for i in range(${frames}): |
| 172 | s.sendall(("screendump /storage/frames/f%03d.ppm\\n"%i).encode()); time.sleep(0.2) |
| 173 | try: s.recv(65536) |
| 174 | except Exception: pass`; |
| 175 | const b64 = Buffer.from(py).toString("base64"); |
| 176 | const remote = |
| 177 | `S=${storage}; rm -rf "$S/frames"; mkdir -p "$S/frames"; ` + |
| 178 | `docker exec ${container} python3 -c "import base64;exec(base64.b64decode('${b64}'))"; ` + |
| 179 | `ffmpeg -y -framerate 4 -i "$S/frames/f%03d.ppm" -pix_fmt yuv420p -movflags +faststart "$S/win.mp4" >/dev/null 2>&1`; |
| 180 | await execFileP("ssh", ["-o", "ConnectTimeout=10", host, remote], { |
| 181 | maxBuffer: 16 * 1024 * 1024, |
| 182 | }).catch(() => undefined); |
| 183 | await execFileP("scp", [ |
| 184 | "-o", |
| 185 | "ConnectTimeout=10", |
| 186 | `${host}:${storage}/win.mp4`, |
| 187 | localMp4, |
| 188 | ]).catch(() => undefined); |
| 189 | return; |
| 190 | } |
| 191 | |
| 192 | if (os === "linux") { |
| 193 | const remote = "/tmp/executor-desktop-vm.mp4"; |
| 194 | await guestSsh( |
| 195 | ip, |
| 196 | `rm -f ${remote}; DISPLAY=:99 ffmpeg -y -f x11grab -video_size 1280x800 -framerate 15 ` + |
| 197 | `-i :99 -t ${seconds} -pix_fmt yuv420p ${remote} >/tmp/ffmpeg.log 2>&1`, |
| 198 | ).catch(() => undefined); |
| 199 | // The mostly-flat console compresses small under x264 — a real capture is |
| 200 | // ~30-60KB, a blank/failed one only a few KB. |
| 201 | if ((await guestFileSize(ip, remote)) > 12_000) { |
| 202 | await guestScpFrom(ip, remote, localMp4).catch(() => undefined); |
| 203 | } |
| 204 | return; |
| 205 | } |
| 206 | |
| 207 | const remoteMov = "/tmp/executor-desktop-vm.mov"; |
| 208 | // Warm the capture subsystem — the first screencapture after the display comes |
| 209 | // up can produce nothing. |
| 210 | await guestSsh(ip, "screencapture -x /tmp/.warm.png 2>/dev/null; rm -f /tmp/.warm.png").catch( |