sessions.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io"
8	"os"
9	"text/tabwriter"
10
11	"github.com/bnema/vev/internal/adapters/clock"
12	"github.com/bnema/vev/internal/adapters/ipc"
13	"github.com/bnema/vev/internal/adapters/sessionwire"
14	"github.com/bnema/vev/internal/persist"
15	"github.com/bnema/vev/internal/ports"
16	"github.com/bnema/vev/internal/protocol"
17	"github.com/bnema/vev/internal/usecase/client"
18)
19
20// Broker connection seams replaced by tests.
21var (
22	connectBroker     = connectProductionBroker
23	connectListBroker = connectExistingBroker
24	dialListDaemon    = ipc.DialContext
25)
26
27var errKillOutcomeUnknown = errors.New("kill outcome unknown")
28
29// errNoLocalDaemon reports that no local daemon socket answers; nothing was
30// started to find out.
31var errNoLocalDaemon = errors.New("no local daemon")
32
33func createDetachedLocalSession(ctx context.Context, name string) error {
34	service, err := connectBroker(ctx)
35	if err != nil {
36		return fmt.Errorf("%w: %v", errDaemonUnreachable, err)
37	}
38	defer func() { _ = service.Close() }()
39	operations, err := client.NewBrokerOperations(service, clock.New())
40	if err != nil {
41		return err
42	}
43	identity, _ := parseVEVEnv(os.Getenv("VEV"))
44	result, err := operations.Command(ctx, localBrokerOperationRoute(service.Snapshot()), protocol.CommandRequest{
45		Version: protocol.Version, Slug: "new-session", Args: []string{name}, TargetSession: identity.session,
46	})
47	if err != nil {
48		return err
49	}
50	return renderDetachedCreationResult(result)
51}
52
53func renderDetachedCreationResult(result protocol.CommandResult) error {
54	return renderCommandResult(io.Discard, result)
55}
56
57func brokerKillResultError(result protocol.KillResult) error {
58	switch result.Outcome {
59	case protocol.KillSucceeded:
60		return nil
61	case protocol.KillFailed:
62		if result.Text == "" {
63			result.Text = "kill failed"
64		}
65		return errors.New(result.Text)
66	default:
67		if result.Text == "" {
68			result.Text = "daemon did not report a final outcome"
69		}
70		return &exitCoded{code: 3, err: fmt.Errorf("%w: %s", errKillOutcomeUnknown, result.Text)}
71	}
72}
73
74// runList reads existing state without starting either background process.
75func runList(ctx context.Context, cmd command) error {
76	service, err := connectListBroker(ctx)
77	if errors.Is(err, errBrokerAbsent) {
78		return runListWithoutBroker(ctx, cmd, os.Stdout)
79	}
80	if err != nil {
81		return unreachableBrokerError(err)
82	}
83	defer func() { _ = service.Close() }()
84	if cmd.listAll || cmd.listHost != "" {
85		return runBrokerSnapshotList(cmd, service.Snapshot(), os.Stdout)
86	}
87	operations, err := client.NewBrokerOperations(service, clock.New())
88	if err != nil {
89		return err
90	}
91	sessions, err := operations.List(ctx, localBrokerOperationRoute(service.Snapshot()))
92	if err != nil {
93		var brokerErr ports.BrokerError
94		if errors.As(err, &brokerErr) && brokerErr.Code == ports.BrokerErrorNoDaemon {
95			printSessions(os.Stdout, nil)
96			return nil
97		}
98		return err
99	}
100	printSessions(os.Stdout, sessions)
101	return nil
102}
103
104// exchangeLocalDaemon sends one request directly to an existing local daemon
105// and returns its reply. It is the broker-less path of read and stop
106// commands: the broker being absent is the normal idle state, and these
107// commands must stay true without starting it. It never spawns a daemon.
108func exchangeLocalDaemon(ctx context.Context, request protocol.ClientMessage) (protocol.ServerMessage, error) {
109	transport, err := dialListDaemon(ctx, ipc.SocketDir())
110	if err != nil {
111		if backendAbsent(err) {
112			return nil, errNoLocalDaemon
113		}
114		return nil, err
115	}
116	connection := sessionwire.NewClientConnection(transport)
117	defer func() { _ = connection.Close() }()
118	if err := connection.SendClient(request); err != nil {
119		return nil, err
120	}
121	return connection.ReceiveServer()
122}
123
124func runListWithoutBroker(ctx context.Context, cmd command, out io.Writer) error {
125	if cmd.listHost != "" {
126		_, err := fmt.Fprintf(out, "%s: not connected (no broker running)\n", cmd.listHost)
127		return err
128	}
129	reply, err := exchangeLocalDaemon(ctx, protocol.List{})
130	switch {
131	case errors.Is(err, errNoLocalDaemon):
132		printSessions(out, nil)
133	case err != nil:
134		return fmt.Errorf("list local sessions: %w", err)
135	default:
136		sessions, ok := reply.(protocol.Sessions)
137		if !ok {
138			return fmt.Errorf("unexpected local list reply %T", reply)
139		}
140		printSessions(out, sessions.Sessions)
141	}
142	if cmd.listAll {
143		return listDisconnectedHosts(out)
144	}
145	return nil
146}
147
148// printSessions renders a session table (or a friendly note when empty).
149func printSessions(w io.Writer, sessions []protocol.SessionInfo) {
150	if len(sessions) == 0 {
151		_, _ = fmt.Fprintln(w, "no sessions")
152		return
153	}
154	tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
155	_, _ = fmt.Fprintln(tw, "NAME\tSTATE\tTABS\tATTACHED")
156	for _, s := range sessions {
157		state := "up"
158		tabs := fmt.Sprintf("%d", s.Tabs)
159		attached := "no"
160		switch s.State {
161		case protocol.SessionDown:
162			state = "down"
163			tabs = "-"
164		case protocol.SessionBroken:
165			state = "broken"
166			tabs = "-"
167		default:
168			if s.Ephemeral {
169				state = "temporary"
170			}
171		}
172		if s.Attached {
173			attached = "yes"
174		}
175		_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", s.Name, state, tabs, attached)
176	}
177	_ = tw.Flush()
178}
179
180func unreadableCatalogueError(stateDir string) error {
181	return fmt.Errorf("%w: durable session state at %s cannot be read and was left untouched.\n"+
182		"vev does not erase it automatically. To start fresh, remove it:\n"+
183		"    rm -rf %s", persist.ErrCatalogueUnreadable, stateDir, stateDir)
184}
185
186// runKill deletes one session or, with sessions set, every session through the
187// broker, starting it if needed.
188func runKill(ctx context.Context, name string, sessions bool) error {
189	service, err := connectBroker(ctx)
190	if err != nil {
191		return unreachableBrokerError(err)
192	}
193	defer func() { _ = service.Close() }()
194	operations, err := client.NewBrokerOperations(service, clock.New())
195	if err != nil {
196		return err
197	}
198	route := localBrokerOperationRoute(service.Snapshot())
199	var result protocol.KillResult
200	if sessions {
201		result, err = operations.KillAll(ctx, route)
202	} else {
203		result, err = operations.Kill(ctx, route, name)
204	}
205	if err != nil {
206		return err
207	}
208	if err := brokerKillResultError(result); err != nil {
209		return err
210	}
211	if sessions {
212		fmt.Println("killed all sessions")
213	} else {
214		fmt.Printf("killed %s\n", name)
215	}
216	return nil
217}

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