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