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