1package daemon
2
3import (
4 "reflect"
5
6 "github.com/bnema/vev/internal/domain"
7)
8
9// movePaneRequest identifies both lifecycles and both topology objects by
10// immutable identity. Session names are advisory; the ID and incarnation are
11// the commit-time authority.
12type movePaneRequest struct {
13 Attachment *attachedClient
14 AttachmentToken attachmentConnectionToken
15 Source moveSessionLocator
16 SourceTabID domain.TabStableID
17 SourcePaneID domain.PaneStableID
18 Destination moveSessionLocator
19 DestinationTabID domain.TabStableID
20}
21
22type moveTabRequest struct {
23 Attachment *attachedClient
24 AttachmentToken attachmentConnectionToken
25 Source moveSessionLocator
26 SourceTabID domain.TabStableID
27 Destination moveSessionLocator
28}
29
30// movePane resolves, reserves, validates, and commits one pane relocation. All
31// fallible work happens before the membership publication section. Once the
32// section starts, the caller holds the ordered architecture locks and only
33// non-failing in-memory writes are permitted.
34func (d *Daemon) movePane(req movePaneRequest) (result error) {
35 defer func() { result = normalizeMoveRejection(result) }()
36 if d == nil || req.Source.ID == "" || req.Destination.ID == "" ||
37 req.SourceTabID == "" || req.SourcePaneID == "" || req.DestinationTabID == "" {
38 return errMovePaneInvalid
39 }
40
41 d.mu.Lock()
42 source := moveSessionForLocatorLocked(d, req.Source)
43 destination := moveSessionForLocatorLocked(d, req.Destination)
44 d.mu.Unlock()
45 if source == nil || destination == nil {
46 return errMoveStaleTarget
47 }
48
49 // Dispatch admission must precede lifecycle reservation. A final close holds
50 // dispatchMu while it waits for teardown ownership; reserving first would let
51 // that close wait on a move which is itself blocked on dispatchMu.
52 if d.beforeMoveDispatch != nil {
53 d.beforeMoveDispatch()
54 }
55 unlockDispatch := lockMoveDispatch(source, destination)
56 dispatchHeld := true
57 defer func() {
58 if dispatchHeld {
59 unlockDispatch()
60 }
61 }()
62
63 reservation, err := d.reserveMoveLifecycles(source, destination)
64 if err != nil {
65 return errMoveStaleTarget
66 }
67 reservationHeld := true
68 defer func() {
69 if reservationHeld {
70 reservation.Release()
71 }
72 }()
73 if d.afterMoveLifecycleReserved != nil {
74 d.afterMoveLifecycleReserved()
75 }
76
77 // Snapshot the exact live objects and generations before waiting on resize
78 // fences. The locked commit repeats every authority check before publication.
79 admission, err := d.snapshotMovePaneAdmission(req, source, destination)
80 if err != nil {
81 return err
82 }
83 sourceTab := admission.sourceTab
84 destinationTab := admission.destinationTab
85 movedPane := admission.movedPane
86 sourceTabInitialGeneration := admission.sourceGeneration
87 destinationTabInitialGeneration := admission.destinationGeneration
88 finalSourceTab := admission.finalSourceTab
89 if d.afterMovePaneSourceSnapshot != nil {
90 d.afterMovePaneSourceSnapshot()
91 }
92
93 // A final source tab is retired without transferring any attachment to the
94 // destination. Freeze every affected source connection before publication so
95 // teardown invalidates exact generations rather than racing input or render.
96 var frozen frozenAttachmentEffectGates
97 var sourceEffectsFrozen bool
98 if finalSourceTab && source != destination && len(admission.sourceAttachments) != 0 {
99 interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments))
100 for _, ac := range admission.sourceAttachments {
101 if transport := admission.sourceTransports[ac]; transport.transport != nil {
102 interrupts = append(interrupts, attachmentTransportInterrupt{ac: ac, transport: transport})
103 }
104 }
105 frozen = freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{interrupts: interrupts, nonblocking: true, afterFrozen: func(ac *attachedClient) {
106 if d.afterAttachmentEffectGateFrozen != nil {
107 d.afterAttachmentEffectGateFrozen("move-pane", ac)
108 }
109 }}, admission.sourceAttachments...)
110 if !frozen.acquired || !frozen.drained {
111 frozen.unfreeze()
112 return errMovePaneInvalid
113 }
114 sourceEffectsFrozen = true
115 }
116 if sourceEffectsFrozen {
117 defer func() {
118 if sourceEffectsFrozen {
119 frozen.unfreeze()
120 }
121 }()
122 }
123
124 commit := movePaneCommit{
125 req: req,
126 source: source,
127 destination: destination,
128 sourceTab: sourceTab,
129 destinationTab: destinationTab,
130 movedPane: movedPane,
131 sourceAttachments: admission.sourceAttachments,
132 sourceTransports: admission.sourceTransports,
133 sourceGeneration: sourceTabInitialGeneration,
134 destinationGeneration: destinationTabInitialGeneration,
135 frozenEffects: frozen,
136 }
137 fences := newMovePaneResizeFences(source, destination, sourceTab, destinationTab, movedPane)
138 if !fences.acquire(func() bool { return commit.publishLocked(d) }) {
139 if commit.err != nil {
140 return commit.err
141 }
142 return errMoveStaleTarget
143 }
144 // Owner/layout fences end at the atomic publication boundary. The immutable
145 // postcommit plan owns every later cleanup and effect.
146 fences.Release()
147 postcommit := movePanePostcommitPlan{
148 source: source,
149 destination: destination,
150 sourceTab: sourceTab,
151 destinationTab: destinationTab,
152 movedPane: movedPane,
153 sourceAttachments: admission.sourceAttachments,
154 syncCleanup: commit.syncCleanup,
155 frozenEffects: frozen,
156 effectsFrozen: sourceEffectsFrozen,
157 unlockDispatch: unlockDispatch,
158 reservation: reservation,
159 sourceTabRemoved: commit.sourceTabRemoved,
160 sourceEmpty: commit.sourceEmpty,
161 retiredParked: commit.retiredParked,
162 retiredAttachments: commit.retiredAttachments,
163 sourceMetadata: commit.sourceMetadata,
164 sourceMetadataValid: commit.sourceMetadataValid,
165 destinationMetadata: commit.destinationMetadata,
166 destinationMetadataValid: commit.destinationMetadataValid,
167 }
168 // Transfer cleanup ownership away from the admission defers only after the
169 // complete postcommit plan exists.
170 sourceEffectsFrozen = false
171 dispatchHeld = false
172 reservationHeld = false
173 postcommit.execute(d)
174 return nil
175}
176
177// moveSessionForLocatorLocked resolves only live sessions. The name is
178// intentionally ignored after the stable ID/incarnation pair is checked.
179func moveSessionForLocatorLocked(d *Daemon, locator moveSessionLocator) *session {
180 if d == nil {
181 return nil
182 }
183 sess := d.sessions[locator.ID]
184 if sess == nil || sess.incarnation != locator.Incarnation {
185 return nil
186 }
187 return sess
188}
189
190func moveSessionLocatorCurrentLocked(sess *session, locator moveSessionLocator) bool {
191 return sess != nil && sess.id == locator.ID && sess.incarnation == locator.Incarnation
192}
193
194func findMoveTabLocked(sess *session, stableID domain.TabStableID) *tab {
195 if sess == nil {
196 return nil
197 }
198 for _, tb := range sess.tabs {
199 if tb != nil && tb.stableID == string(stableID) {
200 return tb
201 }
202 }
203 return nil
204}
205
206func indexMoveTabLocked(sess *session, target *tab) int {
207 for i, tb := range sess.tabs {
208 if tb == target {
209 return i
210 }
211 }
212 return -1
213}
214
215func moveTabMemberLocked(sess *session, target *tab) bool {
216 return indexMoveTabLocked(sess, target) >= 0
217}
218
219func lockMoveTabs(a, b *tab) func() {
220 if a == nil && b == nil {
221 return func() {}
222 }
223 if a == nil {
224 b.mu.Lock()
225 return b.mu.Unlock
226 }
227 if b == nil {
228 a.mu.Lock()
229 return a.mu.Unlock
230 }
231 if a == b {
232 a.mu.Lock()
233 return a.mu.Unlock
234 }
235 first, second := a, b
236 if first.stableID > second.stableID ||
237 (first.stableID == second.stableID && reflect.ValueOf(first).Pointer() > reflect.ValueOf(second).Pointer()) {
238 first, second = second, first
239 }
240 first.mu.Lock()
241 second.mu.Lock()
242 return func() {
243 second.mu.Unlock()
244 first.mu.Unlock()
245 }
246}
247
248func lockMoveDispatch(a, b *session) func() {
249 if a == b {
250 a.dispatchMu.Lock()
251 return a.dispatchMu.Unlock
252 }
253 first, second := a, b
254 if first.id > second.id {
255 first, second = second, first
256 }
257 first.dispatchMu.Lock()
258 second.dispatchMu.Lock()
259 return func() {
260 second.dispatchMu.Unlock()
261 first.dispatchMu.Unlock()
262 }
263}