v0.3.0 99dc266

attachment_transition_publish.go RawHistoryFolder

1package daemon
2
3import (
4	"slices"
5
6	"github.com/bnema/vev/internal/domain"
7)
8
9// transitionSourcePreflightLocked validates an initiating connection while its
10// effect gate is frozen and d.mu is held. No target is created before this
11// succeeds.
12func (d *Daemon) transitionSourcePreflightLocked(req attachmentTransitionRequest) bool {
13	if req.sourceToken == nil || req.source == nil || req.source.core() == nil || d.sessions[req.source.core().id] != req.source {
14		return false
15	}
16	d.notices.routingMu.Lock()
17	defer d.notices.routingMu.Unlock()
18	req.source.core().mu.Lock()
19	defer req.source.core().mu.Unlock()
20	coordinator := req.source.core().coordinator.Load()
21	if coordinator != nil {
22		coordinator.mu.Lock()
23		defer coordinator.mu.Unlock()
24	}
25	if !transitionSourceTokenCurrentLocked(*req.sourceToken, req.source, coordinator, req) {
26		return false
27	}
28	return transitionSourceTabCurrentLocked(req.source, req.expectedSourceTab)
29}
30
31func (d *Daemon) sourceAttachmentTokenCurrentFrozen(token attachmentConnectionToken) bool {
32	if token.sess == nil || token.ac == nil {
33		return false
34	}
35	d.mu.Lock()
36	defer d.mu.Unlock()
37	if d.closing || token.sess.core() == nil || d.sessions[token.sess.core().id] != token.sess {
38		return false
39	}
40	d.notices.routingMu.Lock()
41	defer d.notices.routingMu.Unlock()
42	token.sess.core().mu.Lock()
43	defer token.sess.core().mu.Unlock()
44	coordinator := token.sess.core().coordinator.Load()
45	if coordinator != nil {
46		coordinator.mu.Lock()
47		defer coordinator.mu.Unlock()
48	}
49	req := attachmentTransitionRequest{source: token.sess, next: token.ac, expectedTransport: token.transport}
50	return transitionSourceTokenCurrentLocked(token, token.sess, coordinator, req)
51}
52
53func transitionSourceTabCurrentLocked(source *session, expected *tab) bool {
54	return transitionSourceTabCurrentForRequestLocked(source, expected, false)
55}
56
57func transitionSourceTabCurrentForRequestLocked(source *session, expected *tab, transferExpected bool) bool {
58	if expected == nil {
59		return true
60	}
61	if source == nil || !containsTab(source.tabs, expected) {
62		return false
63	}
64	expected.mu.Lock()
65	defer expected.mu.Unlock()
66	return transferExpected || expected.floating.state != floatingVisible
67}
68
69func containsTab(tabs []*tab, want *tab) bool {
70	return slices.Contains(tabs, want)
71}
72
73// transitionAttachmentLocked is the publication half of transitionAttachment.
74// Caller holds d.mu after the lock-free effect freeze/drain.
75func (d *Daemon) transitionAttachmentLocked(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
76	d.notices.routingMu.Lock()
77	defer d.notices.routingMu.Unlock()
78	return d.transitionAttachmentRoutedLocked(req)
79}
80
81type attachmentPublication struct {
82	req                 attachmentTransitionRequest
83	source              *session
84	targetCoordinator   *renderCoordinator
85	sourceCoordinator   *renderCoordinator
86	releaseCoordinators func()
87	nextGeneration      uint64
88}
89
90func (p *attachmentPublication) unlockCoordinators() {
91	p.releaseCoordinators()
92}
93
94func attachmentLifecycleCurrentLocked(entry *session, fence *attachmentLifecycleFence) bool {
95	if fence == nil {
96		return true
97	}
98	if entry == nil {
99		return false
100	}
101	if fence.checkCreatedAt && (entry.name != fence.name || entry.createdAt != fence.createdAt) {
102		return false
103	}
104	if fence.checkIncarnation && entry.incarnation != fence.incarnation {
105		return false
106	}
107	return !fence.checkTab || fence.tabIndex >= 0 && fence.tabIndex < len(entry.tabs) &&
108		domain.TabStableID(entry.tabs[fence.tabIndex].stableID) == fence.tabID
109}
110
111// validateAttachmentTransitionPrelocked performs every fallible check before
112// the non-failing membership and lease publication. Caller holds d.mu,
113// notices.routingMu, and ordered source/target session locks.
114func (d *Daemon) validateAttachmentTransitionPrelocked(req attachmentTransitionRequest) (*attachmentPublication, error) {
115	source := req.source
116	if source == nil {
117		source = req.target
118	}
119	if source == nil || req.target == nil {
120		return nil, errAttachmentTransition
121	}
122	sourceCore := source.core()
123	targetCore := req.target.core()
124	if sourceCore == nil || targetCore == nil || !req.preflighted || !req.attachmentEffectsFrozen ||
125		req.next == nil || d.closing || d.sessions[sourceCore.id] != source || d.sessions[targetCore.id] != req.target ||
126		req.expectedTransport.transport == nil || !req.next.transportSnapshotCurrent(req.expectedTransport) ||
127		req.preserveAttachment && req.sourceToken == nil {
128		return nil, errAttachmentTransition
129	}
130
131	sourceCoordinator := sourceCore.coordinator.Load()
132	targetCoordinator := d.ensureAttachmentRenderCoordinatorPrelocked(req.target)
133	// transitionSourceTokenCurrentLocked also reads the source lease. Hold the
134	// source coordinator lock before calling it, following session -> coordinator
135	// ordering and preventing a lease rebind from racing validation.
136	releaseCoordinators := lockAttachmentCoordinators(source, sourceCoordinator, req.target, targetCoordinator)
137	if d.afterAttachmentTransitionCoordinatorsLocked != nil {
138		d.afterAttachmentTransitionCoordinatorsLocked()
139	}
140	if req.sourceToken != nil && !transitionSourceTokenCurrentLocked(*req.sourceToken, source, sourceCoordinator, req) {
141		releaseCoordinators()
142		return nil, errAttachmentTransition
143	}
144	if source != req.target && !attachmentRegisteredLocked(source, req.next) {
145		releaseCoordinators()
146		return nil, errAttachmentTransition
147	}
148	if !req.preserveAttachment && source != req.target && attachmentRegisteredLocked(req.target, req.next) {
149		releaseCoordinators()
150		return nil, errAttachmentTransition
151	}
152	if req.preserveAttachment && !attachmentRegisteredLocked(req.target, req.next) {
153		releaseCoordinators()
154		return nil, errAttachmentTransition
155	}
156	if !attachmentLifecycleCurrentLocked(req.target, req.expectedTargetLifecycle) ||
157		req.activateTargetTab && !req.target.validTargetTabLocked(req.targetTabIndex) ||
158		req.sourceToken != nil && !transitionSourceTabCurrentForRequestLocked(source, req.expectedSourceTab, req.transferExpectedSourceTab) {
159		releaseCoordinators()
160		return nil, errAttachmentTransition
161	}
162
163	publication := &attachmentPublication{
164		req: req, source: source,
165		sourceCoordinator: sourceCoordinator, targetCoordinator: targetCoordinator,
166		releaseCoordinators: releaseCoordinators,
167	}
168	return publication, nil
169}
170
171// applyTargetStateLocked commits attachment-local navigation metadata and
172// shared environment state as part of the same publication.
173func (d *Daemon) applyTargetStateLocked(publication *attachmentPublication) bool {
174	req := publication.req
175	if req.activateTargetTab && req.target != nil {
176		req.target.activateAttachmentViewLocked(req.next, req.targetTabIndex)
177	}
178	if req.copySourceEnvironment && publication.source != nil && req.target != nil {
179		req.target.terminal = publication.source.terminal
180		req.target.env = copyEnvironment(publication.source.env)
181	}
182	return req.preserveAttachment
183}
184
185// publishAttachmentOwnershipLocked adds/removes only the exact attachment.
186// Existing target attachments and their independent views/transports remain
187// untouched.
188func publishAttachmentOwnershipLocked(publication *attachmentPublication) {
189	req := publication.req
190	if publication.source != req.target {
191		unregisterAttachmentSessionLocked(publication.source, req.next)
192	}
193	registered := registerAttachmentSessionLocked(req.target, req.next)
194	if !registered && publication.source == req.target && !req.preserveAttachment {
195		// A same-session rebind keeps membership but still represents a fresh
196		// attachment claim with the replacement transport's current size.
197		req.target.claimGeometryOwnerLocked(req.next, req.next.sizeSnapshot())
198	}
199	publication.nextGeneration = req.next.connectionGeneration.Add(1)
200	req.next.setSession(req.target)
201}
202
203func buildAttachmentPostcommitPlanLocked(publication *attachmentPublication) attachmentTransitionResult {
204	req := publication.req
205	result := attachmentTransitionResult{}
206	if publication.source != req.target && publication.sourceCoordinator != nil {
207		result.cleanups = append(result.cleanups, publication.sourceCoordinator.beginDetachLocked(req.next))
208	}
209	var lease *attachmentLease
210	if publication.targetCoordinator != nil && !req.preserveAttachment {
211		if publication.source == req.target {
212			lease = publication.targetCoordinator.rebindAttachmentWithReadinessLocked(req.next, req.ready)
213		} else {
214			lease = publication.targetCoordinator.attachWithReadinessLocked(req.next, req.ready)
215		}
216	}
217	result.published = attachmentConnectionToken{
218		sess: req.target, ac: req.next,
219		generation: publication.nextGeneration,
220		transport:  req.expectedTransport, lease: lease,
221		rebase: publication.source != req.target,
222	}
223	if req.attachmentEffectsFrozen {
224		req.next.publishFrozenAttachmentCapability(result.published)
225	}
226	return result
227}
228
229func (d *Daemon) publishAttachmentTransitionPrelocked(publication *attachmentPublication) attachmentTransitionResult {
230	if publication.req.preserveAttachment {
231		d.applyTargetStateLocked(publication)
232		return attachmentTransitionResult{published: *publication.req.sourceToken}
233	}
234	publishAttachmentOwnershipLocked(publication)
235	d.applyTargetStateLocked(publication)
236	return buildAttachmentPostcommitPlanLocked(publication)
237}
238
239func (d *Daemon) transitionAttachmentRoutedLocked(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
240	source := req.source
241	if source == nil {
242		source = req.target
243	}
244	if source == nil || req.target == nil {
245		return attachmentTransitionResult{}, errAttachmentTransition
246	}
247	unlockSessions := lockAttachmentSessions(source, req.target)
248	defer unlockSessions()
249
250	publication, err := d.validateAttachmentTransitionPrelocked(req)
251	if err != nil {
252		return attachmentTransitionResult{}, err
253	}
254	defer publication.unlockCoordinators()
255	return d.publishAttachmentTransitionPrelocked(publication), nil
256}
257
258// lockAttachmentSessions gives every two-session transition one stable order.
259func lockAttachmentSessions(a, b *session) func() {
260	if a == nil || b == nil {
261		return func() {}
262	}
263	aCore := a.core()
264	bCore := b.core()
265	if aCore == nil || bCore == nil {
266		return func() {}
267	}
268	if aCore == bCore {
269		aCore.mu.Lock()
270		return aCore.mu.Unlock
271	}
272	first, second := aCore, bCore
273	if first.id > second.id {
274		first, second = second, first
275	}
276	first.mu.Lock()
277	second.mu.Lock()
278	return func() {
279		second.mu.Unlock()
280		first.mu.Unlock()
281	}
282}
283
284// lockAttachmentCoordinators follows the same immutable session-ID order as
285// lockAttachmentSessions. Callers already hold ordered session locks.
286func lockAttachmentCoordinators(a *session, aCoordinator *renderCoordinator, b *session, bCoordinator *renderCoordinator) func() {
287	if aCoordinator == nil && bCoordinator == nil {
288		return func() {}
289	}
290	if a == b || aCoordinator == bCoordinator {
291		coordinator := aCoordinator
292		if coordinator == nil {
293			coordinator = bCoordinator
294		}
295		coordinator.mu.Lock()
296		return coordinator.mu.Unlock
297	}
298	first, second := aCoordinator, bCoordinator
299	if a.core().id > b.core().id {
300		first, second = second, first
301	}
302	if first != nil {
303		first.mu.Lock()
304	}
305	if second != nil {
306		second.mu.Lock()
307	}
308	return func() {
309		if second != nil {
310			second.mu.Unlock()
311		}
312		if first != nil {
313			first.mu.Unlock()
314		}
315	}
316}

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