m/installer: log to tty0 and ttyS0

Without this we only log to whatever the default system console is.

Change-Id: I64b43f8617f3b8752332209b511ea470848c4481
Reviewed-on: https://review.monogon.dev/c/monogon/+/1381
Tested-by: Jenkins CI
Reviewed-by: Leopold Schabel <leo@monogon.tech>
diff --git a/metropolis/installer/BUILD.bazel b/metropolis/installer/BUILD.bazel
index 6e1f1f0..f3b6d1e 100644
--- a/metropolis/installer/BUILD.bazel
+++ b/metropolis/installer/BUILD.bazel
@@ -5,7 +5,10 @@
 
 go_library(
     name = "installer_lib",
-    srcs = ["main.go"],
+    srcs = [
+        "log.go",
+        "main.go",
+    ],
     importpath = "source.monogon.dev/metropolis/installer",
     visibility = ["//visibility:private"],
     deps = [
diff --git a/metropolis/installer/log.go b/metropolis/installer/log.go
new file mode 100644
index 0000000..cb7dee6
--- /dev/null
+++ b/metropolis/installer/log.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+var logC = make(chan string)
+
+// logPiper pipes log entries submitted via logf and panicf into whatever
+// consoles are available to the system.
+func logPiper() {
+	var consoles []*os.File
+	for _, p := range []string{"/dev/tty0", "/dev/ttyS0"} {
+		f, err := os.OpenFile(p, os.O_WRONLY, 0)
+		if err != nil {
+			continue
+		}
+		consoles = append(consoles, f)
+	}
+
+	for {
+		s := <-logC
+		for _, c := range consoles {
+			fmt.Fprintf(c, "%s\n", s)
+		}
+	}
+}
+
+// logf logs some format/args into the active consoles.
+func logf(format string, args ...any) {
+	s := fmt.Sprintf(format, args...)
+	logC <- s
+}
+
+// panicf aborts the installation process with a given format/args.
+func panicf(format string, args ...any) {
+	s := fmt.Sprintf(format, args...)
+	// We don't need to print `s` here, as it's gonna get printed by the recovery
+	// code in main.
+	panic(s)
+}
diff --git a/metropolis/installer/main.go b/metropolis/installer/main.go
index 520bc89..7dc1da9 100644
--- a/metropolis/installer/main.go
+++ b/metropolis/installer/main.go
@@ -178,20 +178,13 @@
 	return nil
 }
 
-// panicf is a replacement for log.panicf that doesn't print the error message
-// before calling panic.
-func panicf(format string, v ...interface{}) {
-	s := fmt.Sprintf(format, v...)
-	panic(s)
-}
-
 func main() {
 	// Reboot on panic after a delay. The error string will have been printed
 	// before recover is called.
 	defer func() {
 		if r := recover(); r != nil {
-			fmt.Println(r)
-			fmt.Println("The installation could not be finalized. Please reboot to continue.")
+			logf("Fatal error: %v", r)
+			logf("The installation could not be finalized. Please reboot to continue.")
 			syscall.Pause()
 		}
 	}()
@@ -200,6 +193,12 @@
 	if err := mountPseudoFS(); err != nil {
 		panicf("While mounting pseudo-filesystems: %v", err)
 	}
+
+	go logPiper()
+	logf("Metropolis Installer")
+	logf("Copyright (c) 2023 The Monogon Project Authors")
+	logf("")
+
 	// Read the installer ESP UUID from efivarfs.
 	espUuid, err := efivarfs.ReadLoaderDevicePartUUID()
 	if err != nil {
@@ -292,7 +291,7 @@
 
 	// Use osimage to partition the target block device and set up its ESP.
 	// Create will return an EFI boot entry on success.
-	fmt.Printf("Installing to %s\n", tgtBlkdevPath)
+	logf("Installing to %s...", tgtBlkdevPath)
 	be, err := osimage.Create(&installParams)
 	if err != nil {
 		panicf("While installing: %v", err)
@@ -327,6 +326,6 @@
 
 	// Reboot.
 	unix.Sync()
-	fmt.Println("Installation completed. Rebooting.")
+	logf("Installation completed. Rebooting.")
 	unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
 }