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/ports"
11 "github.com/bnema/vev/internal/usecase/layout"
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, resizeApplying, resizeRetry, resizePending, 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 // resizeApplying gates VT parsing across PTY.Resize. The reader continues
124 // draining into resizePending so output is replayed against the target (or
125 // retained old) screen only after apply resolves.
126 resizeApplying bool
127 resizeRetry bool // PTY resize failed for the committed rectangle; retry on the next accepted plan.
128 resizePending []byte
129 ptyResponses []byte
130 ptyClipboards []string
131 ptyAttention bool
132 popupGeometry floatingGeometry // last geometry committed after a successful floating resize
133 title paneTitleState
134 ctx context.Context
135 cancel context.CancelFunc
136 processCloseOnce sync.Once
137 // onExit is set before the reader starts and never changed. Floating panes
138 // use it to reap their independent slot without touching the layout tree.
139 onExit func()
140}
141
142func closePaneProcess(p *pane) {
143 if p == nil {
144 return
145 }
146 p.processCloseOnce.Do(func() {
147 // Revocation shares pane.mu with VT parsing and owner publication, so no
148 // effect lease remains current once terminal teardown begins.
149 p.mu.Lock()
150 p.clearOwnerLocked()
151 p.mu.Unlock()
152 if p.cancel != nil {
153 p.cancel()
154 }
155 if p.pty != nil {
156 _ = p.pty.Close()
157 }
158 })
159}
160
161func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane {
162 return newPaneWithStableID(id, fallbackStableID("p"), pty, sz)
163}
164
165func newPaneWithStableID(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size) *pane {
166 return newPaneWithStableIDAndTitle(id, stableID, pty, sz, defaultShellTitle)
167}
168
169func newPaneWithStableIDAndTitle(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size, title string) *pane {
170 screen := vt.NewScreenWithHistory(sz.Cols, sz.Rows, vt.HistoryConfig{MaxRows: defaultScrollbackRows, MaxCells: defaultScrollbackCells})
171 return &pane{
172 id: id,
173 stableID: stableID,
174 pty: pty,
175 screen: screen,
176 history: screen.History(),
177 rect: domain.Rect{Width: sz.Cols, Height: sz.Rows},
178 title: paneTitleState{displayFallback: title},
179 }
180}