Help us shape the future of Go! Take the 2026 Go Developer Survey

broker

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

Read-only display broker feasibility spike (A2)

This package is an internal prototype, not a public route. New(stream) owns one already-connected upstream RFB 3.8 stream; Run(ctx) captures it and ServeObserver(ctx, stream) serves independently negotiated observers. Use a fresh Broker for each upstream generation. Each serving call blocks until its observer exits. Close cancels every connection, including blocked handshakes.

Authorization is the caller's job. The only RFB security type is None, appropriate after platform HTTP authorization. The package does not dial KubeVirt, acquire a display claim or expose an unauthenticated listener. NewWebSocketStream adapts an already-authenticated binary WebSocket. The existing VMVNCHandler remains the active exclusive bridge.

Implemented

  • Upstream RFB 3.8 handshake, fixed RGB32 capture, Raw and DesktopSize parsing.
  • Framebuffer bounds, complete initial pixel coverage before publication, and preservation of prior pixels across partial updates. Resizing waits for a fully initialized new frame before publishing it.
  • Independent downstream handshake, 8/16/32-bit true-colour format conversion, region requests, incremental waits, Raw output and DesktopSize notification.
  • ZRLE output (zrle.go) when a participant's SetEncodings advertises it: 64x64 tiles coded as solid / packed palette (2–16 colours) / plain RLE / raw, smallest per tile, one connection-scoped zlib stream sync-flushed per rectangle. Raw remains the mandatory fallback.
  • Remote-pointer channel: the capture negotiates the Cursor/CursorPos pseudo-encodings upstream and the broker forwards the latest shape and position to participants that negotiate them — ahead of pixel rects, on cursor-only changes without waiting for frame damage, and to late joiners with their first frame. Shapes are size-bounded, coverage-independent and converted to each participant's pixel format; a zero-sized shape hides the pointer. Controller pointer moves are published as positions (the console does not echo client-driven moves), so observers track the controller; neither producer clobbers the other. Participants that do not negotiate cursor encodings are unaffected.
  • Guest-audio channel: the capture negotiates the QEMU audio pseudo-encoding upstream and opens one shared session in S16 stereo 44100 Hz once the console acknowledges it. Participants that advertise audio get an ack, then select exactly that format and enable/disable their own tap; PCM batches push spontaneously to enabled participants. Anything else (unnegotiated encodings, mismatched formats, oversize batches) fails closed. No playback anywhere yet — the channel is transport, measured in batches and bytes.
  • All key/pointer/clipboard/resize/extended-key mutations from observers are consumed and discarded. No control-claim placeholder or input forwarding.
  • At most eight attached observers including stalled handshakes. An observer holds one in-flight immutable framebuffer; intermediate generations coalesce rather than accumulating a frame queue. Socket writes expire after five seconds. No socket I/O holds the broker lock.
  • Context cancellation and upstream failure close all transports. Reader workers are joined when their observer returns. A pending incremental request does not block processing a later forced refresh or detecting disconnect.

Resource envelope and limitations

Maximum dimensions are 4096 on either axis and at most 4096×2160 pixels. Snapshot publication copies the whole framebuffer, irrespective of damage. Each viewer converts one tile at a time and writes full requested regions. There is no Tight/JPEG encoding, controller role, audio, cursor extension, observer clipboard delivery, public membership API, owner routing or fencing. Unsupported upstream encodings and downstream extensions terminate the stream. Idle membership expiry is a future membership-layer responsibility.

At 1080p a snapshot is 8,294,400 bytes. Retained pixel storage is bounded by the capture working buffer, the current snapshot, and up to eight in-flight old snapshots, plus initial-coverage bytes, per-client bounded RFB messages and row buffers. Full-frame wire cost per observer is now encoding-dependent: Raw stays ~8.3 MB per 1080p frame; ZRLE measured locally (Linux/amd64, i7-13700K, 2026-09-19) at ~24 B for a solid frame, ~111 KB for desktop-like content (~75× smaller than Raw) and ~6.2 MB for pure noise, at ~5/16/71 ms encode time respectively — enough for typical desktops, with noisy video content the expensive remainder. Live-VM throughput is still unmeasured; see the encoding decision note in the tracking plan.

Local Linux/amd64 publication microbenchmark (2026-09-18, i7-13700K): about 0.84 ms and 8.30 MB allocated per 1080p publication (3 allocations). This is copy/publication cost, not live VM capture throughput or end-to-end latency.

Verification

From the API root:

go test -race ./internal/broker ./internal/rfb
go test ./internal/broker -run '^$' -bench BenchmarkPublish1080p -benchmem

Fixtures exercise independent 32-bit/RGB565 observers, late join, partial updates, resize, coverage, malformed/truncated data, mutation suppression, slow-reader timeout, capacity/release, static-display disconnect and shutdown.

