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}