1package daemon
2
3import (
4 "context"
5 "sync"
6 "sync/atomic"
7
8 vt "github.com/bnema/vev-vt"
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/domain/layout"
11 "github.com/bnema/vev/internal/ports"
12)
13
14// paneProcessLifetime owns the context passed to PTYFactory.Open. Its daemon
15// parent lets an installed pane move between sessions without changing process
16// lifetime. Temporary opening parents bound an unpublished Open; publish
17// atomically detaches those parents before transferring cancellation to pane.
18type paneProcessLifetime struct {
19 ctx context.Context
20 cancel context.CancelFunc
21
22 mu sync.Mutex
23 opening bool
24 openingParents []context.Context
25 stopOpening []func() bool
26}
27
28func (d *Daemon) newPaneProcessLifetime(openingParents ...context.Context) *paneProcessLifetime {
29 parent := d.paneProcessCtx
30 if parent == nil {
31 parent = context.Background()
32 }
33 ctx, rawCancel := context.WithCancel(parent)
34 var cancelOnce sync.Once
35 lifetime := &paneProcessLifetime{
36 ctx: ctx,
37 cancel: func() { cancelOnce.Do(rawCancel) },
38 opening: true,
39 openingParents: append([]context.Context(nil), openingParents...),
40 }
41 for _, openingParent := range lifetime.openingParents {
42 if openingParent == nil {
43 continue
44 }
45 lifetime.stopOpening = append(lifetime.stopOpening, context.AfterFunc(openingParent, func() {
46 lifetime.mu.Lock()
47 if lifetime.opening {
48 lifetime.cancel()
49 }
50 lifetime.mu.Unlock()
51 }))
52 }
53 return lifetime
54}
55
56// publish transfers a successfully opened process to p. A cancellation that
57// wins before this call prevents publication; one that starts afterward no
58// longer belongs to the source session or tab.
59func (l *paneProcessLifetime) publish(p *pane) bool {
60 if l == nil || p == nil {
61 return false
62 }
63 l.mu.Lock()
64 defer l.mu.Unlock()
65 for _, parent := range l.openingParents {
66 if parent != nil && parent.Err() != nil {
67 l.opening = false
68 l.stopOpeningParentsLocked()
69 l.cancel()
70 return false
71 }
72 }
73 if l.ctx.Err() != nil {
74 l.opening = false
75 l.stopOpeningParentsLocked()
76 l.cancel()
77 return false
78 }
79 l.opening = false
80 l.stopOpeningParentsLocked()
81 p.ctx = l.ctx
82 p.cancel = l.cancel
83 return true
84}
85
86func (l *paneProcessLifetime) abort() {
87 if l == nil {
88 return
89 }
90 l.mu.Lock()
91 l.opening = false
92 l.stopOpeningParentsLocked()
93 l.cancel()
94 l.mu.Unlock()
95}
96
97func (l *paneProcessLifetime) stopOpeningParentsLocked() {
98 for _, stop := range l.stopOpening {
99 stop()
100 }
101 l.stopOpening = nil
102 l.openingParents = nil
103}
104
105// pane owns one PTY and its terminal screen state.
106// Lock order when multiple locks are held is:
107// attachedClient.sendMu > Daemon.mu > session.mu > tab.mu > pane.mu.
108// The PTY reader takes only pane.mu, so child output never waits on client IO.
109type pane struct {
110 id layout.PaneID
111 stableID string
112 pty ports.PTY
113 mu sync.Mutex // guards screen, history, syncGen, rect, geometry, graphics scene geometry, resize state, PTY side effects, title, and owner publication
114 owner atomic.Pointer[paneOwner]
115 // ownerGeneration is advanced only while mu is held. Readers consume the
116 // generation through the immutable owner pointer above.
117 ownerGeneration uint64
118 resizeMu sync.Mutex // serializes PTY resizes without holding mu
119 screen *vt.Screen
120 history *vt.History
121 syncGen uint64
122 rect domain.Rect
123 geometry domain.Geometry
124 // graphicsCoordinateGeometry is the pixel-to-cell unit basis of the current
125 // non-empty placement scene. Resizing the attachment changes screen geometry
126 // but must not reinterpret coordinates retained by that scene.
127 graphicsCoordinateGeometry domain.Geometry
128 // graphicsPlacementScene is meaningful only with the coordinate geometry
129 // above and records whether that frozen basis currently owns placements.
130 graphicsPlacementScene bool
131 // resizeApplying gates VT parsing across PTY.Resize. The reader continues
132 // draining into resizePending so output is replayed against the target (or
133 // retained old) screen only after apply resolves.
134 resizeApplying bool
135 resizeRetry bool // PTY resize failed for the committed rectangle; retry on the next accepted plan.
136 resizePending []byte
137 ptyResponses []byte
138 ptyClipboards []string
139 ptyAttention bool
140 popupGeometry floatingGeometry // last geometry committed after a successful floating resize
141 title paneTitleState
142 ctx context.Context
143 cancel context.CancelFunc
144 processCloseOnce sync.Once
145 // onExit is set before the reader starts and never changed. Floating panes
146 // use it to reap their independent slot without touching the layout tree.
147 onExit func()
148 // prestarted marks a floating spare whose reader runs before any owner is
149 // published, so terminal queries from a starting shell are answered.
150 // Set before the reader starts and never changed.
151 prestarted bool
152 // spareExited records that a prestarted spare's reader ended, so a claim
153 // racing that exit still reaps the slot it installed.
154 spareExited atomic.Bool
155}
156
157func closePaneProcess(p *pane) {
158 if p == nil {
159 return
160 }
161 p.processCloseOnce.Do(func() {
162 // Revocation shares pane.mu with VT parsing and owner publication, so no
163 // effect lease remains current once terminal teardown begins.
164 p.mu.Lock()
165 p.clearOwnerLocked()
166 p.mu.Unlock()
167 if p.cancel != nil {
168 p.cancel()
169 }
170 if p.pty != nil {
171 _ = p.pty.Close()
172 }
173 })
174}
175
176func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane {
177 return newPaneWithStableID(id, fallbackStableID("p"), pty, sz)
178}
179
180func newPaneWithStableID(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size) *pane {
181 return newPaneWithStableIDAndTitle(id, stableID, pty, sz, defaultShellTitle, historyConfigFor(domain.DefaultScrollbackConfig()))
182}
183
184func newPaneWithStableIDAndTitle(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size, title string, historyConfig vt.HistoryConfig) *pane {
185 screen := vt.NewScreenWithHistory(sz.Cols, sz.Rows, historyConfig)
186 return &pane{
187 id: id,
188 stableID: stableID,
189 pty: pty,
190 screen: screen,
191 history: screen.History(),
192 rect: domain.Rect{Width: sz.Cols, Height: sz.Rows},
193 title: paneTitleState{displayFallback: title},
194 }
195}