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
+14 -7
View File
@@ -139,6 +139,7 @@ const (
EventDHCPOffer EventType = "dhcp_offer" // server sent DHCP Offer
EventDHCPRequest EventType = "dhcp_request" // VM sent DHCP Request
EventDHCPAck EventType = "dhcp_ack" // server sent DHCP Ack
EventScreenshot EventType = "screenshot" // VM display screenshot (JPEG, base64)
EventTailscale EventType = "tailscale" // Tailscale status change
EventTestStatus EventType = "test_status" // test Running/Passed/Failed
)
@@ -212,12 +213,14 @@ type NICStatus struct {
// NodeStatus tracks the current DHCP and Tailscale state of a VM node
// for rendering on the web UI's initial page load.
type NodeStatus struct {
Name string
OS string
NICs []NICStatus // one per NIC; index matches NIC index
JoinsTailnet bool // whether this node runs Tailscale
Tailscale string // "--", "Up (100.64.0.1)", etc.
Console []string // recent console output lines (ring buffer)
Name string
OS string
NICs []NICStatus // one per NIC; index matches NIC index
JoinsTailnet bool // whether this node runs Tailscale
Tailscale string // "--", "Up (100.64.0.1)", etc.
Console []string // recent console output lines (ring buffer)
Screenshot string // latest screenshot as data URI, or ""
ScreenshotPort int // Host.app screenshot server port, or 0
}
const maxConsoleLines = 200
@@ -249,7 +252,11 @@ func (b *EventBus) Publish(ev VMEvent) {
}
b.mu.Lock()
defer b.mu.Unlock()
b.history = append(b.history, ev)
// Don't store screenshots in history — they're large and only the
// latest one matters (stored in NodeStatus.Screenshot instead).
if ev.Type != EventScreenshot {
b.history = append(b.history, ev)
}
if len(b.history) > eventBusHistorySize {
// Trim old events.
copy(b.history, b.history[len(b.history)-eventBusHistorySize:])