blob: db69b0117fea14abf0b9f21b245f608e97d033ed [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
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +020024type page interface {
25 render(*Console)
26 processEvent(*Console, tcell.Event)
27}
28
Jan Schärb86917b2025-05-14 16:31:08 +000029type Config struct {
30 Terminal Terminal
31 LogTree *logtree.LogTree
Lorenz Brun4bde9312025-08-06 05:04:11 +020032 Network event.Value[*node.NetStatus]
Jan Schärb86917b2025-05-14 16:31:08 +000033 Roles event.Value[*cpb.NodeRoles]
34 CuratorConn event.Value[*roleserve.CuratorConnection]
35}
36
Serge Bazanski0d9e1252024-09-03 12:16:47 +020037// Console is a Terminal Console (TConsole), a user-interactive informational
38// display visible on the TTY of a running Metropolis instance.
39type Console struct {
40 // Quit will be closed when the user press CTRL-C. A new channel will be created
41 // on each New call.
Serge Bazanski0d9e1252024-09-03 12:16:47 +020042 Quit chan struct{}
43 ttyPath string
44 tty tcell.Tty
45 screen tcell.Screen
46 width int
47 height int
48 // palette chosen for the given terminal.
49 palette palette
Serge Bazanskid735a3c2024-09-05 13:51:44 +020050 // activePage expressed within [0...num pages). The number/layout of pages is
51 // constructed dynamically in Run.
52 activePage int
Serge Bazanski0d9e1252024-09-03 12:16:47 +020053
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +020054 config Config
55 logReader *logtree.LogReader
Serge Bazanski0d9e1252024-09-03 12:16:47 +020056}
57
58// New creates a new Console, taking over the TTY at the given path. The given
59// Terminal type selects between a Linux terminal (VTY) and a generic terminal
60// for testing.
61//
62// network, roles, curatorConn point to various Metropolis subsystems that are
63// used to populate the console data.
Jan Schärb86917b2025-05-14 16:31:08 +000064func New(config Config, ttyPath string) (*Console, error) {
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +020065 reader, err := config.LogTree.Read(
66 "",
67 logtree.WithChildren(),
68 logtree.WithStream(),
69 logtree.WithBacklog(logtree.BacklogAllAvailable),
70 )
Serge Bazanski73c632f2024-09-05 13:51:57 +020071 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +020072 return nil, fmt.Errorf("lt.Read: %w", err)
Serge Bazanski73c632f2024-09-05 13:51:57 +020073 }
74
Serge Bazanski0d9e1252024-09-03 12:16:47 +020075 tty, err := tcell.NewDevTtyFromDev(ttyPath)
76 if err != nil {
77 return nil, err
78 }
79 screen, err := tcell.NewTerminfoScreenFromTty(tty)
80 if err != nil {
81 return nil, err
82 }
83 if err := screen.Init(); err != nil {
84 return nil, err
85 }
86 screen.SetStyle(tcell.StyleDefault)
87
88 var pal palette
Jan Schärb86917b2025-05-14 16:31:08 +000089 switch config.Terminal {
Serge Bazanski0d9e1252024-09-03 12:16:47 +020090 case TerminalLinux:
91 pal = paletteLinux
92 tty.Write([]byte(pal.setupLinuxConsole()))
93 case TerminalGeneric:
94 pal = paletteGeneric
95 }
96
97 width, height := screen.Size()
Serge Bazanskid735a3c2024-09-05 13:51:44 +020098
Serge Bazanski0d9e1252024-09-03 12:16:47 +020099 return &Console{
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200100 ttyPath: ttyPath,
101 tty: tty,
102 screen: screen,
103 width: width,
104 height: height,
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200105 palette: pal,
106 Quit: make(chan struct{}),
107 activePage: 0,
Jan Schärb86917b2025-05-14 16:31:08 +0000108 config: config,
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200109 logReader: reader,
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200110 }, nil
111}
112
113// Cleanup should be called when the console exits. This is only used in testing,
114// the Metropolis console always runs.
115func (c *Console) Cleanup() {
116 c.screen.Fini()
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200117 c.logReader.Close()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200118}
119
120func (c *Console) processEvent(ev tcell.Event) {
121 switch ev := ev.(type) {
122 case *tcell.EventKey:
123 if ev.Key() == tcell.KeyCtrlC {
124 close(c.Quit)
125 }
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200126 if ev.Key() == tcell.KeyTab {
127 c.activePage += 1
128 }
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200129 case *tcell.EventResize:
130 c.width, c.height = ev.Size()
131 }
132}
133
134// Run blocks while displaying the Console to the user.
135func (c *Console) Run(ctx context.Context) error {
136 // Build channel for console event processing.
137 evC := make(chan tcell.Event)
138 evQuitC := make(chan struct{})
139 defer close(evQuitC)
140 go c.screen.ChannelEvents(evC, evQuitC)
141
142 // Pipe event values into channels.
Lorenz Brun4bde9312025-08-06 05:04:11 +0200143 netAddrC := make(chan *node.NetStatus)
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200144 rolesC := make(chan *cpb.NodeRoles)
145 curatorConnC := make(chan *roleserve.CuratorConnection)
Jan Schärb86917b2025-05-14 16:31:08 +0000146 if err := supervisor.Run(ctx, "netpipe", event.Pipe(c.config.Network, netAddrC)); err != nil {
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200147 return err
148 }
Jan Schärb86917b2025-05-14 16:31:08 +0000149 if err := supervisor.Run(ctx, "rolespipe", event.Pipe(c.config.Roles, rolesC)); err != nil {
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200150 return err
151 }
Jan Schärb86917b2025-05-14 16:31:08 +0000152 if err := supervisor.Run(ctx, "curatorpipe", event.Pipe(c.config.CuratorConn, curatorConnC)); err != nil {
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200153 return err
154 }
155 supervisor.Signal(ctx, supervisor.SignalHealthy)
156
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200157 // Per-page data.
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200158 pageStatus := pageStatus{
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200159 netAddr: "Waiting...",
160 roles: "Waiting...",
161 id: "Waiting...",
162 fingerprint: "Waiting...",
163 }
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200164 pageLogs := pageLogs{}
165 // Fetch backlog
166 for _, le := range c.logReader.Backlog {
167 pageLogs.appendLine(le)
168 }
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200169
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200170 // Page references and names.
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200171 pages := []page{
172 &pageStatus,
173 &pageLogs,
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200174 }
175 pageNames := []string{
Serge Bazanski73c632f2024-09-05 13:51:57 +0200176 "Status", "Logs",
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200177 }
178
179 // Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in
180 // the status bar.
Serge Bazanski154e6d92024-09-11 17:26:31 +0200181 tickerDraw := time.NewTicker(time.Second / 10)
182 defer tickerDraw.Stop()
183
184 // Ticker used to fully resync the screen every 10 seconds, in case something
185 // scribbled over the TTY.
186 tickerSync := time.NewTicker(time.Second * 10)
187 defer tickerSync.Stop()
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200188
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200189 for {
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200190 // Draw active page.
191 c.activePage %= len(pages)
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200192 page := pages[c.activePage]
193 page.render(c)
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200194
195 // Draw status bar.
196 c.statusBar(c.activePage, pageNames...)
197
198 // Sync to screen.
199 c.screen.Show()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200200
201 select {
Serge Bazanski154e6d92024-09-11 17:26:31 +0200202 case <-tickerDraw.C:
203 case <-tickerSync.C:
204 c.screen.Sync()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200205 case <-ctx.Done():
206 return ctx.Err()
207 case ev := <-evC:
208 c.processEvent(ev)
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200209 page.processEvent(c, ev)
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200210 case t := <-netAddrC:
211 pageStatus.netAddr = t.ExternalAddress.String()
212 case t := <-rolesC:
213 var rlist []string
214 if t.ConsensusMember != nil {
215 rlist = append(rlist, "ConsensusMember")
216 }
217 if t.KubernetesController != nil {
218 rlist = append(rlist, "KubernetesController")
219 }
220 if t.KubernetesWorker != nil {
221 rlist = append(rlist, "KubernetesWorker")
222 }
223 pageStatus.roles = strings.Join(rlist, ", ")
224 if pageStatus.roles == "" {
225 pageStatus.roles = "none"
226 }
227 case t := <-curatorConnC:
228 pageStatus.id = t.Credentials.ID()
229 cert := t.Credentials.ClusterCA()
230 sum := sha256.New()
231 sum.Write(cert.Raw)
232 pageStatus.fingerprint = hex.EncodeToString(sum.Sum(nil))
Tim Windelschmidt0c7da3e2025-07-24 00:21:35 +0200233 case le := <-c.logReader.Stream:
234 pageLogs.appendLine(le)
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200235 }
236 }
237}