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

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