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}