tconsole: add logs page

This is a basic log console. Future work can be performed to make the
display more compact, allow scrollback functionality and maybe scrolling
to the sides to see longer lines.

Change-Id: I81defe874542acfe89137035d0fc6de9861d3e33
Reviewed-on: https://review.monogon.dev/c/monogon/+/3382
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/node/core/main.go b/metropolis/node/core/main.go
index d103693..babee28 100644
--- a/metropolis/node/core/main.go
+++ b/metropolis/node/core/main.go
@@ -214,7 +214,7 @@
 		// Initialize interactive consoles.
 		interactiveConsoles := []string{"/dev/tty0"}
 		for _, c := range interactiveConsoles {
-			console, err := tconsole.New(tconsole.TerminalLinux, c, &networkSvc.Status, &rs.LocalRoles, &rs.CuratorConnection)
+			console, err := tconsole.New(tconsole.TerminalLinux, c, lt, &networkSvc.Status, &rs.LocalRoles, &rs.CuratorConnection)
 			if err != nil {
 				logger.Info("Failed to initialize interactive console at %s: %v", c, err)
 				// TODO: fall back to logger
diff --git a/metropolis/node/core/tconsole/BUILD.bazel b/metropolis/node/core/tconsole/BUILD.bazel
index fadc7f3..e16eb41 100644
--- a/metropolis/node/core/tconsole/BUILD.bazel
+++ b/metropolis/node/core/tconsole/BUILD.bazel
@@ -5,6 +5,7 @@
     srcs = [
         "colors.go",
         "draw.go",
+        "page_logs.go",
         "page_status.go",
         "statusbar.go",
         "tconsole.go",
@@ -17,6 +18,7 @@
         "//metropolis/proto/common",
         "//metropolis/version",
         "//osbase/event",
+        "//osbase/logtree",
         "//osbase/supervisor",
         "//version",
         "@com_github_gdamore_tcell_v2//:tcell",
diff --git a/metropolis/node/core/tconsole/page_logs.go b/metropolis/node/core/tconsole/page_logs.go
new file mode 100644
index 0000000..d408d4d
--- /dev/null
+++ b/metropolis/node/core/tconsole/page_logs.go
@@ -0,0 +1,48 @@
+package tconsole
+
+import "github.com/gdamore/tcell/v2"
+
+// pageLogsData encompasses all data to be shown within the logs page.
+type pageLogsData struct {
+	// log lines, simple deque with the newest log lines appended to the end.
+	lines []string
+}
+
+func (p *pageLogsData) appendLine(s string) {
+	p.lines = append(p.lines, s)
+}
+
+// compactData ensures that there's no more lines stored than maxlines by
+// discarding the oldest lines.
+func (p *pageLogsData) compactData(maxlines int) {
+	if extra := len(p.lines) - maxlines; extra > 0 {
+		p.lines = p.lines[extra:]
+	}
+}
+
+// pageLogs renders the logs page to the user given pageLogsData.
+func (c *Console) pageLogs(data *pageLogsData) {
+	c.screen.Clear()
+	sty1 := tcell.StyleDefault.Background(c.color(colorPink)).Foreground(c.color(colorBlack))
+	sty2 := tcell.StyleDefault.Background(c.color(colorBlue)).Foreground(c.color(colorBlack))
+
+	// Draw frame.
+	c.fillRectangle(0, c.width, 0, c.height, sty2)
+	c.fillRectangle(1, c.width-1, 1, c.height-2, sty1)
+
+	// Inner log area size.
+	nlines := (c.height - 2) - 1
+	linelen := (c.width - 1) - 1
+
+	// Compact and draw log lines.
+	data.compactData(nlines)
+	for y := 0; y < nlines; y++ {
+		if y < len(data.lines) {
+			line := data.lines[y]
+			if len(line) > linelen {
+				line = line[:linelen]
+			}
+			c.drawText(1, 1+y, line, sty1)
+		}
+	}
+}
diff --git a/metropolis/node/core/tconsole/standalone/BUILD.bazel b/metropolis/node/core/tconsole/standalone/BUILD.bazel
index cd7a653..bc48d04 100644
--- a/metropolis/node/core/tconsole/standalone/BUILD.bazel
+++ b/metropolis/node/core/tconsole/standalone/BUILD.bazel
@@ -11,6 +11,7 @@
         "//metropolis/node/core/tconsole",
         "//metropolis/proto/common",
         "//osbase/event/memory",
+        "//osbase/logtree",
         "//osbase/supervisor",
     ],
 )
diff --git a/metropolis/node/core/tconsole/standalone/main.go b/metropolis/node/core/tconsole/standalone/main.go
index 65b3976..d01ab4f 100644
--- a/metropolis/node/core/tconsole/standalone/main.go
+++ b/metropolis/node/core/tconsole/standalone/main.go
@@ -20,6 +20,7 @@
 	"source.monogon.dev/metropolis/node/core/tconsole"
 	cpb "source.monogon.dev/metropolis/proto/common"
 	"source.monogon.dev/osbase/event/memory"
+	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -28,7 +29,9 @@
 	var rolesV memory.Value[*cpb.NodeRoles]
 	var curV memory.Value[*roleserve.CuratorConnection]
 
-	tc, err := tconsole.New(tconsole.TerminalGeneric, "/proc/self/fd/0", &netV, &rolesV, &curV)
+	lt := logtree.New()
+
+	tc, err := tconsole.New(tconsole.TerminalGeneric, "/proc/self/fd/0", lt, &netV, &rolesV, &curV)
 	if err != nil {
 		log.Fatalf("tconsole.New: %v", err)
 	}
@@ -51,6 +54,14 @@
 	signal.Ignore(os.Interrupt)
 	supervisor.New(ctx, func(ctx context.Context) error {
 		supervisor.Run(ctx, "tconsole", tc.Run)
+		supervisor.Run(ctx, "log-dawdle", func(ctx context.Context) error {
+			for {
+				supervisor.Logger(ctx).Infof("It is currently: %s", time.Now().Format(time.DateTime))
+				if err := delay(ctx, time.Second); err != nil {
+					return err
+				}
+			}
+		})
 		supervisor.Run(ctx, "net-dawdle", func(ctx context.Context) error {
 			supervisor.Signal(ctx, supervisor.SignalHealthy)
 			for {
@@ -84,7 +95,7 @@
 		supervisor.Signal(ctx, supervisor.SignalHealthy)
 		<-ctx.Done()
 		return ctx.Err()
-	})
+	}, supervisor.WithExistingLogtree(lt))
 	<-ctx.Done()
 	tc.Cleanup()
 }
diff --git a/metropolis/node/core/tconsole/tconsole.go b/metropolis/node/core/tconsole/tconsole.go
index 963be34..bbdca82 100644
--- a/metropolis/node/core/tconsole/tconsole.go
+++ b/metropolis/node/core/tconsole/tconsole.go
@@ -4,6 +4,7 @@
 	"context"
 	"crypto/sha256"
 	"encoding/hex"
+	"fmt"
 	"strings"
 	"time"
 
@@ -13,6 +14,7 @@
 	"source.monogon.dev/metropolis/node/core/roleserve"
 	cpb "source.monogon.dev/metropolis/proto/common"
 	"source.monogon.dev/osbase/event"
+	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -34,6 +36,7 @@
 	// constructed dynamically in Run.
 	activePage int
 
+	reader      *logtree.LogReader
 	network     event.Value[*network.Status]
 	roles       event.Value[*cpb.NodeRoles]
 	curatorConn event.Value[*roleserve.CuratorConnection]
@@ -45,7 +48,12 @@
 //
 // network, roles, curatorConn point to various Metropolis subsystems that are
 // used to populate the console data.
-func New(terminal Terminal, ttyPath string, network event.Value[*network.Status], roles event.Value[*cpb.NodeRoles], curatorConn event.Value[*roleserve.CuratorConnection]) (*Console, error) {
+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) {
+	reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
+	if err != nil {
+		return nil, fmt.Errorf("lt.Read: %v", err)
+	}
+
 	tty, err := tcell.NewDevTtyFromDev(ttyPath)
 	if err != nil {
 		return nil, err
@@ -80,6 +88,7 @@
 		palette:    pal,
 		Quit:       make(chan struct{}),
 		activePage: 0,
+		reader:     reader,
 
 		roles:       roles,
 		curatorConn: curatorConn,
@@ -90,6 +99,7 @@
 // the Metropolis console always runs.
 func (c *Console) Cleanup() {
 	c.screen.Fini()
+	c.reader.Close()
 }
 
 func (c *Console) processEvent(ev tcell.Event) {
@@ -136,13 +146,15 @@
 		id:          "Waiting...",
 		fingerprint: "Waiting...",
 	}
+	pageLogs := pageLogsData{}
 
 	// Page references and names.
 	pages := []func(){
 		func() { c.pageStatus(&pageStatus) },
+		func() { c.pageLogs(&pageLogs) },
 	}
 	pageNames := []string{
-		"Status",
+		"Status", "Logs",
 	}
 
 	// Ticker used to maintain redraws at minimum 10Hz, to eg. update the clock in
@@ -190,6 +202,8 @@
 			sum := sha256.New()
 			sum.Write(cert.Raw)
 			pageStatus.fingerprint = hex.EncodeToString(sum.Sum(nil))
+		case le := <-c.reader.Stream:
+			pageLogs.appendLine(le.String())
 		}
 	}
 }