The optional external-client test uses the real sibling desktop-client decoder and noVNC 1.7.0 in Chromium. It checks pixel colours for initial and partial updates and a 2×1 → 1×2 resize. It is a loopback synthetic-upstream check, not a live KubeVirt or physical-display test. The nested native module's relative replace expects api and desktop-client to be sibling checkouts.

go -C internal/broker/testdata/interop/native build -o /tmp/kw-broker-native-client .
npm ci --prefix internal/broker/testdata/interop
internal/broker/testdata/interop/node_modules/.bin/playwright install chromium
KW_BROKER_NATIVE_CLIENT=/tmp/kw-broker-native-client \
KW_BROKER_BROWSER_CLIENT="$PWD/internal/broker/testdata/interop/browser.mjs" \
go test -race ./internal/broker -run '^TestExternalClients$' -v

Both clients passed locally against the same capture connection, with the browser joining after the native client. Dependency downloads are test-only; they are not included in the API image or its root Go module.

Next gate

Before enabling routes: define the public membership/control contract in Goa, implement authorization and capture/control lease separation, resolve multi-replica broker routing, and measure the pilot on a real VM with realistic resolutions/observer counts. Production encoding selection remains gated on those performance measurements.

Documentation

Overview

Package broker implements the read-only RFB multi-viewer feasibility spike. It is not wired into public routes. Callers must authenticate/authorize both transports before passing streams here; RFB security None is not platform auth.

Index

Constants

View Source
const (
	MaxParticipants = 8
	MaxPixels       = 4096 * 2160
)

Variables

View Source
var (
	ErrClosed   = errors.New("display broker closed")
	ErrCapacity = errors.New("display broker participant limit reached")
)

Functions

This section is empty.

Types

type Broker

type Broker struct {
	// contains filtered or unexported fields
}

Broker owns a single upstream and up to MaxParticipants observers. Frames are immutable snapshots: each observer holds at most one in-flight snapshot, while intervening updates coalesce into the current snapshot. No socket I/O runs under mu. A controller participant forwards guest-mutating RFB messages upstream through an InputGate, serialized against capture's own write side. Create a new Broker for each upstream connection generation.

func New

func New(upstream Stream) *Broker

func (*Broker) Close

func (b *Broker) Close()

Close is idempotent, including before Run. It wakes waiters and terminates blocked handshakes/readers/writers without holding the broker lock.

func (*Broker) Run

func (b *Broker) Run(ctx context.Context) error

Run owns capture until EOF, protocol failure or cancellation. It is single-use and always closes the upstream and all attached observers before returning.

func (*Broker) ServeObserver

func (b *Broker) ServeObserver(ctx context.Context, c Stream) error

ServeObserver owns c until disconnect, cancellation or broker shutdown. It blocks; callers run separate observers in separate goroutines. All input mutations are discarded.

func (*Broker) ServeParticipant

func (b *Broker) ServeParticipant(ctx context.Context, c Stream, controller bool, gate InputGate) error

ServeParticipant serves c like ServeObserver but, when controller and gate are set, forwards guest-mutating RFB messages (keys, pointer, clipboard, desktop resize, extended keys) upstream through gate. Dropped writes and the gate's fencing errors surface to the caller. Observer-local messages (framebuffer requests, pixel format, encodings for the participant's own view) never reach the upstream.

type InputGate

type InputGate interface {
	DispatchInput(fn func()) error
}

InputGate admits controller input writes. display.Fence satisfies it: writes only land while the caller still holds the registry controller role and the control lease is renewable, and stop at the fencing bound otherwise.

type Stream

type Stream interface {
	io.ReadWriteCloser
	SetDeadline(time.Time) error
	SetWriteDeadline(time.Time) error
}

Stream is a deadline-capable byte stream (net.Conn or a binary WebSocket adapter). Close must unblock concurrent reads/writes. A WebSocket message is not an RFB message; its adapter must flatten message boundaries. Streams must tolerate one concurrent reader and one concurrent writer.

type WebSocketStream

type WebSocketStream struct {
	// contains filtered or unexported fields
}

WebSocketStream flattens binary WebSocket messages into an RFB byte stream. A single goroutine reads and a single goroutine writes; Close may run from another goroutine. It does not dial, upgrade or authorize a connection.

func NewWebSocketStream

func NewWebSocketStream(c *websocket.Conn) *WebSocketStream

func (*WebSocketStream) Close

func (s *WebSocketStream) Close() error

func (*WebSocketStream) Read

func (s *WebSocketStream) Read(p []byte) (int, error)

func (*WebSocketStream) SetDeadline

func (s *WebSocketStream) SetDeadline(t time.Time) error

func (*WebSocketStream) SetWriteDeadline

func (s *WebSocketStream) SetWriteDeadline(t time.Time) error

func (*WebSocketStream) Write

func (s *WebSocketStream) Write(p []byte) (int, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL