tstest/natlab/vmtest: add macOS VM screenshot streaming to web UI

When --vmtest-web is set, Host.app is launched with --screenshot-port 0
to start a localhost HTTP server that captures the VZVirtualMachineView
display. The Go test harness parses the SCREENSHOT_PORT=<port> line from
stdout, then polls every 2 seconds for JPEG thumbnails and pushes them
over WebSocket to the web dashboard.

Clicking a screenshot thumbnail opens a full-resolution image proxied
through the web UI's /screenshot/{node} endpoint.

Screenshot events are excluded from the EventBus history (they're large
and only the latest matters, stored in NodeStatus.Screenshot).

Updates #13038

Change-Id: I9bc67ddd1cc72948b33c555d4be3d8db06a41f6d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-28 12:46:39 -07:00
committed by Brad Fitzpatrick
parent 78627c132f
commit 4cec06b8f2
9 changed files with 168 additions and 21 deletions
+20
View File
@@ -108,6 +108,7 @@ func (e *Env) maybeStartWebServer() {
mux := http.NewServeMux()
mux.HandleFunc("GET /", e.serveIndex)
mux.HandleFunc("GET /ws", e.serveWebSocket)
mux.HandleFunc("GET /screenshot/{node}", e.serveScreenshot)
mux.HandleFunc("GET /style.css", serveStaticAsset("style.css"))
srv := &http.Server{Handler: mux}
@@ -155,6 +156,25 @@ func (e *Env) serveIndex(w http.ResponseWriter, r *http.Request) {
}
}
// serveScreenshot proxies a full-resolution screenshot from the Host.app
// screenshot server. Returns raw JPEG with no HTML wrapper.
func (e *Env) serveScreenshot(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("node")
port := e.nodeScreenshotPort(name)
if port == 0 {
http.Error(w, "no screenshot server for node", http.StatusNotFound)
return
}
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/screenshot?full=1", port))
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "image/jpeg")
io.Copy(w, resp.Body)
}
func (e *Env) serveWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {