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 the client confirmation with the daemon's
45// endpoint-empty offer. A client cannot select an arbitrary local session by
46// manufacturing this frame.
47func (ac *attachedClient) consumeSamePeerOffer(target protocol.ExactSessionTarget) bool {
48 ac.samePeerOfferMu.Lock()
49 defer ac.samePeerOfferMu.Unlock()
50 if ac.samePeerOffer == nil || *ac.samePeerOffer != target {
51 return false
52 }
53 ac.samePeerOffer = nil
54 return true
55}
56
57// switchSamePeerForAttachment commits one client-confirmed endpoint-empty
58// target. The request never names an endpoint: its authority is the current
59// authenticated attachment plus the target's exact lifecycle identity.
60func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request protocol.SamePeerSwitchRequest) {
61 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {
62 return
63 }
64 if !effect.ac.consumeSamePeerOffer(request.Target) {
65 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
66 return
67 }
68
69 target, targetTabIndex, ok := d.samePeerTarget(request)
70 if !ok || target == effect.sess {
71 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
72 return
73 }
74
75 capability := effect.capability()
76 transition, err := d.transitionAttachment(attachmentTransitionRequest{
77 source: effect.sess,
78 target: target,
79 next: effect.ac,
80 expectedTransport: effect.transport,
81 sourceCapability: &capability,
82 sourceEffect: effect,
83 action: "same-peer-switch",
84 expectedTargetLifecycle: &attachmentLifecycleFence{
85 name: request.Target.SessionName,
86 incarnation: request.Target.LifecycleID,
87 checkIncarnation: true,
88 },
89 activateTargetTab: true,
90 targetTabIndex: targetTabIndex,
91 ready: true,
92 })
93 if err != nil {
94 d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
95 return
96 }
97
98 if fresh, admitted := effect.ac.beginAttachmentEffect(transition.published); admitted {
99 fresh.End()
100 }
101 d.touchMRU(target)
102 d.deferAttachmentTransitionCleanups(transition)
103 d.firstPaintForTransition(transition.published)
104}
105
106// samePeerTarget resolves the exact target and the client-owned tab cursor
107// under the daemon/session locks. A missing cursor deliberately falls back to
108// the target session's current default rather than leaking the source view.
109func (d *Daemon) samePeerTarget(request protocol.SamePeerSwitchRequest) (*session, int, bool) {
110 d.mu.Lock()
111 defer d.mu.Unlock()
112
113 target := d.findByNameLocked(request.Target.SessionName)
114 if target == nil {
115 return nil, 0, false
116 }
117 target.mu.Lock()
118 defer target.mu.Unlock()
119 if target.incarnation != request.Target.LifecycleID || len(target.tabs) == 0 {
120 return nil, 0, false
121 }
122 tabIndex := 0
123 if request.PreferredTabID == "" {
124 return target, tabIndex, true
125 }
126 for index, tab := range target.tabs {
127 if tab != nil && domain.TabStableID(tab.stableID) == request.PreferredTabID {
128 return target, index, true
129 }
130 }
131 return target, tabIndex, true
132}
133
134func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code protocol.SamePeerSwitchFailureCode) {
135 if effect == nil || effect.ac == nil {
136 return
137 }
138 sender := effect
139 if !sender.current() {
140 var admitted bool
141 sender, admitted = effect.ac.beginAttachmentEffect(effect.capability())
142 if !admitted {
143 return
144 }
145 defer sender.End()
146 }
147 failure := protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code}
148 if failure.Validate() != nil {
149 return
150 }
151 if err := sender.sendControl(failure); err != nil {
152 d.detachOnAttachmentSendError(sender.capability(), sender.transport.transport)
153 }
154}