v0.8.1 1f54f61

same_peer_switch.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close