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}