Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions cmd/cryptos-console/fontfallback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

/*
Copyright The CryptOS Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import (
"log"

"github.com/CryptOS-PKI/cryptos-node/internal/console"
)

// fontSwitcher abstracts the console font switch behind an interface so
// ensureConsoleFits is unit-tested with a fake, never the real KDFONTOP ioctl.
type fontSwitcher interface {
// setDefaultFont switches the console tty to the kernel's built-in
// default font (VGA8x16, CONFIG_FONT_8x16), which leaves more rows and
// columns than the prod cmdline's TER16x32 on the same framebuffer.
setDefaultFont() error
}

// ensureConsoleFits requests the kernel's built-in 8x16 font when cols x rows
// is below the floor the dashboard needs to drop out of its compact render
// (console.NeedsCompact), then re-reads the size with readSize so the caller
// draws at whatever the switch produced. A switch failure is logged and the
// original size is kept unchanged; the compact view still runs at that size.
func ensureConsoleFits(sw fontSwitcher, readSize func() (cols, rows int), cols, rows int) (int, int) {
if !console.NeedsCompact(cols, rows) {
return cols, rows
}
if err := sw.setDefaultFont(); err != nil {
log.Printf("console: font: %dx%d is below the compact floor; VGA8x16 fallback failed: %v", cols, rows, err)
return cols, rows
}
newCols, newRows := readSize()
log.Printf("console: font: %dx%d was below the compact floor; switched to VGA8x16, now %dx%d", cols, rows, newCols, newRows)
return newCols, newRows
}
64 changes: 64 additions & 0 deletions cmd/cryptos-console/fontfallback_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build linux

package main

/*
Copyright The CryptOS Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import (
"unsafe"

"golang.org/x/sys/unix"
)

// KDFONTOP and the console_font_op layout (linux/kd.h) are not exposed by
// golang.org/x/sys/unix, so they are declared here.
const (
kdFontOp = 0x4B72 // KDFONTOP
kdFontOpSetDefault = 2 // KD_FONT_OP_SET_DEFAULT
)

// consoleFontOp mirrors struct console_font_op from linux/kd.h for the
// KD_FONT_OP_SET_DEFAULT operation, where data points to the font name and
// the other fields are unused. The pad field matches the compiler-inserted
// padding before the pointer on 64-bit (amd64, arm64), the only targets this
// binary builds for.
type consoleFontOp struct {
op uint32
flags uint32
width uint32
height uint32
charcount uint32
pad uint32
data *byte
}

// vga8x16Name is the kernel's built-in 8x16 font name (CONFIG_FONT_8x16),
// NUL-terminated as KD_FONT_OP_SET_DEFAULT expects.
var vga8x16Name = append([]byte("VGA8x16"), 0)

// kdFontSwitcher switches the console at fd to the kernel's built-in default
// font via the KDFONTOP ioctl.
type kdFontSwitcher struct{ fd int }

func (s kdFontSwitcher) setDefaultFont() error {
op := consoleFontOp{op: kdFontOpSetDefault, data: &vga8x16Name[0]}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(s.fd), uintptr(kdFontOp), uintptr(unsafe.Pointer(&op)))
if errno != 0 {
return errno
}
return nil
}
29 changes: 29 additions & 0 deletions cmd/cryptos-console/fontfallback_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build !linux

package main

/*
Copyright The CryptOS Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import "errors"

// kdFontSwitcher is a no-op off Linux; the console only runs on the node's
// Linux framebuffer console.
type kdFontSwitcher struct{ fd int }

func (s kdFontSwitcher) setDefaultFont() error {
return errors.New("console font fallback is only available on linux")
}
75 changes: 75 additions & 0 deletions cmd/cryptos-console/fontfallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

/*
Copyright The CryptOS Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import (
"errors"
"testing"
)

// fakeFontSwitcher is a fontSwitcher fake so the fallback decision is tested
// without a real console tty.
type fakeFontSwitcher struct {
calls int
err error
}

func (f *fakeFontSwitcher) setDefaultFont() error {
f.calls++
return f.err
}

func TestEnsureConsoleFitsRequestsTheFallbackBelowTheFloor(t *testing.T) {
sw := &fakeFontSwitcher{}
cols, rows := ensureConsoleFits(sw, func() (int, int) { return 80, 24 }, 38, 11)
if sw.calls != 1 {
t.Fatalf("setDefaultFont calls = %d, want 1", sw.calls)
}
if cols != 80 || rows != 24 {
t.Fatalf("got %dx%d, want the re-read 80x24", cols, rows)
}
}

func TestEnsureConsoleFitsSkipsTheFallbackAtOrAboveTheFloor(t *testing.T) {
sw := &fakeFontSwitcher{}
readSize := func() (int, int) {
t.Fatal("readSize must not be called when the console already fits")
return 0, 0
}
cols, rows := ensureConsoleFits(sw, readSize, 40, 12)
if sw.calls != 0 {
t.Fatalf("setDefaultFont calls = %d, want 0", sw.calls)
}
if cols != 40 || rows != 12 {
t.Fatalf("got %dx%d, want the original 40x12 unchanged", cols, rows)
}
}

func TestEnsureConsoleFitsKeepsTheOriginalSizeOnFallbackError(t *testing.T) {
sw := &fakeFontSwitcher{err: errors.New("ioctl failed")}
readSize := func() (int, int) {
t.Fatal("readSize must not be called when the fallback failed")
return 0, 0
}
cols, rows := ensureConsoleFits(sw, readSize, 38, 11)
if sw.calls != 1 {
t.Fatalf("setDefaultFont calls = %d, want 1", sw.calls)
}
if cols != 38 || rows != 11 {
t.Fatalf("got %dx%d, want the original 38x11 kept", cols, rows)
}
}
8 changes: 7 additions & 1 deletion cmd/cryptos-console/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ func main() {

// Query the console size once at startup; the kernel sets a large font so
// the frame fills the whole screen. On any failure this falls back to 80x24.
cols, rows := consoleSize(int(os.Stdout.Fd()))
fd := int(os.Stdout.Fd())
cols, rows := consoleSize(fd)
// On a framebuffer too small for the prod cmdline's TER16x32 font, the VT
// can report fewer rows/cols than even the compact view needs. Fall back
// to the kernel's built-in 8x16 font before the first draw so the real
// console geometry, not just the reported one, has room for it.
cols, rows = ensureConsoleFits(kdFontSwitcher{fd: fd}, func() (int, int) { return consoleSize(fd) }, cols, rows)
ver := buildinfo.Get().Version

// Dial the local socket, retrying on failure so the console comes up even
Expand Down
45 changes: 45 additions & 0 deletions internal/console/needscompact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package console_test

/*
Copyright The CryptOS Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import (
"testing"

"github.com/CryptOS-PKI/cryptos-node/internal/console"
)

// NeedsCompact must agree with the threshold screen.render already uses to
// fall back to its compact render, since a userspace caller outside the
// package (the console font fallback) needs the same floor without
// duplicating the numbers.
func TestNeedsCompactMatchesTheFramedFloor(t *testing.T) {
cases := []struct {
cols, rows int
want bool
}{
{40, 12, false},
{80, 24, false},
{39, 12, true},
{40, 11, true},
{38, 11, true},
}
for _, c := range cases {
if got := console.NeedsCompact(c.cols, c.rows); got != c.want {
t.Errorf("NeedsCompact(%d, %d) = %v, want %v", c.cols, c.rows, got, c.want)
}
}
}
9 changes: 9 additions & 0 deletions internal/console/screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ const (
minRows = 12
)

// NeedsCompact reports whether cols x rows is below the floor a framed screen
// needs, the same threshold render uses to fall back to the compact view. A
// caller outside the package (the console's font fallback) uses this to
// decide when the real console geometry is too small, without duplicating
// the floor.
func NeedsCompact(cols, rows int) bool {
return cols < minCols || rows < minRows
}

// bodyLines lays the screen's body items out as rows within a frame inner
// columns wide. This is the part of the framed layout that never gets
// dropped or shortened, so its row count against rows-3 (the interior rows
Expand Down
Loading