blob: 315dca9fb4c411f85576cf624444e735b85af261 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski0d9e1252024-09-03 12:16:47 +02004package tconsole
5
6import (
7 "context"
8 "crypto/sha256"
9 "encoding/hex"
Serge Bazanski73c632f2024-09-05 13:51:57 +020010 "fmt"
Serge Bazanski0d9e1252024-09-03 12:16:47 +020011 "strings"
Serge Bazanskid735a3c2024-09-05 13:51:44 +020012 "time"
Serge Bazanski0d9e1252024-09-03 12:16:47 +020013
14 "github.com/gdamore/tcell/v2"
15
Lorenz Brun4bde9312025-08-06 05:04:11 +020016 "source.monogon.dev/metropolis/node"
Serge Bazanski0d9e1252024-09-03 12:16:47 +020017 "source.monogon.dev/metropolis/node/core/roleserve"
18 cpb "source.monogon.dev/metropolis/proto/common"
19 "source.monogon.dev/osbase/event"
Serge Bazanski73c632f2024-09-05 13:51:57 +020020 "source.monogon.dev/osbase/logtree"
Serge Bazanski0d9e1252024-09-03 12:16:47 +020021 "source.monogon.dev/osbase/supervisor"
22)
23
Jan Schärb86917b2025-05-14 16:31:08 +000024type Config struct {
25 Terminal Terminal
26 LogTree *logtree.LogTree
Lorenz Brun4bde9312025-08-06 05:04:11 +020027 Network event.Value[*node.NetStatus]
Jan Schärb86917b2025-05-14 16:31:08 +000028 Roles event.Value[*cpb.NodeRoles]
29 CuratorConn event.Value[*roleserve.CuratorConnection]
30}
31
Serge Bazanski0d9e1252024-09-03 12:16:47 +020032// Console is a Terminal Console (TConsole), a user-interactive informational
33// display visible on the TTY of a running Metropolis instance.
34type Console struct {
35 // Quit will be closed when the user press CTRL-C. A new channel will be created
36 // on each New call.
Serge Bazanski0d9e1252024-09-03 12:16:47 +020037 Quit chan struct{}
38 ttyPath string
39 tty tcell.Tty
40 screen tcell.Screen
41 width int
42 height int
43 // palette chosen for the given terminal.
44 palette palette
Serge Bazanskid735a3c2024-09-05 13:51:44 +020045 // activePage expressed within [0...num pages). The number/layout of pages is
46 // constructed dynamically in Run.
47 activePage int
Serge Bazanski0d9e1252024-09-03 12:16:47 +020048
Jan Schärb86917b2025-05-14 16:31:08 +000049 config Config
50 reader *logtree.LogReader
Serge Bazanski0d9e1252024-09-03 12:16:47 +020051}
52
53// New creates a new Console, taking over the TTY at the given path. The given
54// Terminal type selects between a Linux terminal (VTY) and a generic terminal
55// for testing.
56//
57// network, roles, curatorConn point to various Metropolis subsystems that are
58// used to populate the console data.
Jan Schärb86917b2025-05-14 16:31:08 +000059func New(config Config, ttyPath string) (*Console, error) {
60 reader, err := config.LogTree.Read("", logtree.WithChildren(), logtree.WithStream())
Serge Bazanski73c632f2024-09-05 13:51:57 +020061 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +020062 return nil, fmt.Errorf("lt.Read: %w", err)
Serge Bazanski73c632f2024-09-05 13:51:57 +020063 }
64
Serge Bazanski0d9e1252024-09-03 12:16:47 +020065 tty, err := tcell.NewDevTtyFromDev(ttyPath)
66 if err != nil {
67 return nil, err
68 }
69 screen, err := tcell.NewTerminfoScreenFromTty(tty)
70 if err != nil {
71 return nil, err
72 }
73 if err := screen.Init(); err != nil {
74 return nil, err
75 }
76 screen.SetStyle(tcell.StyleDefault)
77
78 var pal palette
Jan Schärb86917b2025-05-14 16:31:08 +000079 switch config.Terminal {
Serge Bazanski0d9e1252024-09-03 12:16:47 +020080 case TerminalLinux:
81 pal = paletteLinux
82 tty.Write([]byte(pal.setupLinuxConsole()))
83 case TerminalGeneric:
84 pal = paletteGeneric
85 }
86
87 width, height := screen.Size()
Serge Bazanskid735a3c2024-09-05 13:51:44 +020088
Serge Bazanski0d9e1252024-09-03 12:16:47 +020089 return &Console{
Serge Bazanskid735a3c2024-09-05 13:51:44 +020090 ttyPath: ttyPath,
91 tty: tty,
92 screen: screen,
93 width: width,
94 height: height,
Serge Bazanskid735a3c2024-09-05 13:51:44 +020095 palette: pal,
96 Quit: make(chan struct{}),
97 activePage: 0,
Jan Schärb86917b2025-05-14 16:31:08 +000098 config: config,
Serge Bazanski73c632f2024-09-05 13:51:57 +020099 reader: reader,
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200100 }, nil
101}
102
103// Cleanup should be called when the console exits. This is only used in testing,
104// the Metropolis console always runs.
105func (c *Console) Cleanup() {
106 c.screen.Fini()
Serge Bazanski73c632f2024-09-05 13:51:57 +0200107 c.reader.Close()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200108}
109
110func (c *Console) processEvent(ev tcell.Event) {
111 switch ev := ev.(type) {
112 case *tcell.EventKey:
113 if ev.Key() == tcell.KeyCtrlC {
114 close(c.Quit)
115 }
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200116 if ev.Key() == tcell.KeyTab {
117 c.activePage += 1
118 }
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200119 case *tcell.EventResize:
120 c.width, c.height = ev.Size()
121 }
122}
123
124// Run blocks while displaying the Console to the user.
125func (c *Console) Run(ctx context.Context) error {
126 // Build channel for console event processing.
127 evC := make(chan tcell.Event)
128 evQuitC := make(chan struct{})
129 defer close(evQuitC)
130 go c.screen.ChannelEvents(evC, evQuitC)
131
132 // Pipe event values into channels.
Lorenz Brun4bde9312025-08-06 05:04:11 +0200133 netAddrC := make(chan *node.NetStatus)
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200134 rolesC := make(chan *cpb.NodeRoles)
135 curatorConnC := make(chan *roleserve.CuratorConnection)
Jan Schärb86917b2025-05-14 16:31:08 +0000136 if err := supervisor.Run(ctx, "netpipe", event.Pipe(c.config.Network, netAddrC)); err != nil {
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200137 return err
138 }
Jan Schärb86917b2025-05-14 16:31:08 +0000139 if err := supervisor.Run(ctx, "rolespipe", event.Pipe(c.config.Roles, rolesC)); err != nil {
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200140 return err
141 }
Jan Schärb86917b2025-05-14 16:31:08 +0000142 if err := supervisor.Run(ctx, "curatorpipe", event.Pipe(c.config.CuratorConn, curatorConnC)); err != nil {
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200143 return err
144 }
145 supervisor.Signal(ctx, supervisor.SignalHealthy)
146
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200147 // Per-page data.
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200148 pageStatus := pageStatusData{
149 netAddr: "Waiting...",
150 roles: "Waiting...",
151 id: "Waiting...",
152 fingerprint: "Waiting...",
153 }
Serge Bazanski73c632f2024-09-05 13:51:57 +0200154 pageLogs := pageLogsData{}
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200155
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200156 // Page references and names.
157 pages := []func(){
158 func() { c.pageStatus(&pageStatus) },
Serge Bazanski73c632f2024-09-05 13:51:57 +0200159 func() { c.pageLogs(&pageLogs) },
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200160 }
161 pageNames := []string{
Serge Bazanski73c632f2024-09-05 13:51:57 +0200162 "Status", "Logs",
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200163 }
164
165 // Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in
166 // the status bar.
Serge Bazanski154e6d92024-09-11 17:26:31 +0200167 tickerDraw := time.NewTicker(time.Second / 10)
168 defer tickerDraw.Stop()
169
170 // Ticker used to fully resync the screen every 10 seconds, in case something
171 // scribbled over the TTY.
172 tickerSync := time.NewTicker(time.Second * 10)
173 defer tickerSync.Stop()
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200174
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200175 for {
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200176 // Draw active page.
177 c.activePage %= len(pages)
178 pages[c.activePage]()
179
180 // Draw status bar.
181 c.statusBar(c.activePage, pageNames...)
182
183 // Sync to screen.
184 c.screen.Show()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200185
186 select {
Serge Bazanski154e6d92024-09-11 17:26:31 +0200187 case <-tickerDraw.C:
188 case <-tickerSync.C:
189 c.screen.Sync()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200190 case <-ctx.Done():
191 return ctx.Err()
192 case ev := <-evC:
193 c.processEvent(ev)
194 case t := <-netAddrC:
195 pageStatus.netAddr = t.ExternalAddress.String()
196 case t := <-rolesC:
197 var rlist []string
198 if t.ConsensusMember != nil {
199 rlist = append(rlist, "ConsensusMember")
200 }
201 if t.KubernetesController != nil {
202 rlist = append(rlist, "KubernetesController")
203 }
204 if t.KubernetesWorker != nil {
205 rlist = append(rlist, "KubernetesWorker")
206 }
207 pageStatus.roles = strings.Join(rlist, ", ")
208 if pageStatus.roles == "" {
209 pageStatus.roles = "none"
210 }
211 case t := <-curatorConnC:
212 pageStatus.id = t.Credentials.ID()
213 cert := t.Credentials.ClusterCA()
214 sum := sha256.New()
215 sum.Write(cert.Raw)
216 pageStatus.fingerprint = hex.EncodeToString(sum.Sum(nil))
Serge Bazanski73c632f2024-09-05 13:51:57 +0200217 case le := <-c.reader.Stream:
218 pageLogs.appendLine(le.String())
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200219 }
220 }
221}