1package daemon
2
3import (
4 "context"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/usecase/layout"
8)
9
10type moveTabAdmission struct {
11 tab *tab
12 sourceIndex int
13 sourceAttachments []*attachedClient
14 sourceTransports map[*attachedClient]transportSnapshot
15 layoutGeneration uint64
16 floatingState floatingState
17 floatingPane *pane
18 floatingGeneration uint64
19 panes []*pane
20 destinationActive *tab
21 destinationSize domain.Size
22 finalSource bool
23}
24
25type moveTabCommit struct {
26 req moveTabRequest
27 source *session
28 destination *session
29 admission moveTabAdmission
30 sourceEffectsFrozen bool
31 frozenEffects frozenAttachmentEffectGates
32 syncCleanup syncTimerCleanup
33 oldTabCancel context.CancelFunc
34 sourceEmpty bool
35 retiredParked []parkedAttachmentRetirement
36 retiredAttachments []detachedAttachmentSnapshot
37 sourceMetadata domain.CatalogueMetadataUpdate
38 sourceMetadataValid bool
39 destMetadata domain.CatalogueMetadataUpdate
40 destMetadataValid bool
41 err error
42}
43
44func (d *Daemon) moveTab(req moveTabRequest) (result error) {
45 defer func() { result = normalizeMoveRejection(result) }()
46 if d == nil || req.Source.ID == "" || req.Destination.ID == "" || req.SourceTabID == "" || req.Source.ID == req.Destination.ID {
47 return errMovePaneInvalid
48 }
49 d.mu.Lock()
50 source := moveSessionForLocatorLocked(d, req.Source)
51 destination := moveSessionForLocatorLocked(d, req.Destination)
52 d.mu.Unlock()
53 if source == nil || destination == nil {
54 return errMoveStaleTarget
55 }
56 if source == destination {
57 return errMovePaneInvalid
58 }
59 // Dispatch admission must precede lifecycle reservation. A final close holds
60 // dispatchMu while it waits for teardown ownership; reserving first would let
61 // that close wait on a move which is itself blocked on dispatchMu.
62 if d.beforeMoveDispatch != nil {
63 d.beforeMoveDispatch()
64 }
65 unlockDispatch := lockMoveDispatch(source, destination)
66 dispatchHeld := true
67 defer func() {
68 if dispatchHeld {
69 unlockDispatch()
70 }
71 }()
72
73 reservation, err := d.reserveMoveLifecycles(source, destination)
74 if err != nil {
75 return errMoveStaleTarget
76 }
77 reservationHeld := true
78 defer func() {
79 if reservationHeld {
80 reservation.Release()
81 }
82 }()
83 if d.afterMoveLifecycleReserved != nil {
84 d.afterMoveLifecycleReserved()
85 }
86
87 admission, err := d.snapshotMoveTabAdmission(req, source, destination)
88 if err != nil {
89 return err
90 }
91 if d.afterMoveTabSourceSnapshot != nil {
92 d.afterMoveTabSourceSnapshot()
93 }
94 commit := moveTabCommit{req: req, source: source, destination: destination, admission: *admission}
95 var frozen frozenAttachmentEffectGates
96 if admission.finalSource && len(admission.sourceAttachments) != 0 {
97 interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments))
98 for _, ac := range admission.sourceAttachments {
99 if transport := admission.sourceTransports[ac]; transport.transport != nil {
100 interrupts = append(interrupts, attachmentTransportInterrupt{ac: ac, transport: transport})
101 }
102 }
103 frozen = freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{interrupts: interrupts, nonblocking: true, afterFrozen: func(ac *attachedClient) {
104 if d.afterAttachmentEffectGateFrozen != nil {
105 d.afterAttachmentEffectGateFrozen("move-tab", ac)
106 }
107 }}, admission.sourceAttachments...)
108 if !frozen.acquired || !frozen.drained {
109 frozen.unfreeze()
110 return errMovePaneInvalid
111 }
112 commit.sourceEffectsFrozen = true
113 }
114 commit.frozenEffects = frozen
115 effectsFrozen := commit.sourceEffectsFrozen
116 defer func() {
117 if effectsFrozen {
118 frozen.unfreeze()
119 }
120 }()
121
122 fences := newMoveTabResizeFences(source, destination, admission.tab)
123 if !fences.acquire(func() bool { return commit.publishLocked(d, fences.panes) }) {
124 if commit.err != nil {
125 return commit.err
126 }
127 return errMoveStaleTarget
128 }
129 fences.Release()
130 postcommit := movePanePostcommitPlan{
131 source: source,
132 destination: destination,
133 sourceTab: admission.tab,
134 destinationTab: admission.tab,
135 movedPane: firstMovePane(admission.panes),
136 movedPanes: admission.panes,
137 sourceAttachments: admission.sourceAttachments,
138 syncCleanup: commit.syncCleanup,
139 frozenEffects: frozen,
140 effectsFrozen: effectsFrozen,
141 unlockDispatch: unlockDispatch,
142 reservation: reservation,
143 oldTabCancel: commit.oldTabCancel,
144 sourceTabRemoved: true,
145 sourceEmpty: commit.sourceEmpty,
146 retiredParked: commit.retiredParked,
147 retiredAttachments: commit.retiredAttachments,
148 sourceMetadata: commit.sourceMetadata,
149 sourceMetadataValid: commit.sourceMetadataValid,
150 destinationMetadata: commit.destMetadata,
151 destinationMetadataValid: commit.destMetadataValid,
152 }
153 effectsFrozen = false
154 dispatchHeld = false
155 reservationHeld = false
156 postcommit.execute(d)
157 return nil
158}
159
160func (d *Daemon) snapshotMoveTabAdmission(req moveTabRequest, source, destination *session) (*moveTabAdmission, error) {
161 d.mu.Lock()
162 defer d.mu.Unlock()
163 if d.closing || d.sessions[source.id] != source || d.sessions[destination.id] != destination {
164 return nil, errMoveStaleTarget
165 }
166 unlockSessions := lockAttachmentSessions(source, destination)
167 defer unlockSessions()
168 if !moveSessionLocatorCurrentLocked(source, req.Source) || !moveSessionLocatorCurrentLocked(destination, req.Destination) {
169 return nil, errMoveStaleTarget
170 }
171 if req.Attachment != nil && !attachmentRegisteredLocked(source, req.Attachment) {
172 return nil, errMoveStaleTarget
173 }
174 if req.AttachmentToken.ac != nil && (req.AttachmentToken.ac != req.Attachment || !moveAttachmentTokenCurrentLocked(req.AttachmentToken, source)) {
175 return nil, errMoveStaleTarget
176 }
177 moved := findMoveTabLocked(source, req.SourceTabID)
178 if moved == nil || len(destination.tabs) == 0 {
179 return nil, errMoveStaleTarget
180 }
181 destinationActive := destination.tabs[0]
182 destinationActive.mu.Lock()
183 destinationSize := destinationActive.size
184 destinationActive.mu.Unlock()
185 moved.mu.Lock()
186 defer moved.mu.Unlock()
187 if !moved.floatingTransferableLocked() {
188 return nil, errMoveFloatingWarming
189 }
190 if moved.tree == nil || moved.tree.Root == nil {
191 return nil, errMovePaneInvalid
192 }
193 if _, ok := layout.Solve(moved.tree.Root, domain.Rect{Width: destinationSize.Cols, Height: destinationSize.Rows}); !ok {
194 return nil, errMoveTooSmall
195 }
196 panes := make([]*pane, 0, len(moved.panes)+1)
197 for _, p := range moved.panes {
198 panes = append(panes, p)
199 }
200 if moved.floating.pane != nil && (moved.floating.state == floatingHidden || moved.floating.state == floatingVisible) {
201 panes = append(panes, moved.floating.pane)
202 }
203 idx := indexMoveTabLocked(source, moved)
204 attachments := source.snapshotAttachmentsLocked()
205 transports := make(map[*attachedClient]transportSnapshot, len(attachments))
206 for _, ac := range attachments {
207 transports[ac] = ac.transportSnapshot()
208 }
209 return &moveTabAdmission{
210 tab: moved, sourceIndex: idx, sourceAttachments: attachments, sourceTransports: transports,
211 layoutGeneration: moved.layoutGeneration, floatingState: moved.floating.state,
212 floatingPane: moved.floating.pane, floatingGeneration: moved.floating.generation,
213 panes: panes, destinationActive: destinationActive, destinationSize: destinationSize,
214 finalSource: len(source.tabs) == 1,
215 }, nil
216}
217
218func (c *moveTabCommit) publishLocked(d *Daemon, fencedPanes []*pane) bool {
219 d.mu.Lock()
220 if d.closing || d.sessions[c.source.id] != c.source || d.sessions[c.destination.id] != c.destination {
221 d.mu.Unlock()
222 return false
223 }
224 d.notices.routingMu.Lock()
225 unlockSessions := lockAttachmentSessions(c.source, c.destination)
226 defer unlockSessions()
227 defer d.notices.routingMu.Unlock()
228 defer d.mu.Unlock()
229 if !moveSessionLocatorCurrentLocked(c.source, c.req.Source) || !moveSessionLocatorCurrentLocked(c.destination, c.req.Destination) {
230 return false
231 }
232 if c.req.Attachment != nil && !attachmentRegisteredLocked(c.source, c.req.Attachment) {
233 return false
234 }
235 if c.req.AttachmentToken.ac != nil && (c.req.AttachmentToken.ac != c.req.Attachment || !moveAttachmentTokenCurrentLocked(c.req.AttachmentToken, c.source)) {
236 return false
237 }
238 moved := c.admission.tab
239 if indexMoveTabLocked(c.source, moved) != c.admission.sourceIndex || findMoveTabLocked(c.destination, c.req.SourceTabID) != nil ||
240 len(c.source.tabs) == 1 != c.admission.finalSource {
241 return false
242 }
243 if c.admission.finalSource && !sameMoveAttachmentsLocked(c.source, c.admission.sourceAttachments) {
244 return false
245 }
246 if len(c.destination.tabs) == 0 || c.destination.tabs[0] != c.admission.destinationActive {
247 return false
248 }
249 c.admission.destinationActive.mu.Lock()
250 destinationSizeCurrent := c.admission.destinationActive.size == c.admission.destinationSize
251 c.admission.destinationActive.mu.Unlock()
252 if !destinationSizeCurrent {
253 return false
254 }
255 moved.mu.Lock()
256 defer moved.mu.Unlock()
257 if moved.layoutGeneration != c.admission.layoutGeneration || !moved.floatingTransferableLocked() ||
258 moved.floating.state != c.admission.floatingState || moved.floating.pane != c.admission.floatingPane ||
259 moved.floating.generation != c.admission.floatingGeneration || !sameMoveTabPanesLocked(moved, c.admission.panes, fencedPanes) {
260 return false
261 }
262 for _, p := range fencedPanes {
263 p.mu.Lock()
264 defer p.mu.Unlock()
265 owner := p.ownerSnapshot()
266 if owner == nil || owner.session != c.source || owner.tab != moved {
267 return false
268 }
269 }
270
271 idx := c.admission.sourceIndex
272 if len(c.source.tabs) > 1 {
273 c.source.prepareAttachmentViewsForRemovedTabLocked(moved, idx)
274 }
275 c.source.tabs = append(c.source.tabs[:idx], c.source.tabs[idx+1:]...)
276 c.destination.tabs = append(c.destination.tabs, moved)
277 moved.size = c.admission.destinationSize
278 moved.bumpLayoutGenerationLocked()
279 c.oldTabCancel = moved.cancel
280 parent := c.destination.ctx
281 if parent == nil {
282 parent = d.serveCtx
283 }
284 moved.ctx, moved.cancel = context.WithCancel(parent)
285 type ownerChange struct {
286 pane *pane
287 oldOwner paneEffectLease
288 newOwner paneEffectLease
289 }
290 ownerChanges := make([]ownerChange, 0, len(fencedPanes))
291 for _, p := range fencedPanes {
292 floatingGeneration := uint64(0)
293 if p == moved.floating.pane {
294 floatingGeneration = moved.floating.generation
295 }
296 oldOwner, newOwner := p.publishOwnerLocked(c.destination, moved, floatingGeneration)
297 ownerChanges = append(ownerChanges, ownerChange{pane: p, oldOwner: oldOwner, newOwner: newOwner})
298 }
299 if d.beforeMoveTabCommit != nil {
300 d.beforeMoveTabCommit()
301 }
302 for _, change := range ownerChanges {
303 c.syncCleanup.append(d.migratePaneSyncOwnerLocked(change.pane, change.oldOwner, change.newOwner))
304 }
305 if len(c.source.tabs) == 0 {
306 d.unregisterSessionLocked(c.source)
307 c.retiredAttachments = detachMoveAttachmentsLocked(c.source, c.admission.sourceTransports)
308 c.source.tabs = nil
309 d.purgeParkingForSessionLocked(c.source)
310 c.retiredParked = d.purgeParkedForSessionLocked(c.source)
311 }
312 c.sourceEmpty = len(c.source.tabs) == 0
313 if !c.source.ephemeral {
314 c.sourceMetadata = c.source.persistRecordLocked(max(d.nowUnixNano(), c.source.createdAt, int64(1))).MetadataUpdate()
315 c.sourceMetadataValid = true
316 }
317 if !c.destination.ephemeral {
318 c.destMetadata = c.destination.persistRecordLocked(max(d.nowUnixNano(), c.destination.createdAt, int64(1))).MetadataUpdate()
319 c.destMetadataValid = true
320 }
321 return true
322}
323
324func sameMoveTabPanesLocked(tb *tab, admitted, fenced []*pane) bool {
325 current := make(map[*pane]struct{}, len(tb.panes)+1)
326 for _, p := range tb.panes {
327 current[p] = struct{}{}
328 }
329 if tb.floating.pane != nil && (tb.floating.state == floatingHidden || tb.floating.state == floatingVisible) {
330 current[tb.floating.pane] = struct{}{}
331 }
332 if len(current) != len(admitted) || len(current) != len(fenced) {
333 return false
334 }
335 for _, p := range admitted {
336 if _, ok := current[p]; !ok {
337 return false
338 }
339 }
340 for _, p := range fenced {
341 if _, ok := current[p]; !ok {
342 return false
343 }
344 }
345 return true
346}