1package daemon
2
3import (
4 "github.com/bnema/vev/internal/ports"
5 "github.com/bnema/vev/internal/protocol"
6)
7
8// clientGone detaches ac if it is still the session's current client. The
9// session remains registered and headless after the client is gone.
10func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit bool) {
11 d.clientGoneWithNotice(sess, ac, failed, explicit, true)
12}
13
14func (d *Daemon) clientGoneWithoutNotice(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit bool) {
15 d.clientGoneWithNotice(sess, ac, failed, explicit, false)
16}
17
18func (d *Daemon) clientGoneWithNotice(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool) {
19 d.clientGoneWithNoticeReason(sess, ac, failed, explicit, notice, protocol.ReasonDetach)
20}
21
22func (d *Daemon) clientGoneWithNoticeReason(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool, reason uint8) {
23 if sess == nil || ac == nil {
24 return
25 }
26 expected := transportSnapshot{}
27 if failed != nil {
28 expected = ac.transportSnapshot()
29 if expected.transport != failed {
30 return // stale connection loop; a newer transport owns this client
31 }
32 }
33 // A warm attachment already retains its live session state. Closure evicts
34 // it rather than creating a second, parked lifetime.
35 if ac.attachmentActivity() != attachmentActive {
36 explicit = true
37 notice = false
38 }
39 // Advertise parking before detach so IntentResume never observes both the
40 // live seat and parking/parked registries empty for a still-valid token.
41 var parkingToken uint64
42 if !explicit {
43 parkingToken = d.markParkingInFlight(sess, ac)
44 }
45 if d.beforeClientGoneDetach != nil {
46 d.beforeClientGoneDetach()
47 }
48 if !d.detachIfCurrentTransport(sess, ac, expected) {
49 d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken)
50 return // displaced, or the link was rebound after the precheck
51 }
52 d.finishClientGoneReason(sess, ac, failed, explicit, notice, reason)
53}
54
55func (d *Daemon) clientGoneForAttachment(effect *attachmentEffect, explicit bool) bool {
56 return d.clientGoneForAttachmentReason(effect, explicit, protocol.ReasonDetach)
57}
58
59func (d *Daemon) clientGoneForAttachmentReason(effect *attachmentEffect, explicit bool, reason uint8) bool {
60 if effect == nil {
61 return false
62 }
63 effect.bindActionEnd(d, "detach")
64 effect.End()
65 capability := effect.capability()
66 sess := capability.sess
67 if sess == nil {
68 return false
69 }
70 var parkingToken uint64
71 if !explicit {
72 parkingToken = d.markParkingInFlight(sess, capability.ac)
73 }
74 if !d.detachIfAttachmentCurrent(capability) {
75 d.clearParkingInFlightIfAbandoned(sess, capability.ac, parkingToken)
76 return false
77 }
78 d.finishClientGoneReason(sess, capability.ac, capability.transport.transport, explicit, true, reason)
79 return true
80}
81
82func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool) {
83 d.finishClientGoneReason(sess, ac, failed, explicit, notice, protocol.ReasonDetach)
84}
85
86func (d *Daemon) finishClientGoneReason(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool, reason uint8) {
87 if sess == nil || ac == nil {
88 return
89 }
90 ac.clearSamePeerOffer()
91 if d.afterClientGoneDetach != nil {
92 d.afterClientGoneDetach()
93 }
94 if rc := sess.renderCoordinator(); rc != nil {
95 rc.noteDetach(ac)
96 }
97 sess.geometry.reconcileAndInvalidate(d, sess, nil, "client_transport_errors.go")
98 sess.mu.Lock()
99 ephemeral := sess.ephemeral
100 name := sess.name
101 sess.mu.Unlock()
102 if !ephemeral {
103 d.refreshSessionCwd(sess)
104 }
105 d.log.Info("client detach begin", "session", name, "explicit", explicit, "ephemeral", ephemeral)
106 oldTr := failed
107 if oldTr == nil {
108 oldTr = ac.transport()
109 }
110 if !explicit && d.parkAttachment(sess, ac) {
111 _ = ac.closeCapturedTransport(oldTr)
112 d.log.Info("client parked", "session", name)
113 return
114 }
115 // Explicit winners (and non-explicit park failures) must drop any same-
116 // attachment parking marker left by a raced non-explicit teardown so
117 // IntentResume waiters are not stranded on a never-published park.
118 d.clearParkingInFlight(d.resumeTokenSnapshot(ac), ac)
119 d.retirePicker(ac)
120 d.closePalette(ac)
121 if err := d.cleanupAttachmentOutput(ac); err != nil {
122 d.log.Warn("output cleanup failed during detach", "err", err, "session", name)
123 }
124
125 d.resetScreenDefaultColors(sess)
126 if explicit && notice {
127 // Synchronous so the ack is delivered before the transport closes
128 // (the client is actively awaiting it), but deadline-bounded so a
129 // wedged client cannot pin this conn handler and hang Serve's
130 // connWg.Wait.
131 d.boundedSend(ac, serverDetached(reason))
132 }
133 _ = ac.closeCapturedTransport(oldTr)
134 d.log.Info("client detached", "session", name, "explicit", explicit)
135}
136
137// detachOnSendError drops a client whose transport failed, leaving the session
138// registered and headless.
139func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed ports.ServerConnection) {
140 expected := transportSnapshot{}
141 if failed != nil {
142 expected = ac.transportSnapshot()
143 if expected.transport != failed {
144 return
145 }
146 }
147 parkingToken := d.markParkingInFlight(sess, ac)
148 if d.detachIfCurrentTransport(sess, ac, expected) {
149 d.finishSendErrorDetach(sess, ac, failed)
150 return
151 }
152 d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken)
153}
154
155func (d *Daemon) detachOnAttachmentSendError(token attachmentCapability, failed ports.ServerConnection) {
156 d.detachOnAttachmentSendErrorUntil(token, failed, nil)
157}
158
159func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, failed ports.ServerConnection, done func() <-chan struct{}) {
160 // A delayed sender may report after the client has rebound. Only the exact
161 // transport captured by this attachment is allowed to detach either lifecycle.
162 if failed == nil || failed != token.transport.transport {
163 return
164 }
165 sess := token.sess
166 if sess == nil {
167 return
168 }
169 parkingToken := d.markParkingInFlight(sess, token.ac)
170 if d.detachIfAttachmentCurrentUntil(token, done) {
171 d.finishSendErrorDetach(sess, token.ac, failed)
172 return
173 }
174 d.clearParkingInFlightIfAbandoned(sess, token.ac, parkingToken)
175}
176
177// reserveAttachmentSendErrorCleanup accounts for cleanup before End releases the
178// attachment gate. This closes the WaitGroup Add/Wait race with terminal teardown;
179// the returned launch function must be invoked immediately after ticket End.
180func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, failed ports.ServerConnection) func() {
181 d.attachmentCleanupWg.Add(1)
182 return func() {
183 go func() {
184 defer d.attachmentCleanupWg.Done()
185 if d.afterAttachmentSendErrorCleanup != nil {
186 defer d.afterAttachmentSendErrorCleanup()
187 }
188 if d.beforeAttachmentSendErrorCleanup != nil {
189 d.beforeAttachmentSendErrorCleanup(token)
190 }
191 deadline := newAttachmentEffectDrainDeadline(d.clock)
192 defer deadline.stop()
193 d.detachOnAttachmentSendErrorUntil(token, failed, deadline.Done)
194 }()
195 }
196}
197
198func (d *Daemon) finishSendErrorDetach(sess *session, ac *attachedClient, failed ports.ServerConnection) {
199 ac.clearSamePeerOffer()
200 name := sess.nameSnapshot()
201 if rc := sess.renderCoordinator(); rc != nil {
202 rc.noteDetach(ac)
203 }
204 sess.geometry.reconcileAndInvalidate(d, sess, nil, "client_transport_errors.go")
205 if ac.attachmentActivity() == attachmentActive && d.parkAttachment(sess, ac) {
206 _ = ac.closeCapturedTransport(failed)
207 d.log.Warn("parked client after send error", "session", name)
208 return
209 }
210 d.clearParkingInFlight(d.resumeTokenSnapshot(ac), ac)
211 d.retirePicker(ac)
212 d.closePalette(ac)
213 if err := d.cleanupAttachmentOutput(ac); err != nil {
214 d.log.Warn("output cleanup failed after send error", "err", err, "session", name)
215 }
216 d.resetScreenDefaultColors(sess)
217 _ = ac.closeCapturedTransport(failed)
218 d.log.Warn("detached client after send error", "session", name)
219}