From b9fe0a2d019d62087f59445335d64aff80dd140d Mon Sep 17 00:00:00 2001 From: clover caruso Date: Tue, 6 Oct 2026 23:11:12 -0700 Subject: [PATCH] Provision Windows and macOS guests and report desktop setup progress --- dashboard/web/components/Sidebar.tsx | 2 +- dashboard/web/components/VMConsole.tsx | 52 ++++-- dashboard/web/pages/VMs.css | 1 + dashboard/web/pages/VMs.tsx | 17 +- dashboard/web/types/vms.ts | 17 +- guest/install.ps1 | 27 ++- guest/osx-provision.py | 227 +++++++++++++++++++++++++ guest/windows-provision.ps1 | 102 +++++++++++ guest/windows.c | 3 + nixos/configuration.nix | 36 +++- tools/benchmark-windows-init.py | 34 ++++ tools/dashboard-vms-test.py | 19 ++- tools/prepare-windows-preset.py | 67 ++++++++ tools/vm-ssh.py | 63 +++++++ tools/vms.py | 207 +++++++++++++++++----- 15 files changed, 800 insertions(+), 74 deletions(-) create mode 100644 guest/osx-provision.py create mode 100644 guest/windows-provision.ps1 create mode 100644 tools/benchmark-windows-init.py create mode 100644 tools/prepare-windows-preset.py create mode 100644 tools/vm-ssh.py diff --git a/dashboard/web/components/Sidebar.tsx b/dashboard/web/components/Sidebar.tsx index 47c80634a5bff60bc22ea9118674ee0f17bcc933..93a9263c2c63927f2d249b19cbb100d1dc5055dc 100644 --- a/dashboard/web/components/Sidebar.tsx +++ b/dashboard/web/components/Sidebar.tsx @@ -157,7 +157,7 @@ function VMNav() { diff --git a/dashboard/web/components/VMConsole.tsx b/dashboard/web/components/VMConsole.tsx index 602099de40a6935c43359cf99f3ebaf389917cbc..1263edcf49ce81a097475716d40eb62406248690 100644 --- a/dashboard/web/components/VMConsole.tsx +++ b/dashboard/web/components/VMConsole.tsx @@ -1,5 +1,5 @@ import { VMMenu } from "./VMMenu.tsx"; -import { createEffect, createMemo, createSignal, type JSX, onCleanup, Show } from "solid-js"; +import { createEffect, createMemo, createSignal, type JSX, on, onCleanup, Show } from "solid-js"; import { reason } from "../api.ts"; import Keyboard from "lucide-solid/icons/keyboard"; import Clipboard from "lucide-solid/icons/clipboard"; @@ -7,6 +7,8 @@ import Maximize from "lucide-solid/icons/maximize"; import RefreshCw from "lucide-solid/icons/refresh-cw"; import Check from "lucide-solid/icons/check"; import RemoteKeyboard from "@novnc/keyboard"; +import { VM_SETUP, type Domain } from "../types/vms.ts"; +import { StatusLabel } from "./Status.tsx"; type WindowSurface = { id: number; x: number; y: number; width: number; height: number; atlas_x: number; atlas_y: number; @@ -14,8 +16,10 @@ type WindowSurface = { }; type WindowFrame = { windows: WindowSurface[]; rtpTimestamp: number }; -export function VMConsole(props: { name: string; enabled: boolean; active: boolean; toolbar: JSX.Element; content: (toolbar: HTMLDivElement) => JSX.Element }) { +export function VMConsole(props: { name: string; enabled: boolean; active: boolean; setup?: Domain["setup"]; toolbar: JSX.Element; content: (toolbar: HTMLDivElement) => JSX.Element }) { const enabled = createMemo(() => props.enabled); + const preparing = () => !!props.setup && props.setup !== "ready"; + let reconnectTimer: number | undefined; let attempt = 0; let video!: HTMLVideoElement; let canvas!: HTMLCanvasElement; @@ -121,6 +125,13 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole const keyboard = new RemoteKeyboard(video); const focus = () => { if (input?.readyState === "open") keyboard.grab(); }; const blur = () => { keyboard.ungrab(); release(); }; + const releaseKeyboard = (event: KeyboardEvent) => { + if (event.key !== "Escape" || !event.ctrlKey || !event.altKey) return; + event.preventDefault(); + event.stopImmediatePropagation(); + tools()?.querySelector("summary")?.focus(); + }; + video.addEventListener("keydown", releaseKeyboard, true); video.addEventListener("focus", focus); video.addEventListener("blur", blur); let peer: RTCPeerConnection | undefined; @@ -171,13 +182,23 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole const fail = (message: string) => { if (current !== attempt || disposed) return; setState("Disconnected"); - setProblem(message); disconnect(); + if (preparing() && props.setup !== "failed") { + setProblem(""); + reconnectTimer = window.setTimeout(connect, 1000); + } else setProblem(message); }; const timeout = window.setTimeout(() => fail("The screen couldn't connect. Reconnect to try again."), 20000); + const ready = () => { + if (current !== attempt || disposed || video.readyState < 2 || input?.readyState !== "open") return; + clearTimeout(timeout); + setState("Connected"); + }; disconnect = () => { + clearTimeout(reconnectTimer); clearTimeout(timeout); blur(); + video.removeEventListener("keydown", releaseKeyboard, true); video.removeEventListener("focus", focus); video.removeEventListener("blur", blur); socket.onclose = null; @@ -219,6 +240,7 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole movement = peer.createDataChannel("pointer", { ordered: false, maxRetransmits: 0 }); keyboard.onkeyevent = (key, _code, down) => send({ type: "key", key, down }); input.onopen = () => { + ready(); if (document.activeElement === video) focus(); if (windowsAvailable()) send({ type: "experiment", enabled: separateWindows() }); }; @@ -308,10 +330,8 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole }; video.onplaying = () => { if (current !== attempt || disposed) return; - clearTimeout(timeout); - setState("Connected"); + ready(); compose(); - if (props.active) video.focus(); }; } catch (failure) { console.error("VM screen connection", failure); @@ -330,7 +350,10 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole setProblem(""); } }); - createEffect(() => { if (props.active && connected()) video.focus(); else release(); }); + createEffect(() => { if (props.active && connected() && !preparing()) video.focus(); else release(); }); + createEffect(on(() => props.setup, setup => { + if (setup === "ready" && enabled() && state() === "Disconnected") void connect(); + }, { defer: true })); onCleanup(() => { disposed = true; disconnect(); }); const pointer = (event: PointerEvent | WheelEvent, mask = buttons) => { if (!connected() || !width || !height) return; @@ -380,7 +403,8 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole - {props.enabled ? state() : "Off"} + Ctrl+Alt+Esc releases keyboard + {preparing() ? "" : props.enabled ? state() : "Off"} @@ -390,7 +414,7 @@ export function VMConsole(props: { name: string; enabled: boolean; active: boole {windowCapture() ? "Separate windows" : "Desktop fallback"} - +