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}