v0.7.0 4c16dd8

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	// Every attach, resume, and session handoff refreshes the target from the
177	// attachment's own client environment, inside the publication so a failed
178	// transition changes nothing. A daemon-owned (remote) attachment never
179	// changes it; a preserved attachment stays in its session.
180	if !req.preserveAttachment && req.target != nil && req.next != nil {
181		if env, ok := req.next.clientEnvironment(); ok {
182			req.target.adoptClientEnvironmentLocked(env)
183		}
184	}
185	return req.preserveAttachment
186}
187
188// publishAttachmentOwnershipLocked adds/removes only the exact attachment.
189// Existing target attachments and their independent views/transports remain
190// untouched.
191func publishAttachmentOwnershipLocked(publication *attachmentPublication) {
192	req := publication.req
193	if publication.source != req.target {
194		req.next.clearSamePeerOffer()
195		unregisterAttachmentSessionLocked(publication.source, req.next)
196	}
197	registered := registerAttachmentSessionLocked(req.target, req.next)
198	if !registered && publication.source == req.target && !req.preserveAttachment {
199		// A same-session rebind keeps membership but still represents a fresh
200		// attachment claim with the replacement transport's current size.
201		req.target.geometry.claimLocked(req.target.core(), req.next, req.next.geometrySnapshot())
202	}
203	req.next.setSession(req.target)
204}
205
206func buildAttachmentPostcommitPlanLocked(publication *attachmentPublication) attachmentTransitionResult {
207	req := publication.req
208	result := attachmentTransitionResult{}
209	if publication.source != req.target && publication.sourceCoordinator != nil {
210		result.cleanups = append(result.cleanups, publication.sourceCoordinator.beginDetachLocked(req.next))
211	}
212	var lease *attachmentLease
213	if publication.targetCoordinator != nil && !req.preserveAttachment {
214		if publication.source == req.target {
215			lease = publication.targetCoordinator.rebindAttachmentWithReadinessLocked(req.next, req.ready)
216		} else {
217			lease = publication.targetCoordinator.attachWithReadinessLocked(req.next, req.ready)
218		}
219	}
220	result.published = attachmentCapability{
221		sess: req.target, ac: req.next,
222		transport: req.expectedTransport, lease: lease,
223		rebase: publication.source != req.target,
224	}
225	if req.attachmentEffectsFrozen {
226		result.published = req.next.publishFrozenAttachmentCapability(result.published)
227	}
228	return result
229}
230
231func (d *Daemon) publishAttachmentTransitionPrelocked(publication *attachmentPublication) attachmentTransitionResult {
232	if publication.req.preserveAttachment {
233		d.applyTargetStateLocked(publication)
234		return attachmentTransitionResult{published: *publication.req.sourceCapability}
235	}
236	publishAttachmentOwnershipLocked(publication)
237	d.applyTargetStateLocked(publication)
238	return buildAttachmentPostcommitPlanLocked(publication)
239}
240
241func (d *Daemon) transitionAttachmentRoutedLocked(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
242	source := req.source
243	if source == nil {
244		source = req.target
245	}
246	if source == nil || req.target == nil {
247		return attachmentTransitionResult{}, errAttachmentTransition
248	}
249	unlockSessions := lockAttachmentSessions(source, req.target)
250	defer unlockSessions()
251
252	publication, err := d.validateAttachmentTransitionPrelocked(req)
253	if err != nil {
254		return attachmentTransitionResult{}, err
255	}
256	defer publication.unlockCoordinators()
257	return d.publishAttachmentTransitionPrelocked(publication), nil
258}
259
260// lockAttachmentSessions gives every two-session transition one stable order.
261func lockAttachmentSessions(a, b *session) func() {
262	if a == nil || b == nil {
263		return func() {}
264	}
265	aCore := a.core()
266	bCore := b.core()
267	if aCore == nil || bCore == nil {
268		return func() {}
269	}
270	if aCore == bCore {
271		aCore.mu.Lock()
272		return aCore.mu.Unlock
273	}
274	first, second := aCore, bCore
275	if first.id > second.id {
276		first, second = second, first
277	}
278	first.mu.Lock()
279	second.mu.Lock()
280	return func() {
281		second.mu.Unlock()
282		first.mu.Unlock()
283	}
284}
285
286// lockAttachmentCoordinators follows the same immutable session-ID order as
287// lockAttachmentSessions. Callers already hold ordered session locks.
288func lockAttachmentCoordinators(a *session, aCoordinator *renderCoordinator, b *session, bCoordinator *renderCoordinator) func() {
289	if aCoordinator == nil && bCoordinator == nil {
290		return func() {}
291	}
292	if a == b || aCoordinator == bCoordinator {
293		coordinator := aCoordinator
294		if coordinator == nil {
295			coordinator = bCoordinator
296		}
297		coordinator.mu.Lock()
298		return coordinator.mu.Unlock
299	}
300	first, second := aCoordinator, bCoordinator
301	if a.core().id > b.core().id {
302		first, second = second, first
303	}
304	if first != nil {
305		first.mu.Lock()
306	}
307	if second != nil {
308		second.mu.Lock()
309	}
310	return func() {
311		if second != nil {
312			second.mu.Unlock()
313		}
314		if first != nil {
315			first.mu.Unlock()
316		}
317	}
318}

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