1package daemon
2
3import (
4 "cmp"
5 "slices"
6)
7
8// moveResizeFences excludes concurrent layout publishers while a move
9// revalidates and publishes ownership. It is not an architecture lock. Fences
10// are always acquired session -> tab -> pane by immutable stable identity and
11// released in exact reverse order.
12type moveResizeFences struct {
13 sessions []*session
14 tabs []*tab
15 panes []*pane
16 held bool
17}
18
19// newMovePaneResizeFences covers both sessions, both affected layouts, and
20// only the pane whose owner will change. Stable-identity deduplication also
21// handles moves between tabs in the same session.
22func newMovePaneResizeFences(source, destination *session, sourceTab, destinationTab *tab, moved *pane) *moveResizeFences {
23 return newMoveResizeFences(
24 []*session{source, destination},
25 []*tab{sourceTab, destinationTab},
26 []*pane{moved},
27 )
28}
29
30// newMoveTabResizeFences snapshots every process-bearing pane transferred with
31// a tab. The acquire callback must revalidate this exact tiled membership and
32// installed floating slot after all fences are held, because this snapshot is
33// intentionally taken before waiting on any resize transaction.
34func newMoveTabResizeFences(source, destination *session, moved *tab) *moveResizeFences {
35 if moved == nil {
36 return newMoveResizeFences([]*session{source, destination}, nil, nil)
37 }
38 moved.mu.Lock()
39 panes := make([]*pane, 0, len(moved.panes)+1)
40 for _, p := range moved.panes {
41 panes = append(panes, p)
42 }
43 if moved.floating.pane != nil && (moved.floating.state == floatingHidden || moved.floating.state == floatingVisible) {
44 panes = append(panes, moved.floating.pane)
45 }
46 moved.mu.Unlock()
47 return newMoveResizeFences([]*session{source, destination}, []*tab{moved}, panes)
48}
49
50func newMoveResizeFences(sessions []*session, tabs []*tab, panes []*pane) *moveResizeFences {
51 byID := make(map[string]*session, len(sessions))
52 for _, sess := range sessions {
53 if sess == nil {
54 continue
55 }
56 if _, exists := byID[string(sess.id)]; !exists {
57 byID[string(sess.id)] = sess
58 }
59 }
60 ordered := make([]*session, 0, len(byID))
61 for _, sess := range byID {
62 ordered = append(ordered, sess)
63 }
64 slices.SortFunc(ordered, func(left, right *session) int {
65 return cmp.Compare(left.id, right.id)
66 })
67
68 tabsByID := make(map[string]*tab, len(tabs))
69 for _, tb := range tabs {
70 if tb == nil {
71 continue
72 }
73 if _, exists := tabsByID[tb.stableID]; !exists {
74 tabsByID[tb.stableID] = tb
75 }
76 }
77 orderedTabs := make([]*tab, 0, len(tabsByID))
78 for _, tb := range tabsByID {
79 orderedTabs = append(orderedTabs, tb)
80 }
81 slices.SortFunc(orderedTabs, func(left, right *tab) int {
82 return cmp.Compare(left.stableID, right.stableID)
83 })
84
85 panesByID := make(map[string]*pane, len(panes))
86 for _, p := range panes {
87 if p == nil {
88 continue
89 }
90 if _, exists := panesByID[p.stableID]; !exists {
91 panesByID[p.stableID] = p
92 }
93 }
94 orderedPanes := make([]*pane, 0, len(panesByID))
95 for _, p := range panesByID {
96 orderedPanes = append(orderedPanes, p)
97 }
98 slices.SortFunc(orderedPanes, func(left, right *pane) int {
99 return cmp.Compare(left.stableID, right.stableID)
100 })
101 return &moveResizeFences{sessions: ordered, tabs: orderedTabs, panes: orderedPanes}
102}
103
104// acquire waits for every resize fence before invoking revalidateAndPublish.
105// Callers must enter with no daemon, session, tab, or pane architecture lock
106// held. The callback is the only architecture-lock window: under the canonical
107// architecture lock order it must revalidate the sessions, tabs, tiled pane
108// memberships, and installed floating slot captured before this wait; publish
109// every changed pane owner; and bump each affected tab's layoutGeneration. A
110// false result (or panic) releases every acquired fence in reverse order. A true
111// result retains all fences so no resize can observe half-published ownership;
112// the caller must Release after architecture locks are dropped and before PTY
113// I/O, applyTabLayout, or applySessionLayout.
114func (f *moveResizeFences) acquire(revalidateAndPublish func() bool) (accepted bool) {
115 if f == nil || f.held {
116 return false
117 }
118 for _, sess := range f.sessions {
119 sess.layoutApplyMu.Lock()
120 }
121 for _, tb := range f.tabs {
122 tb.layoutApplyMu.Lock()
123 }
124 for _, p := range f.panes {
125 p.resizeMu.Lock()
126 }
127 f.held = true
128 defer func() {
129 if !accepted {
130 f.Release()
131 }
132 }()
133 if revalidateAndPublish == nil {
134 return false
135 }
136 accepted = revalidateAndPublish()
137 return accepted
138}
139
140func (d *Daemon) observeBeforeResizeOwnerPostEffect(effect resizeOwnerPostEffect) {
141 if d != nil && d.beforeResizeOwnerPostEffect != nil {
142 d.beforeResizeOwnerPostEffect(effect)
143 }
144}
145
146// acquireResizeOwnerPostEffectFences acquires and validates the complete
147// canonical session/tab/pane fence set. It performs no callbacks or external
148// operations and returns the fences held on success.
149func acquireResizeOwnerPostEffectFences(members []resizeMember) *moveResizeFences {
150 if len(members) == 0 {
151 return nil
152 }
153 sessions := make([]*session, 0, len(members))
154 tabs := make([]*tab, 0, len(members))
155 panes := make([]*pane, 0, len(members))
156 for i := range members {
157 member := &members[i]
158 sessions = append(sessions, member.session)
159 tabs = append(tabs, member.tab)
160 panes = append(panes, member.pane)
161 }
162 fences := newMoveResizeFences(sessions, tabs, panes)
163 if !fences.acquire(func() bool { return resizeMembersOwnerCurrent(members) }) {
164 return nil
165 }
166 return fences
167}
168
169// publishResizeOwnerPostEffect linearizes an in-memory resize effect with pane
170// ownership publication. The deterministic seam runs before fence acquisition,
171// then the complete canonical session/tab/pane fence set is acquired and every
172// immutable owner lease is revalidated. The callback must publish in-memory
173// state only; timer, PTY, transport, repository, and renderer work belongs
174// after this method returns.
175func (d *Daemon) publishResizeOwnerPostEffect(members []resizeMember, effect resizeOwnerPostEffect, publish func()) bool {
176 if publish == nil {
177 return false
178 }
179 d.observeBeforeResizeOwnerPostEffect(effect)
180 if len(members) == 0 {
181 publish()
182 return true
183 }
184 fences := acquireResizeOwnerPostEffectFences(members)
185 if fences == nil {
186 return false
187 }
188 defer fences.Release()
189 publish()
190 return true
191}
192
193func (f *moveResizeFences) Release() {
194 if f == nil || !f.held {
195 return
196 }
197 for i := len(f.panes) - 1; i >= 0; i-- {
198 f.panes[i].resizeMu.Unlock()
199 }
200 for i := len(f.tabs) - 1; i >= 0; i-- {
201 f.tabs[i].layoutApplyMu.Unlock()
202 }
203 for i := len(f.sessions) - 1; i >= 0; i-- {
204 f.sessions[i].layoutApplyMu.Unlock()
205 }
206 f.held = false
207}