supervisor_picker_kill.go RawHistoryFolder

1package client
2
3import (
4	"context"
5	"errors"
6	"sync"
7
8	"github.com/bnema/vev/internal/domain"
9	"github.com/bnema/vev/internal/ports"
10	"github.com/bnema/vev/internal/protocol"
11)
12
13// Picker kill.
14//
15// `x` on a local session row kills it through the broker operations seam: one
16// independent, bounded control stream to the owning daemon, never the
17// attachment stream. A live session is torn down; a stopped session's history
18// record is deleted by the same typed Kill, which the daemon routes to its
19// stopped-session purge. The operation runs off the supervisor's run goroutine
20// so the picker keeps rendering; its typed outcome comes back as one notice and
21// a catalogue reconcile. An unknown outcome is reported and never replayed.
22
23// pickerKillResolver is the optional kill resolution of the real picker.
24type pickerKillResolver interface {
25	ResolveKill(key string) (pickerKillTarget, error)
26}
27
28// pickerCurrentHost is the optional cursor seam of the real picker.
29type pickerCurrentHost interface {
30	SetCurrent(pickerCurrent)
31}
32
33// pickerKillOutcome is one settled kill.
34type pickerKillOutcome struct {
35	target pickerKillTarget
36	result protocol.KillResult
37	err    error
38}
39
40// pickerKills owns at most one in-flight picker kill. It is only touched from
41// the supervisor's run goroutine; the worker goroutine only sends on done.
42type pickerKills struct {
43	running bool
44	done    chan pickerKillOutcome
45	cancel  context.CancelFunc
46	wg      sync.WaitGroup
47}
48
49// results is the settled-kill wake, nil while no kill runs.
50func (k *pickerKills) results() <-chan pickerKillOutcome {
51	if k == nil || !k.running {
52		return nil
53	}
54	return k.done
55}
56
57// setPickerCurrent names the attachment the picker is presented over.
58func (s *Supervisor) setPickerCurrent(current pickerCurrent) {
59	if host, ok := s.cfg.Picker.(pickerCurrentHost); ok {
60		host.SetCurrent(current)
61	}
62}
63
64// startPickerKill resolves one kill key against the latest catalogue and runs
65// the kill. A refusal is already a picker notice; a second `x` while one kill
66// runs is refused rather than queued.
67func (s *Supervisor) startPickerKill(service ports.BrokerNavigator, key string) {
68	resolver, ok := s.cfg.Picker.(pickerKillResolver)
69	if !ok || key == "" || supervisorNil(service) {
70		return
71	}
72	if s.picker.kills.running {
73		s.notifyPicker(pickerKillRefused("a kill is already in progress"))
74		return
75	}
76	target, err := resolver.ResolveKill(key)
77	if err != nil {
78		return
79	}
80	operations, err := NewBrokerOperations(service, s.cfg.Clock)
81	if err != nil {
82		s.notifyPicker(pickerKillRefused("couldn't kill " + target.name + ": broker unavailable"))
83		return
84	}
85	if s.picker.kills.done == nil {
86		s.picker.kills.done = make(chan pickerKillOutcome, 1)
87	}
88	killCtx, cancel := context.WithCancel(context.Background())
89	s.picker.kills.running = true
90	s.picker.kills.cancel = cancel
91	s.picker.kills.wg.Add(1)
92	go func() {
93		defer s.picker.kills.wg.Done()
94		result, err := operations.Kill(killCtx, target.route, target.name)
95		s.picker.kills.done <- pickerKillOutcome{target: target, result: result, err: err}
96	}()
97}
98
99// finishPickerKill reports one settled kill and asks the broker to re-observe
100// the local daemon, so the killed row leaves the catalogue promptly.
101func (s *Supervisor) finishPickerKill(service ports.BrokerNavigator, outcome pickerKillOutcome) {
102	s.settlePickerKill()
103	s.notifyPicker(pickerKillNotice(outcome))
104	if !supervisorNil(service) {
105		service.RequestReconcile("")
106	}
107	s.renderCurrent()
108}
109
110// retirePickerKill cancels and joins an in-flight kill before its borrowed
111// service closes. Its outcome is still reported, never replayed.
112func (s *Supervisor) retirePickerKill() {
113	if !s.picker.kills.running {
114		return
115	}
116	s.picker.kills.cancel()
117	s.picker.kills.wg.Wait()
118	outcome := <-s.picker.kills.done
119	s.settlePickerKill()
120	s.notifyPicker(pickerKillNotice(outcome))
121}
122
123func (s *Supervisor) settlePickerKill() {
124	if s.picker.kills.cancel != nil {
125		s.picker.kills.cancel()
126	}
127	s.picker.kills.running = false
128	s.picker.kills.cancel = nil
129}
130
131// pickerKillRefused is a kill that was never sent.
132func pickerKillRefused(message string) domain.Notification {
133	return domain.Notification{Code: domain.NoticeSessionKill, Severity: domain.NoticeWarn, Message: message}
134}
135
136// pickerKillNotice renders one typed kill outcome.
137func pickerKillNotice(outcome pickerKillOutcome) domain.Notification {
138	sev := domain.NoticeError
139	if outcome.err == nil && outcome.result.Outcome == protocol.KillSucceeded {
140		sev = domain.NoticeInfo
141	}
142	// Each target is its own subject, so kills in a row stack instead of
143	// replacing each other.
144	return domain.Notification{Code: domain.NoticeSessionKill, Severity: sev, Message: pickerKillText(outcome), Scope: outcome.target.name}
145}
146
147func pickerKillText(outcome pickerKillOutcome) string {
148	name := outcome.target.name
149	verb, done := "kill", "killed session "
150	if outcome.target.stopped {
151		verb, done = "delete stopped session", "deleted stopped session "
152	}
153	if outcome.err != nil {
154		if errors.Is(outcome.err, ErrBrokerOperationNotSent) {
155			return "couldn't " + verb + " " + name + ": not sent"
156		}
157		return "couldn't " + verb + " " + name
158	}
159	switch outcome.result.Outcome {
160	case protocol.KillSucceeded:
161		return done + name
162	case protocol.KillFailed:
163		if outcome.result.Text != "" {
164			return "couldn't " + verb + " " + name + ": " + outcome.result.Text
165		}
166		return "couldn't " + verb + " " + name
167	default:
168		// The request may have reached the daemon: never retried.
169		return verb + " " + name + ": outcome unknown"
170	}
171}

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