blob: aa3df7be9f1cbf52059b438085594df8db05decd [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
16 "source.monogon.dev/metropolis/node/core/network"
17 "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
24// Console is a Terminal Console (TConsole), a user-interactive informational
25// display visible on the TTY of a running Metropolis instance.
26type Console struct {
27 // Quit will be closed when the user press CTRL-C. A new channel will be created
28 // on each New call.
29
30 Quit chan struct{}
31 ttyPath string
32 tty tcell.Tty
33 screen tcell.Screen
34 width int
35 height int
36 // palette chosen for the given terminal.
37 palette palette
Serge Bazanskid735a3c2024-09-05 13:51:44 +020038 // activePage expressed within [0...num pages). The number/layout of pages is
39 // constructed dynamically in Run.
40 activePage int
Serge Bazanski0d9e1252024-09-03 12:16:47 +020041
Serge Bazanski73c632f2024-09-05 13:51:57 +020042 reader *logtree.LogReader
Serge Bazanski0d9e1252024-09-03 12:16:47 +020043 network event.Value[*network.Status]
44 roles event.Value[*cpb.NodeRoles]
45 curatorConn event.Value[*roleserve.CuratorConnection]
46}
47
48// New creates a new Console, taking over the TTY at the given path. The given
49// Terminal type selects between a Linux terminal (VTY) and a generic terminal
50// for testing.
51//
52// network, roles, curatorConn point to various Metropolis subsystems that are
53// used to populate the console data.
Serge Bazanski73c632f2024-09-05 13:51:57 +020054func New(terminal Terminal, ttyPath string, lt *logtree.LogTree, network event.Value[*network.Status], roles event.Value[*cpb.NodeRoles], curatorConn event.Value[*roleserve.CuratorConnection]) (*Console, error) {
55 reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
56 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +020057 return nil, fmt.Errorf("lt.Read: %w", err)
Serge Bazanski73c632f2024-09-05 13:51:57 +020058 }
59
Serge Bazanski0d9e1252024-09-03 12:16:47 +020060 tty, err := tcell.NewDevTtyFromDev(ttyPath)
61 if err != nil {
62 return nil, err
63 }
64 screen, err := tcell.NewTerminfoScreenFromTty(tty)
65 if err != nil {
66 return nil, err
67 }
68 if err := screen.Init(); err != nil {
69 return nil, err
70 }
71 screen.SetStyle(tcell.StyleDefault)
72
73 var pal palette
74 switch terminal {
75 case TerminalLinux:
76 pal = paletteLinux
77 tty.Write([]byte(pal.setupLinuxConsole()))
78 case TerminalGeneric:
79 pal = paletteGeneric
80 }
81
82 width, height := screen.Size()
Serge Bazanskid735a3c2024-09-05 13:51:44 +020083
Serge Bazanski0d9e1252024-09-03 12:16:47 +020084 return &Console{
Serge Bazanskid735a3c2024-09-05 13:51:44 +020085 ttyPath: ttyPath,
86 tty: tty,
87 screen: screen,
88 width: width,
89 height: height,
90 network: network,
91 palette: pal,
92 Quit: make(chan struct{}),
93 activePage: 0,
Serge Bazanski73c632f2024-09-05 13:51:57 +020094 reader: reader,
Serge Bazanski0d9e1252024-09-03 12:16:47 +020095
96 roles: roles,
97 curatorConn: curatorConn,
98 }, nil
99}
100
101// Cleanup should be called when the console exits. This is only used in testing,
102// the Metropolis console always runs.
103func (c *Console) Cleanup() {
104 c.screen.Fini()
Serge Bazanski73c632f2024-09-05 13:51:57 +0200105 c.reader.Close()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200106}
107
108func (c *Console) processEvent(ev tcell.Event) {
109 switch ev := ev.(type) {
110 case *tcell.EventKey:
111 if ev.Key() == tcell.KeyCtrlC {
112 close(c.Quit)
113 }
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200114 if ev.Key() == tcell.KeyTab {
115 c.activePage += 1
116 }
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200117 case *tcell.EventResize:
118 c.width, c.height = ev.Size()
119 }
120}
121
122// Run blocks while displaying the Console to the user.
123func (c *Console) Run(ctx context.Context) error {
124 // Build channel for console event processing.
125 evC := make(chan tcell.Event)
126 evQuitC := make(chan struct{})
127 defer close(evQuitC)
128 go c.screen.ChannelEvents(evC, evQuitC)
129
130 // Pipe event values into channels.
131 netAddrC := make(chan *network.Status)
132 rolesC := make(chan *cpb.NodeRoles)
133 curatorConnC := make(chan *roleserve.CuratorConnection)
134 if err := supervisor.Run(ctx, "netpipe", event.Pipe(c.network, netAddrC)); err != nil {
135 return err
136 }
137 if err := supervisor.Run(ctx, "rolespipe", event.Pipe(c.roles, rolesC)); err != nil {
138 return err
139 }
140 if err := supervisor.Run(ctx, "curatorpipe", event.Pipe(c.curatorConn, curatorConnC)); err != nil {
141 return err
142 }
143 supervisor.Signal(ctx, supervisor.SignalHealthy)
144
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200145 // Per-page data.
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200146 pageStatus := pageStatusData{
147 netAddr: "Waiting...",
148 roles: "Waiting...",
149 id: "Waiting...",
150 fingerprint: "Waiting...",
151 }
Serge Bazanski73c632f2024-09-05 13:51:57 +0200152 pageLogs := pageLogsData{}
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200153
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200154 // Page references and names.
155 pages := []func(){
156 func() { c.pageStatus(&pageStatus) },
Serge Bazanski73c632f2024-09-05 13:51:57 +0200157 func() { c.pageLogs(&pageLogs) },
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200158 }
159 pageNames := []string{
Serge Bazanski73c632f2024-09-05 13:51:57 +0200160 "Status", "Logs",
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200161 }
162
163 // Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in
164 // the status bar.
Serge Bazanski154e6d92024-09-11 17:26:31 +0200165 tickerDraw := time.NewTicker(time.Second / 10)
166 defer tickerDraw.Stop()
167
168 // Ticker used to fully resync the screen every 10 seconds, in case something
169 // scribbled over the TTY.
170 tickerSync := time.NewTicker(time.Second * 10)
171 defer tickerSync.Stop()
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200172
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200173 for {
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200174 // Draw active page.
175 c.activePage %= len(pages)
176 pages[c.activePage]()
177
178 // Draw status bar.
179 c.statusBar(c.activePage, pageNames...)
180
181 // Sync to screen.
182 c.screen.Show()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200183
184 select {
Serge Bazanski154e6d92024-09-11 17:26:31 +0200185 case <-tickerDraw.C:
186 case <-tickerSync.C:
187 c.screen.Sync()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200188 case <-ctx.Done():
189 return ctx.Err()
190 case ev := <-evC:
191 c.processEvent(ev)
192 case t := <-netAddrC:
193 pageStatus.netAddr = t.ExternalAddress.String()
194 case t := <-rolesC:
195 var rlist []string
196 if t.ConsensusMember != nil {
197 rlist = append(rlist, "ConsensusMember")
198 }
199 if t.KubernetesController != nil {
200 rlist = append(rlist, "KubernetesController")
201 }
202 if t.KubernetesWorker != nil {
203 rlist = append(rlist, "KubernetesWorker")
204 }
205 pageStatus.roles = strings.Join(rlist, ", ")
206 if pageStatus.roles == "" {
207 pageStatus.roles = "none"
208 }
209 case t := <-curatorConnC:
210 pageStatus.id = t.Credentials.ID()
211 cert := t.Credentials.ClusterCA()
212 sum := sha256.New()
213 sum.Write(cert.Raw)
214 pageStatus.fingerprint = hex.EncodeToString(sum.Sum(nil))
Serge Bazanski73c632f2024-09-05 13:51:57 +0200215 case le := <-c.reader.Stream:
216 pageLogs.appendLine(le.String())
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200217 }
218 }
219}