1package daemon
2
3import (
4 "context"
5 "errors"
6 "slices"
7 "time"
8
9 renderer "github.com/bnema/vev-vt"
10 "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/protocol"
12)
13
14// remotePreviewRevalidate bounds how long a quiet watch trusts its last frame
15// without a render wake: it re-captures to notice a moved tab or a dead target.
16const remotePreviewRevalidate = 2 * time.Second
17
18// remotePreviewWatch is one live preview stream. Its address keys the render
19// coordinator subscription; the coordinator only ever signals wake.
20type remotePreviewWatch struct {
21 wake chan struct{}
22 rc *renderCoordinator
23}
24
25// notify is the coordinator callback: a non-blocking, lock-free signal. The
26// watch goroutine owns every capture and send.
27func (w *remotePreviewWatch) notify(renderWake) {
28 select {
29 case w.wake <- struct{}{}:
30 default:
31 }
32}
33
34// subscribe follows the target session's coordinator. A headless target gets
35// its own coordinator, which keeps the watched tab renderable while subscribed.
36func (w *remotePreviewWatch) subscribe(d *Daemon, target *session) {
37 var rc *renderCoordinator
38 if target != nil {
39 rc = d.ensureRenderCoordinator(target)
40 }
41 if rc == w.rc {
42 return
43 }
44 w.unsubscribe()
45 if rc != nil && rc.subscribePreviewFor(w, 1, w.notify) {
46 w.rc = rc
47 }
48}
49
50func (w *remotePreviewWatch) unsubscribe() {
51 if w.rc != nil {
52 w.rc.teardownPreviewFor(w, 1)
53 w.rc = nil
54 }
55}
56
57// serveRemotePreviewWatch streams the watched viewport until the client
58// closes the connection, the daemon shuts down, or the target dies. Frames are
59// captured on this goroutine, sent only when they changed, and spaced by at
60// least the requested interval; a dead target ends the stream with one
61// NoSuchTarget frame.
62func (d *Daemon) serveRemotePreviewWatch(tr ports.ServerConnection, watch protocol.RemotePreviewWatch) {
63 if protocol.ValidateRemotePreviewWatch(watch) != nil {
64 _ = d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
65 _ = tr.Close()
66 return
67 }
68 base := d.hardCtx
69 if base == nil {
70 base = context.Background()
71 }
72 ctx, cancel := context.WithCancel(base)
73 defer cancel()
74 // The client cancels by closing or by sending anything at all.
75 readerDone := make(chan struct{})
76 go func() {
77 defer close(readerDone)
78 _, _ = tr.ReceiveClient()
79 cancel()
80 }()
81 stopClose := context.AfterFunc(ctx, func() { _ = tr.Close() })
82 defer func() {
83 stopClose()
84 _ = tr.Close()
85 <-readerDone
86 }()
87
88 w := &remotePreviewWatch{wake: make(chan struct{}, 1)}
89 defer w.unsubscribe()
90 interval := min(max(watch.MinInterval, protocol.RemotePreviewWatchMinInterval), protocol.RemotePreviewWatchMaxInterval)
91 var last protocol.RemotePreview
92 for {
93 // Subscribe before capturing, so output racing the capture still wakes
94 // the next one.
95 w.subscribe(d, d.remotePreviewSession(watch.Request.Target.SessionName, watch.Request.Target.LifecycleID))
96 preview, err := d.captureRemotePreview(watch.Request)
97 if errors.Is(err, errRemotePreviewNoSuchTarget) {
98 _ = d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewNoSuchTarget})
99 return
100 }
101 if err != nil {
102 preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}
103 }
104 if !sameRemotePreview(last, preview) {
105 if d.sendRemotePreview(tr, preview) != nil {
106 return
107 }
108 last = preview
109 }
110 if !d.waitRemotePreview(ctx, interval, nil) || !d.waitRemotePreview(ctx, remotePreviewRevalidate, w.wake) {
111 return
112 }
113 }
114}
115
116// waitRemotePreview blocks for d or until wake fires. It reports false once
117// the watch is cancelled.
118func (d *Daemon) waitRemotePreview(ctx context.Context, delay time.Duration, wake <-chan struct{}) bool {
119 timer := d.clock.NewTimer(delay)
120 defer timer.Stop()
121 select {
122 case <-ctx.Done():
123 return false
124 case <-wake:
125 case <-timer.C():
126 }
127 return ctx.Err() == nil
128}
129
130func (d *Daemon) sendRemotePreview(tr ports.ServerConnection, preview protocol.RemotePreview) error {
131 if protocol.ValidateRemotePreview(preview) != nil {
132 preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed}
133 }
134 return d.boundedControlSend(tr, preview)
135}
136
137func sameRemotePreview(a, b protocol.RemotePreview) bool {
138 return a.Version == b.Version && a.Status == b.Status && a.LifecycleID == b.LifecycleID && a.TabID == b.TabID &&
139 a.Revision == b.Revision && a.Width == b.Width && a.Height == b.Height &&
140 slices.EqualFunc(a.Cells, b.Cells, renderer.Cell.Equal)
141}