1package daemon
2
3import (
4 "sync"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/protocol"
8)
9
10func (ac *attachedClient) offerSamePeerTarget(target protocol.ExactSessionTarget) {
11 ac.samePeerOfferMu.Lock()
12 ac.samePeerOffer = &target
13 ac.samePeerOfferMu.Unlock()
14}
15
16// offerSamePeerAttachTarget arms the exact target before publishing its
17// endpoint-empty handoff, then rolls the offer back if publication fails.
18func offerSamePeerAttachTarget(effect *attachmentEffect, target protocol.ExactSessionTarget) error {
19 if effect == nil || effect.ac == nil || !effect.current() {
20 return errAttachmentTransition
21 }
22 handoff := protocol.AttachTarget{
23 Session: target.SessionName,
24 Intent: protocol.IntentAttach,
25 ExactTarget: &target,
26 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
27 SamePeer: true,
28 }
29 if protocol.ValidateAttachTarget(handoff) != nil {
30 return errAttachmentTransition
31 }
32 effect.ac.offerSamePeerTarget(target)
33 if err := effect.sendControl(handoff); err != nil {
34 effect.ac.clearSamePeerOffer()
35 return err
36 }
37 return nil
38}
39
40func (ac *attachedClient) clearSamePeerOffer() {
41 ac.samePeerOfferMu.Lock()
42 ac.samePeerOffer = nil
43 ac.samePeerOfferMu.Unlock()
44}
45
46// consumeSamePeerOffer linearizes a switch request with the daemon's pending
47// endpoint-empty offer: while one is pending, only its exact target is
48// accepted. Without a pending offer the request is the client's own picker
49// choice. It grants no more than a fresh exact attach would on this
50// authenticated connection, and samePeerTarget still requires a live session
51// with the exact lifecycle.
52func (ac *attachedClient) consumeSamePeerOffer(target protocol.ExactSessionTarget) bool {
53 ac.samePeerOfferMu.Lock()
54 defer ac.samePeerOfferMu.Unlock()
55 if ac.samePeerOffer == nil {
56 return true
57 }
58 if *ac.samePeerOffer != target {
59 return false
60 }
61 ac.samePeerOffer = nil
62 return true
63}
64
65// switchSamePeerForAttachment commits one client-confirmed endpoint-empty
66// target. The request never names an endpoint: its authority is the current
67// authenticated attachment plus the target's exact lifecycle identity.
68func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request protocol.SamePeerSwitchRequest) {
69 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {
70 return
71 }
72 // The switch moves an attachment between sessions, so it must not run
73 // inside the exact set a KillAll purge has captured. A purge is retry
74 // state, so it is checked first and keeps any pending daemon offer.
75 d.mu.Lock()
76 if d.purgeAdmissionClosedLocked() {
77 d.mu.Unlock()
78 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchUnavailable)
79 return
80 }
81 d.purgeAdmissionActive++
82 d.mu.Unlock()
83 // The reservation covers only the membership change; first paint can
84 // block on a slow client and must not stall a purge's drain.
85 var releaseOnce sync.Once
86 release := func() { releaseOnce.Do(d.releasePurgeAdmission) }
87 defer release()
88
89 if !effect.ac.consumeSamePeerOffer(request.Target) {
90 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
91 return
92 }
93
94 target, targetTabIndex, ok := d.samePeerTarget(request)
95 if !ok || target == effect.sess {
96 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
97 return
98 }
99
100 capability := effect.capability()
101 transition, err := d.transitionAttachment(attachmentTransitionRequest{
102 source: effect.sess,
103 target: target,
104 next: effect.ac,
105 expectedTransport: effect.transport,
106 sourceCapability: &capability,
107 sourceEffect: effect,
108 action: "same-peer-switch",
109 expectedTargetLifecycle: &attachmentLifecycleFence{
110 name: request.Target.SessionName,
111 incarnation: request.Target.LifecycleID,
112 checkIncarnation: true,
113 },
114 activateTargetTab: true,
115 targetTabIndex: targetTabIndex,
116 ready: true,
117 })
118 if err != nil {
119 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
120 return
121 }
122 release()
123
124 if fresh, admitted := effect.ac.beginAttachmentEffect(transition.published); admitted {
125 fresh.End()
126 }
127 d.touchMRU(target)
128 d.deferAttachmentTransitionCleanups(transition)
129 d.firstPaintForTransition(transition.published)
130}
131
132// samePeerTarget resolves the exact target and the client-owned tab cursor
133// under the daemon/session locks. A missing cursor deliberately falls back to
134// the target session's current default rather than leaking the source view.
135func (d *Daemon) samePeerTarget(request protocol.SamePeerSwitchRequest) (*session, int, bool) {
136 d.mu.Lock()
137 defer d.mu.Unlock()
138
139 target := d.findByNameLocked(request.Target.SessionName)
140 if target == nil {
141 return nil, 0, false
142 }
143 target.mu.Lock()
144 defer target.mu.Unlock()
145 if target.incarnation != request.Target.LifecycleID || len(target.tabs) == 0 {
146 return nil, 0, false
147 }
148 tabIndex := 0
149 if request.PreferredTabID == "" {
150 return target, tabIndex, true
151 }
152 for index, tab := range target.tabs {
153 if tab != nil && domain.TabStableID(tab.stableID) == request.PreferredTabID {
154 return target, index, true
155 }
156 }
157 return target, tabIndex, true
158}
159
160func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code protocol.SamePeerSwitchFailureCode) {
161 if effect == nil || effect.ac == nil {
162 return
163 }
164 sender := effect
165 if !sender.current() {
166 var admitted bool
167 sender, admitted = effect.ac.beginAttachmentEffect(effect.capability())
168 if !admitted {
169 return
170 }
171 defer sender.End()
172 }
173 failure := protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code}
174 if failure.Validate() != nil {
175 return
176 }
177 if err := sender.sendControl(failure); err != nil {
178 d.detachOnAttachmentSendError(sender.capability(), sender.transport.transport)
179 }
180}