| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 4 | package tconsole |
| 5 | |
| 6 | import ( |
| 7 | "context" |
| 8 | "crypto/sha256" |
| 9 | "encoding/hex" |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 10 | "fmt" |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 11 | "strings" |
| Serge Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 12 | "time" |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 13 | |
| 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 Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 20 | "source.monogon.dev/osbase/logtree" |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 21 | "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. |
| 26 | type 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 Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 38 | // activePage expressed within [0...num pages). The number/layout of pages is |
| 39 | // constructed dynamically in Run. |
| 40 | activePage int |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 41 | |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 42 | reader *logtree.LogReader |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 43 | 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 Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 54 | func 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 Windelschmidt | 5f1a7de | 2024-09-19 02:00:14 +0200 | [diff] [blame] | 57 | return nil, fmt.Errorf("lt.Read: %w", err) |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 58 | } |
| 59 | |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 60 | 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 Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 83 | |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 84 | return &Console{ |
| Serge Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 85 | 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 Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 94 | reader: reader, |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 95 | |
| 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. |
| 103 | func (c *Console) Cleanup() { |
| 104 | c.screen.Fini() |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 105 | c.reader.Close() |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 106 | } |
| 107 | |
| 108 | func (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 Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 114 | if ev.Key() == tcell.KeyTab { |
| 115 | c.activePage += 1 |
| 116 | } |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 117 | case *tcell.EventResize: |
| 118 | c.width, c.height = ev.Size() |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // Run blocks while displaying the Console to the user. |
| 123 | func (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 Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 145 | // Per-page data. |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 146 | pageStatus := pageStatusData{ |
| 147 | netAddr: "Waiting...", |
| 148 | roles: "Waiting...", |
| 149 | id: "Waiting...", |
| 150 | fingerprint: "Waiting...", |
| 151 | } |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 152 | pageLogs := pageLogsData{} |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 153 | |
| Serge Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 154 | // Page references and names. |
| 155 | pages := []func(){ |
| 156 | func() { c.pageStatus(&pageStatus) }, |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 157 | func() { c.pageLogs(&pageLogs) }, |
| Serge Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 158 | } |
| 159 | pageNames := []string{ |
| Serge Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 160 | "Status", "Logs", |
| Serge Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 161 | } |
| 162 | |
| 163 | // Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in |
| 164 | // the status bar. |
| Serge Bazanski | 154e6d9 | 2024-09-11 17:26:31 +0200 | [diff] [blame] | 165 | 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 Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 172 | |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 173 | for { |
| Serge Bazanski | d735a3c | 2024-09-05 13:51:44 +0200 | [diff] [blame] | 174 | // 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 Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 183 | |
| 184 | select { |
| Serge Bazanski | 154e6d9 | 2024-09-11 17:26:31 +0200 | [diff] [blame] | 185 | case <-tickerDraw.C: |
| 186 | case <-tickerSync.C: |
| 187 | c.screen.Sync() |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 188 | 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 Bazanski | 73c632f | 2024-09-05 13:51:57 +0200 | [diff] [blame] | 215 | case le := <-c.reader.Stream: |
| 216 | pageLogs.appendLine(le.String()) |
| Serge Bazanski | 0d9e125 | 2024-09-03 12:16:47 +0200 | [diff] [blame] | 217 | } |
| 218 | } |
| 219 | } |