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