attachment_resize.go RawHistoryFolder

1package daemon
2
3import (
4	"github.com/bnema/vev/internal/domain"
5	"github.com/bnema/vev/internal/ports"
6)
7
8// requestResize reports reset invalidation completion for immediate
9// attached requests, and coordinator schedule acceptance for async requests.
10// Headless requests have no reset invalidation and report geometry completion.
11func (g *sharedPTYGeometry) requestResize(d *Daemon, sess *session, ac *attachedClient, size domain.Size, immediate bool) bool {
12	if ac == nil {
13		return g.requestResizeForLease(d, sess, nil, nil, size, immediate)
14	}
15	rc := d.attachCoordinator(sess, nil, ac, true)
16	return g.requestResizeForLease(d, sess, ac, rc.attachmentLease(ac), size, immediate)
17}
18
19// requestResizeForLease carries the Connection's captured lease
20// through the full resize transaction and every delayed retry callback.
21func (g *sharedPTYGeometry) requestResizeForLease(d *Daemon, sess *session, ac *attachedClient, lease *attachmentLease, size domain.Size, immediate bool) bool {
22	valid := sess != nil && size.Valid()
23	d.observeRuntime(ports.RuntimeResizeRequested, 0, valid)
24	if !valid {
25		return false
26	}
27	if ac == nil {
28		// Headless geometry has no coordinator/transport to coalesce, but keeps
29		// the same prepare/apply/commit ordering.
30		_, ok := g.applySessionLayout(d, sess, size, nil, nil)
31		return ok
32	}
33	rc := sess.renderCoordinator()
34	if rc == nil || !rc.leaseCurrent(lease, true) {
35		return false
36	}
37	geometryClaim, claimed := g.claimSize(sess, ac, size)
38	if !claimed {
39		return false
40	}
41	resize := attachmentResize{d: d, sess: sess, ac: ac, lease: lease}
42	if immediate {
43		resize.epoch = rc.recordResizeRequestForLeaseWithClaim(size, ac, lease, geometryClaim)
44		if resize.epoch == 0 {
45			g.release(sess, geometryClaim)
46			return false
47		}
48		return resize.run()
49	}
50	epoch := rc.scheduleResizeForLeaseWithClaim(size, ac, lease, geometryClaim, func(epoch uint64) {
51		accepted := resize
52		accepted.epoch = epoch
53		accepted.run()
54	})
55	if epoch == 0 {
56		g.release(sess, geometryClaim)
57	}
58	return epoch != 0
59}
60
61// attachmentResize is one accepted resize of an attachment's session
62// geometry. It owns the complete lifecycle after the coordinator admits an
63// epoch: the session layout transaction (applySessionLayout), attachment size
64// publication, bounded background retries for members whose PTY resize
65// failed, and the single reset frame. The coordinator keeps scheduling and
66// epoch fencing; this type keeps what an accepted epoch does.
67type attachmentResize struct {
68	d     *Daemon
69	sess  *session
70	ac    *attachedClient
71	lease *attachmentLease
72	epoch uint64
73}
74
75func (r attachmentResize) retryCurrent(rc *renderCoordinator) bool {
76	return rc.retryCurrentForLease(r.epoch, r.ac, r.lease)
77}
78
79func (r attachmentResize) scheduleRetry(rc *renderCoordinator, failed []resizeMember) {
80	rc.scheduleResizeRetryForLease(r.epoch, r.ac, r.lease, func() { r.retry(failed) })
81}
82
83// run is the accepted attachment resize: claim shared geometry, apply the
84// session layout transaction, publish the attachment size, schedule retries
85// for failed members, and emit exactly one reset frame. Every step is fenced
86// by the attachment effect, the geometry claim, and the resize epoch.
87func (r attachmentResize) run() bool {
88	d, sess, ac, lease, epoch := r.d, r.sess, r.ac, r.lease, r.epoch
89	g := &sess.geometry
90	rc := sess.renderCoordinator()
91	if rc == nil {
92		return false
93	}
94	snap := rc.resizeSnapshot()
95	geometryClaim := snap.geometryClaim
96	if geometryClaim == nil {
97		var claimed bool
98		geometryClaim, claimed = g.claimSize(sess, ac, snap.size)
99		if !claimed {
100			return false
101		}
102	}
103	effect, admitted := beginAttachmentLeaseEffect(sess, ac, lease)
104	if !admitted {
105		return false
106	}
107	defer effect.End()
108	current := func() bool {
109		return effect.current() && sess.geometry.current(geometryClaim) &&
110			rc.resizeCurrentForLease(epoch, ac, lease, false)
111	}
112	if !current() {
113		return false
114	}
115	d.exitCopyMode(ac)
116	failed, ok := g.applySessionLayout(d, sess, snap.size, current, func() bool {
117		return rc.resizeCurrentForLease(epoch, ac, lease, true)
118	})
119	if !ok {
120		return false
121	}
122	// Revalidate while holding the attachment output boundary. A lease can be
123	// retired after the layout commit but before this attachment-local size is
124	// published; stale generations must not publish any remaining effects.
125	d.observeBeforeResizeOwnerPostEffect(resizeOwnerPostCommitPublication)
126	ac.sendMu.Lock()
127	if !current() {
128		ac.sendMu.Unlock()
129		return false
130	}
131	ac.setSize(snap.size)
132	ac.sendMu.Unlock()
133	if !current() {
134		return false
135	}
136	if len(failed) != 0 && rc.resizeRetryAvailableForLease(r.epoch, r.ac, r.lease) {
137		r.scheduleRetry(rc, failed)
138	}
139	if !current() {
140		return false
141	}
142	d.refreshBarScriptsIfDue(sess, d.clock.Now(), true)
143	// A successful transaction publishes exactly one full S2 frame. The
144	// coordinator is the only emission route and stale epochs never reach it.
145	if !current() || !rc.invalidateForLeaseAtResizeEpoch(ac, lease, epoch, renderInvalidation{class: invalidateUrgent, reset: true, producer: "attachment_resize.go"}) {
146		return false
147	}
148	// The resize debounce has already elapsed. Consume this sticky reset now;
149	// fire preserves ACK and synchronized-output gates rather than scheduling a
150	// second urgent deadline.
151	if !current() {
152		return false
153	}
154	rc.fireCurrent(false)
155	return true
156}
157
158// retryResizeMembers retries failed committed members through a freshly
159// prepared tab transaction. The captured members identify retry candidates
160// only: their rectangles must never cross this delayed boundary, because any
161// later layout mutation may have changed their geometry or removed them.
162func (r attachmentResize) retry(members []resizeMember) {
163	d, sess := r.d, r.sess
164	g := &sess.geometry
165	rc := sess.renderCoordinator()
166	if rc == nil || !r.retryCurrent(rc) {
167		return
168	}
169
170	// Retain tab order for deterministic external PTY ordering while collapsing
171	// several failed members from the same tab into one canonical transaction.
172	tabs := make([]*tab, 0, len(members))
173	seen := make(map[*tab]struct{}, len(members))
174	for _, member := range members {
175		if member.tab == nil || member.isFloating || !resizeMemberOwnerCurrent(&member) {
176			continue
177		}
178		if _, ok := seen[member.tab]; ok {
179			continue
180		}
181		seen[member.tab] = struct{}{}
182		tabs = append(tabs, member.tab)
183	}
184
185	failed := make([]resizeMember, 0, len(members))
186	succeeded := false
187	for _, tb := range tabs {
188		if !r.retryCurrent(rc) {
189			return
190		}
191		// A later accepted layout transaction clears resizeRetry. In that case
192		// this delayed callback has no remaining work and, importantly, must not
193		// replay the obsolete rectangle it captured before the mutation.
194		retryPending := false
195		tb.mu.Lock()
196		for _, member := range members {
197			if member.tab != tb || member.pane == nil || tb.panes[member.pane.id] != member.pane {
198				continue
199			}
200			member.pane.mu.Lock()
201			retryPending = retryPending || member.pane.resizeRetry
202			member.pane.mu.Unlock()
203		}
204		tb.mu.Unlock()
205		if !retryPending {
206			continue
207		}
208
209		// applyTabLayoutTransaction captures the current generation, pane
210		// pointers, and solved rectangles, and validates them again before any
211		// VT/rectangle publication. It also preserves the resize gate and
212		// degradation notice behavior for another failed external attempt.
213		freshFailed, ok := g.applyTabLayoutTransaction(d, sess, tb, func() bool {
214			return r.retryCurrent(rc)
215		})
216		if !ok || !r.retryCurrent(rc) {
217			return
218		}
219		failed = append(failed, freshFailed...)
220		// A retry success is a formerly failed, still-current target whose
221		// canonical apply cleared its retry bit. A collapsed or removed target
222		// is neither a retry completion nor a reason to publish a reset.
223		tb.mu.Lock()
224		for _, member := range members {
225			if member.tab != tb || member.pane == nil || tb.panes[member.pane.id] != member.pane {
226				continue
227			}
228			member.pane.mu.Lock()
229			succeeded = succeeded || !member.pane.resizeRetry
230			member.pane.mu.Unlock()
231		}
232		tb.mu.Unlock()
233	}
234	for _, member := range members {
235		if !member.isFloating || member.tab == nil || member.pane == nil {
236			continue
237		}
238		if !r.retryCurrent(rc) {
239			return
240		}
241		// Floating panes are outside tb.panes. Validate the exact accepted slot
242		// before preparing a fresh geometry; applyVisibleFloatingLayout repeats
243		// the same pointer/generation check around external PTY.Resize.
244		member.tab.mu.Lock()
245		currentSlot := member.tab.floating.state == floatingVisible &&
246			member.tab.floating.generation == member.floatingGeneration &&
247			member.tab.floating.pane == member.pane && resizeMemberOwnerCurrent(&member)
248		retryPending := false
249		if currentSlot {
250			member.pane.mu.Lock()
251			retryPending = member.pane.resizeRetry
252			member.pane.mu.Unlock()
253		}
254		member.tab.mu.Unlock()
255		if !currentSlot || !retryPending {
256			continue
257		}
258		freshFailed, ok := g.applyVisibleFloatingLayoutForMember(d, sess, member.tab, func() bool {
259			return r.retryCurrent(rc)
260		}, &member)
261		if !ok || !r.retryCurrent(rc) {
262			return
263		}
264		failed = append(failed, freshFailed...)
265		member.tab.mu.Lock()
266		stillCurrent := member.tab.floating.state == floatingVisible &&
267			member.tab.floating.generation == member.floatingGeneration &&
268			member.tab.floating.pane == member.pane && resizeMemberOwnerCurrent(&member)
269		member.tab.mu.Unlock()
270		if stillCurrent && len(freshFailed) == 0 {
271			member.pane.mu.Lock()
272			succeeded = succeeded || !member.pane.resizeRetry
273			member.pane.mu.Unlock()
274		}
275	}
276	if len(failed) != 0 && rc.resizeRetryAvailableForLease(r.epoch, r.ac, r.lease) {
277		r.scheduleRetry(rc, failed)
278	}
279	if succeeded {
280		// Retry completion changes VT state after the original layout commit.
281		// Keep a named session's eventual snapshot generation aligned with it.
282		markSnapshotDirty(sess)
283		rc.invalidateForLease(r.ac, r.lease, renderInvalidation{class: invalidateUrgent, reset: true, producer: "attachment_resize.go"})
284	}
285}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close