blob: 963be3425abf534c15a5cb2097f1af43c8b0169c [file] [log] [blame]
Serge Bazanski0d9e1252024-09-03 12:16:47 +02001package tconsole
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "strings"
Serge Bazanskid735a3c2024-09-05 13:51:44 +02008 "time"
Serge Bazanski0d9e1252024-09-03 12:16:47 +02009
10 "github.com/gdamore/tcell/v2"
11
12 "source.monogon.dev/metropolis/node/core/network"
13 "source.monogon.dev/metropolis/node/core/roleserve"
14 cpb "source.monogon.dev/metropolis/proto/common"
15 "source.monogon.dev/osbase/event"
16 "source.monogon.dev/osbase/supervisor"
17)
18
19// Console is a Terminal Console (TConsole), a user-interactive informational
20// display visible on the TTY of a running Metropolis instance.
21type Console struct {
22 // Quit will be closed when the user press CTRL-C. A new channel will be created
23 // on each New call.
24
25 Quit chan struct{}
26 ttyPath string
27 tty tcell.Tty
28 screen tcell.Screen
29 width int
30 height int
31 // palette chosen for the given terminal.
32 palette palette
Serge Bazanskid735a3c2024-09-05 13:51:44 +020033 // activePage expressed within [0...num pages). The number/layout of pages is
34 // constructed dynamically in Run.
35 activePage int
Serge Bazanski0d9e1252024-09-03 12:16:47 +020036
37 network event.Value[*network.Status]
38 roles event.Value[*cpb.NodeRoles]
39 curatorConn event.Value[*roleserve.CuratorConnection]
40}
41
42// New creates a new Console, taking over the TTY at the given path. The given
43// Terminal type selects between a Linux terminal (VTY) and a generic terminal
44// for testing.
45//
46// network, roles, curatorConn point to various Metropolis subsystems that are
47// used to populate the console data.
48func New(terminal Terminal, ttyPath string, network event.Value[*network.Status], roles event.Value[*cpb.NodeRoles], curatorConn event.Value[*roleserve.CuratorConnection]) (*Console, error) {
49 tty, err := tcell.NewDevTtyFromDev(ttyPath)
50 if err != nil {
51 return nil, err
52 }
53 screen, err := tcell.NewTerminfoScreenFromTty(tty)
54 if err != nil {
55 return nil, err
56 }
57 if err := screen.Init(); err != nil {
58 return nil, err
59 }
60 screen.SetStyle(tcell.StyleDefault)
61
62 var pal palette
63 switch terminal {
64 case TerminalLinux:
65 pal = paletteLinux
66 tty.Write([]byte(pal.setupLinuxConsole()))
67 case TerminalGeneric:
68 pal = paletteGeneric
69 }
70
71 width, height := screen.Size()
Serge Bazanskid735a3c2024-09-05 13:51:44 +020072
Serge Bazanski0d9e1252024-09-03 12:16:47 +020073 return &Console{
Serge Bazanskid735a3c2024-09-05 13:51:44 +020074 ttyPath: ttyPath,
75 tty: tty,
76 screen: screen,
77 width: width,
78 height: height,
79 network: network,
80 palette: pal,
81 Quit: make(chan struct{}),
82 activePage: 0,
Serge Bazanski0d9e1252024-09-03 12:16:47 +020083
84 roles: roles,
85 curatorConn: curatorConn,
86 }, nil
87}
88
89// Cleanup should be called when the console exits. This is only used in testing,
90// the Metropolis console always runs.
91func (c *Console) Cleanup() {
92 c.screen.Fini()
93}
94
95func (c *Console) processEvent(ev tcell.Event) {
96 switch ev := ev.(type) {
97 case *tcell.EventKey:
98 if ev.Key() == tcell.KeyCtrlC {
99 close(c.Quit)
100 }
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200101 if ev.Key() == tcell.KeyTab {
102 c.activePage += 1
103 }
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200104 case *tcell.EventResize:
105 c.width, c.height = ev.Size()
106 }
107}
108
109// Run blocks while displaying the Console to the user.
110func (c *Console) Run(ctx context.Context) error {
111 // Build channel for console event processing.
112 evC := make(chan tcell.Event)
113 evQuitC := make(chan struct{})
114 defer close(evQuitC)
115 go c.screen.ChannelEvents(evC, evQuitC)
116
117 // Pipe event values into channels.
118 netAddrC := make(chan *network.Status)
119 rolesC := make(chan *cpb.NodeRoles)
120 curatorConnC := make(chan *roleserve.CuratorConnection)
121 if err := supervisor.Run(ctx, "netpipe", event.Pipe(c.network, netAddrC)); err != nil {
122 return err
123 }
124 if err := supervisor.Run(ctx, "rolespipe", event.Pipe(c.roles, rolesC)); err != nil {
125 return err
126 }
127 if err := supervisor.Run(ctx, "curatorpipe", event.Pipe(c.curatorConn, curatorConnC)); err != nil {
128 return err
129 }
130 supervisor.Signal(ctx, supervisor.SignalHealthy)
131
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200132 // Per-page data.
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200133 pageStatus := pageStatusData{
134 netAddr: "Waiting...",
135 roles: "Waiting...",
136 id: "Waiting...",
137 fingerprint: "Waiting...",
138 }
139
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200140 // Page references and names.
141 pages := []func(){
142 func() { c.pageStatus(&pageStatus) },
143 }
144 pageNames := []string{
145 "Status",
146 }
147
148 // Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in
149 // the status bar.
150 ticker := time.NewTicker(time.Second / 10)
151 defer ticker.Stop()
152
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200153 for {
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200154 // Draw active page.
155 c.activePage %= len(pages)
156 pages[c.activePage]()
157
158 // Draw status bar.
159 c.statusBar(c.activePage, pageNames...)
160
161 // Sync to screen.
162 c.screen.Show()
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200163
164 select {
Serge Bazanskid735a3c2024-09-05 13:51:44 +0200165 case <-ticker.C:
Serge Bazanski0d9e1252024-09-03 12:16:47 +0200166 case <-ctx.Done():
167 return ctx.Err()
168 case ev := <-evC:
169 c.processEvent(ev)
170 case t := <-netAddrC:
171 pageStatus.netAddr = t.ExternalAddress.String()
172 case t := <-rolesC:
173 var rlist []string
174 if t.ConsensusMember != nil {
175 rlist = append(rlist, "ConsensusMember")
176 }
177 if t.KubernetesController != nil {
178 rlist = append(rlist, "KubernetesController")
179 }
180 if t.KubernetesWorker != nil {
181 rlist = append(rlist, "KubernetesWorker")
182 }
183 pageStatus.roles = strings.Join(rlist, ", ")
184 if pageStatus.roles == "" {
185 pageStatus.roles = "none"
186 }
187 case t := <-curatorConnC:
188 pageStatus.id = t.Credentials.ID()
189 cert := t.Credentials.ClusterCA()
190 sum := sha256.New()
191 sum.Write(cert.Raw)
192 pageStatus.fingerprint = hex.EncodeToString(sum.Sum(nil))
193 }
194 }
195}